@karmaniverous/jeeves-meta 0.15.3 → 0.15.4
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/cli/jeeves-meta/architect.md +17 -0
- package/dist/cli/jeeves-meta/index.js +693 -720
- package/dist/index.d.ts +343 -423
- package/dist/index.js +1421 -1827
- package/dist/prompts/architect.md +17 -0
- package/package.json +2 -2
|
@@ -8418,222 +8418,6 @@ function sleepAsync(ms) {
|
|
|
8418
8418
|
return new Promise((r) => setTimeout(r, ms));
|
|
8419
8419
|
}
|
|
8420
8420
|
|
|
8421
|
-
/**
|
|
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.
|
|
8431
|
-
*
|
|
8432
|
-
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
8433
|
-
* hot-reload in `bootstrap.ts`.
|
|
8434
|
-
*/
|
|
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
|
-
}
|
|
8483
|
-
|
|
8484
|
-
/**
|
|
8485
|
-
* Zod schema for jeeves-meta service configuration.
|
|
8486
|
-
*
|
|
8487
|
-
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
8488
|
-
*
|
|
8489
|
-
* @module schema/config
|
|
8490
|
-
*/
|
|
8491
|
-
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
8492
|
-
const metaConfigSchema = z.object({
|
|
8493
|
-
/** Watcher service base URL. */
|
|
8494
|
-
watcherUrl: z.url(),
|
|
8495
|
-
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8496
|
-
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8497
|
-
/** Optional API key for gateway authentication. */
|
|
8498
|
-
gatewayApiKey: z.string().optional(),
|
|
8499
|
-
/** Run architect every N cycles (per meta). */
|
|
8500
|
-
architectEvery: z.number().int().min(1).default(10),
|
|
8501
|
-
/** Exponent for depth weighting in staleness formula. */
|
|
8502
|
-
depthWeight: z.number().min(0).default(0.5),
|
|
8503
|
-
/** Maximum archive snapshots to retain per meta. */
|
|
8504
|
-
maxArchive: z.number().int().min(1).default(20),
|
|
8505
|
-
/** Maximum lines of context to include in subprocess prompts. */
|
|
8506
|
-
maxLines: z.number().int().min(50).default(500),
|
|
8507
|
-
/** Architect subprocess timeout in seconds. */
|
|
8508
|
-
architectTimeout: z.number().int().min(30).default(180),
|
|
8509
|
-
/** Builder subprocess timeout in seconds. */
|
|
8510
|
-
builderTimeout: z.number().int().min(60).default(360),
|
|
8511
|
-
/** Critic subprocess timeout in seconds. */
|
|
8512
|
-
criticTimeout: z.number().int().min(30).default(240),
|
|
8513
|
-
/** Thinking level for spawned synthesis sessions. */
|
|
8514
|
-
thinking: z.string().default('low'),
|
|
8515
|
-
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8516
|
-
defaultArchitect: z.string().optional(),
|
|
8517
|
-
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8518
|
-
defaultCritic: z.string().optional(),
|
|
8519
|
-
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8520
|
-
skipUnchanged: z.boolean().default(true),
|
|
8521
|
-
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8522
|
-
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8523
|
-
/** Watcher metadata properties applied to archive snapshots. */
|
|
8524
|
-
metaArchiveProperty: z
|
|
8525
|
-
.record(z.string(), z.unknown())
|
|
8526
|
-
.default({ _meta: 'archive' }),
|
|
8527
|
-
});
|
|
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
|
-
|
|
8567
|
-
/**
|
|
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.
|
|
8576
|
-
*
|
|
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.
|
|
8604
|
-
*
|
|
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
|
-
*/
|
|
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
|
-
/**
|
|
8616
|
-
* Load service config from a JSON file.
|
|
8617
|
-
*
|
|
8618
|
-
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
8619
|
-
* and substitutes environment-variable placeholders throughout.
|
|
8620
|
-
*
|
|
8621
|
-
* @param configPath - Path to config JSON file.
|
|
8622
|
-
* @returns Validated ServiceConfig.
|
|
8623
|
-
*/
|
|
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);
|
|
8635
|
-
}
|
|
8636
|
-
|
|
8637
8421
|
/**
|
|
8638
8422
|
* Compute summary statistics from an array of MetaEntry objects.
|
|
8639
8423
|
*
|
|
@@ -9290,6 +9074,263 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
9290
9074
|
return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
|
|
9291
9075
|
}
|
|
9292
9076
|
|
|
9077
|
+
/**
|
|
9078
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
9079
|
+
*
|
|
9080
|
+
* @module cache
|
|
9081
|
+
*/
|
|
9082
|
+
const TTL_MS = 60_000;
|
|
9083
|
+
/**
|
|
9084
|
+
* Caches listMetas results to avoid expensive repeated filesystem walks.
|
|
9085
|
+
* Supports concurrent refresh coalescing and manual invalidation.
|
|
9086
|
+
*/
|
|
9087
|
+
class MetaCache {
|
|
9088
|
+
result = null;
|
|
9089
|
+
updatedAt = 0;
|
|
9090
|
+
refreshPromise = null;
|
|
9091
|
+
/** Get cached result or refresh if stale. */
|
|
9092
|
+
async get(config, watcher) {
|
|
9093
|
+
if (this.result && Date.now() - this.updatedAt < TTL_MS) {
|
|
9094
|
+
return this.result;
|
|
9095
|
+
}
|
|
9096
|
+
return this.refresh(config, watcher);
|
|
9097
|
+
}
|
|
9098
|
+
/** Force-expire the cache so next get() triggers a refresh. */
|
|
9099
|
+
invalidate() {
|
|
9100
|
+
this.updatedAt = 0;
|
|
9101
|
+
}
|
|
9102
|
+
async refresh(config, watcher) {
|
|
9103
|
+
if (this.refreshPromise)
|
|
9104
|
+
return this.refreshPromise;
|
|
9105
|
+
this.refreshPromise = listMetas(config, watcher)
|
|
9106
|
+
.then((result) => {
|
|
9107
|
+
this.result = result;
|
|
9108
|
+
this.updatedAt = Date.now();
|
|
9109
|
+
return result;
|
|
9110
|
+
})
|
|
9111
|
+
.finally(() => {
|
|
9112
|
+
this.refreshPromise = null;
|
|
9113
|
+
});
|
|
9114
|
+
return this.refreshPromise;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
|
|
9118
|
+
/**
|
|
9119
|
+
* Shared live config hot-reload support.
|
|
9120
|
+
*
|
|
9121
|
+
* Used by both file-watch reloads in bootstrap and POST /config/apply
|
|
9122
|
+
* via the component descriptor's onConfigApply callback.
|
|
9123
|
+
*
|
|
9124
|
+
* @module configHotReload
|
|
9125
|
+
*/
|
|
9126
|
+
/**
|
|
9127
|
+
* Fields that require a service restart to take effect.
|
|
9128
|
+
*
|
|
9129
|
+
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
9130
|
+
* hot-reload in `bootstrap.ts`.
|
|
9131
|
+
*/
|
|
9132
|
+
const RESTART_REQUIRED_FIELDS = [
|
|
9133
|
+
'port',
|
|
9134
|
+
'watcherUrl',
|
|
9135
|
+
'gatewayUrl',
|
|
9136
|
+
'gatewayApiKey',
|
|
9137
|
+
'defaultArchitect',
|
|
9138
|
+
'defaultCritic',
|
|
9139
|
+
];
|
|
9140
|
+
let runtime = null;
|
|
9141
|
+
/** Register the active service runtime for config-apply hot reload. */
|
|
9142
|
+
function registerConfigHotReloadRuntime(nextRuntime) {
|
|
9143
|
+
runtime = nextRuntime;
|
|
9144
|
+
}
|
|
9145
|
+
/** Apply hot-reloadable config changes to the live shared config object. */
|
|
9146
|
+
function applyHotReloadedConfig(newConfig) {
|
|
9147
|
+
if (!runtime)
|
|
9148
|
+
return;
|
|
9149
|
+
const { config, logger, scheduler } = runtime;
|
|
9150
|
+
for (const field of RESTART_REQUIRED_FIELDS) {
|
|
9151
|
+
const oldVal = config[field];
|
|
9152
|
+
const nextVal = newConfig[field];
|
|
9153
|
+
if (oldVal !== nextVal) {
|
|
9154
|
+
logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
if (newConfig.schedule !== config.schedule) {
|
|
9158
|
+
scheduler?.updateSchedule(newConfig.schedule);
|
|
9159
|
+
config.schedule = newConfig.schedule;
|
|
9160
|
+
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
9161
|
+
}
|
|
9162
|
+
if (newConfig.logging.level !== config.logging.level) {
|
|
9163
|
+
logger.level = newConfig.logging.level;
|
|
9164
|
+
config.logging.level = newConfig.logging.level;
|
|
9165
|
+
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
9166
|
+
}
|
|
9167
|
+
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
9168
|
+
for (const key of Object.keys(newConfig)) {
|
|
9169
|
+
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
9170
|
+
continue;
|
|
9171
|
+
}
|
|
9172
|
+
const oldVal = config[key];
|
|
9173
|
+
const nextVal = newConfig[key];
|
|
9174
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
9175
|
+
config[key] = nextVal;
|
|
9176
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
}
|
|
9180
|
+
|
|
9181
|
+
/**
|
|
9182
|
+
* Zod schema for jeeves-meta service configuration.
|
|
9183
|
+
*
|
|
9184
|
+
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
9185
|
+
*
|
|
9186
|
+
* @module schema/config
|
|
9187
|
+
*/
|
|
9188
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
9189
|
+
const metaConfigSchema = z.object({
|
|
9190
|
+
/** Watcher service base URL. */
|
|
9191
|
+
watcherUrl: z.url(),
|
|
9192
|
+
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
9193
|
+
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
9194
|
+
/** Optional API key for gateway authentication. */
|
|
9195
|
+
gatewayApiKey: z.string().optional(),
|
|
9196
|
+
/** Run architect every N cycles (per meta). */
|
|
9197
|
+
architectEvery: z.number().int().min(1).default(10),
|
|
9198
|
+
/** Exponent for depth weighting in staleness formula. */
|
|
9199
|
+
depthWeight: z.number().min(0).default(0.5),
|
|
9200
|
+
/** Maximum archive snapshots to retain per meta. */
|
|
9201
|
+
maxArchive: z.number().int().min(1).default(20),
|
|
9202
|
+
/** Maximum lines of context to include in subprocess prompts. */
|
|
9203
|
+
maxLines: z.number().int().min(50).default(500),
|
|
9204
|
+
/** Architect subprocess timeout in seconds. */
|
|
9205
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
9206
|
+
/** Builder subprocess timeout in seconds. */
|
|
9207
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
9208
|
+
/** Critic subprocess timeout in seconds. */
|
|
9209
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
9210
|
+
/** Thinking level for spawned synthesis sessions. */
|
|
9211
|
+
thinking: z.string().default('low'),
|
|
9212
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
9213
|
+
defaultArchitect: z.string().optional(),
|
|
9214
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
9215
|
+
defaultCritic: z.string().optional(),
|
|
9216
|
+
/** Skip unchanged candidates, bump _generatedAt. */
|
|
9217
|
+
skipUnchanged: z.boolean().default(true),
|
|
9218
|
+
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
9219
|
+
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
9220
|
+
/** Watcher metadata properties applied to archive snapshots. */
|
|
9221
|
+
metaArchiveProperty: z
|
|
9222
|
+
.record(z.string(), z.unknown())
|
|
9223
|
+
.default({ _meta: 'archive' }),
|
|
9224
|
+
});
|
|
9225
|
+
/** Zod schema for logging configuration. */
|
|
9226
|
+
const loggingSchema = z.object({
|
|
9227
|
+
/** Log level. */
|
|
9228
|
+
level: z.string().default('info'),
|
|
9229
|
+
/** Optional file path for log output. */
|
|
9230
|
+
file: z.string().optional(),
|
|
9231
|
+
});
|
|
9232
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
9233
|
+
const autoSeedRuleSchema = z.object({
|
|
9234
|
+
/** Glob pattern matched against watcher walk results. */
|
|
9235
|
+
match: z.string(),
|
|
9236
|
+
/** Optional steering prompt for seeded metas. */
|
|
9237
|
+
steer: z.string().optional(),
|
|
9238
|
+
/** Optional cross-references for seeded metas. */
|
|
9239
|
+
crossRefs: z.array(z.string()).optional(),
|
|
9240
|
+
});
|
|
9241
|
+
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
9242
|
+
const serviceConfigSchema = metaConfigSchema.extend({
|
|
9243
|
+
/** HTTP port for the service (default: 1938). */
|
|
9244
|
+
port: z.number().int().min(1).max(65535).default(1938),
|
|
9245
|
+
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
9246
|
+
schedule: z.string().default('*/30 * * * *'),
|
|
9247
|
+
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
9248
|
+
reportChannel: z.string().optional(),
|
|
9249
|
+
/** Channel/user ID to send progress messages to. */
|
|
9250
|
+
reportTarget: z.string().optional(),
|
|
9251
|
+
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
9252
|
+
serverBaseUrl: z.string().optional(),
|
|
9253
|
+
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
9254
|
+
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
9255
|
+
/** Logging configuration. */
|
|
9256
|
+
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
9257
|
+
/**
|
|
9258
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
9259
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
9260
|
+
*/
|
|
9261
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
9262
|
+
});
|
|
9263
|
+
|
|
9264
|
+
/**
|
|
9265
|
+
* Load and resolve jeeves-meta service config.
|
|
9266
|
+
*
|
|
9267
|
+
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
9268
|
+
*
|
|
9269
|
+
* @module configLoader
|
|
9270
|
+
*/
|
|
9271
|
+
/**
|
|
9272
|
+
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
9273
|
+
*
|
|
9274
|
+
* @param value - Arbitrary JSON-compatible value.
|
|
9275
|
+
* @returns Value with env-var placeholders resolved.
|
|
9276
|
+
*/
|
|
9277
|
+
function substituteEnvVars(value) {
|
|
9278
|
+
if (typeof value === 'string') {
|
|
9279
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
9280
|
+
const envVal = process.env[name];
|
|
9281
|
+
if (envVal === undefined) {
|
|
9282
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
9283
|
+
}
|
|
9284
|
+
return envVal;
|
|
9285
|
+
});
|
|
9286
|
+
}
|
|
9287
|
+
if (Array.isArray(value)) {
|
|
9288
|
+
return value.map(substituteEnvVars);
|
|
9289
|
+
}
|
|
9290
|
+
if (value !== null && typeof value === 'object') {
|
|
9291
|
+
const result = {};
|
|
9292
|
+
for (const [key, val] of Object.entries(value)) {
|
|
9293
|
+
result[key] = substituteEnvVars(val);
|
|
9294
|
+
}
|
|
9295
|
+
return result;
|
|
9296
|
+
}
|
|
9297
|
+
return value;
|
|
9298
|
+
}
|
|
9299
|
+
/**
|
|
9300
|
+
* Resolve \@file: references in a config value.
|
|
9301
|
+
*
|
|
9302
|
+
* @param value - String value that may start with "\@file:".
|
|
9303
|
+
* @param baseDir - Base directory for resolving relative paths.
|
|
9304
|
+
* @returns The resolved string (file contents or original value).
|
|
9305
|
+
*/
|
|
9306
|
+
function resolveFileRef(value, baseDir) {
|
|
9307
|
+
if (!value.startsWith('@file:'))
|
|
9308
|
+
return value;
|
|
9309
|
+
const filePath = join(baseDir, value.slice(6));
|
|
9310
|
+
return readFileSync(filePath, 'utf8');
|
|
9311
|
+
}
|
|
9312
|
+
/**
|
|
9313
|
+
* Load service config from a JSON file.
|
|
9314
|
+
*
|
|
9315
|
+
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
9316
|
+
* and substitutes environment-variable placeholders throughout.
|
|
9317
|
+
*
|
|
9318
|
+
* @param configPath - Path to config JSON file.
|
|
9319
|
+
* @returns Validated ServiceConfig.
|
|
9320
|
+
*/
|
|
9321
|
+
function loadServiceConfig(configPath) {
|
|
9322
|
+
const rawText = readFileSync(configPath, 'utf8');
|
|
9323
|
+
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
9324
|
+
const baseDir = dirname(configPath);
|
|
9325
|
+
if (typeof raw['defaultArchitect'] === 'string') {
|
|
9326
|
+
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
9327
|
+
}
|
|
9328
|
+
if (typeof raw['defaultCritic'] === 'string') {
|
|
9329
|
+
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
9330
|
+
}
|
|
9331
|
+
return serviceConfigSchema.parse(raw);
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9293
9334
|
/**
|
|
9294
9335
|
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
9295
9336
|
*
|
|
@@ -9388,21 +9429,29 @@ class GatewayExecutor {
|
|
|
9388
9429
|
}
|
|
9389
9430
|
return data;
|
|
9390
9431
|
}
|
|
9391
|
-
/** Look up
|
|
9392
|
-
async
|
|
9432
|
+
/** Look up session metadata (tokens, completion status) via sessions_list. */
|
|
9433
|
+
async getSessionInfo(sessionKey) {
|
|
9393
9434
|
try {
|
|
9394
9435
|
const result = await this.invoke('sessions_list', {
|
|
9395
|
-
limit:
|
|
9436
|
+
limit: 200,
|
|
9396
9437
|
messageLimit: 0,
|
|
9397
9438
|
});
|
|
9398
9439
|
const sessions = (result.result?.details?.sessions ??
|
|
9399
9440
|
result.result?.sessions ??
|
|
9400
9441
|
[]);
|
|
9401
9442
|
const match = sessions.find((s) => s.key === sessionKey);
|
|
9402
|
-
|
|
9443
|
+
if (!match) {
|
|
9444
|
+
// Session absent from list — likely cleaned up after completion.
|
|
9445
|
+
// With limit=200 this is reliable; a false positive here only
|
|
9446
|
+
// means we read the output file slightly early (still correct
|
|
9447
|
+
// if the file exists).
|
|
9448
|
+
return { completed: true };
|
|
9449
|
+
}
|
|
9450
|
+
const done = match.status === 'completed' || match.status === 'done';
|
|
9451
|
+
return { tokens: match.totalTokens, completed: done };
|
|
9403
9452
|
}
|
|
9404
9453
|
catch {
|
|
9405
|
-
return
|
|
9454
|
+
return { completed: false };
|
|
9406
9455
|
}
|
|
9407
9456
|
}
|
|
9408
9457
|
/** Whether this executor has been aborted by the operator. */
|
|
@@ -9468,48 +9517,53 @@ class GatewayExecutor {
|
|
|
9468
9517
|
historyResult.result?.messages ??
|
|
9469
9518
|
[];
|
|
9470
9519
|
const msgArray = messages;
|
|
9520
|
+
// Check 1: terminal stop reason in history
|
|
9521
|
+
let historyDone = false;
|
|
9471
9522
|
if (msgArray.length > 0) {
|
|
9472
9523
|
const lastMsg = msgArray[msgArray.length - 1];
|
|
9473
|
-
// Complete when last message is assistant with a terminal stop reason
|
|
9474
9524
|
if (lastMsg.role === 'assistant' &&
|
|
9475
9525
|
lastMsg.stopReason &&
|
|
9476
9526
|
lastMsg.stopReason !== 'toolUse' &&
|
|
9477
9527
|
lastMsg.stopReason !== 'error') {
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9528
|
+
historyDone = true;
|
|
9529
|
+
}
|
|
9530
|
+
}
|
|
9531
|
+
// Check 2: session completion status via sessions_list
|
|
9532
|
+
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
9533
|
+
if (historyDone || sessionInfo.completed) {
|
|
9534
|
+
const tokens = sessionInfo.tokens;
|
|
9535
|
+
// Read output from file (sub-agent wrote it via Write tool)
|
|
9536
|
+
if (existsSync(outputPath)) {
|
|
9537
|
+
try {
|
|
9538
|
+
const output = readFileSync(outputPath, 'utf8');
|
|
9539
|
+
return { output, tokens };
|
|
9540
|
+
}
|
|
9541
|
+
finally {
|
|
9482
9542
|
try {
|
|
9483
|
-
|
|
9484
|
-
return { output, tokens };
|
|
9543
|
+
unlinkSync(outputPath);
|
|
9485
9544
|
}
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
unlinkSync(outputPath);
|
|
9489
|
-
}
|
|
9490
|
-
catch {
|
|
9491
|
-
/* cleanup best-effort */
|
|
9492
|
-
}
|
|
9545
|
+
catch {
|
|
9546
|
+
/* cleanup best-effort */
|
|
9493
9547
|
}
|
|
9494
9548
|
}
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9549
|
+
}
|
|
9550
|
+
// Fallback: extract from message content if file wasn't written
|
|
9551
|
+
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9552
|
+
const msg = msgArray[i];
|
|
9553
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
9554
|
+
const text = typeof msg.content === 'string'
|
|
9555
|
+
? msg.content
|
|
9556
|
+
: Array.isArray(msg.content)
|
|
9500
9557
|
? msg.content
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
if (text)
|
|
9508
|
-
return { output: text, tokens };
|
|
9509
|
-
}
|
|
9558
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9559
|
+
.map((b) => b.text)
|
|
9560
|
+
.join('\n')
|
|
9561
|
+
: '';
|
|
9562
|
+
if (text)
|
|
9563
|
+
return { output: text, tokens };
|
|
9510
9564
|
}
|
|
9511
|
-
return { output: '', tokens };
|
|
9512
9565
|
}
|
|
9566
|
+
return { output: '', tokens };
|
|
9513
9567
|
}
|
|
9514
9568
|
}
|
|
9515
9569
|
catch {
|
|
@@ -9872,6 +9926,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
|
|
|
9872
9926
|
*
|
|
9873
9927
|
* @module orchestrator/buildTask
|
|
9874
9928
|
*/
|
|
9929
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
9875
9930
|
/** Build the template context from synthesis inputs. */
|
|
9876
9931
|
function buildTemplateContext(ctx, meta, config) {
|
|
9877
9932
|
return {
|
|
@@ -10026,134 +10081,6 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
10026
10081
|
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10027
10082
|
}
|
|
10028
10083
|
|
|
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
10084
|
/**
|
|
10158
10085
|
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
10159
10086
|
*
|
|
@@ -10215,222 +10142,6 @@ async function buildMinimalNode(metaPath, watcher) {
|
|
|
10215
10142
|
return node;
|
|
10216
10143
|
}
|
|
10217
10144
|
|
|
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
10145
|
/**
|
|
10435
10146
|
* Pure phase-state transition functions.
|
|
10436
10147
|
*
|
|
@@ -10480,7 +10191,42 @@ function enforceInvariant(state) {
|
|
|
10480
10191
|
// running in non-first position would be a bug, but don't mask it
|
|
10481
10192
|
}
|
|
10482
10193
|
}
|
|
10483
|
-
return result;
|
|
10194
|
+
return result;
|
|
10195
|
+
}
|
|
10196
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
10197
|
+
/**
|
|
10198
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
10199
|
+
* Triggers: _structureHash change, _steer change, _architect change,
|
|
10200
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
10201
|
+
*/
|
|
10202
|
+
function invalidateArchitect(state) {
|
|
10203
|
+
return enforceInvariant({
|
|
10204
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
10205
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
10206
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10207
|
+
});
|
|
10208
|
+
}
|
|
10209
|
+
/**
|
|
10210
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
10211
|
+
* builder → pending; critic → stale.
|
|
10212
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
10213
|
+
*/
|
|
10214
|
+
function invalidateBuilder(state) {
|
|
10215
|
+
if (state.architect !== 'fresh') {
|
|
10216
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
10217
|
+
return enforceInvariant({
|
|
10218
|
+
...state,
|
|
10219
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
10220
|
+
? 'stale'
|
|
10221
|
+
: state.builder,
|
|
10222
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10223
|
+
});
|
|
10224
|
+
}
|
|
10225
|
+
return enforceInvariant({
|
|
10226
|
+
...state,
|
|
10227
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
10228
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
10229
|
+
});
|
|
10484
10230
|
}
|
|
10485
10231
|
// ── Phase success transitions ──────────────────────────────────────────
|
|
10486
10232
|
/**
|
|
@@ -10650,7 +10396,9 @@ function derivePhaseState(meta, inputs) {
|
|
|
10650
10396
|
}
|
|
10651
10397
|
// Check architect invalidation (when inputs are provided)
|
|
10652
10398
|
if (inputs) {
|
|
10653
|
-
|
|
10399
|
+
// Progressive metas: structure changes invalidate builder, not architect
|
|
10400
|
+
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
10401
|
+
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10654
10402
|
inputs.steerChanged ||
|
|
10655
10403
|
inputs.architectChanged ||
|
|
10656
10404
|
inputs.crossRefsChanged ||
|
|
@@ -10662,6 +10410,14 @@ function derivePhaseState(meta, inputs) {
|
|
|
10662
10410
|
critic: 'stale',
|
|
10663
10411
|
};
|
|
10664
10412
|
}
|
|
10413
|
+
// Progressive meta with structure change: builder-only invalidation
|
|
10414
|
+
if (inputs.structureChanged && meta._state !== undefined) {
|
|
10415
|
+
return {
|
|
10416
|
+
architect: 'fresh',
|
|
10417
|
+
builder: 'pending',
|
|
10418
|
+
critic: 'stale',
|
|
10419
|
+
};
|
|
10420
|
+
}
|
|
10665
10421
|
}
|
|
10666
10422
|
// Has _builder but no _content: builder is pending
|
|
10667
10423
|
if (meta._builder && !meta._content) {
|
|
@@ -10683,6 +10439,154 @@ function derivePhaseState(meta, inputs) {
|
|
|
10683
10439
|
return freshPhaseState();
|
|
10684
10440
|
}
|
|
10685
10441
|
|
|
10442
|
+
/**
|
|
10443
|
+
* Compute a structure hash from a sorted file listing.
|
|
10444
|
+
*
|
|
10445
|
+
* Used to detect when directory structure changes, triggering
|
|
10446
|
+
* an architect re-run.
|
|
10447
|
+
*
|
|
10448
|
+
* @module structureHash
|
|
10449
|
+
*/
|
|
10450
|
+
/**
|
|
10451
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
10452
|
+
*
|
|
10453
|
+
* @param filePaths - Array of file paths in scope.
|
|
10454
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10455
|
+
*/
|
|
10456
|
+
function computeStructureHash(filePaths) {
|
|
10457
|
+
const sorted = [...filePaths].sort();
|
|
10458
|
+
const content = sorted.join('\n');
|
|
10459
|
+
return createHash('sha256').update(content).digest('hex');
|
|
10460
|
+
}
|
|
10461
|
+
|
|
10462
|
+
/**
|
|
10463
|
+
* Per-tick invalidation pass.
|
|
10464
|
+
*
|
|
10465
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
10466
|
+
* then applies the cascade to update _phaseState.
|
|
10467
|
+
*
|
|
10468
|
+
* @module phaseState/invalidate
|
|
10469
|
+
*/
|
|
10470
|
+
/**
|
|
10471
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10472
|
+
*
|
|
10473
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
10474
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
10475
|
+
* @param config - MetaConfig for architectEvery.
|
|
10476
|
+
* @param node - MetaNode for archive access.
|
|
10477
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
10478
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
10479
|
+
* @returns Updated phase state and invalidation details.
|
|
10480
|
+
*/
|
|
10481
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
10482
|
+
let phaseState = meta._phaseState ?? {
|
|
10483
|
+
architect: 'fresh',
|
|
10484
|
+
builder: 'fresh',
|
|
10485
|
+
critic: 'fresh',
|
|
10486
|
+
};
|
|
10487
|
+
// ── Architect-level inputs ──
|
|
10488
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
10489
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
10490
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10491
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10492
|
+
// _architect change: compare current vs. archive
|
|
10493
|
+
const architectChanged = latestArchive
|
|
10494
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
10495
|
+
: Boolean(meta._architect);
|
|
10496
|
+
// _crossRefs declaration change
|
|
10497
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10498
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
10499
|
+
.slice()
|
|
10500
|
+
.sort()
|
|
10501
|
+
.join(',');
|
|
10502
|
+
const crossRefsDeclChanged = latestArchive
|
|
10503
|
+
? currentRefs !== archiveRefs
|
|
10504
|
+
: currentRefs.length > 0;
|
|
10505
|
+
const architectInvalidators = [];
|
|
10506
|
+
if (structureChanged) {
|
|
10507
|
+
if (meta._state !== undefined) {
|
|
10508
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
10509
|
+
phaseState = invalidateBuilder(phaseState);
|
|
10510
|
+
}
|
|
10511
|
+
else {
|
|
10512
|
+
architectInvalidators.push('structureHash');
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
if (steerChanged)
|
|
10516
|
+
architectInvalidators.push('steer');
|
|
10517
|
+
if (architectChanged)
|
|
10518
|
+
architectInvalidators.push('_architect');
|
|
10519
|
+
if (crossRefsDeclChanged)
|
|
10520
|
+
architectInvalidators.push('_crossRefs');
|
|
10521
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
10522
|
+
architectInvalidators.push('architectEvery');
|
|
10523
|
+
}
|
|
10524
|
+
// First-run check: no _builder means architect must run
|
|
10525
|
+
const firstRun = !meta._builder;
|
|
10526
|
+
if (architectInvalidators.length > 0 || firstRun) {
|
|
10527
|
+
phaseState = invalidateArchitect(phaseState);
|
|
10528
|
+
}
|
|
10529
|
+
// ── Builder-level inputs ──
|
|
10530
|
+
// Scope file mtime check — if any file newer than _generatedAt
|
|
10531
|
+
const scopeMtimeMax = null;
|
|
10532
|
+
// Note: actual mtime check is done by the caller or via isStale;
|
|
10533
|
+
// here we just detect cross-ref content changes for the cascade.
|
|
10534
|
+
// Cross-ref _content change (builder-invalidating)
|
|
10535
|
+
let crossRefContentChanged = false;
|
|
10536
|
+
return {
|
|
10537
|
+
phaseState,
|
|
10538
|
+
architectInvalidators,
|
|
10539
|
+
stalenessInputs: {
|
|
10540
|
+
structureHash,
|
|
10541
|
+
steerChanged,
|
|
10542
|
+
architectChanged,
|
|
10543
|
+
crossRefsDeclChanged,
|
|
10544
|
+
scopeMtimeMax,
|
|
10545
|
+
crossRefContentChanged,
|
|
10546
|
+
},
|
|
10547
|
+
structureHash,
|
|
10548
|
+
steerChanged,
|
|
10549
|
+
};
|
|
10550
|
+
}
|
|
10551
|
+
|
|
10552
|
+
/**
|
|
10553
|
+
* Weighted staleness formula for candidate selection.
|
|
10554
|
+
*
|
|
10555
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10556
|
+
*
|
|
10557
|
+
* @module scheduling/weightedFormula
|
|
10558
|
+
*/
|
|
10559
|
+
/**
|
|
10560
|
+
* Compute effective staleness for a set of candidates.
|
|
10561
|
+
*
|
|
10562
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10563
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10564
|
+
*
|
|
10565
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10566
|
+
* metas to tune how much their tree position affects scheduling.
|
|
10567
|
+
*
|
|
10568
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10569
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10570
|
+
* @returns Same array with effectiveStaleness computed.
|
|
10571
|
+
*/
|
|
10572
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10573
|
+
if (candidates.length === 0)
|
|
10574
|
+
return [];
|
|
10575
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
10576
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10577
|
+
// Normalize: shift so minimum becomes 0
|
|
10578
|
+
const minDepth = Math.min(...depths);
|
|
10579
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10580
|
+
return candidates.map((c, i) => {
|
|
10581
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
10582
|
+
return {
|
|
10583
|
+
...c,
|
|
10584
|
+
effectiveStaleness: c.actualStaleness *
|
|
10585
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10586
|
+
};
|
|
10587
|
+
});
|
|
10588
|
+
}
|
|
10589
|
+
|
|
10686
10590
|
/**
|
|
10687
10591
|
* Corpus-wide phase scheduler.
|
|
10688
10592
|
*
|
|
@@ -10695,18 +10599,30 @@ function derivePhaseState(meta, inputs) {
|
|
|
10695
10599
|
/**
|
|
10696
10600
|
* Build phase candidates from listMetas entries.
|
|
10697
10601
|
*
|
|
10698
|
-
* Derives phase state
|
|
10602
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
10603
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
10699
10604
|
* Used by orchestratePhase, queue route, and status route.
|
|
10700
10605
|
*/
|
|
10701
|
-
function buildPhaseCandidates(entries) {
|
|
10702
|
-
return entries.map((entry) =>
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10709
|
-
|
|
10606
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
10607
|
+
return entries.map((entry) => {
|
|
10608
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
10609
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
10610
|
+
if (entry.meta._phaseState) {
|
|
10611
|
+
const needsArchitect = !entry.meta._builder ||
|
|
10612
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
10613
|
+
if (needsArchitect && ps.architect === 'fresh') {
|
|
10614
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
return {
|
|
10618
|
+
node: entry.node,
|
|
10619
|
+
meta: entry.meta,
|
|
10620
|
+
phaseState: ps,
|
|
10621
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10622
|
+
locked: entry.locked,
|
|
10623
|
+
disabled: entry.disabled,
|
|
10624
|
+
};
|
|
10625
|
+
});
|
|
10710
10626
|
}
|
|
10711
10627
|
/**
|
|
10712
10628
|
* Rank all eligible phase candidates by priority.
|
|
@@ -10769,6 +10685,124 @@ function selectPhaseCandidate(metas, depthWeight) {
|
|
|
10769
10685
|
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10770
10686
|
}
|
|
10771
10687
|
|
|
10688
|
+
/**
|
|
10689
|
+
* Shared error utilities.
|
|
10690
|
+
*
|
|
10691
|
+
* @module errors
|
|
10692
|
+
*/
|
|
10693
|
+
/**
|
|
10694
|
+
* Wrap an unknown caught value into a MetaError.
|
|
10695
|
+
*
|
|
10696
|
+
* @param step - Which synthesis step failed.
|
|
10697
|
+
* @param err - The caught error value.
|
|
10698
|
+
* @param code - Error classification code.
|
|
10699
|
+
* @returns A structured MetaError.
|
|
10700
|
+
*/
|
|
10701
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
10702
|
+
return {
|
|
10703
|
+
step,
|
|
10704
|
+
code,
|
|
10705
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10706
|
+
};
|
|
10707
|
+
}
|
|
10708
|
+
|
|
10709
|
+
/**
|
|
10710
|
+
* Parse subprocess outputs for each synthesis step.
|
|
10711
|
+
*
|
|
10712
|
+
* - Architect: returns text \> _builder
|
|
10713
|
+
* - Builder: returns JSON \> _content + structured fields
|
|
10714
|
+
* - Critic: returns text \> _feedback
|
|
10715
|
+
*
|
|
10716
|
+
* @module orchestrator/parseOutput
|
|
10717
|
+
*/
|
|
10718
|
+
/**
|
|
10719
|
+
* Parse architect output. The architect returns a task brief as text.
|
|
10720
|
+
*
|
|
10721
|
+
* @param output - Raw subprocess output.
|
|
10722
|
+
* @returns The task brief string.
|
|
10723
|
+
*/
|
|
10724
|
+
function parseArchitectOutput(output) {
|
|
10725
|
+
return output.trim();
|
|
10726
|
+
}
|
|
10727
|
+
/**
|
|
10728
|
+
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10729
|
+
*
|
|
10730
|
+
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10731
|
+
*
|
|
10732
|
+
* @param output - Raw subprocess output.
|
|
10733
|
+
* @returns Parsed builder output with content and structured fields.
|
|
10734
|
+
*/
|
|
10735
|
+
function parseBuilderOutput(output) {
|
|
10736
|
+
const trimmed = output.trim();
|
|
10737
|
+
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10738
|
+
const direct = tryParseJson(trimmed);
|
|
10739
|
+
if (direct)
|
|
10740
|
+
return direct;
|
|
10741
|
+
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10742
|
+
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10743
|
+
const fenceMatches = [];
|
|
10744
|
+
let match;
|
|
10745
|
+
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10746
|
+
fenceMatches.push(match[1].trim());
|
|
10747
|
+
}
|
|
10748
|
+
// Try last fence first (most likely to be the actual output)
|
|
10749
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10750
|
+
const result = tryParseJson(fenceMatches[i]);
|
|
10751
|
+
if (result)
|
|
10752
|
+
return result;
|
|
10753
|
+
}
|
|
10754
|
+
// Strategy 3: Find outermost { ... } braces
|
|
10755
|
+
const firstBrace = trimmed.indexOf('{');
|
|
10756
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
10757
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
10758
|
+
const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
|
|
10759
|
+
if (result)
|
|
10760
|
+
return result;
|
|
10761
|
+
}
|
|
10762
|
+
// Fallback: treat entire output as content
|
|
10763
|
+
return { content: trimmed, fields: {} };
|
|
10764
|
+
}
|
|
10765
|
+
/** Try to parse a string as JSON and extract builder output fields. */
|
|
10766
|
+
function tryParseJson(str) {
|
|
10767
|
+
try {
|
|
10768
|
+
const raw = JSON.parse(str);
|
|
10769
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
10770
|
+
return null;
|
|
10771
|
+
}
|
|
10772
|
+
const parsed = raw;
|
|
10773
|
+
// Extract _content
|
|
10774
|
+
const content = typeof parsed['_content'] === 'string'
|
|
10775
|
+
? parsed['_content']
|
|
10776
|
+
: typeof parsed['content'] === 'string'
|
|
10777
|
+
? parsed['content']
|
|
10778
|
+
: null;
|
|
10779
|
+
if (content === null)
|
|
10780
|
+
return null;
|
|
10781
|
+
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10782
|
+
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10783
|
+
// Extract non-underscore fields
|
|
10784
|
+
const fields = {};
|
|
10785
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
10786
|
+
if (!key.startsWith('_') && key !== 'content') {
|
|
10787
|
+
fields[key] = value;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10791
|
+
}
|
|
10792
|
+
catch {
|
|
10793
|
+
return null;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
/**
|
|
10797
|
+
* Parse critic output. The critic returns evaluation text.
|
|
10798
|
+
*
|
|
10799
|
+
* @param output - Raw subprocess output.
|
|
10800
|
+
* @returns The feedback string.
|
|
10801
|
+
*/
|
|
10802
|
+
function parseCriticOutput(output) {
|
|
10803
|
+
return output.trim();
|
|
10804
|
+
}
|
|
10805
|
+
|
|
10772
10806
|
/**
|
|
10773
10807
|
* Per-phase executors for the phase-state machine.
|
|
10774
10808
|
*
|
|
@@ -11027,7 +11061,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
11027
11061
|
if (metaResult.entries.length === 0)
|
|
11028
11062
|
return { executed: false };
|
|
11029
11063
|
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11030
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
11064
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
11031
11065
|
// Select best phase candidate
|
|
11032
11066
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11033
11067
|
if (!winner) {
|
|
@@ -11702,46 +11736,6 @@ function buildMetaRules(config) {
|
|
|
11702
11736
|
},
|
|
11703
11737
|
renderAs: 'md',
|
|
11704
11738
|
},
|
|
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
11739
|
];
|
|
11746
11740
|
}
|
|
11747
11741
|
/**
|
|
@@ -11985,13 +11979,15 @@ class Scheduler {
|
|
|
11985
11979
|
queue;
|
|
11986
11980
|
logger;
|
|
11987
11981
|
watcher;
|
|
11982
|
+
cache;
|
|
11988
11983
|
registrar = null;
|
|
11989
11984
|
currentExpression;
|
|
11990
|
-
constructor(config, queue, logger, watcher) {
|
|
11985
|
+
constructor(config, queue, logger, watcher, cache) {
|
|
11991
11986
|
this.config = config;
|
|
11992
11987
|
this.queue = queue;
|
|
11993
11988
|
this.logger = logger;
|
|
11994
11989
|
this.watcher = watcher;
|
|
11990
|
+
this.cache = cache;
|
|
11995
11991
|
this.currentExpression = config.schedule;
|
|
11996
11992
|
}
|
|
11997
11993
|
/** Set the rule registrar for watcher restart detection. */
|
|
@@ -12108,8 +12104,8 @@ class Scheduler {
|
|
|
12108
12104
|
*/
|
|
12109
12105
|
async discoverNextPhase() {
|
|
12110
12106
|
try {
|
|
12111
|
-
const result = await
|
|
12112
|
-
const candidates = buildPhaseCandidates(result.entries);
|
|
12107
|
+
const result = await this.cache.get(this.config, this.watcher);
|
|
12108
|
+
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12113
12109
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12114
12110
|
if (!winner)
|
|
12115
12111
|
return null;
|
|
@@ -12534,11 +12530,11 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12534
12530
|
*/
|
|
12535
12531
|
function registerPreviewRoute(app, deps) {
|
|
12536
12532
|
app.get('/preview', async (request, reply) => {
|
|
12537
|
-
const { config, watcher } = deps;
|
|
12533
|
+
const { config, watcher, cache } = deps;
|
|
12538
12534
|
const query = request.query;
|
|
12539
12535
|
let result;
|
|
12540
12536
|
try {
|
|
12541
|
-
result = await
|
|
12537
|
+
result = await cache.get(config, watcher);
|
|
12542
12538
|
}
|
|
12543
12539
|
catch {
|
|
12544
12540
|
return reply.status(503).send({
|
|
@@ -12558,40 +12554,24 @@ function registerPreviewRoute(app, deps) {
|
|
|
12558
12554
|
}
|
|
12559
12555
|
}
|
|
12560
12556
|
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) {
|
|
12557
|
+
// Select best phase candidate
|
|
12558
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12559
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12560
|
+
if (!winner) {
|
|
12571
12561
|
return { message: 'No stale metas found. Nothing to synthesize.' };
|
|
12572
12562
|
}
|
|
12573
|
-
targetNode = findNode(result.tree,
|
|
12563
|
+
targetNode = findNode(result.tree, winner.node.metaPath);
|
|
12574
12564
|
}
|
|
12575
12565
|
const meta = await readMetaJson(targetNode.metaPath);
|
|
12576
12566
|
// Scope files
|
|
12577
12567
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12578
|
-
|
|
12568
|
+
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12569
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12570
|
+
const { architectInvalidators, stalenessInputs } = invalidation;
|
|
12571
|
+
const { structureHash } = invalidation;
|
|
12579
12572
|
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;
|
|
12573
|
+
const { steerChanged } = invalidation;
|
|
12574
|
+
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12595
12575
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12596
12576
|
// Delta files
|
|
12597
12577
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -12616,30 +12596,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12616
12596
|
});
|
|
12617
12597
|
const owedPhase = getOwedPhase(phaseState);
|
|
12618
12598
|
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
12599
|
return {
|
|
12644
12600
|
path: targetNode.metaPath,
|
|
12645
12601
|
staleness: {
|
|
@@ -12713,8 +12669,8 @@ function registerQueueRoutes(app, deps) {
|
|
|
12713
12669
|
// ranked by scheduler priority (computed on read, not persisted)
|
|
12714
12670
|
let automatic = [];
|
|
12715
12671
|
try {
|
|
12716
|
-
const metaResult = await
|
|
12717
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12672
|
+
const metaResult = await deps.cache.get(deps.config, deps.watcher);
|
|
12673
|
+
const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
|
|
12718
12674
|
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12719
12675
|
automatic = ranked.map((c) => ({
|
|
12720
12676
|
path: c.node.metaPath,
|
|
@@ -12937,7 +12893,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12937
12893
|
name: SERVICE_NAME,
|
|
12938
12894
|
version: SERVICE_VERSION,
|
|
12939
12895
|
getHealth: async () => {
|
|
12940
|
-
const { config, queue, scheduler, stats, watcher } = deps;
|
|
12896
|
+
const { config, queue, scheduler, stats, watcher, cache } = deps;
|
|
12941
12897
|
// On-demand dependency checks
|
|
12942
12898
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
12943
12899
|
checkWatcher(config.watcherUrl),
|
|
@@ -12951,7 +12907,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12951
12907
|
};
|
|
12952
12908
|
let nextPhase = null;
|
|
12953
12909
|
try {
|
|
12954
|
-
const metaResult = await
|
|
12910
|
+
const metaResult = await cache.get(config, watcher);
|
|
12955
12911
|
// Count raw phase states (before retry) for display
|
|
12956
12912
|
for (const entry of metaResult.entries) {
|
|
12957
12913
|
const ps = derivePhaseState(entry.meta);
|
|
@@ -12960,7 +12916,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12960
12916
|
}
|
|
12961
12917
|
}
|
|
12962
12918
|
// Build candidates (with auto-retry) for scheduling
|
|
12963
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12919
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
12964
12920
|
// Find next phase candidate
|
|
12965
12921
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12966
12922
|
if (winner) {
|
|
@@ -13023,7 +12979,7 @@ const synthesizeBodySchema = z.object({
|
|
|
13023
12979
|
function registerSynthesizeRoute(app, deps) {
|
|
13024
12980
|
app.post('/synthesize', async (request, reply) => {
|
|
13025
12981
|
const body = synthesizeBodySchema.parse(request.body);
|
|
13026
|
-
const { config, watcher, queue } = deps;
|
|
12982
|
+
const { config, watcher, queue, cache } = deps;
|
|
13027
12983
|
if (body.path) {
|
|
13028
12984
|
// Path-targeted trigger: create override entry
|
|
13029
12985
|
const targetPath = resolveMetaDir(body.path);
|
|
@@ -13060,7 +13016,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13060
13016
|
// Corpus-wide trigger: discover stalest candidate
|
|
13061
13017
|
let result;
|
|
13062
13018
|
try {
|
|
13063
|
-
result = await
|
|
13019
|
+
result = await cache.get(config, watcher);
|
|
13064
13020
|
}
|
|
13065
13021
|
catch {
|
|
13066
13022
|
return reply.status(503).send({
|
|
@@ -13068,20 +13024,15 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13068
13024
|
message: 'Watcher unreachable — cannot discover candidates',
|
|
13069
13025
|
});
|
|
13070
13026
|
}
|
|
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) {
|
|
13027
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
13028
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
13029
|
+
if (!winner) {
|
|
13080
13030
|
return reply.code(200).send({
|
|
13081
13031
|
status: 'skipped',
|
|
13082
13032
|
message: 'No stale metas found. Nothing to synthesize.',
|
|
13083
13033
|
});
|
|
13084
13034
|
}
|
|
13035
|
+
const stalest = winner.node.metaPath;
|
|
13085
13036
|
const enqueueResult = queue.enqueue(stalest);
|
|
13086
13037
|
return reply.code(202).send({
|
|
13087
13038
|
status: 'accepted',
|
|
@@ -13182,6 +13133,18 @@ function createServer(options) {
|
|
|
13182
13133
|
// Fastify 5 requires `loggerInstance` for external pino loggers
|
|
13183
13134
|
const app = Fastify({
|
|
13184
13135
|
loggerInstance: options.logger,
|
|
13136
|
+
requestTimeout: 30_000,
|
|
13137
|
+
});
|
|
13138
|
+
// Readiness gate: return 503 while service is initializing
|
|
13139
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
13140
|
+
if (options.deps.ready)
|
|
13141
|
+
return;
|
|
13142
|
+
const url = request.url;
|
|
13143
|
+
if (url === '/config' || url.startsWith('/config/apply'))
|
|
13144
|
+
return;
|
|
13145
|
+
return reply
|
|
13146
|
+
.status(503)
|
|
13147
|
+
.send({ status: 'starting', message: 'Service initializing' });
|
|
13185
13148
|
});
|
|
13186
13149
|
registerRoutes(app, options.deps);
|
|
13187
13150
|
return app;
|
|
@@ -13357,8 +13320,9 @@ async function startService(config, configPath) {
|
|
|
13357
13320
|
lastCycleAt: null,
|
|
13358
13321
|
};
|
|
13359
13322
|
const queue = new SynthesisQueue(logger);
|
|
13323
|
+
const cache = new MetaCache();
|
|
13360
13324
|
// Scheduler (needs watcher for discovery)
|
|
13361
|
-
const scheduler = new Scheduler(config, queue, logger, watcher);
|
|
13325
|
+
const scheduler = new Scheduler(config, queue, logger, watcher, cache);
|
|
13362
13326
|
const routeDeps = {
|
|
13363
13327
|
config,
|
|
13364
13328
|
logger,
|
|
@@ -13366,6 +13330,8 @@ async function startService(config, configPath) {
|
|
|
13366
13330
|
watcher,
|
|
13367
13331
|
scheduler,
|
|
13368
13332
|
stats,
|
|
13333
|
+
cache,
|
|
13334
|
+
ready: false,
|
|
13369
13335
|
executor,
|
|
13370
13336
|
configPath,
|
|
13371
13337
|
};
|
|
@@ -13416,6 +13382,9 @@ async function startService(config, configPath) {
|
|
|
13416
13382
|
}
|
|
13417
13383
|
await progress.report(evt);
|
|
13418
13384
|
}, logger);
|
|
13385
|
+
// Invalidate cache only when a phase was actually executed
|
|
13386
|
+
if (result.executed)
|
|
13387
|
+
cache.invalidate();
|
|
13419
13388
|
const durationMs = Date.now() - startMs;
|
|
13420
13389
|
if (!result.executed) {
|
|
13421
13390
|
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
@@ -13479,9 +13448,13 @@ async function startService(config, configPath) {
|
|
|
13479
13448
|
scheduler.setRegistrar(registrar);
|
|
13480
13449
|
routeDeps.registrar = registrar;
|
|
13481
13450
|
void registrar.register().then(() => {
|
|
13451
|
+
routeDeps.ready = true;
|
|
13482
13452
|
if (registrar.isRegistered) {
|
|
13483
13453
|
void verifyRuleApplication(watcher, logger);
|
|
13484
13454
|
}
|
|
13455
|
+
}, () => {
|
|
13456
|
+
// Registration failed after max retries — mark ready anyway
|
|
13457
|
+
routeDeps.ready = true;
|
|
13485
13458
|
});
|
|
13486
13459
|
// Periodic watcher health check (independent of scheduler)
|
|
13487
13460
|
const healthCheck = new WatcherHealthCheck({
|