@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
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync, copyFileSync,
|
|
1
|
+
import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, statSync, mkdirSync, copyFileSync, watchFile } from 'node:fs';
|
|
2
2
|
import path, { join, dirname, resolve, basename, relative, posix } from 'node:path';
|
|
3
3
|
import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -8116,381 +8116,120 @@ function registerCustomCliCommands(program) {
|
|
|
8116
8116
|
}
|
|
8117
8117
|
|
|
8118
8118
|
/**
|
|
8119
|
-
*
|
|
8119
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
8120
8120
|
*
|
|
8121
|
-
*
|
|
8122
|
-
* via the component descriptor's onConfigApply callback.
|
|
8121
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8123
8122
|
*
|
|
8124
|
-
* @module
|
|
8123
|
+
* @module discovery/computeSummary
|
|
8125
8124
|
*/
|
|
8126
8125
|
/**
|
|
8127
|
-
*
|
|
8126
|
+
* Compute summary statistics from a list of meta entries.
|
|
8128
8127
|
*
|
|
8129
|
-
*
|
|
8130
|
-
*
|
|
8128
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
8129
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8130
|
+
* @returns Aggregated summary statistics.
|
|
8131
8131
|
*/
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
let
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
config.logging.level = newConfig.logging.level;
|
|
8165
|
-
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
8166
|
-
}
|
|
8167
|
-
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
8168
|
-
for (const key of Object.keys(newConfig)) {
|
|
8169
|
-
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
8170
|
-
continue;
|
|
8132
|
+
function computeSummary(entries, depthWeight) {
|
|
8133
|
+
let staleCount = 0;
|
|
8134
|
+
let errorCount = 0;
|
|
8135
|
+
let lockedCount = 0;
|
|
8136
|
+
let disabledCount = 0;
|
|
8137
|
+
let neverSynthesizedCount = 0;
|
|
8138
|
+
let totalArchitectTokens = 0;
|
|
8139
|
+
let totalBuilderTokens = 0;
|
|
8140
|
+
let totalCriticTokens = 0;
|
|
8141
|
+
let stalestPath = null;
|
|
8142
|
+
let stalestEffective = -1;
|
|
8143
|
+
let lastSynthesizedPath = null;
|
|
8144
|
+
let lastSynthesizedAt = null;
|
|
8145
|
+
for (const e of entries) {
|
|
8146
|
+
if (e.stalenessSeconds > 0)
|
|
8147
|
+
staleCount++;
|
|
8148
|
+
if (e.hasError)
|
|
8149
|
+
errorCount++;
|
|
8150
|
+
if (e.locked)
|
|
8151
|
+
lockedCount++;
|
|
8152
|
+
if (e.disabled)
|
|
8153
|
+
disabledCount++;
|
|
8154
|
+
if (e.lastSynthesized === null)
|
|
8155
|
+
neverSynthesizedCount++;
|
|
8156
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8157
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8158
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
8159
|
+
// Track last synthesized
|
|
8160
|
+
if (e.lastSynthesized &&
|
|
8161
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8162
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
8163
|
+
lastSynthesizedPath = e.path;
|
|
8171
8164
|
}
|
|
8172
|
-
|
|
8173
|
-
const
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8165
|
+
// Track stalest (effective staleness for scheduling)
|
|
8166
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8167
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8168
|
+
if (effectiveStaleness > stalestEffective) {
|
|
8169
|
+
stalestEffective = effectiveStaleness;
|
|
8170
|
+
stalestPath = e.path;
|
|
8177
8171
|
}
|
|
8178
8172
|
}
|
|
8173
|
+
return {
|
|
8174
|
+
total: entries.length,
|
|
8175
|
+
stale: staleCount,
|
|
8176
|
+
errors: errorCount,
|
|
8177
|
+
locked: lockedCount,
|
|
8178
|
+
disabled: disabledCount,
|
|
8179
|
+
neverSynthesized: neverSynthesizedCount,
|
|
8180
|
+
tokens: {
|
|
8181
|
+
architect: totalArchitectTokens,
|
|
8182
|
+
builder: totalBuilderTokens,
|
|
8183
|
+
critic: totalCriticTokens,
|
|
8184
|
+
},
|
|
8185
|
+
stalestPath,
|
|
8186
|
+
lastSynthesizedPath,
|
|
8187
|
+
lastSynthesizedAt,
|
|
8188
|
+
};
|
|
8179
8189
|
}
|
|
8180
8190
|
|
|
8181
8191
|
/**
|
|
8182
|
-
*
|
|
8192
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8183
8193
|
*
|
|
8184
|
-
*
|
|
8194
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8195
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8185
8196
|
*
|
|
8186
|
-
* @module
|
|
8197
|
+
* @module discovery/discoverMetas
|
|
8187
8198
|
*/
|
|
8188
|
-
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
8189
|
-
const metaConfigSchema = z.object({
|
|
8190
|
-
/** Watcher service base URL. */
|
|
8191
|
-
watcherUrl: z.url(),
|
|
8192
|
-
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8193
|
-
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8194
|
-
/** Optional API key for gateway authentication. */
|
|
8195
|
-
gatewayApiKey: z.string().optional(),
|
|
8196
|
-
/** Run architect every N cycles (per meta). */
|
|
8197
|
-
architectEvery: z.number().int().min(1).default(10),
|
|
8198
|
-
/** Exponent for depth weighting in staleness formula. */
|
|
8199
|
-
depthWeight: z.number().min(0).default(0.5),
|
|
8200
|
-
/** Maximum archive snapshots to retain per meta. */
|
|
8201
|
-
maxArchive: z.number().int().min(1).default(20),
|
|
8202
|
-
/** Maximum lines of context to include in subprocess prompts. */
|
|
8203
|
-
maxLines: z.number().int().min(50).default(500),
|
|
8204
|
-
/** Architect subprocess timeout in seconds. */
|
|
8205
|
-
architectTimeout: z.number().int().min(30).default(180),
|
|
8206
|
-
/** Builder subprocess timeout in seconds. */
|
|
8207
|
-
builderTimeout: z.number().int().min(60).default(360),
|
|
8208
|
-
/** Critic subprocess timeout in seconds. */
|
|
8209
|
-
criticTimeout: z.number().int().min(30).default(240),
|
|
8210
|
-
/** Thinking level for spawned synthesis sessions. */
|
|
8211
|
-
thinking: z.string().default('low'),
|
|
8212
|
-
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8213
|
-
defaultArchitect: z.string().optional(),
|
|
8214
|
-
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8215
|
-
defaultCritic: z.string().optional(),
|
|
8216
|
-
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8217
|
-
skipUnchanged: z.boolean().default(true),
|
|
8218
|
-
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8219
|
-
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8220
|
-
/** Watcher metadata properties applied to archive snapshots. */
|
|
8221
|
-
metaArchiveProperty: z
|
|
8222
|
-
.record(z.string(), z.unknown())
|
|
8223
|
-
.default({ _meta: 'archive' }),
|
|
8224
|
-
});
|
|
8225
|
-
/** Zod schema for logging configuration. */
|
|
8226
|
-
const loggingSchema = z.object({
|
|
8227
|
-
/** Log level. */
|
|
8228
|
-
level: z.string().default('info'),
|
|
8229
|
-
/** Optional file path for log output. */
|
|
8230
|
-
file: z.string().optional(),
|
|
8231
|
-
});
|
|
8232
|
-
/** Zod schema for a single auto-seed policy rule. */
|
|
8233
|
-
const autoSeedRuleSchema = z.object({
|
|
8234
|
-
/** Glob pattern matched against watcher walk results. */
|
|
8235
|
-
match: z.string(),
|
|
8236
|
-
/** Optional steering prompt for seeded metas. */
|
|
8237
|
-
steer: z.string().optional(),
|
|
8238
|
-
/** Optional cross-references for seeded metas. */
|
|
8239
|
-
crossRefs: z.array(z.string()).optional(),
|
|
8240
|
-
});
|
|
8241
|
-
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8242
|
-
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8243
|
-
/** HTTP port for the service (default: 1938). */
|
|
8244
|
-
port: z.number().int().min(1).max(65535).default(1938),
|
|
8245
|
-
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8246
|
-
schedule: z.string().default('*/30 * * * *'),
|
|
8247
|
-
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8248
|
-
reportChannel: z.string().optional(),
|
|
8249
|
-
/** Channel/user ID to send progress messages to. */
|
|
8250
|
-
reportTarget: z.string().optional(),
|
|
8251
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8252
|
-
serverBaseUrl: z.string().optional(),
|
|
8253
|
-
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8254
|
-
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8255
|
-
/** Logging configuration. */
|
|
8256
|
-
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8257
|
-
/**
|
|
8258
|
-
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8259
|
-
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8260
|
-
*/
|
|
8261
|
-
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8262
|
-
});
|
|
8263
|
-
|
|
8264
8199
|
/**
|
|
8265
|
-
*
|
|
8266
|
-
*
|
|
8267
|
-
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
8200
|
+
* Discover all .meta/ directories via watcher walk.
|
|
8268
8201
|
*
|
|
8269
|
-
*
|
|
8270
|
-
|
|
8271
|
-
/**
|
|
8272
|
-
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
8202
|
+
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8203
|
+
* and returns deduplicated meta directory paths.
|
|
8273
8204
|
*
|
|
8274
|
-
* @param
|
|
8275
|
-
* @returns
|
|
8205
|
+
* @param watcher - WatcherClient for walk queries.
|
|
8206
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
8276
8207
|
*/
|
|
8277
|
-
function
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
if (value !== null && typeof value === 'object') {
|
|
8291
|
-
const result = {};
|
|
8292
|
-
for (const [key, val] of Object.entries(value)) {
|
|
8293
|
-
result[key] = substituteEnvVars(val);
|
|
8294
|
-
}
|
|
8295
|
-
return result;
|
|
8208
|
+
async function discoverMetas(watcher) {
|
|
8209
|
+
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
8210
|
+
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
8211
|
+
const seen = new Set();
|
|
8212
|
+
const metaPaths = [];
|
|
8213
|
+
for (const filePath of allPaths) {
|
|
8214
|
+
const fp = normalizePath(filePath);
|
|
8215
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
8216
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
8217
|
+
if (seen.has(metaPath))
|
|
8218
|
+
continue;
|
|
8219
|
+
seen.add(metaPath);
|
|
8220
|
+
metaPaths.push(metaPath);
|
|
8296
8221
|
}
|
|
8297
|
-
return
|
|
8222
|
+
return metaPaths;
|
|
8298
8223
|
}
|
|
8224
|
+
|
|
8299
8225
|
/**
|
|
8300
|
-
*
|
|
8226
|
+
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8301
8227
|
*
|
|
8302
|
-
*
|
|
8303
|
-
*
|
|
8304
|
-
*
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
if (!value.startsWith('@file:'))
|
|
8308
|
-
return value;
|
|
8309
|
-
const filePath = join(baseDir, value.slice(6));
|
|
8310
|
-
return readFileSync(filePath, 'utf8');
|
|
8311
|
-
}
|
|
8312
|
-
/**
|
|
8313
|
-
* Migrate legacy config path to the new canonical location.
|
|
8314
|
-
*
|
|
8315
|
-
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
8316
|
-
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
8317
|
-
* to the new location and logs a warning.
|
|
8318
|
-
*
|
|
8319
|
-
* @param configRoot - Root directory for configuration files.
|
|
8320
|
-
* @param warn - Optional callback for logging the migration warning.
|
|
8321
|
-
*/
|
|
8322
|
-
function migrateConfigPath(configRoot, warn) {
|
|
8323
|
-
const oldPath = join(configRoot, 'jeeves-meta.config.json');
|
|
8324
|
-
const newDir = join(configRoot, 'jeeves-meta');
|
|
8325
|
-
const newPath = join(newDir, 'config.json');
|
|
8326
|
-
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
8327
|
-
mkdirSync(newDir, { recursive: true });
|
|
8328
|
-
copyFileSync(oldPath, newPath);
|
|
8329
|
-
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
8330
|
-
if (warn) {
|
|
8331
|
-
warn(message);
|
|
8332
|
-
}
|
|
8333
|
-
else {
|
|
8334
|
-
console.warn(`[jeeves-meta] ${message}`);
|
|
8335
|
-
}
|
|
8336
|
-
}
|
|
8337
|
-
}
|
|
8338
|
-
/**
|
|
8339
|
-
* Resolve config path from --config flag or JEEVES_META_CONFIG env var.
|
|
8340
|
-
*
|
|
8341
|
-
* @param args - CLI arguments (process.argv.slice(2)).
|
|
8342
|
-
* @returns Resolved config path.
|
|
8343
|
-
* @throws If no config path found.
|
|
8344
|
-
*/
|
|
8345
|
-
function resolveConfigPath(args) {
|
|
8346
|
-
let configIdx = args.indexOf('--config');
|
|
8347
|
-
if (configIdx === -1)
|
|
8348
|
-
configIdx = args.indexOf('-c');
|
|
8349
|
-
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
8350
|
-
return args[configIdx + 1];
|
|
8351
|
-
}
|
|
8352
|
-
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
8353
|
-
if (envPath)
|
|
8354
|
-
return envPath;
|
|
8355
|
-
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
8356
|
-
}
|
|
8357
|
-
/**
|
|
8358
|
-
* Load service config from a JSON file.
|
|
8359
|
-
*
|
|
8360
|
-
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
8361
|
-
* and substitutes environment-variable placeholders throughout.
|
|
8362
|
-
*
|
|
8363
|
-
* @param configPath - Path to config JSON file.
|
|
8364
|
-
* @returns Validated ServiceConfig.
|
|
8365
|
-
*/
|
|
8366
|
-
function loadServiceConfig(configPath) {
|
|
8367
|
-
const rawText = readFileSync(configPath, 'utf8');
|
|
8368
|
-
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
8369
|
-
const baseDir = dirname(configPath);
|
|
8370
|
-
if (typeof raw['defaultArchitect'] === 'string') {
|
|
8371
|
-
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
8372
|
-
}
|
|
8373
|
-
if (typeof raw['defaultCritic'] === 'string') {
|
|
8374
|
-
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
8375
|
-
}
|
|
8376
|
-
return serviceConfigSchema.parse(raw);
|
|
8377
|
-
}
|
|
8378
|
-
|
|
8379
|
-
/**
|
|
8380
|
-
* Compute summary statistics from an array of MetaEntry objects.
|
|
8381
|
-
*
|
|
8382
|
-
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
8383
|
-
*
|
|
8384
|
-
* @module discovery/computeSummary
|
|
8385
|
-
*/
|
|
8386
|
-
/**
|
|
8387
|
-
* Compute summary statistics from a list of meta entries.
|
|
8388
|
-
*
|
|
8389
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
8390
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
8391
|
-
* @returns Aggregated summary statistics.
|
|
8392
|
-
*/
|
|
8393
|
-
function computeSummary(entries, depthWeight) {
|
|
8394
|
-
let staleCount = 0;
|
|
8395
|
-
let errorCount = 0;
|
|
8396
|
-
let lockedCount = 0;
|
|
8397
|
-
let disabledCount = 0;
|
|
8398
|
-
let neverSynthesizedCount = 0;
|
|
8399
|
-
let totalArchitectTokens = 0;
|
|
8400
|
-
let totalBuilderTokens = 0;
|
|
8401
|
-
let totalCriticTokens = 0;
|
|
8402
|
-
let stalestPath = null;
|
|
8403
|
-
let stalestEffective = -1;
|
|
8404
|
-
let lastSynthesizedPath = null;
|
|
8405
|
-
let lastSynthesizedAt = null;
|
|
8406
|
-
for (const e of entries) {
|
|
8407
|
-
if (e.stalenessSeconds > 0)
|
|
8408
|
-
staleCount++;
|
|
8409
|
-
if (e.hasError)
|
|
8410
|
-
errorCount++;
|
|
8411
|
-
if (e.locked)
|
|
8412
|
-
lockedCount++;
|
|
8413
|
-
if (e.disabled)
|
|
8414
|
-
disabledCount++;
|
|
8415
|
-
if (e.lastSynthesized === null)
|
|
8416
|
-
neverSynthesizedCount++;
|
|
8417
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
8418
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
8419
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
8420
|
-
// Track last synthesized
|
|
8421
|
-
if (e.lastSynthesized &&
|
|
8422
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
8423
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
8424
|
-
lastSynthesizedPath = e.path;
|
|
8425
|
-
}
|
|
8426
|
-
// Track stalest (effective staleness for scheduling)
|
|
8427
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
8428
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
8429
|
-
if (effectiveStaleness > stalestEffective) {
|
|
8430
|
-
stalestEffective = effectiveStaleness;
|
|
8431
|
-
stalestPath = e.path;
|
|
8432
|
-
}
|
|
8433
|
-
}
|
|
8434
|
-
return {
|
|
8435
|
-
total: entries.length,
|
|
8436
|
-
stale: staleCount,
|
|
8437
|
-
errors: errorCount,
|
|
8438
|
-
locked: lockedCount,
|
|
8439
|
-
disabled: disabledCount,
|
|
8440
|
-
neverSynthesized: neverSynthesizedCount,
|
|
8441
|
-
tokens: {
|
|
8442
|
-
architect: totalArchitectTokens,
|
|
8443
|
-
builder: totalBuilderTokens,
|
|
8444
|
-
critic: totalCriticTokens,
|
|
8445
|
-
},
|
|
8446
|
-
stalestPath,
|
|
8447
|
-
lastSynthesizedPath,
|
|
8448
|
-
lastSynthesizedAt,
|
|
8449
|
-
};
|
|
8450
|
-
}
|
|
8451
|
-
|
|
8452
|
-
/**
|
|
8453
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
8454
|
-
*
|
|
8455
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
8456
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
8457
|
-
*
|
|
8458
|
-
* @module discovery/discoverMetas
|
|
8459
|
-
*/
|
|
8460
|
-
/**
|
|
8461
|
-
* Discover all .meta/ directories via watcher walk.
|
|
8462
|
-
*
|
|
8463
|
-
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
8464
|
-
* and returns deduplicated meta directory paths.
|
|
8465
|
-
*
|
|
8466
|
-
* @param watcher - WatcherClient for walk queries.
|
|
8467
|
-
* @returns Array of normalized .meta/ directory paths.
|
|
8468
|
-
*/
|
|
8469
|
-
async function discoverMetas(watcher) {
|
|
8470
|
-
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
8471
|
-
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
8472
|
-
const seen = new Set();
|
|
8473
|
-
const metaPaths = [];
|
|
8474
|
-
for (const filePath of allPaths) {
|
|
8475
|
-
const fp = normalizePath(filePath);
|
|
8476
|
-
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
8477
|
-
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
8478
|
-
if (seen.has(metaPath))
|
|
8479
|
-
continue;
|
|
8480
|
-
seen.add(metaPath);
|
|
8481
|
-
metaPaths.push(metaPath);
|
|
8482
|
-
}
|
|
8483
|
-
return metaPaths;
|
|
8484
|
-
}
|
|
8485
|
-
|
|
8486
|
-
/**
|
|
8487
|
-
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
8488
|
-
*
|
|
8489
|
-
* Lock file: .meta/.lock containing `_lockPid` + `_lockStartedAt` (underscore-prefixed
|
|
8490
|
-
* reserved keys, consistent with meta.json conventions).
|
|
8491
|
-
* Stale timeout: 30 minutes.
|
|
8492
|
-
*
|
|
8493
|
-
* @module lock
|
|
8228
|
+
* Lock file: .meta/.lock containing `_lockPid` + `_lockStartedAt` (underscore-prefixed
|
|
8229
|
+
* reserved keys, consistent with meta.json conventions).
|
|
8230
|
+
* Stale timeout: 30 minutes.
|
|
8231
|
+
*
|
|
8232
|
+
* @module lock
|
|
8494
8233
|
*/
|
|
8495
8234
|
const LOCK_FILE = '.lock';
|
|
8496
8235
|
/**
|
|
@@ -9030,1483 +8769,928 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
9030
8769
|
}
|
|
9031
8770
|
|
|
9032
8771
|
/**
|
|
9033
|
-
*
|
|
8772
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
9034
8773
|
*
|
|
9035
|
-
* @module
|
|
8774
|
+
* @module cache
|
|
9036
8775
|
*/
|
|
9037
|
-
|
|
9038
|
-
class SpawnAbortedError extends Error {
|
|
9039
|
-
constructor(message = 'Synthesis was aborted') {
|
|
9040
|
-
super(message);
|
|
9041
|
-
this.name = 'SpawnAbortedError';
|
|
9042
|
-
}
|
|
9043
|
-
}
|
|
9044
|
-
|
|
8776
|
+
const TTL_MS = 60_000;
|
|
9045
8777
|
/**
|
|
9046
|
-
*
|
|
9047
|
-
*
|
|
9048
|
-
* Carries the output file path so callers can attempt partial output recovery.
|
|
9049
|
-
*
|
|
9050
|
-
* @module executor/SpawnTimeoutError
|
|
8778
|
+
* Caches listMetas results to avoid expensive repeated filesystem walks.
|
|
8779
|
+
* Supports concurrent refresh coalescing and manual invalidation.
|
|
9051
8780
|
*/
|
|
9052
|
-
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
|
|
9057
|
-
|
|
9058
|
-
this.
|
|
9059
|
-
|
|
8781
|
+
class MetaCache {
|
|
8782
|
+
result = null;
|
|
8783
|
+
updatedAt = 0;
|
|
8784
|
+
refreshPromise = null;
|
|
8785
|
+
/** Get cached result or refresh if stale. */
|
|
8786
|
+
async get(config, watcher) {
|
|
8787
|
+
if (this.result && Date.now() - this.updatedAt < TTL_MS) {
|
|
8788
|
+
return this.result;
|
|
8789
|
+
}
|
|
8790
|
+
return this.refresh(config, watcher);
|
|
8791
|
+
}
|
|
8792
|
+
/** Force-expire the cache so next get() triggers a refresh. */
|
|
8793
|
+
invalidate() {
|
|
8794
|
+
this.updatedAt = 0;
|
|
8795
|
+
}
|
|
8796
|
+
async refresh(config, watcher) {
|
|
8797
|
+
if (this.refreshPromise)
|
|
8798
|
+
return this.refreshPromise;
|
|
8799
|
+
this.refreshPromise = listMetas(config, watcher)
|
|
8800
|
+
.then((result) => {
|
|
8801
|
+
this.result = result;
|
|
8802
|
+
this.updatedAt = Date.now();
|
|
8803
|
+
return result;
|
|
8804
|
+
})
|
|
8805
|
+
.finally(() => {
|
|
8806
|
+
this.refreshPromise = null;
|
|
8807
|
+
});
|
|
8808
|
+
return this.refreshPromise;
|
|
9060
8809
|
}
|
|
9061
8810
|
}
|
|
9062
8811
|
|
|
9063
8812
|
/**
|
|
9064
|
-
*
|
|
8813
|
+
* Shared live config hot-reload support.
|
|
9065
8814
|
*
|
|
9066
|
-
*
|
|
9067
|
-
*
|
|
9068
|
-
* polls for completion, and extracts output text.
|
|
8815
|
+
* Used by both file-watch reloads in bootstrap and POST /config/apply
|
|
8816
|
+
* via the component descriptor's onConfigApply callback.
|
|
9069
8817
|
*
|
|
9070
|
-
* @module
|
|
8818
|
+
* @module configHotReload
|
|
9071
8819
|
*/
|
|
9072
|
-
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
9073
|
-
const DEFAULT_TIMEOUT_MS$1 = 600_000; // 10 minutes
|
|
9074
8820
|
/**
|
|
9075
|
-
*
|
|
9076
|
-
* `/tools/invoke` endpoint.
|
|
8821
|
+
* Fields that require a service restart to take effect.
|
|
9077
8822
|
*
|
|
9078
|
-
*
|
|
9079
|
-
*
|
|
9080
|
-
* optional `apiKey` — typically sourced from `MetaConfig`.
|
|
8823
|
+
* Shared between the descriptor's `onConfigApply` and the file-watcher
|
|
8824
|
+
* hot-reload in `bootstrap.ts`.
|
|
9081
8825
|
*/
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
async invoke(tool, args, sessionKey) {
|
|
9106
|
-
const headers = {
|
|
9107
|
-
'Content-Type': 'application/json',
|
|
9108
|
-
};
|
|
9109
|
-
if (this.apiKey) {
|
|
9110
|
-
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
9111
|
-
}
|
|
9112
|
-
const body = { tool, args };
|
|
9113
|
-
if (sessionKey)
|
|
9114
|
-
body.sessionKey = sessionKey;
|
|
9115
|
-
const res = await fetch(this.gatewayUrl + '/tools/invoke', {
|
|
9116
|
-
method: 'POST',
|
|
9117
|
-
headers,
|
|
9118
|
-
body: JSON.stringify(body),
|
|
9119
|
-
});
|
|
9120
|
-
if (!res.ok) {
|
|
9121
|
-
const text = await res.text();
|
|
9122
|
-
throw new Error(`Gateway ${tool} failed: HTTP ${res.status.toString()} - ${text}`);
|
|
9123
|
-
}
|
|
9124
|
-
const data = (await res.json());
|
|
9125
|
-
if (data.ok === false || data.error) {
|
|
9126
|
-
throw new Error(`Gateway ${tool} error: ${data.error?.message ?? JSON.stringify(data)}`);
|
|
9127
|
-
}
|
|
9128
|
-
return data;
|
|
9129
|
-
}
|
|
9130
|
-
/** Look up totalTokens for a session via sessions_list. */
|
|
9131
|
-
async getSessionTokens(sessionKey) {
|
|
9132
|
-
try {
|
|
9133
|
-
const result = await this.invoke('sessions_list', {
|
|
9134
|
-
limit: 20,
|
|
9135
|
-
messageLimit: 0,
|
|
9136
|
-
});
|
|
9137
|
-
const sessions = (result.result?.details?.sessions ??
|
|
9138
|
-
result.result?.sessions ??
|
|
9139
|
-
[]);
|
|
9140
|
-
const match = sessions.find((s) => s.key === sessionKey);
|
|
9141
|
-
return match?.totalTokens ?? undefined;
|
|
9142
|
-
}
|
|
9143
|
-
catch {
|
|
9144
|
-
return undefined;
|
|
8826
|
+
const RESTART_REQUIRED_FIELDS = [
|
|
8827
|
+
'port',
|
|
8828
|
+
'watcherUrl',
|
|
8829
|
+
'gatewayUrl',
|
|
8830
|
+
'gatewayApiKey',
|
|
8831
|
+
'defaultArchitect',
|
|
8832
|
+
'defaultCritic',
|
|
8833
|
+
];
|
|
8834
|
+
let runtime = null;
|
|
8835
|
+
/** Register the active service runtime for config-apply hot reload. */
|
|
8836
|
+
function registerConfigHotReloadRuntime(nextRuntime) {
|
|
8837
|
+
runtime = nextRuntime;
|
|
8838
|
+
}
|
|
8839
|
+
/** Apply hot-reloadable config changes to the live shared config object. */
|
|
8840
|
+
function applyHotReloadedConfig(newConfig) {
|
|
8841
|
+
if (!runtime)
|
|
8842
|
+
return;
|
|
8843
|
+
const { config, logger, scheduler } = runtime;
|
|
8844
|
+
for (const field of RESTART_REQUIRED_FIELDS) {
|
|
8845
|
+
const oldVal = config[field];
|
|
8846
|
+
const nextVal = newConfig[field];
|
|
8847
|
+
if (oldVal !== nextVal) {
|
|
8848
|
+
logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
|
|
9145
8849
|
}
|
|
9146
8850
|
}
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
8851
|
+
if (newConfig.schedule !== config.schedule) {
|
|
8852
|
+
scheduler?.updateSchedule(newConfig.schedule);
|
|
8853
|
+
config.schedule = newConfig.schedule;
|
|
8854
|
+
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
9150
8855
|
}
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
8856
|
+
if (newConfig.logging.level !== config.logging.level) {
|
|
8857
|
+
logger.level = newConfig.logging.level;
|
|
8858
|
+
config.logging.level = newConfig.logging.level;
|
|
8859
|
+
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
9154
8860
|
}
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
9160
|
-
const deadline = Date.now() + timeoutMs;
|
|
9161
|
-
// Ensure workspace dir exists
|
|
9162
|
-
if (!existsSync(this.workspaceDir)) {
|
|
9163
|
-
mkdirSync(this.workspaceDir, { recursive: true });
|
|
9164
|
-
}
|
|
9165
|
-
// Generate unique output path for file-based output
|
|
9166
|
-
const outputId = randomUUID();
|
|
9167
|
-
const outputPath = this.workspaceDir + '/output-' + outputId + '.json';
|
|
9168
|
-
// Append file output instruction to the task
|
|
9169
|
-
const taskWithOutput = task +
|
|
9170
|
-
'\n\n## OUTPUT DELIVERY\n\n' +
|
|
9171
|
-
'Write your complete output to a file using the Write tool at:\n' +
|
|
9172
|
-
outputPath +
|
|
9173
|
-
'\n\n' +
|
|
9174
|
-
'After writing the file, reply with ONLY: NO_REPLY';
|
|
9175
|
-
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9176
|
-
// "label already in use" errors — gateway labels persist after session completion)
|
|
9177
|
-
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
9178
|
-
const label = labelBase + '-' + outputId.slice(0, 8);
|
|
9179
|
-
const spawnResult = await this.invoke('sessions_spawn', {
|
|
9180
|
-
task: taskWithOutput,
|
|
9181
|
-
label,
|
|
9182
|
-
runTimeoutSeconds: timeoutSeconds,
|
|
9183
|
-
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9184
|
-
...(options?.model ? { model: options.model } : {}),
|
|
9185
|
-
});
|
|
9186
|
-
const details = (spawnResult.result?.details ?? spawnResult.result);
|
|
9187
|
-
const sessionKey = details?.childSessionKey ?? details?.sessionKey;
|
|
9188
|
-
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
9189
|
-
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
9190
|
-
JSON.stringify(spawnResult));
|
|
8861
|
+
const restartSet = new Set(RESTART_REQUIRED_FIELDS);
|
|
8862
|
+
for (const key of Object.keys(newConfig)) {
|
|
8863
|
+
if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
|
|
8864
|
+
continue;
|
|
9191
8865
|
}
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
this.cleanupOutputFile(outputPath);
|
|
9198
|
-
throw new SpawnAbortedError();
|
|
9199
|
-
}
|
|
9200
|
-
try {
|
|
9201
|
-
const historyResult = await this.invoke('sessions_history', {
|
|
9202
|
-
sessionKey,
|
|
9203
|
-
limit: 5,
|
|
9204
|
-
includeTools: false,
|
|
9205
|
-
});
|
|
9206
|
-
const messages = historyResult.result?.details?.messages ??
|
|
9207
|
-
historyResult.result?.messages ??
|
|
9208
|
-
[];
|
|
9209
|
-
const msgArray = messages;
|
|
9210
|
-
if (msgArray.length > 0) {
|
|
9211
|
-
const lastMsg = msgArray[msgArray.length - 1];
|
|
9212
|
-
// Complete when last message is assistant with a terminal stop reason
|
|
9213
|
-
if (lastMsg.role === 'assistant' &&
|
|
9214
|
-
lastMsg.stopReason &&
|
|
9215
|
-
lastMsg.stopReason !== 'toolUse' &&
|
|
9216
|
-
lastMsg.stopReason !== 'error') {
|
|
9217
|
-
// Fetch token usage from session metadata
|
|
9218
|
-
const tokens = await this.getSessionTokens(sessionKey);
|
|
9219
|
-
// Read output from file (sub-agent wrote it via Write tool)
|
|
9220
|
-
if (existsSync(outputPath)) {
|
|
9221
|
-
try {
|
|
9222
|
-
const output = readFileSync(outputPath, 'utf8');
|
|
9223
|
-
return { output, tokens };
|
|
9224
|
-
}
|
|
9225
|
-
finally {
|
|
9226
|
-
try {
|
|
9227
|
-
unlinkSync(outputPath);
|
|
9228
|
-
}
|
|
9229
|
-
catch {
|
|
9230
|
-
/* cleanup best-effort */
|
|
9231
|
-
}
|
|
9232
|
-
}
|
|
9233
|
-
}
|
|
9234
|
-
// Fallback: extract from message content if file wasn't written
|
|
9235
|
-
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9236
|
-
const msg = msgArray[i];
|
|
9237
|
-
if (msg.role === 'assistant' && msg.content) {
|
|
9238
|
-
const text = typeof msg.content === 'string'
|
|
9239
|
-
? msg.content
|
|
9240
|
-
: Array.isArray(msg.content)
|
|
9241
|
-
? msg.content
|
|
9242
|
-
.filter((b) => b.type === 'text' && b.text)
|
|
9243
|
-
.map((b) => b.text)
|
|
9244
|
-
.join('\n')
|
|
9245
|
-
: '';
|
|
9246
|
-
if (text)
|
|
9247
|
-
return { output: text, tokens };
|
|
9248
|
-
}
|
|
9249
|
-
}
|
|
9250
|
-
return { output: '', tokens };
|
|
9251
|
-
}
|
|
9252
|
-
}
|
|
9253
|
-
}
|
|
9254
|
-
catch {
|
|
9255
|
-
// Transient poll failure — keep trying
|
|
9256
|
-
}
|
|
9257
|
-
await sleepAsync(this.pollIntervalMs);
|
|
8866
|
+
const oldVal = config[key];
|
|
8867
|
+
const nextVal = newConfig[key];
|
|
8868
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
|
|
8869
|
+
config[key] = nextVal;
|
|
8870
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
9258
8871
|
}
|
|
9259
|
-
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
9260
8872
|
}
|
|
9261
8873
|
}
|
|
9262
8874
|
|
|
9263
8875
|
/**
|
|
9264
|
-
*
|
|
8876
|
+
* Zod schema for jeeves-meta service configuration.
|
|
9265
8877
|
*
|
|
9266
|
-
*
|
|
9267
|
-
*/
|
|
9268
|
-
/**
|
|
9269
|
-
* Create a pino logger instance.
|
|
8878
|
+
* The service config is a strict superset of the core (library-compatible) meta config.
|
|
9270
8879
|
*
|
|
9271
|
-
* @
|
|
9272
|
-
* @returns Configured pino logger.
|
|
8880
|
+
* @module schema/config
|
|
9273
8881
|
*/
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
8882
|
+
/** Zod schema for the core (library-compatible) meta configuration. */
|
|
8883
|
+
const metaConfigSchema = z.object({
|
|
8884
|
+
/** Watcher service base URL. */
|
|
8885
|
+
watcherUrl: z.url(),
|
|
8886
|
+
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
8887
|
+
gatewayUrl: z.url().default('http://127.0.0.1:18789'),
|
|
8888
|
+
/** Optional API key for gateway authentication. */
|
|
8889
|
+
gatewayApiKey: z.string().optional(),
|
|
8890
|
+
/** Run architect every N cycles (per meta). */
|
|
8891
|
+
architectEvery: z.number().int().min(1).default(10),
|
|
8892
|
+
/** Exponent for depth weighting in staleness formula. */
|
|
8893
|
+
depthWeight: z.number().min(0).default(0.5),
|
|
8894
|
+
/** Maximum archive snapshots to retain per meta. */
|
|
8895
|
+
maxArchive: z.number().int().min(1).default(20),
|
|
8896
|
+
/** Maximum lines of context to include in subprocess prompts. */
|
|
8897
|
+
maxLines: z.number().int().min(50).default(500),
|
|
8898
|
+
/** Architect subprocess timeout in seconds. */
|
|
8899
|
+
architectTimeout: z.number().int().min(30).default(180),
|
|
8900
|
+
/** Builder subprocess timeout in seconds. */
|
|
8901
|
+
builderTimeout: z.number().int().min(60).default(360),
|
|
8902
|
+
/** Critic subprocess timeout in seconds. */
|
|
8903
|
+
criticTimeout: z.number().int().min(30).default(240),
|
|
8904
|
+
/** Thinking level for spawned synthesis sessions. */
|
|
8905
|
+
thinking: z.string().default('low'),
|
|
8906
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
8907
|
+
defaultArchitect: z.string().optional(),
|
|
8908
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
8909
|
+
defaultCritic: z.string().optional(),
|
|
8910
|
+
/** Skip unchanged candidates, bump _generatedAt. */
|
|
8911
|
+
skipUnchanged: z.boolean().default(true),
|
|
8912
|
+
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
8913
|
+
metaProperty: z.record(z.string(), z.unknown()).default({ _meta: 'current' }),
|
|
8914
|
+
/** Watcher metadata properties applied to archive snapshots. */
|
|
8915
|
+
metaArchiveProperty: z
|
|
8916
|
+
.record(z.string(), z.unknown())
|
|
8917
|
+
.default({ _meta: 'archive' }),
|
|
8918
|
+
});
|
|
8919
|
+
/** Zod schema for logging configuration. */
|
|
8920
|
+
const loggingSchema = z.object({
|
|
8921
|
+
/** Log level. */
|
|
8922
|
+
level: z.string().default('info'),
|
|
8923
|
+
/** Optional file path for log output. */
|
|
8924
|
+
file: z.string().optional(),
|
|
8925
|
+
});
|
|
8926
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
8927
|
+
const autoSeedRuleSchema = z.object({
|
|
8928
|
+
/** Glob pattern matched against watcher walk results. */
|
|
8929
|
+
match: z.string(),
|
|
8930
|
+
/** Optional steering prompt for seeded metas. */
|
|
8931
|
+
steer: z.string().optional(),
|
|
8932
|
+
/** Optional cross-references for seeded metas. */
|
|
8933
|
+
crossRefs: z.array(z.string()).optional(),
|
|
8934
|
+
});
|
|
8935
|
+
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
8936
|
+
const serviceConfigSchema = metaConfigSchema.extend({
|
|
8937
|
+
/** HTTP port for the service (default: 1938). */
|
|
8938
|
+
port: z.number().int().min(1).max(65535).default(1938),
|
|
8939
|
+
/** Cron schedule for synthesis cycles (default: every 30 min). */
|
|
8940
|
+
schedule: z.string().default('*/30 * * * *'),
|
|
8941
|
+
/** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
|
|
8942
|
+
reportChannel: z.string().optional(),
|
|
8943
|
+
/** Channel/user ID to send progress messages to. */
|
|
8944
|
+
reportTarget: z.string().optional(),
|
|
8945
|
+
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
8946
|
+
serverBaseUrl: z.string().optional(),
|
|
8947
|
+
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
8948
|
+
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
8949
|
+
/** Logging configuration. */
|
|
8950
|
+
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
8951
|
+
/**
|
|
8952
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
8953
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
8954
|
+
*/
|
|
8955
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
8956
|
+
});
|
|
9285
8957
|
|
|
9286
8958
|
/**
|
|
9287
|
-
*
|
|
8959
|
+
* Load and resolve jeeves-meta service config.
|
|
9288
8960
|
*
|
|
9289
|
-
*
|
|
9290
|
-
* Loaded at runtime relative to the compiled module location.
|
|
9291
|
-
*
|
|
9292
|
-
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
9293
|
-
* config. Most installations should use the built-in defaults.
|
|
9294
|
-
*
|
|
9295
|
-
* @module prompts
|
|
9296
|
-
*/
|
|
9297
|
-
const packageRoot = packageDirectorySync({
|
|
9298
|
-
cwd: fileURLToPath(import.meta.url),
|
|
9299
|
-
});
|
|
9300
|
-
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
9301
|
-
/** Built-in default architect prompt. */
|
|
9302
|
-
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
9303
|
-
/** Built-in default critic prompt. */
|
|
9304
|
-
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
9305
|
-
|
|
9306
|
-
/**
|
|
9307
|
-
* Build the MetaContext for a synthesis cycle.
|
|
9308
|
-
*
|
|
9309
|
-
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
9310
|
-
* previous content/feedback, steer, and archive paths.
|
|
9311
|
-
*
|
|
9312
|
-
* @module orchestrator/contextPackage
|
|
9313
|
-
*/
|
|
9314
|
-
/**
|
|
9315
|
-
* Condense a file list into glob-like summaries.
|
|
9316
|
-
* Groups by directory + extension pattern.
|
|
9317
|
-
*
|
|
9318
|
-
* @param files - Array of file paths.
|
|
9319
|
-
* @param maxIndividual - Show individual files up to this count.
|
|
9320
|
-
* @returns Condensed summary string.
|
|
9321
|
-
*/
|
|
9322
|
-
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
9323
|
-
if (files.length <= maxIndividual)
|
|
9324
|
-
return files.join('\n');
|
|
9325
|
-
// Group by dir + extension
|
|
9326
|
-
const groups = new Map();
|
|
9327
|
-
for (const f of files) {
|
|
9328
|
-
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
9329
|
-
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
9330
|
-
const key = dir + '*' + ext;
|
|
9331
|
-
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
9332
|
-
}
|
|
9333
|
-
// Sort by count descending
|
|
9334
|
-
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
9335
|
-
return sorted
|
|
9336
|
-
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
9337
|
-
.join('\n');
|
|
9338
|
-
}
|
|
9339
|
-
/**
|
|
9340
|
-
* Read a meta.json file and extract its `_content` field.
|
|
9341
|
-
*
|
|
9342
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
9343
|
-
* @returns The `_content` string, or null if missing/unreadable.
|
|
9344
|
-
*/
|
|
9345
|
-
async function readMetaContent(metaJsonPath) {
|
|
9346
|
-
try {
|
|
9347
|
-
const raw = await readFile(metaJsonPath, 'utf8');
|
|
9348
|
-
const meta = JSON.parse(raw);
|
|
9349
|
-
return meta._content ?? null;
|
|
9350
|
-
}
|
|
9351
|
-
catch {
|
|
9352
|
-
return null;
|
|
9353
|
-
}
|
|
9354
|
-
}
|
|
9355
|
-
/**
|
|
9356
|
-
* Build the context package for a synthesis cycle.
|
|
9357
|
-
*
|
|
9358
|
-
* @param node - The meta node being synthesized.
|
|
9359
|
-
* @param meta - Current meta.json content.
|
|
9360
|
-
* @param watcher - WatcherClient for scope enumeration.
|
|
9361
|
-
* @returns The computed context package.
|
|
9362
|
-
*/
|
|
9363
|
-
async function buildContextPackage(node, meta, watcher, logger) {
|
|
9364
|
-
// Scope and delta files via watcher walk
|
|
9365
|
-
const scopeStart = Date.now();
|
|
9366
|
-
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
9367
|
-
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
9368
|
-
logger?.debug({
|
|
9369
|
-
scopeFiles: scopeFiles.length,
|
|
9370
|
-
deltaFiles: deltaFiles.length,
|
|
9371
|
-
durationMs: Date.now() - scopeStart,
|
|
9372
|
-
}, 'scope and delta files computed');
|
|
9373
|
-
// Child meta outputs (parallel reads)
|
|
9374
|
-
const childMetas = {};
|
|
9375
|
-
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
9376
|
-
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
9377
|
-
return [child.ownerPath, content];
|
|
9378
|
-
}));
|
|
9379
|
-
for (const [path, content] of childEntries) {
|
|
9380
|
-
childMetas[path] = content;
|
|
9381
|
-
}
|
|
9382
|
-
// Cross-referenced meta outputs (parallel reads)
|
|
9383
|
-
const crossRefMetas = {};
|
|
9384
|
-
const seen = new Set();
|
|
9385
|
-
const crossRefPaths = [];
|
|
9386
|
-
for (const refPath of meta._crossRefs ?? []) {
|
|
9387
|
-
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
9388
|
-
continue;
|
|
9389
|
-
if (seen.has(refPath))
|
|
9390
|
-
continue;
|
|
9391
|
-
seen.add(refPath);
|
|
9392
|
-
crossRefPaths.push(refPath);
|
|
9393
|
-
}
|
|
9394
|
-
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
9395
|
-
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
9396
|
-
return [refPath, content];
|
|
9397
|
-
}));
|
|
9398
|
-
for (const [path, content] of crossRefEntries) {
|
|
9399
|
-
crossRefMetas[path] = content;
|
|
9400
|
-
}
|
|
9401
|
-
// Archive paths
|
|
9402
|
-
const archives = listArchiveFiles(node.metaPath);
|
|
9403
|
-
return {
|
|
9404
|
-
path: node.metaPath,
|
|
9405
|
-
scopeFiles,
|
|
9406
|
-
deltaFiles,
|
|
9407
|
-
childMetas,
|
|
9408
|
-
crossRefMetas,
|
|
9409
|
-
previousContent: meta._content ?? null,
|
|
9410
|
-
previousFeedback: meta._feedback ?? null,
|
|
9411
|
-
steer: meta._steer ?? null,
|
|
9412
|
-
previousState: meta._state ?? null,
|
|
9413
|
-
archives,
|
|
9414
|
-
};
|
|
9415
|
-
}
|
|
9416
|
-
|
|
9417
|
-
/**
|
|
9418
|
-
* Build task prompts for each synthesis step.
|
|
9419
|
-
*
|
|
9420
|
-
* Prompts are compiled as Handlebars templates with access to config,
|
|
9421
|
-
* meta, and scope context. The architect can write template expressions
|
|
9422
|
-
* into its _builder output; these resolve when the builder task is compiled.
|
|
8961
|
+
* Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
|
|
9423
8962
|
*
|
|
9424
|
-
* @module
|
|
8963
|
+
* @module configLoader
|
|
9425
8964
|
*/
|
|
9426
|
-
/** Build the template context from synthesis inputs. */
|
|
9427
|
-
function buildTemplateContext(ctx, meta, config) {
|
|
9428
|
-
return {
|
|
9429
|
-
config,
|
|
9430
|
-
meta,
|
|
9431
|
-
scope: {
|
|
9432
|
-
fileCount: ctx.scopeFiles.length,
|
|
9433
|
-
deltaCount: ctx.deltaFiles.length,
|
|
9434
|
-
childCount: Object.keys(ctx.childMetas).length,
|
|
9435
|
-
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
9436
|
-
},
|
|
9437
|
-
};
|
|
9438
|
-
}
|
|
9439
8965
|
/**
|
|
9440
|
-
*
|
|
9441
|
-
* Returns the original string unchanged if compilation fails.
|
|
9442
|
-
*/
|
|
9443
|
-
function compileTemplate(text, context) {
|
|
9444
|
-
try {
|
|
9445
|
-
return Handlebars.compile(text, { noEscape: true })(context);
|
|
9446
|
-
}
|
|
9447
|
-
catch {
|
|
9448
|
-
return text;
|
|
9449
|
-
}
|
|
9450
|
-
}
|
|
9451
|
-
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
9452
|
-
function appendMetaSections(sections, heading, metas) {
|
|
9453
|
-
if (Object.keys(metas).length === 0)
|
|
9454
|
-
return;
|
|
9455
|
-
sections.push('', heading);
|
|
9456
|
-
for (const [path, content] of Object.entries(metas)) {
|
|
9457
|
-
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
9458
|
-
}
|
|
9459
|
-
}
|
|
9460
|
-
/** Append optional context sections shared across all step prompts. */
|
|
9461
|
-
function appendSharedSections(sections, ctx, options) {
|
|
9462
|
-
const opts = {
|
|
9463
|
-
includeSteer: true,
|
|
9464
|
-
includePreviousContent: true,
|
|
9465
|
-
includePreviousFeedback: true,
|
|
9466
|
-
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
9467
|
-
includeChildMetas: true,
|
|
9468
|
-
includeCrossRefs: true,
|
|
9469
|
-
...options,
|
|
9470
|
-
};
|
|
9471
|
-
if (opts.includeSteer && ctx.steer) {
|
|
9472
|
-
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
9473
|
-
}
|
|
9474
|
-
if (opts.includePreviousContent && ctx.previousContent) {
|
|
9475
|
-
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
9476
|
-
}
|
|
9477
|
-
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
9478
|
-
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
9479
|
-
}
|
|
9480
|
-
if (opts.includeChildMetas) {
|
|
9481
|
-
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas);
|
|
9482
|
-
}
|
|
9483
|
-
if (opts.includeCrossRefs) {
|
|
9484
|
-
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
9485
|
-
}
|
|
9486
|
-
}
|
|
9487
|
-
/**
|
|
9488
|
-
* Build the architect task prompt.
|
|
8966
|
+
* Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
|
|
9489
8967
|
*
|
|
9490
|
-
* @param
|
|
9491
|
-
* @
|
|
9492
|
-
* @param config - Synthesis config.
|
|
9493
|
-
* @returns The architect task prompt string.
|
|
8968
|
+
* @param value - Arbitrary JSON-compatible value.
|
|
8969
|
+
* @returns Value with env-var placeholders resolved.
|
|
9494
8970
|
*/
|
|
9495
|
-
function
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
9505
|
-
'',
|
|
9506
|
-
'### File listing (scope)',
|
|
9507
|
-
condenseScopeFiles(ctx.scopeFiles),
|
|
9508
|
-
];
|
|
9509
|
-
// Inject previous _builder so architect can see its own prior output
|
|
9510
|
-
if (meta._builder) {
|
|
9511
|
-
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
8971
|
+
function substituteEnvVars(value) {
|
|
8972
|
+
if (typeof value === 'string') {
|
|
8973
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
|
|
8974
|
+
const envVal = process.env[name];
|
|
8975
|
+
if (envVal === undefined) {
|
|
8976
|
+
throw new Error(`Environment variable ${name} is not set`);
|
|
8977
|
+
}
|
|
8978
|
+
return envVal;
|
|
8979
|
+
});
|
|
9512
8980
|
}
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
|
|
8981
|
+
if (Array.isArray(value)) {
|
|
8982
|
+
return value.map(substituteEnvVars);
|
|
9516
8983
|
}
|
|
9517
|
-
|
|
9518
|
-
}
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
* @param meta - Current meta.json.
|
|
9524
|
-
* @param config - Synthesis config.
|
|
9525
|
-
* @returns The builder task prompt string.
|
|
9526
|
-
*/
|
|
9527
|
-
function buildBuilderTask(ctx, meta, config) {
|
|
9528
|
-
const sections = [
|
|
9529
|
-
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
9530
|
-
'',
|
|
9531
|
-
'## TASK BRIEF (from Architect)',
|
|
9532
|
-
meta._builder ?? '(No architect brief available)',
|
|
9533
|
-
'',
|
|
9534
|
-
'## SCOPE',
|
|
9535
|
-
`Path: ${ctx.path}`,
|
|
9536
|
-
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9537
|
-
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9538
|
-
];
|
|
9539
|
-
if (ctx.previousState != null) {
|
|
9540
|
-
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
8984
|
+
if (value !== null && typeof value === 'object') {
|
|
8985
|
+
const result = {};
|
|
8986
|
+
for (const [key, val] of Object.entries(value)) {
|
|
8987
|
+
result[key] = substituteEnvVars(val);
|
|
8988
|
+
}
|
|
8989
|
+
return result;
|
|
9541
8990
|
}
|
|
9542
|
-
|
|
9543
|
-
includeSteer: false,
|
|
9544
|
-
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
9545
|
-
});
|
|
9546
|
-
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.');
|
|
9547
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
9548
|
-
}
|
|
9549
|
-
/**
|
|
9550
|
-
* Build the critic task prompt.
|
|
9551
|
-
*
|
|
9552
|
-
* @param ctx - Synthesis context.
|
|
9553
|
-
* @param meta - Current meta.json (with _content already set by builder).
|
|
9554
|
-
* @param config - Synthesis config.
|
|
9555
|
-
* @returns The critic task prompt string.
|
|
9556
|
-
*/
|
|
9557
|
-
function buildCriticTask(ctx, meta, config) {
|
|
9558
|
-
const sections = [
|
|
9559
|
-
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9560
|
-
'',
|
|
9561
|
-
meta._critic ?? config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9562
|
-
'',
|
|
9563
|
-
'## SYNTHESIS TO EVALUATE',
|
|
9564
|
-
meta._content ?? '(No content produced)',
|
|
9565
|
-
'',
|
|
9566
|
-
'## SCOPE',
|
|
9567
|
-
`Path: ${ctx.path}`,
|
|
9568
|
-
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9569
|
-
];
|
|
9570
|
-
appendSharedSections(sections, ctx, {
|
|
9571
|
-
includePreviousContent: false,
|
|
9572
|
-
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
9573
|
-
includeChildMetas: false,
|
|
9574
|
-
includeCrossRefs: false,
|
|
9575
|
-
});
|
|
9576
|
-
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
9577
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
8991
|
+
return value;
|
|
9578
8992
|
}
|
|
9579
|
-
|
|
9580
|
-
/**
|
|
9581
|
-
* Exponential moving average helper for token tracking.
|
|
9582
|
-
*
|
|
9583
|
-
* @module ema
|
|
9584
|
-
*/
|
|
9585
|
-
const DEFAULT_DECAY = 0.3;
|
|
9586
8993
|
/**
|
|
9587
|
-
*
|
|
8994
|
+
* Resolve \@file: references in a config value.
|
|
9588
8995
|
*
|
|
9589
|
-
* @param
|
|
9590
|
-
* @param
|
|
9591
|
-
* @
|
|
9592
|
-
* @returns Updated EMA.
|
|
8996
|
+
* @param value - String value that may start with "\@file:".
|
|
8997
|
+
* @param baseDir - Base directory for resolving relative paths.
|
|
8998
|
+
* @returns The resolved string (file contents or original value).
|
|
9593
8999
|
*/
|
|
9594
|
-
function
|
|
9595
|
-
if (
|
|
9596
|
-
return
|
|
9597
|
-
|
|
9000
|
+
function resolveFileRef(value, baseDir) {
|
|
9001
|
+
if (!value.startsWith('@file:'))
|
|
9002
|
+
return value;
|
|
9003
|
+
const filePath = join(baseDir, value.slice(6));
|
|
9004
|
+
return readFileSync(filePath, 'utf8');
|
|
9598
9005
|
}
|
|
9599
|
-
|
|
9600
|
-
/**
|
|
9601
|
-
* Structured error from a synthesis step failure.
|
|
9602
|
-
*
|
|
9603
|
-
* @module schema/error
|
|
9604
|
-
*/
|
|
9605
|
-
/** Zod schema for synthesis step errors. */
|
|
9606
|
-
const metaErrorSchema = z.object({
|
|
9607
|
-
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
9608
|
-
step: z.enum(['architect', 'builder', 'critic']),
|
|
9609
|
-
/** Error classification code. */
|
|
9610
|
-
code: z.string(),
|
|
9611
|
-
/** Human-readable error message. */
|
|
9612
|
-
message: z.string(),
|
|
9613
|
-
});
|
|
9614
|
-
|
|
9615
9006
|
/**
|
|
9616
|
-
*
|
|
9007
|
+
* Migrate legacy config path to the new canonical location.
|
|
9617
9008
|
*
|
|
9618
|
-
*
|
|
9619
|
-
*
|
|
9009
|
+
* If the old path `{configRoot}/jeeves-meta.config.json` exists and the new
|
|
9010
|
+
* path `{configRoot}/jeeves-meta/config.json` does NOT exist, copies the file
|
|
9011
|
+
* to the new location and logs a warning.
|
|
9620
9012
|
*
|
|
9621
|
-
* @
|
|
9013
|
+
* @param configRoot - Root directory for configuration files.
|
|
9014
|
+
* @param warn - Optional callback for logging the migration warning.
|
|
9622
9015
|
*/
|
|
9623
|
-
|
|
9624
|
-
const
|
|
9625
|
-
'
|
|
9626
|
-
'
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
}
|
|
9639
|
-
/**
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
/** Critic system prompt used this turn. Defaults from config. */
|
|
9659
|
-
_critic: z.string().optional(),
|
|
9660
|
-
/** Timestamp of last synthesis. ISO 8601. */
|
|
9661
|
-
_generatedAt: z.iso.datetime().optional(),
|
|
9662
|
-
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
9663
|
-
_content: z.string().optional(),
|
|
9664
|
-
/**
|
|
9665
|
-
* Hash of sorted file listing in scope. Detects directory structure
|
|
9666
|
-
* changes that trigger an architect re-run.
|
|
9667
|
-
*/
|
|
9668
|
-
_structureHash: z.string().optional(),
|
|
9669
|
-
/**
|
|
9670
|
-
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
9671
|
-
* Used with architectEvery to trigger periodic re-prompting.
|
|
9672
|
-
*/
|
|
9673
|
-
_synthesisCount: z.number().int().min(0).optional(),
|
|
9674
|
-
/** Critic evaluation of the last synthesis. */
|
|
9675
|
-
_feedback: z.string().optional(),
|
|
9676
|
-
/**
|
|
9677
|
-
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
9678
|
-
* metas.
|
|
9679
|
-
*/
|
|
9680
|
-
_archived: z.boolean().optional(),
|
|
9681
|
-
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
9682
|
-
_archivedAt: z.iso.datetime().optional(),
|
|
9683
|
-
/**
|
|
9684
|
-
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
9685
|
-
* normalized to min 0 at scheduling time.
|
|
9686
|
-
*/
|
|
9687
|
-
_depth: z.number().optional(),
|
|
9688
|
-
/**
|
|
9689
|
-
* Emphasis multiplier for depth weighting in scheduling.
|
|
9690
|
-
* Default 1. Higher values increase this meta's scheduling priority
|
|
9691
|
-
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
9692
|
-
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
9693
|
-
*/
|
|
9694
|
-
_emphasis: z.number().min(0).optional(),
|
|
9695
|
-
/** Token count from last architect subprocess call. */
|
|
9696
|
-
_architectTokens: z.number().int().optional(),
|
|
9697
|
-
/** Token count from last builder subprocess call. */
|
|
9698
|
-
_builderTokens: z.number().int().optional(),
|
|
9699
|
-
/** Token count from last critic subprocess call. */
|
|
9700
|
-
_criticTokens: z.number().int().optional(),
|
|
9701
|
-
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
9702
|
-
_architectTokensAvg: z.number().optional(),
|
|
9703
|
-
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
9704
|
-
_builderTokensAvg: z.number().optional(),
|
|
9705
|
-
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
9706
|
-
_criticTokensAvg: z.number().optional(),
|
|
9707
|
-
/**
|
|
9708
|
-
* Opaque state carried across synthesis cycles for progressive work.
|
|
9709
|
-
* Set by the builder, passed back as context on next cycle.
|
|
9710
|
-
*/
|
|
9711
|
-
_state: z.unknown().optional(),
|
|
9712
|
-
/**
|
|
9713
|
-
* Structured error from last cycle. Present when a step failed.
|
|
9714
|
-
* Cleared on successful cycle.
|
|
9715
|
-
*/
|
|
9716
|
-
_error: metaErrorSchema.optional(),
|
|
9717
|
-
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
9718
|
-
_disabled: z.boolean().optional(),
|
|
9719
|
-
/**
|
|
9720
|
-
* Per-phase state machine record. Engine-managed.
|
|
9721
|
-
* Keyed by phase name (architect, builder, critic) with status values.
|
|
9722
|
-
* Persisted to survive ticks; derived on first load for back-compat.
|
|
9723
|
-
*/
|
|
9724
|
-
_phaseState: phaseStateSchema.optional(),
|
|
9725
|
-
})
|
|
9726
|
-
.loose();
|
|
9727
|
-
|
|
9016
|
+
function migrateConfigPath(configRoot, warn) {
|
|
9017
|
+
const oldPath = join(configRoot, 'jeeves-meta.config.json');
|
|
9018
|
+
const newDir = join(configRoot, 'jeeves-meta');
|
|
9019
|
+
const newPath = join(newDir, 'config.json');
|
|
9020
|
+
if (existsSync(oldPath) && !existsSync(newPath)) {
|
|
9021
|
+
mkdirSync(newDir, { recursive: true });
|
|
9022
|
+
copyFileSync(oldPath, newPath);
|
|
9023
|
+
const message = `Migrated config from ${oldPath} to ${newPath}. The old file can be removed.`;
|
|
9024
|
+
if (warn) {
|
|
9025
|
+
warn(message);
|
|
9026
|
+
}
|
|
9027
|
+
else {
|
|
9028
|
+
console.warn(`[jeeves-meta] ${message}`);
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
/**
|
|
9033
|
+
* Resolve config path from --config flag or JEEVES_META_CONFIG env var.
|
|
9034
|
+
*
|
|
9035
|
+
* @param args - CLI arguments (process.argv.slice(2)).
|
|
9036
|
+
* @returns Resolved config path.
|
|
9037
|
+
* @throws If no config path found.
|
|
9038
|
+
*/
|
|
9039
|
+
function resolveConfigPath(args) {
|
|
9040
|
+
let configIdx = args.indexOf('--config');
|
|
9041
|
+
if (configIdx === -1)
|
|
9042
|
+
configIdx = args.indexOf('-c');
|
|
9043
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
9044
|
+
return args[configIdx + 1];
|
|
9045
|
+
}
|
|
9046
|
+
const envPath = process.env['JEEVES_META_CONFIG'];
|
|
9047
|
+
if (envPath)
|
|
9048
|
+
return envPath;
|
|
9049
|
+
throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
|
|
9050
|
+
}
|
|
9728
9051
|
/**
|
|
9729
|
-
*
|
|
9052
|
+
* Load service config from a JSON file.
|
|
9730
9053
|
*
|
|
9731
|
-
*
|
|
9732
|
-
*
|
|
9733
|
-
* Validates against schema before writing.
|
|
9054
|
+
* Resolves \@file: references for defaultArchitect and defaultCritic,
|
|
9055
|
+
* and substitutes environment-variable placeholders throughout.
|
|
9734
9056
|
*
|
|
9735
|
-
* @
|
|
9057
|
+
* @param configPath - Path to config JSON file.
|
|
9058
|
+
* @returns Validated ServiceConfig.
|
|
9736
9059
|
*/
|
|
9060
|
+
function loadServiceConfig(configPath) {
|
|
9061
|
+
const rawText = readFileSync(configPath, 'utf8');
|
|
9062
|
+
const raw = substituteEnvVars(JSON.parse(rawText));
|
|
9063
|
+
const baseDir = dirname(configPath);
|
|
9064
|
+
if (typeof raw['defaultArchitect'] === 'string') {
|
|
9065
|
+
raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
|
|
9066
|
+
}
|
|
9067
|
+
if (typeof raw['defaultCritic'] === 'string') {
|
|
9068
|
+
raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
|
|
9069
|
+
}
|
|
9070
|
+
return serviceConfigSchema.parse(raw);
|
|
9071
|
+
}
|
|
9072
|
+
|
|
9737
9073
|
/**
|
|
9738
|
-
*
|
|
9074
|
+
* Error thrown when a spawned subprocess is aborted via AbortController.
|
|
9739
9075
|
*
|
|
9740
|
-
* @
|
|
9741
|
-
* @returns The updated MetaJson.
|
|
9742
|
-
* @throws If validation fails (malformed output).
|
|
9076
|
+
* @module executor/SpawnAbortedError
|
|
9743
9077
|
*/
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
_depth: options.current._depth,
|
|
9750
|
-
_emphasis: options.current._emphasis,
|
|
9751
|
-
// Engine fields
|
|
9752
|
-
_architect: options.architect,
|
|
9753
|
-
_builder: options.builder,
|
|
9754
|
-
_critic: options.critic,
|
|
9755
|
-
_generatedAt: options.stateOnly
|
|
9756
|
-
? options.current._generatedAt
|
|
9757
|
-
: new Date().toISOString(),
|
|
9758
|
-
_structureHash: options.structureHash,
|
|
9759
|
-
_synthesisCount: options.synthesisCount,
|
|
9760
|
-
// Token tracking
|
|
9761
|
-
_architectTokens: options.architectTokens,
|
|
9762
|
-
_builderTokens: options.builderTokens,
|
|
9763
|
-
_criticTokens: options.criticTokens,
|
|
9764
|
-
_architectTokensAvg: options.architectTokens !== undefined
|
|
9765
|
-
? computeEma(options.architectTokens, options.current._architectTokensAvg)
|
|
9766
|
-
: options.current._architectTokensAvg,
|
|
9767
|
-
_builderTokensAvg: options.builderTokens !== undefined
|
|
9768
|
-
? computeEma(options.builderTokens, options.current._builderTokensAvg)
|
|
9769
|
-
: options.current._builderTokensAvg,
|
|
9770
|
-
_criticTokensAvg: options.criticTokens !== undefined
|
|
9771
|
-
? computeEma(options.criticTokens, options.current._criticTokensAvg)
|
|
9772
|
-
: options.current._criticTokensAvg,
|
|
9773
|
-
// Content from builder (stateOnly preserves previous content)
|
|
9774
|
-
_content: options.stateOnly
|
|
9775
|
-
? options.current._content
|
|
9776
|
-
: (options.builderOutput?.content ?? options.current._content),
|
|
9777
|
-
// Feedback from critic
|
|
9778
|
-
_feedback: options.feedback ?? options.current._feedback,
|
|
9779
|
-
// Progressive state
|
|
9780
|
-
_state: options.state,
|
|
9781
|
-
// Error handling
|
|
9782
|
-
_error: options.error ?? undefined,
|
|
9783
|
-
// Phase state machine
|
|
9784
|
-
_phaseState: options.phaseState,
|
|
9785
|
-
// Spread structured fields from builder
|
|
9786
|
-
...options.builderOutput?.fields,
|
|
9787
|
-
};
|
|
9788
|
-
// Clean up undefined optional fields
|
|
9789
|
-
if (merged._steer === undefined)
|
|
9790
|
-
delete merged._steer;
|
|
9791
|
-
if (merged._depth === undefined)
|
|
9792
|
-
delete merged._depth;
|
|
9793
|
-
if (merged._emphasis === undefined)
|
|
9794
|
-
delete merged._emphasis;
|
|
9795
|
-
if (merged._architectTokens === undefined)
|
|
9796
|
-
delete merged._architectTokens;
|
|
9797
|
-
if (merged._builderTokens === undefined)
|
|
9798
|
-
delete merged._builderTokens;
|
|
9799
|
-
if (merged._criticTokens === undefined)
|
|
9800
|
-
delete merged._criticTokens;
|
|
9801
|
-
if (merged._architectTokensAvg === undefined)
|
|
9802
|
-
delete merged._architectTokensAvg;
|
|
9803
|
-
if (merged._builderTokensAvg === undefined)
|
|
9804
|
-
delete merged._builderTokensAvg;
|
|
9805
|
-
if (merged._criticTokensAvg === undefined)
|
|
9806
|
-
delete merged._criticTokensAvg;
|
|
9807
|
-
if (merged._state === undefined)
|
|
9808
|
-
delete merged._state;
|
|
9809
|
-
if (merged._error === undefined)
|
|
9810
|
-
delete merged._error;
|
|
9811
|
-
if (merged._content === undefined)
|
|
9812
|
-
delete merged._content;
|
|
9813
|
-
if (merged._feedback === undefined)
|
|
9814
|
-
delete merged._feedback;
|
|
9815
|
-
if (merged._phaseState === undefined)
|
|
9816
|
-
delete merged._phaseState;
|
|
9817
|
-
// Validate
|
|
9818
|
-
const result = metaJsonSchema.safeParse(merged);
|
|
9819
|
-
if (!result.success) {
|
|
9820
|
-
throw new Error(`Meta validation failed: ${result.error.message}`);
|
|
9078
|
+
/** Error indicating a spawn was deliberately aborted. */
|
|
9079
|
+
class SpawnAbortedError extends Error {
|
|
9080
|
+
constructor(message = 'Synthesis was aborted') {
|
|
9081
|
+
super(message);
|
|
9082
|
+
this.name = 'SpawnAbortedError';
|
|
9821
9083
|
}
|
|
9822
|
-
// Write to specified path (lock staging) or default meta.json
|
|
9823
|
-
const filePath = options.outputPath ?? join(options.metaPath, 'meta.json');
|
|
9824
|
-
await writeFile(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
9825
|
-
return result.data;
|
|
9826
9084
|
}
|
|
9827
9085
|
|
|
9828
9086
|
/**
|
|
9829
|
-
*
|
|
9087
|
+
* Error thrown when a spawned subprocess times out.
|
|
9830
9088
|
*
|
|
9831
|
-
*
|
|
9832
|
-
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9833
|
-
* `.meta/` directories.
|
|
9089
|
+
* Carries the output file path so callers can attempt partial output recovery.
|
|
9834
9090
|
*
|
|
9835
|
-
* @module
|
|
9091
|
+
* @module executor/SpawnTimeoutError
|
|
9836
9092
|
*/
|
|
9093
|
+
/** Error indicating a spawn timeout with a recoverable output path. */
|
|
9094
|
+
class SpawnTimeoutError extends Error {
|
|
9095
|
+
/** Path to the (possibly partial) output file written before timeout. */
|
|
9096
|
+
outputPath;
|
|
9097
|
+
constructor(message, outputPath) {
|
|
9098
|
+
super(message);
|
|
9099
|
+
this.name = 'SpawnTimeoutError';
|
|
9100
|
+
this.outputPath = outputPath;
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
|
|
9837
9104
|
/**
|
|
9838
|
-
*
|
|
9105
|
+
* MetaExecutor implementation using the OpenClaw gateway HTTP API.
|
|
9839
9106
|
*
|
|
9840
|
-
*
|
|
9841
|
-
*
|
|
9107
|
+
* Lives in the library package so both plugin and runner can import it.
|
|
9108
|
+
* Spawns sub-agent sessions via the gateway's `/tools/invoke` endpoint,
|
|
9109
|
+
* polls for completion, and extracts output text.
|
|
9842
9110
|
*
|
|
9843
|
-
* @
|
|
9844
|
-
* @param watcher - WatcherClient for filesystem enumeration.
|
|
9845
|
-
* @returns MetaNode with direct children wired.
|
|
9111
|
+
* @module executor/GatewayExecutor
|
|
9846
9112
|
*/
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9113
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
9114
|
+
const DEFAULT_TIMEOUT_MS$1 = 600_000; // 10 minutes
|
|
9115
|
+
/**
|
|
9116
|
+
* MetaExecutor that spawns OpenClaw sessions via the gateway's
|
|
9117
|
+
* `/tools/invoke` endpoint.
|
|
9118
|
+
*
|
|
9119
|
+
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
9120
|
+
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
9121
|
+
* optional `apiKey` — typically sourced from `MetaConfig`.
|
|
9122
|
+
*/
|
|
9123
|
+
class GatewayExecutor {
|
|
9124
|
+
gatewayUrl;
|
|
9125
|
+
apiKey;
|
|
9126
|
+
pollIntervalMs;
|
|
9127
|
+
workspaceDir;
|
|
9128
|
+
controller = new AbortController();
|
|
9129
|
+
constructor(options = {}) {
|
|
9130
|
+
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
|
|
9131
|
+
this.apiKey = options.apiKey;
|
|
9132
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
9133
|
+
this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
|
|
9868
9134
|
}
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9135
|
+
/** Remove a temp output file if it exists. */
|
|
9136
|
+
cleanupOutputFile(outputPath) {
|
|
9137
|
+
try {
|
|
9138
|
+
if (existsSync(outputPath))
|
|
9139
|
+
unlinkSync(outputPath);
|
|
9140
|
+
}
|
|
9141
|
+
catch {
|
|
9142
|
+
/* best-effort cleanup */
|
|
9143
|
+
}
|
|
9144
|
+
}
|
|
9145
|
+
/** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
|
|
9146
|
+
async invoke(tool, args, sessionKey) {
|
|
9147
|
+
const headers = {
|
|
9148
|
+
'Content-Type': 'application/json',
|
|
9149
|
+
};
|
|
9150
|
+
if (this.apiKey) {
|
|
9151
|
+
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
9152
|
+
}
|
|
9153
|
+
const body = { tool, args };
|
|
9154
|
+
if (sessionKey)
|
|
9155
|
+
body.sessionKey = sessionKey;
|
|
9156
|
+
const res = await fetch(this.gatewayUrl + '/tools/invoke', {
|
|
9157
|
+
method: 'POST',
|
|
9158
|
+
headers,
|
|
9159
|
+
body: JSON.stringify(body),
|
|
9160
|
+
});
|
|
9161
|
+
if (!res.ok) {
|
|
9162
|
+
const text = await res.text();
|
|
9163
|
+
throw new Error(`Gateway ${tool} failed: HTTP ${res.status.toString()} - ${text}`);
|
|
9164
|
+
}
|
|
9165
|
+
const data = (await res.json());
|
|
9166
|
+
if (data.ok === false || data.error) {
|
|
9167
|
+
throw new Error(`Gateway ${tool} error: ${data.error?.message ?? JSON.stringify(data)}`);
|
|
9168
|
+
}
|
|
9169
|
+
return data;
|
|
9170
|
+
}
|
|
9171
|
+
/** Look up session metadata (tokens, completion status) via sessions_list. */
|
|
9172
|
+
async getSessionInfo(sessionKey) {
|
|
9173
|
+
try {
|
|
9174
|
+
const result = await this.invoke('sessions_list', {
|
|
9175
|
+
limit: 200,
|
|
9176
|
+
messageLimit: 0,
|
|
9177
|
+
});
|
|
9178
|
+
const sessions = (result.result?.details?.sessions ??
|
|
9179
|
+
result.result?.sessions ??
|
|
9180
|
+
[]);
|
|
9181
|
+
const match = sessions.find((s) => s.key === sessionKey);
|
|
9182
|
+
if (!match) {
|
|
9183
|
+
// Session absent from list — likely cleaned up after completion.
|
|
9184
|
+
// With limit=200 this is reliable; a false positive here only
|
|
9185
|
+
// means we read the output file slightly early (still correct
|
|
9186
|
+
// if the file exists).
|
|
9187
|
+
return { completed: true };
|
|
9188
|
+
}
|
|
9189
|
+
const done = match.status === 'completed' || match.status === 'done';
|
|
9190
|
+
return { tokens: match.totalTokens, completed: done };
|
|
9191
|
+
}
|
|
9192
|
+
catch {
|
|
9193
|
+
return { completed: false };
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
/** Whether this executor has been aborted by the operator. */
|
|
9197
|
+
get aborted() {
|
|
9198
|
+
return this.controller.signal.aborted;
|
|
9199
|
+
}
|
|
9200
|
+
/** Abort the currently running spawn, if any. */
|
|
9201
|
+
abort() {
|
|
9202
|
+
this.controller.abort();
|
|
9203
|
+
}
|
|
9204
|
+
async spawn(task, options) {
|
|
9205
|
+
// Fresh controller for each spawn call
|
|
9206
|
+
this.controller = new AbortController();
|
|
9207
|
+
const timeoutSeconds = options?.timeout ?? DEFAULT_TIMEOUT_MS$1 / 1000;
|
|
9208
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
9209
|
+
const deadline = Date.now() + timeoutMs;
|
|
9210
|
+
// Ensure workspace dir exists
|
|
9211
|
+
if (!existsSync(this.workspaceDir)) {
|
|
9212
|
+
mkdirSync(this.workspaceDir, { recursive: true });
|
|
9213
|
+
}
|
|
9214
|
+
// Generate unique output path for file-based output
|
|
9215
|
+
const outputId = randomUUID();
|
|
9216
|
+
const outputPath = this.workspaceDir + '/output-' + outputId + '.json';
|
|
9217
|
+
// Append file output instruction to the task
|
|
9218
|
+
const taskWithOutput = task +
|
|
9219
|
+
'\n\n## OUTPUT DELIVERY\n\n' +
|
|
9220
|
+
'Write your complete output to a file using the Write tool at:\n' +
|
|
9221
|
+
outputPath +
|
|
9222
|
+
'\n\n' +
|
|
9223
|
+
'After writing the file, reply with ONLY: NO_REPLY';
|
|
9224
|
+
// Step 1: Spawn the sub-agent session (unique label per cycle to avoid
|
|
9225
|
+
// "label already in use" errors — gateway labels persist after session completion)
|
|
9226
|
+
const labelBase = options?.label ?? 'jeeves-meta-synthesis';
|
|
9227
|
+
const label = labelBase + '-' + outputId.slice(0, 8);
|
|
9228
|
+
const spawnResult = await this.invoke('sessions_spawn', {
|
|
9229
|
+
task: taskWithOutput,
|
|
9230
|
+
label,
|
|
9231
|
+
runTimeoutSeconds: timeoutSeconds,
|
|
9232
|
+
...(options?.thinking ? { thinking: options.thinking } : {}),
|
|
9233
|
+
...(options?.model ? { model: options.model } : {}),
|
|
9234
|
+
});
|
|
9235
|
+
const details = (spawnResult.result?.details ?? spawnResult.result);
|
|
9236
|
+
const sessionKey = details?.childSessionKey ?? details?.sessionKey;
|
|
9237
|
+
if (typeof sessionKey !== 'string' || !sessionKey) {
|
|
9238
|
+
throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
|
|
9239
|
+
JSON.stringify(spawnResult));
|
|
9240
|
+
}
|
|
9241
|
+
// Step 2: Poll for completion via sessions_history
|
|
9242
|
+
await sleepAsync(3000);
|
|
9243
|
+
while (Date.now() < deadline) {
|
|
9244
|
+
// Check for abort before each poll iteration
|
|
9245
|
+
if (this.controller.signal.aborted) {
|
|
9246
|
+
this.cleanupOutputFile(outputPath);
|
|
9247
|
+
throw new SpawnAbortedError();
|
|
9248
|
+
}
|
|
9249
|
+
try {
|
|
9250
|
+
const historyResult = await this.invoke('sessions_history', {
|
|
9251
|
+
sessionKey,
|
|
9252
|
+
limit: 5,
|
|
9253
|
+
includeTools: false,
|
|
9254
|
+
});
|
|
9255
|
+
const messages = historyResult.result?.details?.messages ??
|
|
9256
|
+
historyResult.result?.messages ??
|
|
9257
|
+
[];
|
|
9258
|
+
const msgArray = messages;
|
|
9259
|
+
// Check 1: terminal stop reason in history
|
|
9260
|
+
let historyDone = false;
|
|
9261
|
+
if (msgArray.length > 0) {
|
|
9262
|
+
const lastMsg = msgArray[msgArray.length - 1];
|
|
9263
|
+
if (lastMsg.role === 'assistant' &&
|
|
9264
|
+
lastMsg.stopReason &&
|
|
9265
|
+
lastMsg.stopReason !== 'toolUse' &&
|
|
9266
|
+
lastMsg.stopReason !== 'error') {
|
|
9267
|
+
historyDone = true;
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
// Check 2: session completion status via sessions_list
|
|
9271
|
+
const sessionInfo = await this.getSessionInfo(sessionKey);
|
|
9272
|
+
if (historyDone || sessionInfo.completed) {
|
|
9273
|
+
const tokens = sessionInfo.tokens;
|
|
9274
|
+
// Read output from file (sub-agent wrote it via Write tool)
|
|
9275
|
+
if (existsSync(outputPath)) {
|
|
9276
|
+
try {
|
|
9277
|
+
const output = readFileSync(outputPath, 'utf8');
|
|
9278
|
+
return { output, tokens };
|
|
9279
|
+
}
|
|
9280
|
+
finally {
|
|
9281
|
+
try {
|
|
9282
|
+
unlinkSync(outputPath);
|
|
9283
|
+
}
|
|
9284
|
+
catch {
|
|
9285
|
+
/* cleanup best-effort */
|
|
9286
|
+
}
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
9289
|
+
// Fallback: extract from message content if file wasn't written
|
|
9290
|
+
for (let i = msgArray.length - 1; i >= 0; i--) {
|
|
9291
|
+
const msg = msgArray[i];
|
|
9292
|
+
if (msg.role === 'assistant' && msg.content) {
|
|
9293
|
+
const text = typeof msg.content === 'string'
|
|
9294
|
+
? msg.content
|
|
9295
|
+
: Array.isArray(msg.content)
|
|
9296
|
+
? msg.content
|
|
9297
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
9298
|
+
.map((b) => b.text)
|
|
9299
|
+
.join('\n')
|
|
9300
|
+
: '';
|
|
9301
|
+
if (text)
|
|
9302
|
+
return { output: text, tokens };
|
|
9303
|
+
}
|
|
9304
|
+
}
|
|
9305
|
+
return { output: '', tokens };
|
|
9306
|
+
}
|
|
9307
|
+
}
|
|
9308
|
+
catch {
|
|
9309
|
+
// Transient poll failure — keep trying
|
|
9310
|
+
}
|
|
9311
|
+
await sleepAsync(this.pollIntervalMs);
|
|
9312
|
+
}
|
|
9313
|
+
throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
|
|
9885
9314
|
}
|
|
9886
|
-
return node;
|
|
9887
9315
|
}
|
|
9888
9316
|
|
|
9889
9317
|
/**
|
|
9890
|
-
*
|
|
9318
|
+
* Pino logger factory.
|
|
9891
9319
|
*
|
|
9892
|
-
*
|
|
9320
|
+
* @module logger
|
|
9321
|
+
*/
|
|
9322
|
+
/**
|
|
9323
|
+
* Create a pino logger instance.
|
|
9893
9324
|
*
|
|
9894
|
-
* @
|
|
9325
|
+
* @param config - Optional logger configuration.
|
|
9326
|
+
* @returns Configured pino logger.
|
|
9895
9327
|
*/
|
|
9328
|
+
function createLogger(config) {
|
|
9329
|
+
const level = config?.level ?? 'info';
|
|
9330
|
+
if (config?.file) {
|
|
9331
|
+
const transport = pino.transport({
|
|
9332
|
+
target: 'pino/file',
|
|
9333
|
+
options: { destination: config.file, mkdir: true },
|
|
9334
|
+
});
|
|
9335
|
+
return pino({ level }, transport);
|
|
9336
|
+
}
|
|
9337
|
+
return pino({ level });
|
|
9338
|
+
}
|
|
9339
|
+
|
|
9896
9340
|
/**
|
|
9897
|
-
*
|
|
9341
|
+
* Built-in default prompts for the synthesis pipeline.
|
|
9898
9342
|
*
|
|
9899
|
-
*
|
|
9900
|
-
*
|
|
9343
|
+
* Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
|
|
9344
|
+
* Loaded at runtime relative to the compiled module location.
|
|
9901
9345
|
*
|
|
9902
|
-
*
|
|
9903
|
-
*
|
|
9346
|
+
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
9347
|
+
* config. Most installations should use the built-in defaults.
|
|
9904
9348
|
*
|
|
9905
|
-
* @
|
|
9906
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
9907
|
-
* @returns Same array with effectiveStaleness computed.
|
|
9349
|
+
* @module prompts
|
|
9908
9350
|
*/
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
return candidates.map((c, i) => {
|
|
9918
|
-
const emphasis = c.meta._emphasis ?? 1;
|
|
9919
|
-
return {
|
|
9920
|
-
...c,
|
|
9921
|
-
effectiveStaleness: c.actualStaleness *
|
|
9922
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
9923
|
-
};
|
|
9924
|
-
});
|
|
9925
|
-
}
|
|
9351
|
+
const packageRoot = packageDirectorySync({
|
|
9352
|
+
cwd: fileURLToPath(import.meta.url),
|
|
9353
|
+
});
|
|
9354
|
+
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
9355
|
+
/** Built-in default architect prompt. */
|
|
9356
|
+
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
9357
|
+
/** Built-in default critic prompt. */
|
|
9358
|
+
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
9926
9359
|
|
|
9927
9360
|
/**
|
|
9928
|
-
*
|
|
9361
|
+
* Build the MetaContext for a synthesis cycle.
|
|
9929
9362
|
*
|
|
9930
|
-
*
|
|
9363
|
+
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
9364
|
+
* previous content/feedback, steer, and archive paths.
|
|
9931
9365
|
*
|
|
9932
|
-
* @module
|
|
9366
|
+
* @module orchestrator/contextPackage
|
|
9933
9367
|
*/
|
|
9934
9368
|
/**
|
|
9935
|
-
*
|
|
9369
|
+
* Condense a file list into glob-like summaries.
|
|
9370
|
+
* Groups by directory + extension pattern.
|
|
9936
9371
|
*
|
|
9937
|
-
* @param
|
|
9938
|
-
* @
|
|
9372
|
+
* @param files - Array of file paths.
|
|
9373
|
+
* @param maxIndividual - Show individual files up to this count.
|
|
9374
|
+
* @returns Condensed summary string.
|
|
9939
9375
|
*/
|
|
9940
|
-
function
|
|
9941
|
-
if (
|
|
9942
|
-
return
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9376
|
+
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
9377
|
+
if (files.length <= maxIndividual)
|
|
9378
|
+
return files.join('\n');
|
|
9379
|
+
// Group by dir + extension
|
|
9380
|
+
const groups = new Map();
|
|
9381
|
+
for (const f of files) {
|
|
9382
|
+
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
9383
|
+
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
9384
|
+
const key = dir + '*' + ext;
|
|
9385
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
9948
9386
|
}
|
|
9949
|
-
|
|
9387
|
+
// Sort by count descending
|
|
9388
|
+
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
9389
|
+
return sorted
|
|
9390
|
+
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
9391
|
+
.join('\n');
|
|
9950
9392
|
}
|
|
9951
9393
|
/**
|
|
9952
|
-
*
|
|
9953
|
-
*
|
|
9954
|
-
* Consolidates the repeated pattern of:
|
|
9955
|
-
* filter → computeEffectiveStaleness → selectCandidate → return path
|
|
9394
|
+
* Read a meta.json file and extract its `_content` field.
|
|
9956
9395
|
*
|
|
9957
|
-
* @param
|
|
9958
|
-
* @
|
|
9959
|
-
* @returns The stalest candidate's metaPath, or null if none are stale.
|
|
9396
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
9397
|
+
* @returns The `_content` string, or null if missing/unreadable.
|
|
9960
9398
|
*/
|
|
9961
|
-
function
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9399
|
+
async function readMetaContent(metaJsonPath) {
|
|
9400
|
+
try {
|
|
9401
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
9402
|
+
const meta = JSON.parse(raw);
|
|
9403
|
+
return meta._content ?? null;
|
|
9404
|
+
}
|
|
9405
|
+
catch {
|
|
9406
|
+
return null;
|
|
9407
|
+
}
|
|
9965
9408
|
}
|
|
9966
|
-
|
|
9967
|
-
/**
|
|
9968
|
-
* Shared error utilities.
|
|
9969
|
-
*
|
|
9970
|
-
* @module errors
|
|
9971
|
-
*/
|
|
9972
9409
|
/**
|
|
9973
|
-
*
|
|
9410
|
+
* Build the context package for a synthesis cycle.
|
|
9974
9411
|
*
|
|
9975
|
-
* @param
|
|
9976
|
-
* @param
|
|
9977
|
-
* @param
|
|
9978
|
-
* @returns
|
|
9412
|
+
* @param node - The meta node being synthesized.
|
|
9413
|
+
* @param meta - Current meta.json content.
|
|
9414
|
+
* @param watcher - WatcherClient for scope enumeration.
|
|
9415
|
+
* @returns The computed context package.
|
|
9979
9416
|
*/
|
|
9980
|
-
function
|
|
9417
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
9418
|
+
// Scope and delta files via watcher walk
|
|
9419
|
+
const scopeStart = Date.now();
|
|
9420
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
9421
|
+
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
9422
|
+
logger?.debug({
|
|
9423
|
+
scopeFiles: scopeFiles.length,
|
|
9424
|
+
deltaFiles: deltaFiles.length,
|
|
9425
|
+
durationMs: Date.now() - scopeStart,
|
|
9426
|
+
}, 'scope and delta files computed');
|
|
9427
|
+
// Child meta outputs (parallel reads)
|
|
9428
|
+
const childMetas = {};
|
|
9429
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
9430
|
+
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
9431
|
+
return [child.ownerPath, content];
|
|
9432
|
+
}));
|
|
9433
|
+
for (const [path, content] of childEntries) {
|
|
9434
|
+
childMetas[path] = content;
|
|
9435
|
+
}
|
|
9436
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
9437
|
+
const crossRefMetas = {};
|
|
9438
|
+
const seen = new Set();
|
|
9439
|
+
const crossRefPaths = [];
|
|
9440
|
+
for (const refPath of meta._crossRefs ?? []) {
|
|
9441
|
+
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
9442
|
+
continue;
|
|
9443
|
+
if (seen.has(refPath))
|
|
9444
|
+
continue;
|
|
9445
|
+
seen.add(refPath);
|
|
9446
|
+
crossRefPaths.push(refPath);
|
|
9447
|
+
}
|
|
9448
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
9449
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
9450
|
+
return [refPath, content];
|
|
9451
|
+
}));
|
|
9452
|
+
for (const [path, content] of crossRefEntries) {
|
|
9453
|
+
crossRefMetas[path] = content;
|
|
9454
|
+
}
|
|
9455
|
+
// Archive paths
|
|
9456
|
+
const archives = listArchiveFiles(node.metaPath);
|
|
9981
9457
|
return {
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9458
|
+
path: node.metaPath,
|
|
9459
|
+
scopeFiles,
|
|
9460
|
+
deltaFiles,
|
|
9461
|
+
childMetas,
|
|
9462
|
+
crossRefMetas,
|
|
9463
|
+
previousContent: meta._content ?? null,
|
|
9464
|
+
previousFeedback: meta._feedback ?? null,
|
|
9465
|
+
steer: meta._steer ?? null,
|
|
9466
|
+
previousState: meta._state ?? null,
|
|
9467
|
+
archives,
|
|
9985
9468
|
};
|
|
9986
9469
|
}
|
|
9987
9470
|
|
|
9988
9471
|
/**
|
|
9989
|
-
*
|
|
9990
|
-
*
|
|
9991
|
-
* Used to detect when directory structure changes, triggering
|
|
9992
|
-
* an architect re-run.
|
|
9993
|
-
*
|
|
9994
|
-
* @module structureHash
|
|
9995
|
-
*/
|
|
9996
|
-
/**
|
|
9997
|
-
* Compute a SHA-256 hash of a sorted file listing.
|
|
9998
|
-
*
|
|
9999
|
-
* @param filePaths - Array of file paths in scope.
|
|
10000
|
-
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10001
|
-
*/
|
|
10002
|
-
function computeStructureHash(filePaths) {
|
|
10003
|
-
const sorted = [...filePaths].sort();
|
|
10004
|
-
const content = sorted.join('\n');
|
|
10005
|
-
return createHash('sha256').update(content).digest('hex');
|
|
10006
|
-
}
|
|
10007
|
-
|
|
10008
|
-
/**
|
|
10009
|
-
* Lock-staged cycle finalization: write to .lock, copy to meta.json, archive, prune.
|
|
10010
|
-
*
|
|
10011
|
-
* @module orchestrator/finalizeCycle
|
|
10012
|
-
*/
|
|
10013
|
-
/** Finalize a cycle using lock staging: write to .lock → copy to meta.json + archive → delete .lock. */
|
|
10014
|
-
async function finalizeCycle(opts) {
|
|
10015
|
-
const lockPath = join(opts.metaPath, '.lock');
|
|
10016
|
-
const metaJsonPath = join(opts.metaPath, 'meta.json');
|
|
10017
|
-
// Stage: write merged result to .lock (sequential — ordering matters)
|
|
10018
|
-
const updated = await mergeAndWrite({
|
|
10019
|
-
metaPath: opts.metaPath,
|
|
10020
|
-
current: opts.current,
|
|
10021
|
-
architect: opts.architect,
|
|
10022
|
-
builder: opts.builder,
|
|
10023
|
-
critic: opts.critic,
|
|
10024
|
-
builderOutput: opts.builderOutput,
|
|
10025
|
-
feedback: opts.feedback,
|
|
10026
|
-
structureHash: opts.structureHash,
|
|
10027
|
-
synthesisCount: opts.synthesisCount,
|
|
10028
|
-
error: opts.error,
|
|
10029
|
-
architectTokens: opts.architectTokens,
|
|
10030
|
-
builderTokens: opts.builderTokens,
|
|
10031
|
-
criticTokens: opts.criticTokens,
|
|
10032
|
-
outputPath: lockPath,
|
|
10033
|
-
state: opts.state,
|
|
10034
|
-
stateOnly: opts.stateOnly,
|
|
10035
|
-
});
|
|
10036
|
-
// Commit: copy .lock → meta.json
|
|
10037
|
-
await copyFile(lockPath, metaJsonPath);
|
|
10038
|
-
// Archive + prune from the committed meta.json (sequential)
|
|
10039
|
-
await createSnapshot(opts.metaPath, updated);
|
|
10040
|
-
await pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
10041
|
-
// .lock is cleaned up by the finally block (releaseLock)
|
|
10042
|
-
return updated;
|
|
10043
|
-
}
|
|
10044
|
-
|
|
10045
|
-
/**
|
|
10046
|
-
* Parse subprocess outputs for each synthesis step.
|
|
10047
|
-
*
|
|
10048
|
-
* - Architect: returns text \> _builder
|
|
10049
|
-
* - Builder: returns JSON \> _content + structured fields
|
|
10050
|
-
* - Critic: returns text \> _feedback
|
|
9472
|
+
* Build task prompts for each synthesis step.
|
|
10051
9473
|
*
|
|
10052
|
-
*
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
* Parse architect output. The architect returns a task brief as text.
|
|
9474
|
+
* Prompts are compiled as Handlebars templates with access to config,
|
|
9475
|
+
* meta, and scope context. The architect can write template expressions
|
|
9476
|
+
* into its _builder output; these resolve when the builder task is compiled.
|
|
10056
9477
|
*
|
|
10057
|
-
* @
|
|
10058
|
-
* @returns The task brief string.
|
|
9478
|
+
* @module orchestrator/buildTask
|
|
10059
9479
|
*/
|
|
10060
|
-
|
|
10061
|
-
|
|
9480
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
9481
|
+
/** Build the template context from synthesis inputs. */
|
|
9482
|
+
function buildTemplateContext(ctx, meta, config) {
|
|
9483
|
+
return {
|
|
9484
|
+
config,
|
|
9485
|
+
meta,
|
|
9486
|
+
scope: {
|
|
9487
|
+
fileCount: ctx.scopeFiles.length,
|
|
9488
|
+
deltaCount: ctx.deltaFiles.length,
|
|
9489
|
+
childCount: Object.keys(ctx.childMetas).length,
|
|
9490
|
+
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
9491
|
+
},
|
|
9492
|
+
};
|
|
10062
9493
|
}
|
|
10063
9494
|
/**
|
|
10064
|
-
*
|
|
10065
|
-
*
|
|
10066
|
-
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10067
|
-
*
|
|
10068
|
-
* @param output - Raw subprocess output.
|
|
10069
|
-
* @returns Parsed builder output with content and structured fields.
|
|
9495
|
+
* Compile a string as a Handlebars template with the given context.
|
|
9496
|
+
* Returns the original string unchanged if compilation fails.
|
|
10070
9497
|
*/
|
|
10071
|
-
function
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
const direct = tryParseJson(trimmed);
|
|
10075
|
-
if (direct)
|
|
10076
|
-
return direct;
|
|
10077
|
-
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10078
|
-
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10079
|
-
const fenceMatches = [];
|
|
10080
|
-
let match;
|
|
10081
|
-
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10082
|
-
fenceMatches.push(match[1].trim());
|
|
9498
|
+
function compileTemplate(text, context) {
|
|
9499
|
+
try {
|
|
9500
|
+
return Handlebars.compile(text, { noEscape: true })(context);
|
|
10083
9501
|
}
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
const result = tryParseJson(fenceMatches[i]);
|
|
10087
|
-
if (result)
|
|
10088
|
-
return result;
|
|
9502
|
+
catch {
|
|
9503
|
+
return text;
|
|
10089
9504
|
}
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
if (
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
9505
|
+
}
|
|
9506
|
+
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
9507
|
+
function appendMetaSections(sections, heading, metas) {
|
|
9508
|
+
if (Object.keys(metas).length === 0)
|
|
9509
|
+
return;
|
|
9510
|
+
sections.push('', heading);
|
|
9511
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
9512
|
+
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
10097
9513
|
}
|
|
10098
|
-
// Fallback: treat entire output as content
|
|
10099
|
-
return { content: trimmed, fields: {} };
|
|
10100
9514
|
}
|
|
10101
|
-
/**
|
|
10102
|
-
function
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
: null;
|
|
10115
|
-
if (content === null)
|
|
10116
|
-
return null;
|
|
10117
|
-
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10118
|
-
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10119
|
-
// Extract non-underscore fields
|
|
10120
|
-
const fields = {};
|
|
10121
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
10122
|
-
if (!key.startsWith('_') && key !== 'content') {
|
|
10123
|
-
fields[key] = value;
|
|
10124
|
-
}
|
|
10125
|
-
}
|
|
10126
|
-
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
9515
|
+
/** Append optional context sections shared across all step prompts. */
|
|
9516
|
+
function appendSharedSections(sections, ctx, options) {
|
|
9517
|
+
const opts = {
|
|
9518
|
+
includeSteer: true,
|
|
9519
|
+
includePreviousContent: true,
|
|
9520
|
+
includePreviousFeedback: true,
|
|
9521
|
+
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
9522
|
+
includeChildMetas: true,
|
|
9523
|
+
includeCrossRefs: true,
|
|
9524
|
+
...options,
|
|
9525
|
+
};
|
|
9526
|
+
if (opts.includeSteer && ctx.steer) {
|
|
9527
|
+
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
10127
9528
|
}
|
|
10128
|
-
|
|
10129
|
-
|
|
9529
|
+
if (opts.includePreviousContent && ctx.previousContent) {
|
|
9530
|
+
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
9531
|
+
}
|
|
9532
|
+
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
9533
|
+
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
9534
|
+
}
|
|
9535
|
+
if (opts.includeChildMetas) {
|
|
9536
|
+
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas);
|
|
9537
|
+
}
|
|
9538
|
+
if (opts.includeCrossRefs) {
|
|
9539
|
+
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
10130
9540
|
}
|
|
10131
9541
|
}
|
|
10132
9542
|
/**
|
|
10133
|
-
*
|
|
9543
|
+
* Build the architect task prompt.
|
|
10134
9544
|
*
|
|
10135
|
-
* @param
|
|
10136
|
-
* @
|
|
9545
|
+
* @param ctx - Synthesis context.
|
|
9546
|
+
* @param meta - Current meta.json.
|
|
9547
|
+
* @param config - Synthesis config.
|
|
9548
|
+
* @returns The architect task prompt string.
|
|
10137
9549
|
*/
|
|
10138
|
-
function
|
|
10139
|
-
|
|
9550
|
+
function buildArchitectTask(ctx, meta, config) {
|
|
9551
|
+
const sections = [
|
|
9552
|
+
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
9553
|
+
'',
|
|
9554
|
+
meta._architect ?? config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
9555
|
+
'',
|
|
9556
|
+
'## SCOPE',
|
|
9557
|
+
`Path: ${ctx.path}`,
|
|
9558
|
+
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9559
|
+
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
9560
|
+
'',
|
|
9561
|
+
'### File listing (scope)',
|
|
9562
|
+
condenseScopeFiles(ctx.scopeFiles),
|
|
9563
|
+
];
|
|
9564
|
+
// Inject previous _builder so architect can see its own prior output
|
|
9565
|
+
if (meta._builder) {
|
|
9566
|
+
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
9567
|
+
}
|
|
9568
|
+
appendSharedSections(sections, ctx);
|
|
9569
|
+
if (ctx.archives.length > 0) {
|
|
9570
|
+
sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
|
|
9571
|
+
}
|
|
9572
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10140
9573
|
}
|
|
10141
|
-
|
|
10142
|
-
/**
|
|
10143
|
-
* Timeout recovery — salvage partial builder state after a SpawnTimeoutError.
|
|
10144
|
-
*
|
|
10145
|
-
* @module orchestrator/timeoutRecovery
|
|
10146
|
-
*/
|
|
10147
9574
|
/**
|
|
10148
|
-
*
|
|
9575
|
+
* Build the builder task prompt.
|
|
10149
9576
|
*
|
|
10150
|
-
*
|
|
10151
|
-
*
|
|
9577
|
+
* @param ctx - Synthesis context.
|
|
9578
|
+
* @param meta - Current meta.json.
|
|
9579
|
+
* @param config - Synthesis config.
|
|
9580
|
+
* @returns The builder task prompt string.
|
|
10152
9581
|
*/
|
|
10153
|
-
|
|
10154
|
-
const
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
const timeoutError = {
|
|
10168
|
-
step: 'builder',
|
|
10169
|
-
code: 'TIMEOUT',
|
|
10170
|
-
message: err.message,
|
|
10171
|
-
};
|
|
10172
|
-
await finalizeCycle({
|
|
10173
|
-
metaPath,
|
|
10174
|
-
current: currentMeta,
|
|
10175
|
-
config,
|
|
10176
|
-
architect: currentMeta._architect ?? '',
|
|
10177
|
-
builder: builderBrief,
|
|
10178
|
-
critic: currentMeta._critic ?? '',
|
|
10179
|
-
builderOutput: null,
|
|
10180
|
-
feedback: null,
|
|
10181
|
-
structureHash,
|
|
10182
|
-
synthesisCount,
|
|
10183
|
-
error: timeoutError,
|
|
10184
|
-
state: partialOutput.state,
|
|
10185
|
-
stateOnly: true,
|
|
10186
|
-
});
|
|
10187
|
-
return {
|
|
10188
|
-
synthesized: true,
|
|
10189
|
-
metaPath,
|
|
10190
|
-
error: timeoutError,
|
|
10191
|
-
};
|
|
10192
|
-
}
|
|
9582
|
+
function buildBuilderTask(ctx, meta, config) {
|
|
9583
|
+
const sections = [
|
|
9584
|
+
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
9585
|
+
'',
|
|
9586
|
+
'## TASK BRIEF (from Architect)',
|
|
9587
|
+
meta._builder ?? '(No architect brief available)',
|
|
9588
|
+
'',
|
|
9589
|
+
'## SCOPE',
|
|
9590
|
+
`Path: ${ctx.path}`,
|
|
9591
|
+
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
9592
|
+
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
9593
|
+
];
|
|
9594
|
+
if (ctx.previousState != null) {
|
|
9595
|
+
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
10193
9596
|
}
|
|
10194
|
-
|
|
9597
|
+
appendSharedSections(sections, ctx, {
|
|
9598
|
+
includeSteer: false,
|
|
9599
|
+
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
9600
|
+
});
|
|
9601
|
+
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.');
|
|
9602
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10195
9603
|
}
|
|
10196
|
-
|
|
10197
9604
|
/**
|
|
10198
|
-
*
|
|
9605
|
+
* Build the critic task prompt.
|
|
10199
9606
|
*
|
|
10200
|
-
* @
|
|
9607
|
+
* @param ctx - Synthesis context.
|
|
9608
|
+
* @param meta - Current meta.json (with _content already set by builder).
|
|
9609
|
+
* @param config - Synthesis config.
|
|
9610
|
+
* @returns The critic task prompt string.
|
|
10201
9611
|
*/
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
currentMeta._generatedAt = new Date().toISOString();
|
|
10221
|
-
const lockPath = join(node.metaPath, '.lock');
|
|
10222
|
-
const metaJsonPath = join(node.metaPath, 'meta.json');
|
|
10223
|
-
await writeFile(lockPath, JSON.stringify(currentMeta, null, 2));
|
|
10224
|
-
await copyFile(lockPath, metaJsonPath);
|
|
10225
|
-
logger?.debug({ path: node.ownerPath }, 'Skipping empty-scope entity');
|
|
10226
|
-
return { synthesized: false };
|
|
10227
|
-
}
|
|
10228
|
-
// Step 5 (deferred): Structure hash from context scope files
|
|
10229
|
-
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
10230
|
-
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
10231
|
-
// Step 8: Architect (conditional)
|
|
10232
|
-
const architectTriggered = isArchitectTriggered(currentMeta, structureChanged, steerChanged, config.architectEvery);
|
|
10233
|
-
let builderBrief = currentMeta._builder ?? '';
|
|
10234
|
-
let synthesisCount = currentMeta._synthesisCount ?? 0;
|
|
10235
|
-
let stepError = null;
|
|
10236
|
-
let architectTokens;
|
|
10237
|
-
let builderTokens;
|
|
10238
|
-
let criticTokens;
|
|
10239
|
-
// Shared base options for all finalizeCycle calls.
|
|
10240
|
-
// Note: synthesisCount is excluded because it mutates during the pipeline.
|
|
10241
|
-
const baseFinalizeOptions = {
|
|
10242
|
-
metaPath: node.metaPath,
|
|
10243
|
-
current: currentMeta,
|
|
10244
|
-
config,
|
|
10245
|
-
architect: currentMeta._architect ?? '',
|
|
10246
|
-
critic: currentMeta._critic ?? '',
|
|
10247
|
-
structureHash: newStructureHash,
|
|
10248
|
-
};
|
|
10249
|
-
if (architectTriggered) {
|
|
10250
|
-
try {
|
|
10251
|
-
await onProgress?.({
|
|
10252
|
-
type: 'phase_start',
|
|
10253
|
-
path: node.ownerPath,
|
|
10254
|
-
phase: 'architect',
|
|
10255
|
-
});
|
|
10256
|
-
const phaseStart = Date.now();
|
|
10257
|
-
const architectTask = buildArchitectTask(ctx, currentMeta, config);
|
|
10258
|
-
const architectResult = await executor.spawn(architectTask, {
|
|
10259
|
-
thinking: config.thinking,
|
|
10260
|
-
timeout: config.architectTimeout,
|
|
10261
|
-
label: 'meta-architect',
|
|
10262
|
-
});
|
|
10263
|
-
builderBrief = parseArchitectOutput(architectResult.output);
|
|
10264
|
-
architectTokens = architectResult.tokens;
|
|
10265
|
-
synthesisCount = 0;
|
|
10266
|
-
await onProgress?.({
|
|
10267
|
-
type: 'phase_complete',
|
|
10268
|
-
path: node.ownerPath,
|
|
10269
|
-
phase: 'architect',
|
|
10270
|
-
tokens: architectTokens,
|
|
10271
|
-
durationMs: Date.now() - phaseStart,
|
|
10272
|
-
});
|
|
10273
|
-
}
|
|
10274
|
-
catch (err) {
|
|
10275
|
-
stepError = toMetaError('architect', err);
|
|
10276
|
-
if (!currentMeta._builder) {
|
|
10277
|
-
// No cached builder — cycle fails
|
|
10278
|
-
await finalizeCycle({
|
|
10279
|
-
...baseFinalizeOptions,
|
|
10280
|
-
builder: '',
|
|
10281
|
-
builderOutput: null,
|
|
10282
|
-
feedback: null,
|
|
10283
|
-
synthesisCount,
|
|
10284
|
-
error: stepError,
|
|
10285
|
-
architectTokens,
|
|
10286
|
-
});
|
|
10287
|
-
return {
|
|
10288
|
-
synthesized: true,
|
|
10289
|
-
metaPath: node.metaPath,
|
|
10290
|
-
error: stepError,
|
|
10291
|
-
};
|
|
10292
|
-
}
|
|
10293
|
-
// Has cached builder — continue with existing
|
|
10294
|
-
}
|
|
10295
|
-
}
|
|
10296
|
-
// Step 9: Builder
|
|
10297
|
-
const metaForBuilder = { ...currentMeta, _builder: builderBrief };
|
|
10298
|
-
let builderOutput;
|
|
10299
|
-
try {
|
|
10300
|
-
await onProgress?.({
|
|
10301
|
-
type: 'phase_start',
|
|
10302
|
-
path: node.ownerPath,
|
|
10303
|
-
phase: 'builder',
|
|
10304
|
-
});
|
|
10305
|
-
const builderStart = Date.now();
|
|
10306
|
-
const builderTask = buildBuilderTask(ctx, metaForBuilder, config);
|
|
10307
|
-
const builderResult = await executor.spawn(builderTask, {
|
|
10308
|
-
thinking: config.thinking,
|
|
10309
|
-
timeout: config.builderTimeout,
|
|
10310
|
-
label: 'meta-builder',
|
|
10311
|
-
});
|
|
10312
|
-
builderOutput = parseBuilderOutput(builderResult.output);
|
|
10313
|
-
builderTokens = builderResult.tokens;
|
|
10314
|
-
synthesisCount++;
|
|
10315
|
-
await onProgress?.({
|
|
10316
|
-
type: 'phase_complete',
|
|
10317
|
-
path: node.ownerPath,
|
|
10318
|
-
phase: 'builder',
|
|
10319
|
-
tokens: builderTokens,
|
|
10320
|
-
durationMs: Date.now() - builderStart,
|
|
10321
|
-
});
|
|
10322
|
-
}
|
|
10323
|
-
catch (err) {
|
|
10324
|
-
if (err instanceof SpawnTimeoutError) {
|
|
10325
|
-
const recovered = await attemptTimeoutRecovery({
|
|
10326
|
-
err,
|
|
10327
|
-
currentMeta,
|
|
10328
|
-
metaPath: node.metaPath,
|
|
10329
|
-
config,
|
|
10330
|
-
builderBrief,
|
|
10331
|
-
structureHash: newStructureHash,
|
|
10332
|
-
synthesisCount,
|
|
10333
|
-
});
|
|
10334
|
-
if (recovered)
|
|
10335
|
-
return recovered;
|
|
10336
|
-
}
|
|
10337
|
-
stepError = toMetaError('builder', err);
|
|
10338
|
-
await finalizeCycle({
|
|
10339
|
-
...baseFinalizeOptions,
|
|
10340
|
-
builder: builderBrief,
|
|
10341
|
-
builderOutput: null,
|
|
10342
|
-
feedback: null,
|
|
10343
|
-
synthesisCount,
|
|
10344
|
-
error: stepError,
|
|
10345
|
-
});
|
|
10346
|
-
return { synthesized: true, metaPath: node.metaPath, error: stepError };
|
|
10347
|
-
}
|
|
10348
|
-
// Step 10: Critic
|
|
10349
|
-
const metaForCritic = {
|
|
10350
|
-
...currentMeta,
|
|
10351
|
-
_content: builderOutput.content,
|
|
10352
|
-
};
|
|
10353
|
-
let feedback = null;
|
|
10354
|
-
try {
|
|
10355
|
-
await onProgress?.({
|
|
10356
|
-
type: 'phase_start',
|
|
10357
|
-
path: node.ownerPath,
|
|
10358
|
-
phase: 'critic',
|
|
10359
|
-
});
|
|
10360
|
-
const criticStart = Date.now();
|
|
10361
|
-
const criticTask = buildCriticTask(ctx, metaForCritic, config);
|
|
10362
|
-
const criticResult = await executor.spawn(criticTask, {
|
|
10363
|
-
thinking: config.thinking,
|
|
10364
|
-
timeout: config.criticTimeout,
|
|
10365
|
-
label: 'meta-critic',
|
|
10366
|
-
});
|
|
10367
|
-
feedback = parseCriticOutput(criticResult.output);
|
|
10368
|
-
criticTokens = criticResult.tokens;
|
|
10369
|
-
stepError = null; // Clear any architect error on full success
|
|
10370
|
-
await onProgress?.({
|
|
10371
|
-
type: 'phase_complete',
|
|
10372
|
-
path: node.ownerPath,
|
|
10373
|
-
phase: 'critic',
|
|
10374
|
-
tokens: criticTokens,
|
|
10375
|
-
durationMs: Date.now() - criticStart,
|
|
10376
|
-
});
|
|
10377
|
-
}
|
|
10378
|
-
catch (err) {
|
|
10379
|
-
stepError = stepError ?? toMetaError('critic', err);
|
|
10380
|
-
}
|
|
10381
|
-
// Steps 11-12: Merge, archive, prune
|
|
10382
|
-
await finalizeCycle({
|
|
10383
|
-
...baseFinalizeOptions,
|
|
10384
|
-
builder: builderBrief,
|
|
10385
|
-
builderOutput,
|
|
10386
|
-
feedback,
|
|
10387
|
-
synthesisCount,
|
|
10388
|
-
error: stepError,
|
|
10389
|
-
architectTokens,
|
|
10390
|
-
builderTokens,
|
|
10391
|
-
criticTokens,
|
|
10392
|
-
state: builderOutput.state,
|
|
9612
|
+
function buildCriticTask(ctx, meta, config) {
|
|
9613
|
+
const sections = [
|
|
9614
|
+
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
9615
|
+
'',
|
|
9616
|
+
meta._critic ?? config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
9617
|
+
'',
|
|
9618
|
+
'## SYNTHESIS TO EVALUATE',
|
|
9619
|
+
meta._content ?? '(No content produced)',
|
|
9620
|
+
'',
|
|
9621
|
+
'## SCOPE',
|
|
9622
|
+
`Path: ${ctx.path}`,
|
|
9623
|
+
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
9624
|
+
];
|
|
9625
|
+
appendSharedSections(sections, ctx, {
|
|
9626
|
+
includePreviousContent: false,
|
|
9627
|
+
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
9628
|
+
includeChildMetas: false,
|
|
9629
|
+
includeCrossRefs: false,
|
|
10393
9630
|
});
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
metaPath: node.metaPath,
|
|
10397
|
-
error: stepError ?? undefined,
|
|
10398
|
-
};
|
|
9631
|
+
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
9632
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
10399
9633
|
}
|
|
10400
9634
|
|
|
10401
9635
|
/**
|
|
10402
|
-
*
|
|
9636
|
+
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
9637
|
+
*
|
|
9638
|
+
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
9639
|
+
* the full discovery + ownership tree build. Discovers only immediate child
|
|
9640
|
+
* `.meta/` directories.
|
|
10403
9641
|
*
|
|
10404
|
-
* @module
|
|
9642
|
+
* @module discovery/buildMinimalNode
|
|
10405
9643
|
*/
|
|
10406
|
-
async function orchestrateOnce(config, executor, watcher, targetPath, onProgress, logger) {
|
|
10407
|
-
// When targetPath is provided, skip the expensive full discovery scan.
|
|
10408
|
-
// Build a minimal node from the filesystem instead.
|
|
10409
|
-
if (targetPath) {
|
|
10410
|
-
const normalizedTarget = normalizePath(targetPath);
|
|
10411
|
-
const targetMetaJson = join(normalizedTarget, 'meta.json');
|
|
10412
|
-
if (!existsSync(targetMetaJson))
|
|
10413
|
-
return { synthesized: false };
|
|
10414
|
-
const node = await buildMinimalNode(normalizedTarget, watcher);
|
|
10415
|
-
if (!acquireLock(node.metaPath))
|
|
10416
|
-
return { synthesized: false };
|
|
10417
|
-
try {
|
|
10418
|
-
const currentMeta = await readMetaJson(normalizedTarget);
|
|
10419
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10420
|
-
}
|
|
10421
|
-
finally {
|
|
10422
|
-
releaseLock(node.metaPath);
|
|
10423
|
-
}
|
|
10424
|
-
}
|
|
10425
|
-
// Full discovery path (scheduler-driven, no specific target)
|
|
10426
|
-
// Step 1: Discover via watcher walk
|
|
10427
|
-
const discoveryStart = Date.now();
|
|
10428
|
-
const metaPaths = await discoverMetas(watcher);
|
|
10429
|
-
logger?.debug({ paths: metaPaths.length, durationMs: Date.now() - discoveryStart }, 'discovery complete');
|
|
10430
|
-
if (metaPaths.length === 0)
|
|
10431
|
-
return { synthesized: false };
|
|
10432
|
-
// Read meta.json for each discovered meta
|
|
10433
|
-
const metas = new Map();
|
|
10434
|
-
for (const mp of metaPaths) {
|
|
10435
|
-
try {
|
|
10436
|
-
metas.set(normalizePath(mp), await readMetaJson(mp));
|
|
10437
|
-
}
|
|
10438
|
-
catch {
|
|
10439
|
-
// Skip metas with unreadable meta.json
|
|
10440
|
-
continue;
|
|
10441
|
-
}
|
|
10442
|
-
}
|
|
10443
|
-
// Only build tree from paths with readable meta.json (excludes orphaned/deleted entries)
|
|
10444
|
-
const validPaths = metaPaths.filter((mp) => metas.has(normalizePath(mp)));
|
|
10445
|
-
if (validPaths.length === 0)
|
|
10446
|
-
return { synthesized: false };
|
|
10447
|
-
const tree = buildOwnershipTree(validPaths);
|
|
10448
|
-
// Steps 3-4: Staleness check + candidate selection
|
|
10449
|
-
const candidates = [];
|
|
10450
|
-
for (const treeNode of tree.nodes.values()) {
|
|
10451
|
-
const meta = metas.get(treeNode.metaPath);
|
|
10452
|
-
if (!meta)
|
|
10453
|
-
continue;
|
|
10454
|
-
const staleness = actualStaleness(meta);
|
|
10455
|
-
if (staleness > 0) {
|
|
10456
|
-
candidates.push({ node: treeNode, meta, actualStaleness: staleness });
|
|
10457
|
-
}
|
|
10458
|
-
}
|
|
10459
|
-
const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
|
|
10460
|
-
// Sort by effective staleness descending
|
|
10461
|
-
const ranked = [...weighted].sort((a, b) => b.effectiveStaleness - a.effectiveStaleness);
|
|
10462
|
-
if (ranked.length === 0)
|
|
10463
|
-
return { synthesized: false };
|
|
10464
|
-
// Find the first candidate with actual changes (if skipUnchanged)
|
|
10465
|
-
let winner = null;
|
|
10466
|
-
for (const candidate of ranked) {
|
|
10467
|
-
if (!acquireLock(candidate.node.metaPath))
|
|
10468
|
-
continue;
|
|
10469
|
-
const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
|
|
10470
|
-
if (!verifiedStale && candidate.meta._generatedAt) {
|
|
10471
|
-
// Bump _generatedAt so it doesn't win next cycle
|
|
10472
|
-
const freshMeta = await readMetaJson(candidate.node.metaPath);
|
|
10473
|
-
freshMeta._generatedAt = new Date().toISOString();
|
|
10474
|
-
await writeFile(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
|
|
10475
|
-
releaseLock(candidate.node.metaPath);
|
|
10476
|
-
if (config.skipUnchanged)
|
|
10477
|
-
continue;
|
|
10478
|
-
return { synthesized: false };
|
|
10479
|
-
}
|
|
10480
|
-
winner = candidate;
|
|
10481
|
-
break;
|
|
10482
|
-
}
|
|
10483
|
-
if (!winner)
|
|
10484
|
-
return { synthesized: false };
|
|
10485
|
-
const node = winner.node;
|
|
10486
|
-
try {
|
|
10487
|
-
const currentMeta = await readMetaJson(node.metaPath);
|
|
10488
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
10489
|
-
}
|
|
10490
|
-
finally {
|
|
10491
|
-
// Step 13: Release lock
|
|
10492
|
-
releaseLock(node.metaPath);
|
|
10493
|
-
}
|
|
10494
|
-
}
|
|
10495
9644
|
/**
|
|
10496
|
-
*
|
|
9645
|
+
* Build a minimal MetaNode for a known meta path.
|
|
10497
9646
|
*
|
|
10498
|
-
*
|
|
10499
|
-
*
|
|
9647
|
+
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
9648
|
+
* a shallow ownership tree (self + direct children only).
|
|
10500
9649
|
*
|
|
10501
|
-
* @param
|
|
10502
|
-
* @param
|
|
10503
|
-
* @
|
|
10504
|
-
* @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
|
|
10505
|
-
* @returns Array with a single result.
|
|
9650
|
+
* @param metaPath - Absolute path to the `.meta/` directory.
|
|
9651
|
+
* @param watcher - WatcherClient for filesystem enumeration.
|
|
9652
|
+
* @returns MetaNode with direct children wired.
|
|
10506
9653
|
*/
|
|
10507
|
-
async function
|
|
10508
|
-
const
|
|
10509
|
-
|
|
9654
|
+
async function buildMinimalNode(metaPath, watcher) {
|
|
9655
|
+
const normalized = normalizePath(metaPath);
|
|
9656
|
+
const ownerPath = posix.dirname(normalized);
|
|
9657
|
+
// Find child metas using watcher walk.
|
|
9658
|
+
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
9659
|
+
// to match the ownership semantics used elsewhere.
|
|
9660
|
+
const rawMetaJsonPaths = await watcher.walk([
|
|
9661
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
9662
|
+
]);
|
|
9663
|
+
const candidateMetaPaths = [
|
|
9664
|
+
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
9665
|
+
].filter((p) => p !== normalized);
|
|
9666
|
+
const candidates = candidateMetaPaths
|
|
9667
|
+
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
9668
|
+
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
9669
|
+
const directChildren = [];
|
|
9670
|
+
for (const c of candidates) {
|
|
9671
|
+
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
9672
|
+
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
9673
|
+
if (!nestedUnderExisting)
|
|
9674
|
+
directChildren.push(c);
|
|
9675
|
+
}
|
|
9676
|
+
const children = directChildren.map((c) => ({
|
|
9677
|
+
metaPath: c.metaPath,
|
|
9678
|
+
ownerPath: c.ownerPath,
|
|
9679
|
+
treeDepth: 1,
|
|
9680
|
+
children: [],
|
|
9681
|
+
parent: null,
|
|
9682
|
+
}));
|
|
9683
|
+
const node = {
|
|
9684
|
+
metaPath: normalized,
|
|
9685
|
+
ownerPath,
|
|
9686
|
+
treeDepth: 0,
|
|
9687
|
+
children,
|
|
9688
|
+
parent: null,
|
|
9689
|
+
};
|
|
9690
|
+
for (const child of children) {
|
|
9691
|
+
child.parent = node;
|
|
9692
|
+
}
|
|
9693
|
+
return node;
|
|
10510
9694
|
}
|
|
10511
9695
|
|
|
10512
9696
|
/**
|
|
@@ -10560,6 +9744,41 @@ function enforceInvariant(state) {
|
|
|
10560
9744
|
}
|
|
10561
9745
|
return result;
|
|
10562
9746
|
}
|
|
9747
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
9748
|
+
/**
|
|
9749
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
9750
|
+
* Triggers: _structureHash change, _steer change, _architect change,
|
|
9751
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
9752
|
+
*/
|
|
9753
|
+
function invalidateArchitect(state) {
|
|
9754
|
+
return enforceInvariant({
|
|
9755
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
9756
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
9757
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9758
|
+
});
|
|
9759
|
+
}
|
|
9760
|
+
/**
|
|
9761
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
9762
|
+
* builder → pending; critic → stale.
|
|
9763
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
9764
|
+
*/
|
|
9765
|
+
function invalidateBuilder(state) {
|
|
9766
|
+
if (state.architect !== 'fresh') {
|
|
9767
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
9768
|
+
return enforceInvariant({
|
|
9769
|
+
...state,
|
|
9770
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
9771
|
+
? 'stale'
|
|
9772
|
+
: state.builder,
|
|
9773
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9774
|
+
});
|
|
9775
|
+
}
|
|
9776
|
+
return enforceInvariant({
|
|
9777
|
+
...state,
|
|
9778
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
9779
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
9780
|
+
});
|
|
9781
|
+
}
|
|
10563
9782
|
// ── Phase success transitions ──────────────────────────────────────────
|
|
10564
9783
|
/**
|
|
10565
9784
|
* Architect completes successfully.
|
|
@@ -10728,7 +9947,9 @@ function derivePhaseState(meta, inputs) {
|
|
|
10728
9947
|
}
|
|
10729
9948
|
// Check architect invalidation (when inputs are provided)
|
|
10730
9949
|
if (inputs) {
|
|
10731
|
-
|
|
9950
|
+
// Progressive metas: structure changes invalidate builder, not architect
|
|
9951
|
+
const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
|
|
9952
|
+
const architectInvalidated = structureInvalidatesArchitect ||
|
|
10732
9953
|
inputs.steerChanged ||
|
|
10733
9954
|
inputs.architectChanged ||
|
|
10734
9955
|
inputs.crossRefsChanged ||
|
|
@@ -10740,6 +9961,14 @@ function derivePhaseState(meta, inputs) {
|
|
|
10740
9961
|
critic: 'stale',
|
|
10741
9962
|
};
|
|
10742
9963
|
}
|
|
9964
|
+
// Progressive meta with structure change: builder-only invalidation
|
|
9965
|
+
if (inputs.structureChanged && meta._state !== undefined) {
|
|
9966
|
+
return {
|
|
9967
|
+
architect: 'fresh',
|
|
9968
|
+
builder: 'pending',
|
|
9969
|
+
critic: 'stale',
|
|
9970
|
+
};
|
|
9971
|
+
}
|
|
10743
9972
|
}
|
|
10744
9973
|
// Has _builder but no _content: builder is pending
|
|
10745
9974
|
if (meta._builder && !meta._content) {
|
|
@@ -10761,6 +9990,154 @@ function derivePhaseState(meta, inputs) {
|
|
|
10761
9990
|
return freshPhaseState();
|
|
10762
9991
|
}
|
|
10763
9992
|
|
|
9993
|
+
/**
|
|
9994
|
+
* Compute a structure hash from a sorted file listing.
|
|
9995
|
+
*
|
|
9996
|
+
* Used to detect when directory structure changes, triggering
|
|
9997
|
+
* an architect re-run.
|
|
9998
|
+
*
|
|
9999
|
+
* @module structureHash
|
|
10000
|
+
*/
|
|
10001
|
+
/**
|
|
10002
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
10003
|
+
*
|
|
10004
|
+
* @param filePaths - Array of file paths in scope.
|
|
10005
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
10006
|
+
*/
|
|
10007
|
+
function computeStructureHash(filePaths) {
|
|
10008
|
+
const sorted = [...filePaths].sort();
|
|
10009
|
+
const content = sorted.join('\n');
|
|
10010
|
+
return createHash('sha256').update(content).digest('hex');
|
|
10011
|
+
}
|
|
10012
|
+
|
|
10013
|
+
/**
|
|
10014
|
+
* Per-tick invalidation pass.
|
|
10015
|
+
*
|
|
10016
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
10017
|
+
* then applies the cascade to update _phaseState.
|
|
10018
|
+
*
|
|
10019
|
+
* @module phaseState/invalidate
|
|
10020
|
+
*/
|
|
10021
|
+
/**
|
|
10022
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
10023
|
+
*
|
|
10024
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
10025
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
10026
|
+
* @param config - MetaConfig for architectEvery.
|
|
10027
|
+
* @param node - MetaNode for archive access.
|
|
10028
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
10029
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
10030
|
+
* @returns Updated phase state and invalidation details.
|
|
10031
|
+
*/
|
|
10032
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
10033
|
+
let phaseState = meta._phaseState ?? {
|
|
10034
|
+
architect: 'fresh',
|
|
10035
|
+
builder: 'fresh',
|
|
10036
|
+
critic: 'fresh',
|
|
10037
|
+
};
|
|
10038
|
+
// ── Architect-level inputs ──
|
|
10039
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
10040
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
10041
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
10042
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10043
|
+
// _architect change: compare current vs. archive
|
|
10044
|
+
const architectChanged = latestArchive
|
|
10045
|
+
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
10046
|
+
: Boolean(meta._architect);
|
|
10047
|
+
// _crossRefs declaration change
|
|
10048
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
10049
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
10050
|
+
.slice()
|
|
10051
|
+
.sort()
|
|
10052
|
+
.join(',');
|
|
10053
|
+
const crossRefsDeclChanged = latestArchive
|
|
10054
|
+
? currentRefs !== archiveRefs
|
|
10055
|
+
: currentRefs.length > 0;
|
|
10056
|
+
const architectInvalidators = [];
|
|
10057
|
+
if (structureChanged) {
|
|
10058
|
+
if (meta._state !== undefined) {
|
|
10059
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
10060
|
+
phaseState = invalidateBuilder(phaseState);
|
|
10061
|
+
}
|
|
10062
|
+
else {
|
|
10063
|
+
architectInvalidators.push('structureHash');
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
if (steerChanged)
|
|
10067
|
+
architectInvalidators.push('steer');
|
|
10068
|
+
if (architectChanged)
|
|
10069
|
+
architectInvalidators.push('_architect');
|
|
10070
|
+
if (crossRefsDeclChanged)
|
|
10071
|
+
architectInvalidators.push('_crossRefs');
|
|
10072
|
+
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
10073
|
+
architectInvalidators.push('architectEvery');
|
|
10074
|
+
}
|
|
10075
|
+
// First-run check: no _builder means architect must run
|
|
10076
|
+
const firstRun = !meta._builder;
|
|
10077
|
+
if (architectInvalidators.length > 0 || firstRun) {
|
|
10078
|
+
phaseState = invalidateArchitect(phaseState);
|
|
10079
|
+
}
|
|
10080
|
+
// ── Builder-level inputs ──
|
|
10081
|
+
// Scope file mtime check — if any file newer than _generatedAt
|
|
10082
|
+
const scopeMtimeMax = null;
|
|
10083
|
+
// Note: actual mtime check is done by the caller or via isStale;
|
|
10084
|
+
// here we just detect cross-ref content changes for the cascade.
|
|
10085
|
+
// Cross-ref _content change (builder-invalidating)
|
|
10086
|
+
let crossRefContentChanged = false;
|
|
10087
|
+
return {
|
|
10088
|
+
phaseState,
|
|
10089
|
+
architectInvalidators,
|
|
10090
|
+
stalenessInputs: {
|
|
10091
|
+
structureHash,
|
|
10092
|
+
steerChanged,
|
|
10093
|
+
architectChanged,
|
|
10094
|
+
crossRefsDeclChanged,
|
|
10095
|
+
scopeMtimeMax,
|
|
10096
|
+
crossRefContentChanged,
|
|
10097
|
+
},
|
|
10098
|
+
structureHash,
|
|
10099
|
+
steerChanged,
|
|
10100
|
+
};
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
/**
|
|
10104
|
+
* Weighted staleness formula for candidate selection.
|
|
10105
|
+
*
|
|
10106
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10107
|
+
*
|
|
10108
|
+
* @module scheduling/weightedFormula
|
|
10109
|
+
*/
|
|
10110
|
+
/**
|
|
10111
|
+
* Compute effective staleness for a set of candidates.
|
|
10112
|
+
*
|
|
10113
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
10114
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
10115
|
+
*
|
|
10116
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
10117
|
+
* metas to tune how much their tree position affects scheduling.
|
|
10118
|
+
*
|
|
10119
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
10120
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
10121
|
+
* @returns Same array with effectiveStaleness computed.
|
|
10122
|
+
*/
|
|
10123
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
10124
|
+
if (candidates.length === 0)
|
|
10125
|
+
return [];
|
|
10126
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
10127
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
10128
|
+
// Normalize: shift so minimum becomes 0
|
|
10129
|
+
const minDepth = Math.min(...depths);
|
|
10130
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
10131
|
+
return candidates.map((c, i) => {
|
|
10132
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
10133
|
+
return {
|
|
10134
|
+
...c,
|
|
10135
|
+
effectiveStaleness: c.actualStaleness *
|
|
10136
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
10137
|
+
};
|
|
10138
|
+
});
|
|
10139
|
+
}
|
|
10140
|
+
|
|
10764
10141
|
/**
|
|
10765
10142
|
* Corpus-wide phase scheduler.
|
|
10766
10143
|
*
|
|
@@ -10773,18 +10150,30 @@ function derivePhaseState(meta, inputs) {
|
|
|
10773
10150
|
/**
|
|
10774
10151
|
* Build phase candidates from listMetas entries.
|
|
10775
10152
|
*
|
|
10776
|
-
* Derives phase state
|
|
10153
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
10154
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
10777
10155
|
* Used by orchestratePhase, queue route, and status route.
|
|
10778
10156
|
*/
|
|
10779
|
-
function buildPhaseCandidates(entries) {
|
|
10780
|
-
return entries.map((entry) =>
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10157
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
10158
|
+
return entries.map((entry) => {
|
|
10159
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
10160
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
10161
|
+
if (entry.meta._phaseState) {
|
|
10162
|
+
const needsArchitect = !entry.meta._builder ||
|
|
10163
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
10164
|
+
if (needsArchitect && ps.architect === 'fresh') {
|
|
10165
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
10166
|
+
}
|
|
10167
|
+
}
|
|
10168
|
+
return {
|
|
10169
|
+
node: entry.node,
|
|
10170
|
+
meta: entry.meta,
|
|
10171
|
+
phaseState: ps,
|
|
10172
|
+
actualStaleness: entry.stalenessSeconds,
|
|
10173
|
+
locked: entry.locked,
|
|
10174
|
+
disabled: entry.disabled,
|
|
10175
|
+
};
|
|
10176
|
+
});
|
|
10788
10177
|
}
|
|
10789
10178
|
/**
|
|
10790
10179
|
* Rank all eligible phase candidates by priority.
|
|
@@ -10837,14 +10226,132 @@ function rankPhaseCandidates(metas, depthWeight) {
|
|
|
10837
10226
|
return candidates;
|
|
10838
10227
|
}
|
|
10839
10228
|
/**
|
|
10840
|
-
* Select the best phase candidate across the corpus.
|
|
10229
|
+
* Select the best phase candidate across the corpus.
|
|
10230
|
+
*
|
|
10231
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
10232
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
10233
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
10234
|
+
*/
|
|
10235
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
10236
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10239
|
+
/**
|
|
10240
|
+
* Shared error utilities.
|
|
10241
|
+
*
|
|
10242
|
+
* @module errors
|
|
10243
|
+
*/
|
|
10244
|
+
/**
|
|
10245
|
+
* Wrap an unknown caught value into a MetaError.
|
|
10246
|
+
*
|
|
10247
|
+
* @param step - Which synthesis step failed.
|
|
10248
|
+
* @param err - The caught error value.
|
|
10249
|
+
* @param code - Error classification code.
|
|
10250
|
+
* @returns A structured MetaError.
|
|
10251
|
+
*/
|
|
10252
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
10253
|
+
return {
|
|
10254
|
+
step,
|
|
10255
|
+
code,
|
|
10256
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10257
|
+
};
|
|
10258
|
+
}
|
|
10259
|
+
|
|
10260
|
+
/**
|
|
10261
|
+
* Parse subprocess outputs for each synthesis step.
|
|
10262
|
+
*
|
|
10263
|
+
* - Architect: returns text \> _builder
|
|
10264
|
+
* - Builder: returns JSON \> _content + structured fields
|
|
10265
|
+
* - Critic: returns text \> _feedback
|
|
10266
|
+
*
|
|
10267
|
+
* @module orchestrator/parseOutput
|
|
10268
|
+
*/
|
|
10269
|
+
/**
|
|
10270
|
+
* Parse architect output. The architect returns a task brief as text.
|
|
10271
|
+
*
|
|
10272
|
+
* @param output - Raw subprocess output.
|
|
10273
|
+
* @returns The task brief string.
|
|
10274
|
+
*/
|
|
10275
|
+
function parseArchitectOutput(output) {
|
|
10276
|
+
return output.trim();
|
|
10277
|
+
}
|
|
10278
|
+
/**
|
|
10279
|
+
* Parse builder output. The builder returns JSON with _content and optional fields.
|
|
10280
|
+
*
|
|
10281
|
+
* Attempts JSON parse first. If that fails, treats the entire output as _content.
|
|
10282
|
+
*
|
|
10283
|
+
* @param output - Raw subprocess output.
|
|
10284
|
+
* @returns Parsed builder output with content and structured fields.
|
|
10285
|
+
*/
|
|
10286
|
+
function parseBuilderOutput(output) {
|
|
10287
|
+
const trimmed = output.trim();
|
|
10288
|
+
// Strategy 1: Try to parse the entire output as JSON directly
|
|
10289
|
+
const direct = tryParseJson(trimmed);
|
|
10290
|
+
if (direct)
|
|
10291
|
+
return direct;
|
|
10292
|
+
// Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
|
|
10293
|
+
const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
10294
|
+
const fenceMatches = [];
|
|
10295
|
+
let match;
|
|
10296
|
+
while ((match = fencePattern.exec(trimmed)) !== null) {
|
|
10297
|
+
fenceMatches.push(match[1].trim());
|
|
10298
|
+
}
|
|
10299
|
+
// Try last fence first (most likely to be the actual output)
|
|
10300
|
+
for (let i = fenceMatches.length - 1; i >= 0; i--) {
|
|
10301
|
+
const result = tryParseJson(fenceMatches[i]);
|
|
10302
|
+
if (result)
|
|
10303
|
+
return result;
|
|
10304
|
+
}
|
|
10305
|
+
// Strategy 3: Find outermost { ... } braces
|
|
10306
|
+
const firstBrace = trimmed.indexOf('{');
|
|
10307
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
10308
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
10309
|
+
const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
|
|
10310
|
+
if (result)
|
|
10311
|
+
return result;
|
|
10312
|
+
}
|
|
10313
|
+
// Fallback: treat entire output as content
|
|
10314
|
+
return { content: trimmed, fields: {} };
|
|
10315
|
+
}
|
|
10316
|
+
/** Try to parse a string as JSON and extract builder output fields. */
|
|
10317
|
+
function tryParseJson(str) {
|
|
10318
|
+
try {
|
|
10319
|
+
const raw = JSON.parse(str);
|
|
10320
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
10321
|
+
return null;
|
|
10322
|
+
}
|
|
10323
|
+
const parsed = raw;
|
|
10324
|
+
// Extract _content
|
|
10325
|
+
const content = typeof parsed['_content'] === 'string'
|
|
10326
|
+
? parsed['_content']
|
|
10327
|
+
: typeof parsed['content'] === 'string'
|
|
10328
|
+
? parsed['content']
|
|
10329
|
+
: null;
|
|
10330
|
+
if (content === null)
|
|
10331
|
+
return null;
|
|
10332
|
+
// Extract _state (the ONLY underscore key the builder is allowed to set)
|
|
10333
|
+
const state = '_state' in parsed ? parsed['_state'] : undefined;
|
|
10334
|
+
// Extract non-underscore fields
|
|
10335
|
+
const fields = {};
|
|
10336
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
10337
|
+
if (!key.startsWith('_') && key !== 'content') {
|
|
10338
|
+
fields[key] = value;
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10341
|
+
return { content, fields, ...(state !== undefined ? { state } : {}) };
|
|
10342
|
+
}
|
|
10343
|
+
catch {
|
|
10344
|
+
return null;
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
/**
|
|
10348
|
+
* Parse critic output. The critic returns evaluation text.
|
|
10841
10349
|
*
|
|
10842
|
-
* @param
|
|
10843
|
-
* @
|
|
10844
|
-
* @returns The winning candidate, or null if no phase is ready.
|
|
10350
|
+
* @param output - Raw subprocess output.
|
|
10351
|
+
* @returns The feedback string.
|
|
10845
10352
|
*/
|
|
10846
|
-
function
|
|
10847
|
-
return
|
|
10353
|
+
function parseCriticOutput(output) {
|
|
10354
|
+
return output.trim();
|
|
10848
10355
|
}
|
|
10849
10356
|
|
|
10850
10357
|
/**
|
|
@@ -11105,7 +10612,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
|
|
|
11105
10612
|
if (metaResult.entries.length === 0)
|
|
11106
10613
|
return { executed: false };
|
|
11107
10614
|
// Build candidates with phase state (including invalidation + auto-retry)
|
|
11108
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
10615
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
11109
10616
|
// Select best phase candidate
|
|
11110
10617
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
11111
10618
|
if (!winner) {
|
|
@@ -11780,46 +11287,6 @@ function buildMetaRules(config) {
|
|
|
11780
11287
|
},
|
|
11781
11288
|
renderAs: 'md',
|
|
11782
11289
|
},
|
|
11783
|
-
{
|
|
11784
|
-
name: 'meta-config',
|
|
11785
|
-
description: 'jeeves-meta configuration file',
|
|
11786
|
-
match: {
|
|
11787
|
-
properties: {
|
|
11788
|
-
file: {
|
|
11789
|
-
properties: {
|
|
11790
|
-
path: {
|
|
11791
|
-
type: 'string',
|
|
11792
|
-
glob: '**/jeeves-meta{.config.json,/config.json}',
|
|
11793
|
-
},
|
|
11794
|
-
},
|
|
11795
|
-
},
|
|
11796
|
-
},
|
|
11797
|
-
},
|
|
11798
|
-
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
11799
|
-
render: {
|
|
11800
|
-
frontmatter: [
|
|
11801
|
-
'watcherUrl',
|
|
11802
|
-
'gatewayUrl',
|
|
11803
|
-
'architectEvery',
|
|
11804
|
-
'depthWeight',
|
|
11805
|
-
'maxArchive',
|
|
11806
|
-
'maxLines',
|
|
11807
|
-
],
|
|
11808
|
-
body: [
|
|
11809
|
-
{
|
|
11810
|
-
path: 'json.defaultArchitect',
|
|
11811
|
-
heading: 2,
|
|
11812
|
-
label: 'Default Architect Prompt',
|
|
11813
|
-
},
|
|
11814
|
-
{
|
|
11815
|
-
path: 'json.defaultCritic',
|
|
11816
|
-
heading: 2,
|
|
11817
|
-
label: 'Default Critic Prompt',
|
|
11818
|
-
},
|
|
11819
|
-
],
|
|
11820
|
-
},
|
|
11821
|
-
renderAs: 'md',
|
|
11822
|
-
},
|
|
11823
11290
|
];
|
|
11824
11291
|
}
|
|
11825
11292
|
/**
|
|
@@ -12063,13 +11530,15 @@ class Scheduler {
|
|
|
12063
11530
|
queue;
|
|
12064
11531
|
logger;
|
|
12065
11532
|
watcher;
|
|
11533
|
+
cache;
|
|
12066
11534
|
registrar = null;
|
|
12067
11535
|
currentExpression;
|
|
12068
|
-
constructor(config, queue, logger, watcher) {
|
|
11536
|
+
constructor(config, queue, logger, watcher, cache) {
|
|
12069
11537
|
this.config = config;
|
|
12070
11538
|
this.queue = queue;
|
|
12071
11539
|
this.logger = logger;
|
|
12072
11540
|
this.watcher = watcher;
|
|
11541
|
+
this.cache = cache;
|
|
12073
11542
|
this.currentExpression = config.schedule;
|
|
12074
11543
|
}
|
|
12075
11544
|
/** Set the rule registrar for watcher restart detection. */
|
|
@@ -12186,8 +11655,8 @@ class Scheduler {
|
|
|
12186
11655
|
*/
|
|
12187
11656
|
async discoverNextPhase() {
|
|
12188
11657
|
try {
|
|
12189
|
-
const result = await
|
|
12190
|
-
const candidates = buildPhaseCandidates(result.entries);
|
|
11658
|
+
const result = await this.cache.get(this.config, this.watcher);
|
|
11659
|
+
const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
|
|
12191
11660
|
const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
|
|
12192
11661
|
if (!winner)
|
|
12193
11662
|
return null;
|
|
@@ -12612,11 +12081,11 @@ function registerMetasUpdateRoute(app, deps) {
|
|
|
12612
12081
|
*/
|
|
12613
12082
|
function registerPreviewRoute(app, deps) {
|
|
12614
12083
|
app.get('/preview', async (request, reply) => {
|
|
12615
|
-
const { config, watcher } = deps;
|
|
12084
|
+
const { config, watcher, cache } = deps;
|
|
12616
12085
|
const query = request.query;
|
|
12617
12086
|
let result;
|
|
12618
12087
|
try {
|
|
12619
|
-
result = await
|
|
12088
|
+
result = await cache.get(config, watcher);
|
|
12620
12089
|
}
|
|
12621
12090
|
catch {
|
|
12622
12091
|
return reply.status(503).send({
|
|
@@ -12636,40 +12105,24 @@ function registerPreviewRoute(app, deps) {
|
|
|
12636
12105
|
}
|
|
12637
12106
|
}
|
|
12638
12107
|
else {
|
|
12639
|
-
// Select
|
|
12640
|
-
const
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
node: e.node,
|
|
12644
|
-
meta: e.meta,
|
|
12645
|
-
actualStaleness: e.stalenessSeconds,
|
|
12646
|
-
}));
|
|
12647
|
-
const stalestPath = discoverStalestPath(stale, config.depthWeight);
|
|
12648
|
-
if (!stalestPath) {
|
|
12108
|
+
// Select best phase candidate
|
|
12109
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12110
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12111
|
+
if (!winner) {
|
|
12649
12112
|
return { message: 'No stale metas found. Nothing to synthesize.' };
|
|
12650
12113
|
}
|
|
12651
|
-
targetNode = findNode(result.tree,
|
|
12114
|
+
targetNode = findNode(result.tree, winner.node.metaPath);
|
|
12652
12115
|
}
|
|
12653
12116
|
const meta = await readMetaJson(targetNode.metaPath);
|
|
12654
12117
|
// Scope files
|
|
12655
12118
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
12656
|
-
|
|
12119
|
+
// Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
|
|
12120
|
+
const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
|
|
12121
|
+
const { architectInvalidators, stalenessInputs } = invalidation;
|
|
12122
|
+
const { structureHash } = invalidation;
|
|
12657
12123
|
const structureChanged = structureHash !== meta._structureHash;
|
|
12658
|
-
const
|
|
12659
|
-
const
|
|
12660
|
-
// _architect change detection
|
|
12661
|
-
const architectChanged = latestArchive
|
|
12662
|
-
? (meta._architect ?? '') !== (latestArchive._architect ?? '')
|
|
12663
|
-
: Boolean(meta._architect);
|
|
12664
|
-
// _crossRefs declaration change detection
|
|
12665
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
12666
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
12667
|
-
.slice()
|
|
12668
|
-
.sort()
|
|
12669
|
-
.join(',');
|
|
12670
|
-
const crossRefsDeclChanged = latestArchive
|
|
12671
|
-
? currentRefs !== archiveRefs
|
|
12672
|
-
: currentRefs.length > 0;
|
|
12124
|
+
const { steerChanged } = invalidation;
|
|
12125
|
+
const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
|
|
12673
12126
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
12674
12127
|
// Delta files
|
|
12675
12128
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
@@ -12694,30 +12147,6 @@ function registerPreviewRoute(app, deps) {
|
|
|
12694
12147
|
});
|
|
12695
12148
|
const owedPhase = getOwedPhase(phaseState);
|
|
12696
12149
|
const priorityBand = getPriorityBand(phaseState);
|
|
12697
|
-
// Architect invalidators
|
|
12698
|
-
const architectInvalidators = [];
|
|
12699
|
-
if (owedPhase === 'architect') {
|
|
12700
|
-
if (structureChanged)
|
|
12701
|
-
architectInvalidators.push('structureHash');
|
|
12702
|
-
if (steerChanged)
|
|
12703
|
-
architectInvalidators.push('steer');
|
|
12704
|
-
if (architectChanged)
|
|
12705
|
-
architectInvalidators.push('_architect');
|
|
12706
|
-
if (crossRefsDeclChanged)
|
|
12707
|
-
architectInvalidators.push('_crossRefs');
|
|
12708
|
-
if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
|
|
12709
|
-
architectInvalidators.push('architectEvery');
|
|
12710
|
-
}
|
|
12711
|
-
}
|
|
12712
|
-
// Staleness inputs
|
|
12713
|
-
const stalenessInputs = {
|
|
12714
|
-
structureHash,
|
|
12715
|
-
steerChanged,
|
|
12716
|
-
architectChanged,
|
|
12717
|
-
crossRefsDeclChanged,
|
|
12718
|
-
scopeMtimeMax: null,
|
|
12719
|
-
crossRefContentChanged: false,
|
|
12720
|
-
};
|
|
12721
12150
|
return {
|
|
12722
12151
|
path: targetNode.metaPath,
|
|
12723
12152
|
staleness: {
|
|
@@ -12791,8 +12220,8 @@ function registerQueueRoutes(app, deps) {
|
|
|
12791
12220
|
// ranked by scheduler priority (computed on read, not persisted)
|
|
12792
12221
|
let automatic = [];
|
|
12793
12222
|
try {
|
|
12794
|
-
const metaResult = await
|
|
12795
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12223
|
+
const metaResult = await deps.cache.get(deps.config, deps.watcher);
|
|
12224
|
+
const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
|
|
12796
12225
|
const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
|
|
12797
12226
|
automatic = ranked.map((c) => ({
|
|
12798
12227
|
path: c.node.metaPath,
|
|
@@ -12987,7 +12416,7 @@ function registerStatusRoute(app, deps) {
|
|
|
12987
12416
|
name: SERVICE_NAME,
|
|
12988
12417
|
version: SERVICE_VERSION,
|
|
12989
12418
|
getHealth: async () => {
|
|
12990
|
-
const { config, queue, scheduler, stats, watcher } = deps;
|
|
12419
|
+
const { config, queue, scheduler, stats, watcher, cache } = deps;
|
|
12991
12420
|
// On-demand dependency checks
|
|
12992
12421
|
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
12993
12422
|
checkWatcher(config.watcherUrl),
|
|
@@ -13001,7 +12430,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13001
12430
|
};
|
|
13002
12431
|
let nextPhase = null;
|
|
13003
12432
|
try {
|
|
13004
|
-
const metaResult = await
|
|
12433
|
+
const metaResult = await cache.get(config, watcher);
|
|
13005
12434
|
// Count raw phase states (before retry) for display
|
|
13006
12435
|
for (const entry of metaResult.entries) {
|
|
13007
12436
|
const ps = derivePhaseState(entry.meta);
|
|
@@ -13010,7 +12439,7 @@ function registerStatusRoute(app, deps) {
|
|
|
13010
12439
|
}
|
|
13011
12440
|
}
|
|
13012
12441
|
// Build candidates (with auto-retry) for scheduling
|
|
13013
|
-
const candidates = buildPhaseCandidates(metaResult.entries);
|
|
12442
|
+
const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
|
|
13014
12443
|
// Find next phase candidate
|
|
13015
12444
|
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
13016
12445
|
if (winner) {
|
|
@@ -13073,7 +12502,7 @@ const synthesizeBodySchema = z.object({
|
|
|
13073
12502
|
function registerSynthesizeRoute(app, deps) {
|
|
13074
12503
|
app.post('/synthesize', async (request, reply) => {
|
|
13075
12504
|
const body = synthesizeBodySchema.parse(request.body);
|
|
13076
|
-
const { config, watcher, queue } = deps;
|
|
12505
|
+
const { config, watcher, queue, cache } = deps;
|
|
13077
12506
|
if (body.path) {
|
|
13078
12507
|
// Path-targeted trigger: create override entry
|
|
13079
12508
|
const targetPath = resolveMetaDir(body.path);
|
|
@@ -13110,7 +12539,7 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13110
12539
|
// Corpus-wide trigger: discover stalest candidate
|
|
13111
12540
|
let result;
|
|
13112
12541
|
try {
|
|
13113
|
-
result = await
|
|
12542
|
+
result = await cache.get(config, watcher);
|
|
13114
12543
|
}
|
|
13115
12544
|
catch {
|
|
13116
12545
|
return reply.status(503).send({
|
|
@@ -13118,20 +12547,15 @@ function registerSynthesizeRoute(app, deps) {
|
|
|
13118
12547
|
message: 'Watcher unreachable — cannot discover candidates',
|
|
13119
12548
|
});
|
|
13120
12549
|
}
|
|
13121
|
-
const
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
node: e.node,
|
|
13125
|
-
meta: e.meta,
|
|
13126
|
-
actualStaleness: e.stalenessSeconds,
|
|
13127
|
-
}));
|
|
13128
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
13129
|
-
if (!stalest) {
|
|
12550
|
+
const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
|
|
12551
|
+
const winner = selectPhaseCandidate(candidates, config.depthWeight);
|
|
12552
|
+
if (!winner) {
|
|
13130
12553
|
return reply.code(200).send({
|
|
13131
12554
|
status: 'skipped',
|
|
13132
12555
|
message: 'No stale metas found. Nothing to synthesize.',
|
|
13133
12556
|
});
|
|
13134
12557
|
}
|
|
12558
|
+
const stalest = winner.node.metaPath;
|
|
13135
12559
|
const enqueueResult = queue.enqueue(stalest);
|
|
13136
12560
|
return reply.code(202).send({
|
|
13137
12561
|
status: 'accepted',
|
|
@@ -13232,6 +12656,18 @@ function createServer(options) {
|
|
|
13232
12656
|
// Fastify 5 requires `loggerInstance` for external pino loggers
|
|
13233
12657
|
const app = Fastify({
|
|
13234
12658
|
loggerInstance: options.logger,
|
|
12659
|
+
requestTimeout: 30_000,
|
|
12660
|
+
});
|
|
12661
|
+
// Readiness gate: return 503 while service is initializing
|
|
12662
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
12663
|
+
if (options.deps.ready)
|
|
12664
|
+
return;
|
|
12665
|
+
const url = request.url;
|
|
12666
|
+
if (url === '/config' || url.startsWith('/config/apply'))
|
|
12667
|
+
return;
|
|
12668
|
+
return reply
|
|
12669
|
+
.status(503)
|
|
12670
|
+
.send({ status: 'starting', message: 'Service initializing' });
|
|
13235
12671
|
});
|
|
13236
12672
|
registerRoutes(app, options.deps);
|
|
13237
12673
|
return app;
|
|
@@ -13407,8 +12843,9 @@ async function startService(config, configPath) {
|
|
|
13407
12843
|
lastCycleAt: null,
|
|
13408
12844
|
};
|
|
13409
12845
|
const queue = new SynthesisQueue(logger);
|
|
12846
|
+
const cache = new MetaCache();
|
|
13410
12847
|
// Scheduler (needs watcher for discovery)
|
|
13411
|
-
const scheduler = new Scheduler(config, queue, logger, watcher);
|
|
12848
|
+
const scheduler = new Scheduler(config, queue, logger, watcher, cache);
|
|
13412
12849
|
const routeDeps = {
|
|
13413
12850
|
config,
|
|
13414
12851
|
logger,
|
|
@@ -13416,6 +12853,8 @@ async function startService(config, configPath) {
|
|
|
13416
12853
|
watcher,
|
|
13417
12854
|
scheduler,
|
|
13418
12855
|
stats,
|
|
12856
|
+
cache,
|
|
12857
|
+
ready: false,
|
|
13419
12858
|
executor,
|
|
13420
12859
|
configPath,
|
|
13421
12860
|
};
|
|
@@ -13466,6 +12905,9 @@ async function startService(config, configPath) {
|
|
|
13466
12905
|
}
|
|
13467
12906
|
await progress.report(evt);
|
|
13468
12907
|
}, logger);
|
|
12908
|
+
// Invalidate cache only when a phase was actually executed
|
|
12909
|
+
if (result.executed)
|
|
12910
|
+
cache.invalidate();
|
|
13469
12911
|
const durationMs = Date.now() - startMs;
|
|
13470
12912
|
if (!result.executed) {
|
|
13471
12913
|
logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
|
|
@@ -13529,9 +12971,13 @@ async function startService(config, configPath) {
|
|
|
13529
12971
|
scheduler.setRegistrar(registrar);
|
|
13530
12972
|
routeDeps.registrar = registrar;
|
|
13531
12973
|
void registrar.register().then(() => {
|
|
12974
|
+
routeDeps.ready = true;
|
|
13532
12975
|
if (registrar.isRegistered) {
|
|
13533
12976
|
void verifyRuleApplication(watcher, logger);
|
|
13534
12977
|
}
|
|
12978
|
+
}, () => {
|
|
12979
|
+
// Registration failed after max retries — mark ready anyway
|
|
12980
|
+
routeDeps.ready = true;
|
|
13535
12981
|
});
|
|
13536
12982
|
// Periodic watcher health check (independent of scheduler)
|
|
13537
12983
|
const healthCheck = new WatcherHealthCheck({
|
|
@@ -13613,4 +13059,152 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
13613
13059
|
customCliCommands: registerCustomCliCommands,
|
|
13614
13060
|
});
|
|
13615
13061
|
|
|
13616
|
-
|
|
13062
|
+
/**
|
|
13063
|
+
* Exponential moving average helper for token tracking.
|
|
13064
|
+
*
|
|
13065
|
+
* @module ema
|
|
13066
|
+
*/
|
|
13067
|
+
const DEFAULT_DECAY = 0.3;
|
|
13068
|
+
/**
|
|
13069
|
+
* Compute exponential moving average.
|
|
13070
|
+
*
|
|
13071
|
+
* @param current - New observation.
|
|
13072
|
+
* @param previous - Previous EMA value, or undefined for first observation.
|
|
13073
|
+
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
13074
|
+
* @returns Updated EMA.
|
|
13075
|
+
*/
|
|
13076
|
+
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
13077
|
+
if (previous === undefined)
|
|
13078
|
+
return current;
|
|
13079
|
+
return decay * current + (1 - decay) * previous;
|
|
13080
|
+
}
|
|
13081
|
+
|
|
13082
|
+
/**
|
|
13083
|
+
* Structured error from a synthesis step failure.
|
|
13084
|
+
*
|
|
13085
|
+
* @module schema/error
|
|
13086
|
+
*/
|
|
13087
|
+
/** Zod schema for synthesis step errors. */
|
|
13088
|
+
const metaErrorSchema = z.object({
|
|
13089
|
+
/** Which step failed: 'architect', 'builder', or 'critic'. */
|
|
13090
|
+
step: z.enum(['architect', 'builder', 'critic']),
|
|
13091
|
+
/** Error classification code. */
|
|
13092
|
+
code: z.string(),
|
|
13093
|
+
/** Human-readable error message. */
|
|
13094
|
+
message: z.string(),
|
|
13095
|
+
});
|
|
13096
|
+
|
|
13097
|
+
/**
|
|
13098
|
+
* Zod schema for .meta/meta.json files.
|
|
13099
|
+
*
|
|
13100
|
+
* Reserved properties are underscore-prefixed and engine-managed.
|
|
13101
|
+
* All other keys are open schema (builder output).
|
|
13102
|
+
*
|
|
13103
|
+
* @module schema/meta
|
|
13104
|
+
*/
|
|
13105
|
+
/** Valid states for a synthesis phase. */
|
|
13106
|
+
const phaseStatuses = [
|
|
13107
|
+
'fresh',
|
|
13108
|
+
'stale',
|
|
13109
|
+
'pending',
|
|
13110
|
+
'running',
|
|
13111
|
+
'failed',
|
|
13112
|
+
];
|
|
13113
|
+
/** Zod schema for a per-phase status value. */
|
|
13114
|
+
const phaseStatusSchema = z.enum(phaseStatuses);
|
|
13115
|
+
/** Zod schema for the per-meta phase state record. */
|
|
13116
|
+
const phaseStateSchema = z.object({
|
|
13117
|
+
architect: phaseStatusSchema,
|
|
13118
|
+
builder: phaseStatusSchema,
|
|
13119
|
+
critic: phaseStatusSchema,
|
|
13120
|
+
});
|
|
13121
|
+
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
13122
|
+
const metaJsonSchema = z
|
|
13123
|
+
.object({
|
|
13124
|
+
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
13125
|
+
_id: z.uuid().optional(),
|
|
13126
|
+
/** Human-provided steering prompt. Optional. */
|
|
13127
|
+
_steer: z.string().optional(),
|
|
13128
|
+
/**
|
|
13129
|
+
* Explicit cross-references to other meta owner paths.
|
|
13130
|
+
* Referenced metas' _content is included as architect/builder context.
|
|
13131
|
+
*/
|
|
13132
|
+
_crossRefs: z.array(z.string()).optional(),
|
|
13133
|
+
/** Architect system prompt used this turn. Defaults from config. */
|
|
13134
|
+
_architect: z.string().optional(),
|
|
13135
|
+
/**
|
|
13136
|
+
* Task brief generated by the architect. Cached and reused across cycles;
|
|
13137
|
+
* regenerated only when triggered.
|
|
13138
|
+
*/
|
|
13139
|
+
_builder: z.string().optional(),
|
|
13140
|
+
/** Critic system prompt used this turn. Defaults from config. */
|
|
13141
|
+
_critic: z.string().optional(),
|
|
13142
|
+
/** Timestamp of last synthesis. ISO 8601. */
|
|
13143
|
+
_generatedAt: z.iso.datetime().optional(),
|
|
13144
|
+
/** Narrative synthesis output. Rendered by watcher for embedding. */
|
|
13145
|
+
_content: z.string().optional(),
|
|
13146
|
+
/**
|
|
13147
|
+
* Hash of sorted file listing in scope. Detects directory structure
|
|
13148
|
+
* changes that trigger an architect re-run.
|
|
13149
|
+
*/
|
|
13150
|
+
_structureHash: z.string().optional(),
|
|
13151
|
+
/**
|
|
13152
|
+
* Cycles since last architect run. Reset to 0 when architect runs.
|
|
13153
|
+
* Used with architectEvery to trigger periodic re-prompting.
|
|
13154
|
+
*/
|
|
13155
|
+
_synthesisCount: z.number().int().min(0).optional(),
|
|
13156
|
+
/** Critic evaluation of the last synthesis. */
|
|
13157
|
+
_feedback: z.string().optional(),
|
|
13158
|
+
/**
|
|
13159
|
+
* Present and true on archive snapshots. Distinguishes live vs. archived
|
|
13160
|
+
* metas.
|
|
13161
|
+
*/
|
|
13162
|
+
_archived: z.boolean().optional(),
|
|
13163
|
+
/** Timestamp when this snapshot was archived. ISO 8601. */
|
|
13164
|
+
_archivedAt: z.iso.datetime().optional(),
|
|
13165
|
+
/**
|
|
13166
|
+
* Scheduling priority. Higher = updates more often. Negative allowed;
|
|
13167
|
+
* normalized to min 0 at scheduling time.
|
|
13168
|
+
*/
|
|
13169
|
+
_depth: z.number().optional(),
|
|
13170
|
+
/**
|
|
13171
|
+
* Emphasis multiplier for depth weighting in scheduling.
|
|
13172
|
+
* Default 1. Higher values increase this meta's scheduling priority
|
|
13173
|
+
* relative to its depth. Set to 0.5 to halve the depth effect,
|
|
13174
|
+
* 2 to double it, 0 to ignore depth entirely for this meta.
|
|
13175
|
+
*/
|
|
13176
|
+
_emphasis: z.number().min(0).optional(),
|
|
13177
|
+
/** Token count from last architect subprocess call. */
|
|
13178
|
+
_architectTokens: z.number().int().optional(),
|
|
13179
|
+
/** Token count from last builder subprocess call. */
|
|
13180
|
+
_builderTokens: z.number().int().optional(),
|
|
13181
|
+
/** Token count from last critic subprocess call. */
|
|
13182
|
+
_criticTokens: z.number().int().optional(),
|
|
13183
|
+
/** Exponential moving average of architect token usage (decay 0.3). */
|
|
13184
|
+
_architectTokensAvg: z.number().optional(),
|
|
13185
|
+
/** Exponential moving average of builder token usage (decay 0.3). */
|
|
13186
|
+
_builderTokensAvg: z.number().optional(),
|
|
13187
|
+
/** Exponential moving average of critic token usage (decay 0.3). */
|
|
13188
|
+
_criticTokensAvg: z.number().optional(),
|
|
13189
|
+
/**
|
|
13190
|
+
* Opaque state carried across synthesis cycles for progressive work.
|
|
13191
|
+
* Set by the builder, passed back as context on next cycle.
|
|
13192
|
+
*/
|
|
13193
|
+
_state: z.unknown().optional(),
|
|
13194
|
+
/**
|
|
13195
|
+
* Structured error from last cycle. Present when a step failed.
|
|
13196
|
+
* Cleared on successful cycle.
|
|
13197
|
+
*/
|
|
13198
|
+
_error: metaErrorSchema.optional(),
|
|
13199
|
+
/** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
|
|
13200
|
+
_disabled: z.boolean().optional(),
|
|
13201
|
+
/**
|
|
13202
|
+
* Per-phase state machine record. Engine-managed.
|
|
13203
|
+
* Keyed by phase name (architect, builder, critic) with status values.
|
|
13204
|
+
* Persisted to survive ticks; derived on first load for back-compat.
|
|
13205
|
+
*/
|
|
13206
|
+
_phaseState: phaseStateSchema.optional(),
|
|
13207
|
+
})
|
|
13208
|
+
.loose();
|
|
13209
|
+
|
|
13210
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestratePhase, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, runArchitect, runBuilder, runCritic, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
|