@karmaniverous/jeeves-meta 0.17.0 → 0.18.0-0
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/README.md +1 -2
- package/dist/cli/jeeves-meta/index.js +1235 -1198
- package/dist/prompts/architect.md +31 -59
- package/dist/prompts/critic.md +18 -33
- package/package.json +14 -22
- package/dist/archive/index.d.ts +0 -10
- package/dist/archive/listArchive.d.ts +0 -12
- package/dist/archive/prune.d.ts +0 -14
- package/dist/archive/readArchive.d.ts +0 -30
- package/dist/archive/readLatest.d.ts +0 -13
- package/dist/archive/snapshot.d.ts +0 -17
- package/dist/bootstrap.d.ts +0 -26
- package/dist/cache.d.ts +0 -22
- package/dist/cli/jeeves-meta/architect.md +0 -173
- package/dist/cli/jeeves-meta/critic.md +0 -104
- package/dist/cli.d.ts +0 -10
- package/dist/configHotReload.d.ts +0 -30
- package/dist/configLoader.d.ts +0 -36
- package/dist/constants.d.ts +0 -13
- package/dist/customCliCommands.d.ts +0 -13
- package/dist/descriptor.d.ts +0 -19
- package/dist/discovery/buildMinimalNode.d.ts +0 -22
- package/dist/discovery/computeSummary.d.ts +0 -17
- package/dist/discovery/discoverMetas.d.ts +0 -19
- package/dist/discovery/getAncestorMeta.d.ts +0 -16
- package/dist/discovery/index.d.ts +0 -12
- package/dist/discovery/listMetas.d.ts +0 -63
- package/dist/discovery/ownershipTree.d.ts +0 -25
- package/dist/discovery/scope.d.ts +0 -47
- package/dist/discovery/types.d.ts +0 -25
- package/dist/ema.d.ts +0 -14
- package/dist/errors.d.ts +0 -15
- package/dist/escapeGlob.d.ts +0 -23
- package/dist/executor/GatewayExecutor.d.ts +0 -79
- package/dist/executor/SpawnAbortedError.d.ts +0 -9
- package/dist/executor/SpawnTimeoutError.d.ts +0 -13
- package/dist/executor/index.d.ts +0 -8
- package/dist/index.d.ts +0 -35
- package/dist/index.js +0 -5797
- package/dist/interfaces/MetaContext.d.ts +0 -38
- package/dist/interfaces/MetaExecutor.d.ts +0 -44
- package/dist/interfaces/WatcherClient.d.ts +0 -75
- package/dist/interfaces/index.d.ts +0 -8
- package/dist/lock.d.ts +0 -70
- package/dist/logger/index.d.ts +0 -27
- package/dist/mtimeFilter.d.ts +0 -26
- package/dist/normalizePath.d.ts +0 -6
- package/dist/orchestrator/buildTask.d.ts +0 -38
- package/dist/orchestrator/contextPackage.d.ts +0 -30
- package/dist/orchestrator/index.d.ts +0 -10
- package/dist/orchestrator/orchestratePhase.d.ts +0 -44
- package/dist/orchestrator/parseOutput.d.ts +0 -42
- package/dist/orchestrator/runPhase.d.ts +0 -40
- package/dist/phaseState/derivePhaseState.d.ts +0 -39
- package/dist/phaseState/index.d.ts +0 -9
- package/dist/phaseState/invalidate.d.ts +0 -39
- package/dist/phaseState/phaseScheduler.d.ts +0 -76
- package/dist/phaseState/phaseTransitions.d.ts +0 -83
- package/dist/progress/index.d.ts +0 -73
- package/dist/prompts/index.d.ts +0 -12
- package/dist/queue/index.d.ts +0 -90
- package/dist/readMetaJson.d.ts +0 -17
- package/dist/routes/__testUtils.d.ts +0 -37
- package/dist/routes/config.d.ts +0 -11
- package/dist/routes/configApply.d.ts +0 -13
- package/dist/routes/index.d.ts +0 -50
- package/dist/routes/metas.d.ts +0 -9
- package/dist/routes/metasUpdate.d.ts +0 -11
- package/dist/routes/preview.d.ts +0 -8
- package/dist/routes/queue.d.ts +0 -13
- package/dist/routes/seed.d.ts +0 -8
- package/dist/routes/status.d.ts +0 -13
- package/dist/routes/synthesize.d.ts +0 -12
- package/dist/routes/unlock.d.ts +0 -8
- package/dist/rules/healthCheck.d.ts +0 -36
- package/dist/rules/index.d.ts +0 -37
- package/dist/rules/verify.d.ts +0 -22
- package/dist/scheduler/index.d.ts +0 -71
- package/dist/scheduling/index.d.ts +0 -7
- package/dist/scheduling/staleness.d.ts +0 -68
- package/dist/scheduling/weightedFormula.d.ts +0 -38
- package/dist/schema/config.d.ts +0 -67
- package/dist/schema/error.d.ts +0 -6
- package/dist/schema/index.d.ts +0 -8
- package/dist/schema/meta.d.ts +0 -72
- package/dist/seed/autoSeed.d.ts +0 -30
- package/dist/seed/createMeta.d.ts +0 -38
- package/dist/seed/index.d.ts +0 -7
- package/dist/server.d.ts +0 -24
- package/dist/shutdown/index.d.ts +0 -33
- package/dist/structureHash.d.ts +0 -15
- package/dist/watcher-client/HttpWatcherClient.d.ts +0 -38
- package/dist/watcher-client/index.d.ts +0 -6
|
@@ -2,125 +2,18 @@
|
|
|
2
2
|
import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
|
|
3
3
|
import { normalizePath, metaConfigSchema, getEndpoint, META_COMPONENT } from '@karmaniverous/jeeves-meta-core';
|
|
4
4
|
import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
|
|
5
|
-
import path, { join, dirname, relative, posix, resolve } from 'node:path';
|
|
5
|
+
import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
|
|
6
6
|
import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { randomUUID, createHash } from 'node:crypto';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import Handlebars from 'handlebars';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
13
12
|
import process$1 from 'node:process';
|
|
13
|
+
import Handlebars from 'handlebars';
|
|
14
14
|
import { Cron } from 'croner';
|
|
15
15
|
import Fastify from 'fastify';
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Compute summary statistics from an array of MetaEntry objects.
|
|
19
|
-
*
|
|
20
|
-
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
21
|
-
*
|
|
22
|
-
* @module discovery/computeSummary
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* Compute summary statistics from a list of meta entries.
|
|
26
|
-
*
|
|
27
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
28
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
29
|
-
* @returns Aggregated summary statistics.
|
|
30
|
-
*/
|
|
31
|
-
function computeSummary(entries, depthWeight) {
|
|
32
|
-
let staleCount = 0;
|
|
33
|
-
let errorCount = 0;
|
|
34
|
-
let lockedCount = 0;
|
|
35
|
-
let disabledCount = 0;
|
|
36
|
-
let neverSynthesizedCount = 0;
|
|
37
|
-
let totalArchitectTokens = 0;
|
|
38
|
-
let totalBuilderTokens = 0;
|
|
39
|
-
let totalCriticTokens = 0;
|
|
40
|
-
let stalestPath = null;
|
|
41
|
-
let stalestEffective = -1;
|
|
42
|
-
let lastSynthesizedPath = null;
|
|
43
|
-
let lastSynthesizedAt = null;
|
|
44
|
-
for (const e of entries) {
|
|
45
|
-
if (e.stalenessSeconds > 0)
|
|
46
|
-
staleCount++;
|
|
47
|
-
if (e.hasError)
|
|
48
|
-
errorCount++;
|
|
49
|
-
if (e.locked)
|
|
50
|
-
lockedCount++;
|
|
51
|
-
if (e.disabled)
|
|
52
|
-
disabledCount++;
|
|
53
|
-
if (e.lastSynthesized === null)
|
|
54
|
-
neverSynthesizedCount++;
|
|
55
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
56
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
57
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
58
|
-
// Track last synthesized
|
|
59
|
-
if (e.lastSynthesized &&
|
|
60
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
61
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
62
|
-
lastSynthesizedPath = e.path;
|
|
63
|
-
}
|
|
64
|
-
// Track stalest (effective staleness for scheduling)
|
|
65
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
66
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
67
|
-
if (effectiveStaleness > stalestEffective) {
|
|
68
|
-
stalestEffective = effectiveStaleness;
|
|
69
|
-
stalestPath = e.path;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
total: entries.length,
|
|
74
|
-
stale: staleCount,
|
|
75
|
-
errors: errorCount,
|
|
76
|
-
locked: lockedCount,
|
|
77
|
-
disabled: disabledCount,
|
|
78
|
-
neverSynthesized: neverSynthesizedCount,
|
|
79
|
-
tokens: {
|
|
80
|
-
architect: totalArchitectTokens,
|
|
81
|
-
builder: totalBuilderTokens,
|
|
82
|
-
critic: totalCriticTokens,
|
|
83
|
-
},
|
|
84
|
-
stalestPath,
|
|
85
|
-
lastSynthesizedPath,
|
|
86
|
-
lastSynthesizedAt,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
92
|
-
*
|
|
93
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
94
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
95
|
-
*
|
|
96
|
-
* @module discovery/discoverMetas
|
|
97
|
-
*/
|
|
98
|
-
/**
|
|
99
|
-
* Discover all .meta/ directories via watcher walk.
|
|
100
|
-
*
|
|
101
|
-
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
102
|
-
* and returns deduplicated meta directory paths.
|
|
103
|
-
*
|
|
104
|
-
* @param watcher - WatcherClient for walk queries.
|
|
105
|
-
* @returns Array of normalized .meta/ directory paths.
|
|
106
|
-
*/
|
|
107
|
-
async function discoverMetas(watcher) {
|
|
108
|
-
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
109
|
-
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
110
|
-
const seen = new Set();
|
|
111
|
-
const metaPaths = [];
|
|
112
|
-
for (const filePath of allPaths) {
|
|
113
|
-
const fp = normalizePath(filePath);
|
|
114
|
-
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
115
|
-
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
116
|
-
if (seen.has(metaPath))
|
|
117
|
-
continue;
|
|
118
|
-
seen.add(metaPath);
|
|
119
|
-
metaPaths.push(metaPath);
|
|
120
|
-
}
|
|
121
|
-
return metaPaths;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
17
|
/**
|
|
125
18
|
* Retrieve the nearest ancestor meta node from the ownership tree.
|
|
126
19
|
*
|
|
@@ -426,6 +319,113 @@ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
|
426
319
|
return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
|
|
427
320
|
}
|
|
428
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
324
|
+
*
|
|
325
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
326
|
+
*
|
|
327
|
+
* @module discovery/computeSummary
|
|
328
|
+
*/
|
|
329
|
+
/**
|
|
330
|
+
* Compute summary statistics from a list of meta entries.
|
|
331
|
+
*
|
|
332
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
333
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
334
|
+
* @returns Aggregated summary statistics.
|
|
335
|
+
*/
|
|
336
|
+
function computeSummary(entries, depthWeight) {
|
|
337
|
+
let staleCount = 0;
|
|
338
|
+
let errorCount = 0;
|
|
339
|
+
let lockedCount = 0;
|
|
340
|
+
let disabledCount = 0;
|
|
341
|
+
let neverSynthesizedCount = 0;
|
|
342
|
+
let totalArchitectTokens = 0;
|
|
343
|
+
let totalBuilderTokens = 0;
|
|
344
|
+
let totalCriticTokens = 0;
|
|
345
|
+
let stalestPath = null;
|
|
346
|
+
let stalestEffective = -1;
|
|
347
|
+
let lastSynthesizedPath = null;
|
|
348
|
+
let lastSynthesizedAt = null;
|
|
349
|
+
for (const e of entries) {
|
|
350
|
+
if (e.stalenessSeconds > 0)
|
|
351
|
+
staleCount++;
|
|
352
|
+
if (e.hasError)
|
|
353
|
+
errorCount++;
|
|
354
|
+
if (e.locked)
|
|
355
|
+
lockedCount++;
|
|
356
|
+
if (e.disabled)
|
|
357
|
+
disabledCount++;
|
|
358
|
+
if (e.lastSynthesized === null)
|
|
359
|
+
neverSynthesizedCount++;
|
|
360
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
361
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
362
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
363
|
+
// Track last synthesized
|
|
364
|
+
if (e.lastSynthesized &&
|
|
365
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
366
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
367
|
+
lastSynthesizedPath = e.path;
|
|
368
|
+
}
|
|
369
|
+
// Track stalest (effective staleness for scheduling)
|
|
370
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
371
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
372
|
+
if (effectiveStaleness > stalestEffective) {
|
|
373
|
+
stalestEffective = effectiveStaleness;
|
|
374
|
+
stalestPath = e.path;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
total: entries.length,
|
|
379
|
+
stale: staleCount,
|
|
380
|
+
errors: errorCount,
|
|
381
|
+
locked: lockedCount,
|
|
382
|
+
disabled: disabledCount,
|
|
383
|
+
neverSynthesized: neverSynthesizedCount,
|
|
384
|
+
tokens: {
|
|
385
|
+
architect: totalArchitectTokens,
|
|
386
|
+
builder: totalBuilderTokens,
|
|
387
|
+
critic: totalCriticTokens,
|
|
388
|
+
},
|
|
389
|
+
stalestPath,
|
|
390
|
+
lastSynthesizedPath,
|
|
391
|
+
lastSynthesizedAt,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
397
|
+
*
|
|
398
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
399
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
400
|
+
*
|
|
401
|
+
* @module discovery/discoverMetas
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Discover all .meta/ directories via watcher walk.
|
|
405
|
+
*
|
|
406
|
+
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
407
|
+
* and returns deduplicated meta directory paths.
|
|
408
|
+
*
|
|
409
|
+
* @param watcher - WatcherClient for walk queries.
|
|
410
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
411
|
+
*/
|
|
412
|
+
async function discoverMetas(watcher) {
|
|
413
|
+
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
414
|
+
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
const metaPaths = [];
|
|
417
|
+
for (const filePath of allPaths) {
|
|
418
|
+
const fp = normalizePath(filePath);
|
|
419
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
420
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
421
|
+
if (seen.has(metaPath))
|
|
422
|
+
continue;
|
|
423
|
+
seen.add(metaPath);
|
|
424
|
+
metaPaths.push(metaPath);
|
|
425
|
+
}
|
|
426
|
+
return metaPaths;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
429
|
/**
|
|
430
430
|
* Build the ownership tree from discovered .meta/ paths.
|
|
431
431
|
*
|
|
@@ -1251,1284 +1251,1233 @@ function createLogger(config) {
|
|
|
1251
1251
|
return pino({ level });
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
|
-
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
1255
|
-
|
|
1256
|
-
function findUpSync(name, {
|
|
1257
|
-
cwd = process$1.cwd(),
|
|
1258
|
-
type = 'file',
|
|
1259
|
-
stopAt,
|
|
1260
|
-
} = {}) {
|
|
1261
|
-
let directory = path.resolve(toPath(cwd) ?? '');
|
|
1262
|
-
const {root} = path.parse(directory);
|
|
1263
|
-
stopAt = path.resolve(directory, toPath(stopAt) ?? root);
|
|
1264
|
-
const isAbsoluteName = path.isAbsolute(name);
|
|
1265
|
-
|
|
1266
|
-
while (directory) {
|
|
1267
|
-
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
|
|
1271
|
-
if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
|
|
1272
|
-
return filePath;
|
|
1273
|
-
}
|
|
1274
|
-
} catch {}
|
|
1275
|
-
|
|
1276
|
-
if (directory === stopAt || directory === root) {
|
|
1277
|
-
break;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
directory = path.dirname(directory);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
const isTypeOnlyPackageJsonData = packageData => {
|
|
1285
|
-
if (!packageData || typeof packageData !== 'object' || Array.isArray(packageData)) {
|
|
1286
|
-
return false;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
const keys = Object.keys(packageData);
|
|
1290
|
-
return keys.length === 1 && keys[0] === 'type' && typeof packageData.type === 'string';
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
const isTypeOnlyPackageJsonSync = filePath => {
|
|
1294
|
-
let fileContents;
|
|
1295
|
-
|
|
1296
|
-
try {
|
|
1297
|
-
fileContents = fs.readFileSync(filePath, 'utf8');
|
|
1298
|
-
} catch {
|
|
1299
|
-
return false;
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
try {
|
|
1303
|
-
return isTypeOnlyPackageJsonData(JSON.parse(fileContents));
|
|
1304
|
-
} catch {
|
|
1305
|
-
return false;
|
|
1306
|
-
}
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
const getNextSearchDirectory = filePath => {
|
|
1310
|
-
const packageDirectoryPath = path.dirname(filePath);
|
|
1311
|
-
const parentDirectoryPath = path.dirname(packageDirectoryPath);
|
|
1312
|
-
return parentDirectoryPath === packageDirectoryPath ? undefined : parentDirectoryPath;
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
const findPackageDirectorySync = (directory, ignoreTypeOnlyPackageJson) => {
|
|
1316
|
-
const filePath = findUpSync('package.json', {cwd: directory});
|
|
1317
|
-
if (!filePath) {
|
|
1318
|
-
return undefined;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
const packageDirectoryPath = path.dirname(filePath);
|
|
1322
|
-
if (!ignoreTypeOnlyPackageJson) {
|
|
1323
|
-
return packageDirectoryPath;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
if (!isTypeOnlyPackageJsonSync(filePath)) {
|
|
1327
|
-
return packageDirectoryPath;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const nextDirectory = getNextSearchDirectory(filePath);
|
|
1331
|
-
if (!nextDirectory) {
|
|
1332
|
-
return undefined;
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
return findPackageDirectorySync(nextDirectory, ignoreTypeOnlyPackageJson);
|
|
1336
|
-
};
|
|
1337
|
-
|
|
1338
|
-
function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
1339
|
-
return findPackageDirectorySync(cwd ?? process$1.cwd(), ignoreTypeOnlyPackageJson);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
1254
|
/**
|
|
1343
|
-
*
|
|
1255
|
+
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
1344
1256
|
*
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1257
|
+
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
1258
|
+
* the full discovery + ownership tree build. Discovers only immediate child
|
|
1259
|
+
* `.meta/` directories.
|
|
1347
1260
|
*
|
|
1348
|
-
* @module
|
|
1261
|
+
* @module discovery/buildMinimalNode
|
|
1349
1262
|
*/
|
|
1350
|
-
const packageRoot = packageDirectorySync({
|
|
1351
|
-
cwd: fileURLToPath(import.meta.url),
|
|
1352
|
-
});
|
|
1353
|
-
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
1354
|
-
/** Built-in default architect prompt. */
|
|
1355
|
-
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
1356
|
-
/** Built-in default critic prompt. */
|
|
1357
|
-
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
1358
|
-
|
|
1359
1263
|
/**
|
|
1360
|
-
*
|
|
1264
|
+
* Build a minimal MetaNode for a known meta path.
|
|
1361
1265
|
*
|
|
1362
|
-
*
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* List archive .json files sorted chronologically (oldest first).
|
|
1266
|
+
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
1267
|
+
* a shallow ownership tree (self + direct children only).
|
|
1366
1268
|
*
|
|
1367
|
-
* @param metaPath - Absolute path to the
|
|
1368
|
-
* @
|
|
1269
|
+
* @param metaPath - Absolute path to the `.meta/` directory.
|
|
1270
|
+
* @param watcher - WatcherClient for filesystem enumeration.
|
|
1271
|
+
* @returns MetaNode with direct children wired.
|
|
1369
1272
|
*/
|
|
1370
|
-
function
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1273
|
+
async function buildMinimalNode(metaPath, watcher) {
|
|
1274
|
+
const normalized = normalizePath(metaPath);
|
|
1275
|
+
const ownerPath = posix.dirname(normalized);
|
|
1276
|
+
// Find child metas using watcher walk.
|
|
1277
|
+
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1278
|
+
// to match the ownership semantics used elsewhere.
|
|
1279
|
+
const rawMetaJsonPaths = await watcher.walk([
|
|
1280
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1281
|
+
]);
|
|
1282
|
+
const candidateMetaPaths = [
|
|
1283
|
+
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
1284
|
+
].filter((p) => p !== normalized);
|
|
1285
|
+
const candidates = candidateMetaPaths
|
|
1286
|
+
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
1287
|
+
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
1288
|
+
const directChildren = [];
|
|
1289
|
+
for (const c of candidates) {
|
|
1290
|
+
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
1291
|
+
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
1292
|
+
if (!nestedUnderExisting)
|
|
1293
|
+
directChildren.push(c);
|
|
1377
1294
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1295
|
+
const children = directChildren.map((c) => ({
|
|
1296
|
+
metaPath: c.metaPath,
|
|
1297
|
+
ownerPath: c.ownerPath,
|
|
1298
|
+
treeDepth: 1,
|
|
1299
|
+
children: [],
|
|
1300
|
+
parent: null,
|
|
1301
|
+
}));
|
|
1302
|
+
const node = {
|
|
1303
|
+
metaPath: normalized,
|
|
1304
|
+
ownerPath,
|
|
1305
|
+
treeDepth: 0,
|
|
1306
|
+
children,
|
|
1307
|
+
parent: null,
|
|
1308
|
+
};
|
|
1309
|
+
for (const child of children) {
|
|
1310
|
+
child.parent = node;
|
|
1380
1311
|
}
|
|
1312
|
+
return node;
|
|
1381
1313
|
}
|
|
1382
1314
|
|
|
1383
1315
|
/**
|
|
1384
|
-
*
|
|
1316
|
+
* Pure phase-state transition functions.
|
|
1385
1317
|
*
|
|
1386
|
-
*
|
|
1318
|
+
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
1319
|
+
* No I/O — pure functions over PhaseState and documented inputs.
|
|
1320
|
+
*
|
|
1321
|
+
* @module phaseState/phaseTransitions
|
|
1387
1322
|
*/
|
|
1388
1323
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
* Removes the oldest files.
|
|
1391
|
-
*
|
|
1392
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1393
|
-
* @param maxArchive - Maximum snapshots to retain.
|
|
1394
|
-
* @returns Number of files pruned.
|
|
1324
|
+
* Create a fresh (fully-complete) phase state.
|
|
1395
1325
|
*/
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const toRemove = files.length - maxArchive;
|
|
1399
|
-
if (toRemove <= 0)
|
|
1400
|
-
return 0;
|
|
1401
|
-
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
1402
|
-
return toRemove;
|
|
1326
|
+
function freshPhaseState() {
|
|
1327
|
+
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
1403
1328
|
}
|
|
1404
|
-
|
|
1405
1329
|
/**
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
* Used by the GET /metas/:path route to retrieve archive history.
|
|
1409
|
-
* Prefers watcher scan for performance; falls back to disk reads
|
|
1410
|
-
* when the watcher is unavailable.
|
|
1411
|
-
*
|
|
1412
|
-
* @module archive/readArchive
|
|
1330
|
+
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
1413
1331
|
*/
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
const must = [
|
|
1417
|
-
{
|
|
1418
|
-
key: 'file_path',
|
|
1419
|
-
match: { text: normalizePath(join(metaPath, 'archive')) },
|
|
1420
|
-
},
|
|
1421
|
-
];
|
|
1422
|
-
for (const [key, value] of Object.entries(metaArchiveProperty)) {
|
|
1423
|
-
must.push({ key, match: { value } });
|
|
1424
|
-
}
|
|
1425
|
-
return { must };
|
|
1426
|
-
}
|
|
1427
|
-
/** Extract file_path from a scan point payload. */
|
|
1428
|
-
function getArchiveFilePath(point) {
|
|
1429
|
-
const value = point.payload?.file_path;
|
|
1430
|
-
return typeof value === 'string' ? value : '';
|
|
1431
|
-
}
|
|
1432
|
-
/** Project archive metadata, stripping indexing-internal fields. */
|
|
1433
|
-
function projectArchivePayload(point, projectMeta) {
|
|
1434
|
-
const payload = point.payload ?? {};
|
|
1435
|
-
if (Object.keys(payload).length === 0)
|
|
1436
|
-
return null;
|
|
1437
|
-
const archiveMeta = { ...payload };
|
|
1438
|
-
delete archiveMeta.file_path;
|
|
1439
|
-
delete archiveMeta.chunk_text;
|
|
1440
|
-
delete archiveMeta.chunk_index;
|
|
1441
|
-
delete archiveMeta.total_chunks;
|
|
1442
|
-
delete archiveMeta.content_hash;
|
|
1443
|
-
delete archiveMeta.matched_rules;
|
|
1444
|
-
return projectMeta(archiveMeta);
|
|
1332
|
+
function initialPhaseState() {
|
|
1333
|
+
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
1445
1334
|
}
|
|
1446
1335
|
/**
|
|
1447
|
-
*
|
|
1336
|
+
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
1337
|
+
* and it is the first non-fresh phase in pipeline order.
|
|
1448
1338
|
*
|
|
1449
|
-
*
|
|
1450
|
-
* @param metaPath - Absolute path to the .meta/ directory.
|
|
1451
|
-
* @param metaArchiveProperty - Additional filter properties for archive points.
|
|
1452
|
-
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
1453
|
-
* @param projectMeta - Field projection function.
|
|
1454
|
-
* @returns Array of projected archive entries, newest first.
|
|
1339
|
+
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
1455
1340
|
*/
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1341
|
+
function enforceInvariant(state) {
|
|
1342
|
+
const result = { ...state };
|
|
1343
|
+
let foundNonFresh = false;
|
|
1344
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1345
|
+
const s = result[phase];
|
|
1346
|
+
if (s === 'fresh')
|
|
1347
|
+
continue;
|
|
1348
|
+
if (!foundNonFresh) {
|
|
1349
|
+
foundNonFresh = true;
|
|
1350
|
+
// First non-fresh: if stale, promote to pending
|
|
1351
|
+
if (s === 'stale') {
|
|
1352
|
+
result[phase] = 'pending';
|
|
1353
|
+
}
|
|
1354
|
+
// pending, running, failed stay as-is
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
// Subsequent non-fresh: must not be pending or running
|
|
1358
|
+
if (s === 'pending') {
|
|
1359
|
+
result[phase] = 'stale';
|
|
1360
|
+
}
|
|
1361
|
+
// running in non-first position would be a bug, but don't mask it
|
|
1362
|
+
}
|
|
1461
1363
|
}
|
|
1462
|
-
|
|
1463
|
-
let cursor;
|
|
1464
|
-
do {
|
|
1465
|
-
const result = await watcher.scan({
|
|
1466
|
-
filter: buildArchiveScanFilter(metaPath, metaArchiveProperty),
|
|
1467
|
-
limit: 100,
|
|
1468
|
-
cursor,
|
|
1469
|
-
});
|
|
1470
|
-
points.push(...result.points);
|
|
1471
|
-
cursor = result.cursor ?? undefined;
|
|
1472
|
-
} while (cursor);
|
|
1473
|
-
const sorted = points.sort((a, b) => getArchiveFilePath(a).localeCompare(getArchiveFilePath(b)));
|
|
1474
|
-
return sorted
|
|
1475
|
-
.slice(limit ? -limit : 0)
|
|
1476
|
-
.reverse()
|
|
1477
|
-
.map((point) => projectArchivePayload(point, projectMeta))
|
|
1478
|
-
.filter((value) => value !== null);
|
|
1364
|
+
return result;
|
|
1479
1365
|
}
|
|
1366
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
1480
1367
|
/**
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
1485
|
-
* @param projectMeta - Field projection function.
|
|
1486
|
-
* @returns Array of projected archive entries, newest first.
|
|
1368
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
1369
|
+
* Triggers: first run, _structureHash change, _steer change,
|
|
1370
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
1487
1371
|
*/
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const raw = await readFile(archiveFile, 'utf8');
|
|
1495
|
-
return projectMeta(JSON.parse(raw));
|
|
1496
|
-
}));
|
|
1372
|
+
function invalidateArchitect(state) {
|
|
1373
|
+
return enforceInvariant({
|
|
1374
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
1375
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
1376
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1377
|
+
});
|
|
1497
1378
|
}
|
|
1498
|
-
|
|
1499
1379
|
/**
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1380
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
1381
|
+
* builder → pending; critic → stale.
|
|
1382
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
1503
1383
|
*/
|
|
1384
|
+
function invalidateBuilder(state) {
|
|
1385
|
+
if (state.architect !== 'fresh') {
|
|
1386
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
1387
|
+
return enforceInvariant({
|
|
1388
|
+
...state,
|
|
1389
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
1390
|
+
? 'stale'
|
|
1391
|
+
: state.builder,
|
|
1392
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return enforceInvariant({
|
|
1396
|
+
...state,
|
|
1397
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
1398
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
// ── Phase success transitions ──────────────────────────────────────────
|
|
1504
1402
|
/**
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1507
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1508
|
-
* @returns The latest archived meta, or null if no archives exist.
|
|
1403
|
+
* Architect completes successfully.
|
|
1404
|
+
* architect → fresh; builder → pending; critic → stale.
|
|
1509
1405
|
*/
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1406
|
+
function architectSuccess(state) {
|
|
1407
|
+
return enforceInvariant({
|
|
1408
|
+
architect: 'fresh',
|
|
1409
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
1410
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1411
|
+
});
|
|
1516
1412
|
}
|
|
1517
|
-
|
|
1518
1413
|
/**
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
* Copies current meta.json to archive/\{ISO-timestamp\}.json with
|
|
1522
|
-
* _archived: true and _archivedAt added.
|
|
1523
|
-
*
|
|
1524
|
-
* @module archive/snapshot
|
|
1414
|
+
* Builder completes successfully.
|
|
1415
|
+
* builder → fresh; critic → pending.
|
|
1525
1416
|
*/
|
|
1417
|
+
function builderSuccess(state) {
|
|
1418
|
+
return enforceInvariant({
|
|
1419
|
+
...state,
|
|
1420
|
+
builder: 'fresh',
|
|
1421
|
+
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1526
1424
|
/**
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1530
|
-
* @param meta - Current meta.json content.
|
|
1531
|
-
* @returns The archive file path.
|
|
1425
|
+
* Critic completes successfully.
|
|
1426
|
+
* critic → fresh. Meta becomes fully fresh.
|
|
1532
1427
|
*/
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
const archived = {
|
|
1539
|
-
...meta,
|
|
1540
|
-
_archived: true,
|
|
1541
|
-
_archivedAt: new Date().toISOString(),
|
|
1542
|
-
};
|
|
1543
|
-
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
1544
|
-
return archiveFile;
|
|
1428
|
+
function criticSuccess(state) {
|
|
1429
|
+
return enforceInvariant({
|
|
1430
|
+
...state,
|
|
1431
|
+
critic: 'fresh',
|
|
1432
|
+
});
|
|
1545
1433
|
}
|
|
1546
|
-
|
|
1434
|
+
// ── Failure transition ─────────────────────────────────────────────────
|
|
1547
1435
|
/**
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
1551
|
-
* previous content/feedback, steer, and archive paths.
|
|
1552
|
-
*
|
|
1553
|
-
* @module orchestrator/contextPackage
|
|
1436
|
+
* A phase fails (error, timeout, or abort).
|
|
1437
|
+
* Target phase → failed; upstream and downstream unchanged.
|
|
1554
1438
|
*/
|
|
1439
|
+
function phaseFailed(state, phase) {
|
|
1440
|
+
return enforceInvariant({
|
|
1441
|
+
...state,
|
|
1442
|
+
[phase]: 'failed',
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
1555
1446
|
/**
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
* @param files - Array of file paths.
|
|
1560
|
-
* @param maxIndividual - Show individual files up to this count.
|
|
1561
|
-
* @returns Condensed summary string.
|
|
1447
|
+
* Retry a failed phase: failed → pending.
|
|
1448
|
+
* Only valid when the phase is currently failed.
|
|
1562
1449
|
*/
|
|
1563
|
-
function
|
|
1564
|
-
if (
|
|
1565
|
-
return
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
1571
|
-
const key = dir + '*' + ext;
|
|
1572
|
-
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
1573
|
-
}
|
|
1574
|
-
// Sort by count descending
|
|
1575
|
-
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
1576
|
-
return sorted
|
|
1577
|
-
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
1578
|
-
.join('\n');
|
|
1450
|
+
function retryPhase(state, phase) {
|
|
1451
|
+
if (state[phase] !== 'failed')
|
|
1452
|
+
return state;
|
|
1453
|
+
return enforceInvariant({
|
|
1454
|
+
...state,
|
|
1455
|
+
[phase]: 'pending',
|
|
1456
|
+
});
|
|
1579
1457
|
}
|
|
1580
1458
|
/**
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1584
|
-
* @returns Parsed fields, or defaults if missing/unreadable.
|
|
1459
|
+
* Retry all failed phases: each failed phase → pending.
|
|
1460
|
+
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
1585
1461
|
*/
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
generatedAt: meta._generatedAt,
|
|
1593
|
-
};
|
|
1462
|
+
function retryAllFailed(state) {
|
|
1463
|
+
let result = state;
|
|
1464
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1465
|
+
if (result[phase] === 'failed') {
|
|
1466
|
+
result = retryPhase(result, phase);
|
|
1467
|
+
}
|
|
1594
1468
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
// ── Running transition ─────────────────────────────────────────────────
|
|
1472
|
+
/**
|
|
1473
|
+
* Mark a phase as running (scheduler picks it).
|
|
1474
|
+
*/
|
|
1475
|
+
function phaseRunning(state, phase) {
|
|
1476
|
+
return {
|
|
1477
|
+
...state,
|
|
1478
|
+
[phase]: 'running',
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
1482
|
+
/**
|
|
1483
|
+
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
1484
|
+
*/
|
|
1485
|
+
function getOwedPhase(state) {
|
|
1486
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1487
|
+
if (state[phase] !== 'fresh')
|
|
1488
|
+
return phase;
|
|
1597
1489
|
}
|
|
1490
|
+
return null;
|
|
1598
1491
|
}
|
|
1599
1492
|
/**
|
|
1600
|
-
*
|
|
1601
|
-
*
|
|
1602
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1603
|
-
* @returns The `_content` string, or null if missing/unreadable.
|
|
1493
|
+
* Check if a meta is fully fresh (all phases fresh).
|
|
1604
1494
|
*/
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1495
|
+
function isFullyFresh(state) {
|
|
1496
|
+
return (state.architect === 'fresh' &&
|
|
1497
|
+
state.builder === 'fresh' &&
|
|
1498
|
+
state.critic === 'fresh');
|
|
1608
1499
|
}
|
|
1609
1500
|
/**
|
|
1610
|
-
*
|
|
1501
|
+
* Get the scheduler priority band for a meta's owed phase.
|
|
1502
|
+
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
1503
|
+
*/
|
|
1504
|
+
function getPriorityBand(state) {
|
|
1505
|
+
const owed = getOwedPhase(state);
|
|
1506
|
+
if (!owed)
|
|
1507
|
+
return null;
|
|
1508
|
+
if (owed === 'critic')
|
|
1509
|
+
return 1;
|
|
1510
|
+
if (owed === 'builder')
|
|
1511
|
+
return 2;
|
|
1512
|
+
return 3;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
1611
1517
|
*
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1518
|
+
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
1519
|
+
* the phase state from _content, _builder, _state, _error.step, and
|
|
1520
|
+
* the architect-invalidating inputs.
|
|
1521
|
+
*
|
|
1522
|
+
* @module phaseState/derivePhaseState
|
|
1616
1523
|
*/
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
for
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
// Non-delta: already incorporated in previous synthesis.
|
|
1649
|
-
// Include path with null content so the builder knows it exists.
|
|
1650
|
-
childMetas[path] = null;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
logger?.debug({
|
|
1654
|
-
totalChildren: childEntries.length,
|
|
1655
|
-
deltaChildren: deltaChildCount,
|
|
1656
|
-
}, 'child metas filtered by delta');
|
|
1657
|
-
// Cross-referenced meta outputs (parallel reads)
|
|
1658
|
-
const crossRefMetas = {};
|
|
1659
|
-
const seen = new Set();
|
|
1660
|
-
const crossRefPaths = [];
|
|
1661
|
-
for (const refPath of meta._crossRefs ?? []) {
|
|
1662
|
-
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
1663
|
-
continue;
|
|
1664
|
-
if (seen.has(refPath))
|
|
1665
|
-
continue;
|
|
1666
|
-
seen.add(refPath);
|
|
1667
|
-
crossRefPaths.push(refPath);
|
|
1668
|
-
}
|
|
1669
|
-
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
1670
|
-
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
1671
|
-
return [refPath, content];
|
|
1672
|
-
}));
|
|
1673
|
-
for (const [path, content] of crossRefEntries) {
|
|
1674
|
-
crossRefMetas[path] = content;
|
|
1675
|
-
}
|
|
1676
|
-
// Archive paths
|
|
1677
|
-
const archives = listArchiveFiles(node.metaPath);
|
|
1678
|
-
// Nearest ancestor _builder output
|
|
1679
|
-
let ancestorBuilder;
|
|
1680
|
-
const ancestor = getAncestorMeta(node);
|
|
1681
|
-
if (ancestor) {
|
|
1682
|
-
try {
|
|
1683
|
-
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
1684
|
-
const ancestorMeta = JSON.parse(raw);
|
|
1685
|
-
if (ancestorMeta._builder) {
|
|
1686
|
-
ancestorBuilder = ancestorMeta._builder;
|
|
1524
|
+
/**
|
|
1525
|
+
* Derive _phaseState from existing meta fields.
|
|
1526
|
+
*
|
|
1527
|
+
* If the meta already has _phaseState, returns it as-is.
|
|
1528
|
+
*
|
|
1529
|
+
* Otherwise, reconstructs from available fields:
|
|
1530
|
+
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
1531
|
+
* - Errored meta: the failed phase is mapped from _error.step.
|
|
1532
|
+
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
1533
|
+
* - Fully-fresh meta: all phases fresh.
|
|
1534
|
+
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
1535
|
+
*
|
|
1536
|
+
* @param meta - The meta.json content.
|
|
1537
|
+
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
1538
|
+
* heuristic is used (no architect invalidation check).
|
|
1539
|
+
* @returns The derived PhaseState.
|
|
1540
|
+
*/
|
|
1541
|
+
function derivePhaseState(meta, inputs) {
|
|
1542
|
+
// Already has _phaseState — use it
|
|
1543
|
+
if (meta._phaseState)
|
|
1544
|
+
return meta._phaseState;
|
|
1545
|
+
// Check for errors first — _error.step maps directly to failed phase
|
|
1546
|
+
if (meta._error) {
|
|
1547
|
+
const failedPhase = meta._error.step;
|
|
1548
|
+
const state = freshPhaseState();
|
|
1549
|
+
state[failedPhase] = 'failed';
|
|
1550
|
+
// If architect failed and no _builder, downstream is stale
|
|
1551
|
+
if (failedPhase === 'architect') {
|
|
1552
|
+
if (!meta._builder) {
|
|
1553
|
+
state.builder = 'stale';
|
|
1554
|
+
state.critic = 'stale';
|
|
1687
1555
|
}
|
|
1688
1556
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1557
|
+
// If builder failed, critic is stale
|
|
1558
|
+
if (failedPhase === 'builder') {
|
|
1559
|
+
state.critic = 'stale';
|
|
1691
1560
|
}
|
|
1561
|
+
return state;
|
|
1692
1562
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1563
|
+
// Never synthesized: no _content AND no _builder (and no error)
|
|
1564
|
+
if (!meta._content && !meta._builder) {
|
|
1565
|
+
return initialPhaseState();
|
|
1566
|
+
}
|
|
1567
|
+
// Has _builder but no _content: builder is pending
|
|
1568
|
+
if (meta._builder && !meta._content) {
|
|
1569
|
+
return {
|
|
1570
|
+
architect: 'fresh',
|
|
1571
|
+
builder: 'pending',
|
|
1572
|
+
critic: 'stale',
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
// Has _content but no _feedback: critic is pending
|
|
1576
|
+
if (meta._content && !meta._feedback) {
|
|
1577
|
+
return {
|
|
1578
|
+
architect: 'fresh',
|
|
1579
|
+
builder: 'fresh',
|
|
1580
|
+
critic: 'pending',
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
// Default: fully fresh
|
|
1584
|
+
return freshPhaseState();
|
|
1706
1585
|
}
|
|
1707
1586
|
|
|
1708
1587
|
/**
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
* Prompts are compiled as Handlebars templates with access to config,
|
|
1712
|
-
* meta, and scope context. The architect can write template expressions
|
|
1713
|
-
* into its _builder output; these resolve when the builder task is compiled.
|
|
1588
|
+
* List archive snapshot files in chronological order.
|
|
1714
1589
|
*
|
|
1715
|
-
* @module
|
|
1590
|
+
* @module archive/listArchive
|
|
1716
1591
|
*/
|
|
1717
|
-
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
1718
|
-
/** Build the template context from synthesis inputs. */
|
|
1719
|
-
function buildTemplateContext(ctx, meta, config) {
|
|
1720
|
-
return {
|
|
1721
|
-
config,
|
|
1722
|
-
meta,
|
|
1723
|
-
scope: {
|
|
1724
|
-
fileCount: ctx.scopeFiles.length,
|
|
1725
|
-
deltaCount: ctx.deltaFiles.length,
|
|
1726
|
-
childCount: Object.keys(ctx.childMetas).length,
|
|
1727
|
-
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
1728
|
-
},
|
|
1729
|
-
};
|
|
1730
|
-
}
|
|
1731
1592
|
/**
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1593
|
+
* List archive .json files sorted chronologically (oldest first).
|
|
1594
|
+
*
|
|
1595
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1596
|
+
* @returns Array of absolute paths to archive files, or empty if none.
|
|
1734
1597
|
*/
|
|
1735
|
-
function
|
|
1598
|
+
function listArchiveFiles(metaPath) {
|
|
1599
|
+
const archiveDir = join(metaPath, 'archive');
|
|
1736
1600
|
try {
|
|
1737
|
-
return
|
|
1601
|
+
return readdirSync(archiveDir)
|
|
1602
|
+
.filter((f) => f.endsWith('.json'))
|
|
1603
|
+
.sort()
|
|
1604
|
+
.map((f) => join(archiveDir, f));
|
|
1738
1605
|
}
|
|
1739
1606
|
catch {
|
|
1740
|
-
return
|
|
1607
|
+
return [];
|
|
1741
1608
|
}
|
|
1742
1609
|
}
|
|
1610
|
+
|
|
1743
1611
|
/**
|
|
1744
|
-
*
|
|
1612
|
+
* Prune old archive snapshots beyond maxArchive.
|
|
1745
1613
|
*
|
|
1746
|
-
* @
|
|
1747
|
-
* path listing (delta-aware child metas). When false, null entries get
|
|
1748
|
-
* individual subsections with "(not yet synthesized)" (cross-refs).
|
|
1614
|
+
* @module archive/prune
|
|
1749
1615
|
*/
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const withContent = [];
|
|
1766
|
-
const unchanged = [];
|
|
1767
|
-
const notYetSynthesized = [];
|
|
1768
|
-
for (const [path, content] of Object.entries(metas)) {
|
|
1769
|
-
if (typeof content === 'string') {
|
|
1770
|
-
withContent.push([path, content]);
|
|
1771
|
-
}
|
|
1772
|
-
else if (content === null) {
|
|
1773
|
-
unchanged.push(path);
|
|
1774
|
-
}
|
|
1775
|
-
else {
|
|
1776
|
-
notYetSynthesized.push(path);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
sections.push('', heading);
|
|
1780
|
-
for (const [path, content] of withContent) {
|
|
1781
|
-
sections.push(`### ${path}`, content);
|
|
1782
|
-
}
|
|
1783
|
-
if (notYetSynthesized.length > 0) {
|
|
1784
|
-
sections.push('', `### Not yet synthesized (${notYetSynthesized.length.toString()})`, ...notYetSynthesized.map((p) => `- ${p}`));
|
|
1785
|
-
}
|
|
1786
|
-
if (unchanged.length > 0) {
|
|
1787
|
-
sections.push('', `### Other children (${unchanged.length.toString()} \u2014 unchanged since last synthesis)`, ...unchanged.map((p) => `- ${p}`));
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
/** Inject nearest ancestor's organizational context, if available. */
|
|
1791
|
-
function appendAncestorContext(sections, ctx) {
|
|
1792
|
-
if (ctx.ancestorBuilder) {
|
|
1793
|
-
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
/** Append optional context sections shared across all step prompts. */
|
|
1797
|
-
function appendSharedSections(sections, ctx, options) {
|
|
1798
|
-
const opts = {
|
|
1799
|
-
includeSteer: true,
|
|
1800
|
-
includePreviousContent: true,
|
|
1801
|
-
includePreviousFeedback: true,
|
|
1802
|
-
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
1803
|
-
includeChildMetas: true,
|
|
1804
|
-
includeCrossRefs: true,
|
|
1805
|
-
...options,
|
|
1806
|
-
};
|
|
1807
|
-
if (opts.includeSteer && ctx.steer) {
|
|
1808
|
-
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
1809
|
-
}
|
|
1810
|
-
if (opts.includePreviousContent && ctx.previousContent) {
|
|
1811
|
-
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
1812
|
-
}
|
|
1813
|
-
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
1814
|
-
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
1815
|
-
}
|
|
1816
|
-
if (opts.includeChildMetas) {
|
|
1817
|
-
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas, true);
|
|
1818
|
-
}
|
|
1819
|
-
if (opts.includeCrossRefs) {
|
|
1820
|
-
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
1821
|
-
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Prune archive directory to keep at most maxArchive snapshots.
|
|
1618
|
+
* Removes the oldest files.
|
|
1619
|
+
*
|
|
1620
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1621
|
+
* @param maxArchive - Maximum snapshots to retain.
|
|
1622
|
+
* @returns Number of files pruned.
|
|
1623
|
+
*/
|
|
1624
|
+
async function pruneArchive(metaPath, maxArchive) {
|
|
1625
|
+
const files = listArchiveFiles(metaPath);
|
|
1626
|
+
const toRemove = files.length - maxArchive;
|
|
1627
|
+
if (toRemove <= 0)
|
|
1628
|
+
return 0;
|
|
1629
|
+
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
1630
|
+
return toRemove;
|
|
1822
1631
|
}
|
|
1632
|
+
|
|
1823
1633
|
/**
|
|
1824
|
-
*
|
|
1634
|
+
* Read the latest archive snapshot for steer change detection.
|
|
1825
1635
|
*
|
|
1826
|
-
* @
|
|
1827
|
-
* @param meta - Current meta.json.
|
|
1828
|
-
* @param config - Synthesis config.
|
|
1829
|
-
* @returns The architect task prompt string.
|
|
1636
|
+
* @module archive/readLatest
|
|
1830
1637
|
*/
|
|
1831
|
-
function buildArchitectTask(ctx, meta, config) {
|
|
1832
|
-
const sections = [
|
|
1833
|
-
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
1834
|
-
'',
|
|
1835
|
-
DEFAULT_ARCHITECT_PROMPT,
|
|
1836
|
-
'',
|
|
1837
|
-
'## SCOPE',
|
|
1838
|
-
`Path: ${ctx.path}`,
|
|
1839
|
-
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
1840
|
-
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
1841
|
-
'',
|
|
1842
|
-
'### File listing (scope)',
|
|
1843
|
-
condenseScopeFiles(ctx.scopeFiles),
|
|
1844
|
-
];
|
|
1845
|
-
appendAncestorContext(sections, ctx);
|
|
1846
|
-
// Inject previous _builder so architect can see its own prior output
|
|
1847
|
-
if (meta._builder) {
|
|
1848
|
-
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
1849
|
-
}
|
|
1850
|
-
appendSharedSections(sections, ctx);
|
|
1851
|
-
if (ctx.archives.length > 0) {
|
|
1852
|
-
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.');
|
|
1853
|
-
}
|
|
1854
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1855
|
-
}
|
|
1856
1638
|
/**
|
|
1857
|
-
*
|
|
1639
|
+
* Read the most recent archive snapshot.
|
|
1858
1640
|
*
|
|
1859
|
-
* @param
|
|
1860
|
-
* @
|
|
1861
|
-
* @param config - Synthesis config.
|
|
1862
|
-
* @returns The builder task prompt string.
|
|
1641
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1642
|
+
* @returns The latest archived meta, or null if no archives exist.
|
|
1863
1643
|
*/
|
|
1864
|
-
function
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
'',
|
|
1871
|
-
'## SCOPE',
|
|
1872
|
-
`Path: ${ctx.path}`,
|
|
1873
|
-
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
1874
|
-
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
1875
|
-
];
|
|
1876
|
-
appendAncestorContext(sections, ctx);
|
|
1877
|
-
if (ctx.previousState != null) {
|
|
1878
|
-
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), '```');
|
|
1879
|
-
}
|
|
1880
|
-
appendSharedSections(sections, ctx, {
|
|
1881
|
-
includeSteer: false,
|
|
1882
|
-
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
1883
|
-
});
|
|
1884
|
-
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.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
|
|
1885
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1644
|
+
async function readLatestArchive(metaPath) {
|
|
1645
|
+
const files = listArchiveFiles(metaPath);
|
|
1646
|
+
if (files.length === 0)
|
|
1647
|
+
return null;
|
|
1648
|
+
const raw = await readFile(files[files.length - 1], 'utf8');
|
|
1649
|
+
return JSON.parse(raw);
|
|
1886
1650
|
}
|
|
1651
|
+
|
|
1887
1652
|
/**
|
|
1888
|
-
*
|
|
1653
|
+
* Create archive snapshots of meta.json.
|
|
1889
1654
|
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
* @
|
|
1655
|
+
* Copies current meta.json to archive/\{ISO-timestamp\}.json with
|
|
1656
|
+
* _archived: true and _archivedAt added.
|
|
1657
|
+
*
|
|
1658
|
+
* @module archive/snapshot
|
|
1894
1659
|
*/
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1660
|
+
/**
|
|
1661
|
+
* Create an archive snapshot of the current meta.json.
|
|
1662
|
+
*
|
|
1663
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1664
|
+
* @param meta - Current meta.json content.
|
|
1665
|
+
* @returns The archive file path.
|
|
1666
|
+
*/
|
|
1667
|
+
async function createSnapshot(metaPath, meta) {
|
|
1668
|
+
const archiveDir = join(metaPath, 'archive');
|
|
1669
|
+
await mkdir(archiveDir, { recursive: true });
|
|
1670
|
+
const now = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1671
|
+
const archiveFile = join(archiveDir, now + '.json');
|
|
1672
|
+
const archived = {
|
|
1673
|
+
...meta,
|
|
1674
|
+
_archived: true,
|
|
1675
|
+
_archivedAt: new Date().toISOString(),
|
|
1676
|
+
};
|
|
1677
|
+
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
1678
|
+
return archiveFile;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
1682
|
+
|
|
1683
|
+
function findUpSync(name, {
|
|
1684
|
+
cwd = process$1.cwd(),
|
|
1685
|
+
type = 'file',
|
|
1686
|
+
stopAt,
|
|
1687
|
+
} = {}) {
|
|
1688
|
+
let directory = path.resolve(toPath(cwd) ?? '');
|
|
1689
|
+
const {root} = path.parse(directory);
|
|
1690
|
+
stopAt = path.resolve(directory, toPath(stopAt) ?? root);
|
|
1691
|
+
const isAbsoluteName = path.isAbsolute(name);
|
|
1692
|
+
|
|
1693
|
+
while (directory) {
|
|
1694
|
+
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
1695
|
+
|
|
1696
|
+
try {
|
|
1697
|
+
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
|
|
1698
|
+
if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
|
|
1699
|
+
return filePath;
|
|
1700
|
+
}
|
|
1701
|
+
} catch {}
|
|
1702
|
+
|
|
1703
|
+
if (directory === stopAt || directory === root) {
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
directory = path.dirname(directory);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const isTypeOnlyPackageJsonData = packageData => {
|
|
1712
|
+
if (!packageData || typeof packageData !== 'object' || Array.isArray(packageData)) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const keys = Object.keys(packageData);
|
|
1717
|
+
return keys.length === 1 && keys[0] === 'type' && typeof packageData.type === 'string';
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
const isTypeOnlyPackageJsonSync = filePath => {
|
|
1721
|
+
let fileContents;
|
|
1722
|
+
|
|
1723
|
+
try {
|
|
1724
|
+
fileContents = fs.readFileSync(filePath, 'utf8');
|
|
1725
|
+
} catch {
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
try {
|
|
1730
|
+
return isTypeOnlyPackageJsonData(JSON.parse(fileContents));
|
|
1731
|
+
} catch {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
const getNextSearchDirectory = filePath => {
|
|
1737
|
+
const packageDirectoryPath = path.dirname(filePath);
|
|
1738
|
+
const parentDirectoryPath = path.dirname(packageDirectoryPath);
|
|
1739
|
+
return parentDirectoryPath === packageDirectoryPath ? undefined : parentDirectoryPath;
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const findPackageDirectorySync = (directory, ignoreTypeOnlyPackageJson) => {
|
|
1743
|
+
const filePath = findUpSync('package.json', {cwd: directory});
|
|
1744
|
+
if (!filePath) {
|
|
1745
|
+
return undefined;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const packageDirectoryPath = path.dirname(filePath);
|
|
1749
|
+
if (!ignoreTypeOnlyPackageJson) {
|
|
1750
|
+
return packageDirectoryPath;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (!isTypeOnlyPackageJsonSync(filePath)) {
|
|
1754
|
+
return packageDirectoryPath;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const nextDirectory = getNextSearchDirectory(filePath);
|
|
1758
|
+
if (!nextDirectory) {
|
|
1759
|
+
return undefined;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return findPackageDirectorySync(nextDirectory, ignoreTypeOnlyPackageJson);
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
1766
|
+
return findPackageDirectorySync(cwd ?? process$1.cwd(), ignoreTypeOnlyPackageJson);
|
|
1916
1767
|
}
|
|
1917
1768
|
|
|
1918
1769
|
/**
|
|
1919
|
-
*
|
|
1770
|
+
* Built-in default prompts for the synthesis pipeline.
|
|
1920
1771
|
*
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1772
|
+
* Prompts live as `.md` files next to this module in `src/prompts/`. The
|
|
1773
|
+
* CLI build copies them into `dist/prompts/` (rollup-plugin-copy), and a
|
|
1774
|
+
* build-time assertion verifies the copy.
|
|
1924
1775
|
*
|
|
1925
|
-
*
|
|
1776
|
+
* The prompt directory is resolved relative to the executing module, so
|
|
1777
|
+
* running from source (e.g. vitest) reads `src/prompts/` and the published
|
|
1778
|
+
* bundle reads `dist/prompts/`. Prompts are read lazily on first use, so a
|
|
1779
|
+
* missing file surfaces as one clear error instead of an import-time crash.
|
|
1780
|
+
*
|
|
1781
|
+
* @module prompts
|
|
1926
1782
|
*/
|
|
1927
1783
|
/**
|
|
1928
|
-
*
|
|
1929
|
-
*
|
|
1930
|
-
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
1931
|
-
* a shallow ownership tree (self + direct children only).
|
|
1784
|
+
* Resolve the directory holding the built-in prompt files for a module.
|
|
1932
1785
|
*
|
|
1933
|
-
* @param
|
|
1934
|
-
* @
|
|
1935
|
-
*
|
|
1786
|
+
* @param moduleFile - Absolute path of the executing module.
|
|
1787
|
+
* @returns `<package root>/src/prompts` when the module lives under
|
|
1788
|
+
* `src/`, otherwise `<package root>/dist/prompts` (published bundle).
|
|
1936
1789
|
*/
|
|
1937
|
-
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1942
|
-
// to match the ownership semantics used elsewhere.
|
|
1943
|
-
const rawMetaJsonPaths = await watcher.walk([
|
|
1944
|
-
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1945
|
-
]);
|
|
1946
|
-
const candidateMetaPaths = [
|
|
1947
|
-
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
1948
|
-
].filter((p) => p !== normalized);
|
|
1949
|
-
const candidates = candidateMetaPaths
|
|
1950
|
-
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
1951
|
-
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
1952
|
-
const directChildren = [];
|
|
1953
|
-
for (const c of candidates) {
|
|
1954
|
-
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
1955
|
-
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
1956
|
-
if (!nestedUnderExisting)
|
|
1957
|
-
directChildren.push(c);
|
|
1790
|
+
function resolvePromptDir(moduleFile) {
|
|
1791
|
+
const packageRoot = packageDirectorySync({ cwd: dirname(moduleFile) });
|
|
1792
|
+
if (!packageRoot) {
|
|
1793
|
+
throw new Error(`jeeves-meta: cannot locate package root for prompt module ${moduleFile}`);
|
|
1958
1794
|
}
|
|
1959
|
-
const
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
parent: null,
|
|
1972
|
-
};
|
|
1973
|
-
for (const child of children) {
|
|
1974
|
-
child.parent = node;
|
|
1795
|
+
const [topLevel] = relative(packageRoot, moduleFile).split(sep);
|
|
1796
|
+
return join(packageRoot, topLevel === 'src' ? 'src' : 'dist', 'prompts');
|
|
1797
|
+
}
|
|
1798
|
+
const cache = new Map();
|
|
1799
|
+
function loadPrompt(file) {
|
|
1800
|
+
const cached = cache.get(file);
|
|
1801
|
+
if (cached !== undefined)
|
|
1802
|
+
return cached;
|
|
1803
|
+
const path = join(getPromptDir(), file);
|
|
1804
|
+
let content;
|
|
1805
|
+
try {
|
|
1806
|
+
content = readFileSync(path, 'utf8');
|
|
1975
1807
|
}
|
|
1976
|
-
|
|
1808
|
+
catch (err) {
|
|
1809
|
+
throw new Error(`jeeves-meta: built-in prompt not found at ${path}`, {
|
|
1810
|
+
cause: err,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
cache.set(file, content);
|
|
1814
|
+
return content;
|
|
1815
|
+
}
|
|
1816
|
+
/** Directory the built-in prompts are read from for this module. */
|
|
1817
|
+
function getPromptDir() {
|
|
1818
|
+
return resolvePromptDir(fileURLToPath(import.meta.url));
|
|
1819
|
+
}
|
|
1820
|
+
/** Built-in default architect prompt (read on first use, then cached). */
|
|
1821
|
+
function getDefaultArchitectPrompt() {
|
|
1822
|
+
return loadPrompt('architect.md');
|
|
1823
|
+
}
|
|
1824
|
+
/** Built-in default critic prompt (read on first use, then cached). */
|
|
1825
|
+
function getDefaultCriticPrompt() {
|
|
1826
|
+
return loadPrompt('critic.md');
|
|
1977
1827
|
}
|
|
1978
1828
|
|
|
1979
1829
|
/**
|
|
1980
|
-
*
|
|
1830
|
+
* Compute a structure hash from a sorted file listing.
|
|
1981
1831
|
*
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1832
|
+
* Used to detect when directory structure changes, triggering
|
|
1833
|
+
* an architect re-run.
|
|
1984
1834
|
*
|
|
1985
|
-
* @module
|
|
1835
|
+
* @module structureHash
|
|
1986
1836
|
*/
|
|
1987
1837
|
/**
|
|
1988
|
-
*
|
|
1838
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
1839
|
+
*
|
|
1840
|
+
* @param filePaths - Array of file paths in scope.
|
|
1841
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
1989
1842
|
*/
|
|
1990
|
-
function
|
|
1991
|
-
|
|
1843
|
+
function computeStructureHash(filePaths) {
|
|
1844
|
+
const sorted = [...filePaths].sort();
|
|
1845
|
+
const content = sorted.join('\n');
|
|
1846
|
+
return createHash('sha256').update(content).digest('hex');
|
|
1992
1847
|
}
|
|
1848
|
+
|
|
1993
1849
|
/**
|
|
1994
|
-
*
|
|
1850
|
+
* Per-tick invalidation pass.
|
|
1851
|
+
*
|
|
1852
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
1853
|
+
* then applies the cascade to update _phaseState.
|
|
1854
|
+
*
|
|
1855
|
+
* @module phaseState/invalidate
|
|
1995
1856
|
*/
|
|
1996
|
-
|
|
1997
|
-
|
|
1857
|
+
/**
|
|
1858
|
+
* Check whether a persisted prompt snapshot mismatches the currently-resolved prompt.
|
|
1859
|
+
* Returns true when the snapshot exists and differs from the resolved prompt.
|
|
1860
|
+
* This is informational only — it does NOT trigger invalidation.
|
|
1861
|
+
*/
|
|
1862
|
+
function isPromptStale(snapshot, resolved) {
|
|
1863
|
+
return snapshot !== undefined && snapshot !== resolved;
|
|
1998
1864
|
}
|
|
1999
1865
|
/**
|
|
2000
|
-
*
|
|
2001
|
-
* and it is the first non-fresh phase in pipeline order.
|
|
1866
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
2002
1867
|
*
|
|
2003
|
-
*
|
|
1868
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
1869
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
1870
|
+
* @param config - MetaConfig for architectEvery.
|
|
1871
|
+
* @param node - MetaNode for archive access.
|
|
1872
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
1873
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
1874
|
+
* @returns Updated phase state and invalidation details.
|
|
2004
1875
|
*/
|
|
2005
|
-
function
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1876
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
1877
|
+
let phaseState = meta._phaseState ?? {
|
|
1878
|
+
architect: 'fresh',
|
|
1879
|
+
builder: 'fresh',
|
|
1880
|
+
critic: 'fresh',
|
|
1881
|
+
};
|
|
1882
|
+
// ── Architect-level inputs ──
|
|
1883
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
1884
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
1885
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
1886
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
1887
|
+
// Prompt staleness detection: compare persisted prompt snapshots against
|
|
1888
|
+
// currently-resolved prompts. This is INFORMATIONAL ONLY — reported via
|
|
1889
|
+
// inputStatus so /preview can surface it, but it must NEVER feed into
|
|
1890
|
+
// the invalidation cascade. When a meta naturally reaches architectEvery
|
|
1891
|
+
// through real builder cycles, architect runs with the current prompt and
|
|
1892
|
+
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
1893
|
+
// corpus-wide synthesis storm (see #163).
|
|
1894
|
+
const architectChanged = isPromptStale(meta._architect, getDefaultArchitectPrompt());
|
|
1895
|
+
const criticChanged = isPromptStale(meta._critic, getDefaultCriticPrompt());
|
|
1896
|
+
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
1897
|
+
// _crossRefs declaration change
|
|
1898
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
1899
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
1900
|
+
.slice()
|
|
1901
|
+
.sort()
|
|
1902
|
+
.join(',');
|
|
1903
|
+
const crossRefsDeclChanged = latestArchive
|
|
1904
|
+
? currentRefs !== archiveRefs
|
|
1905
|
+
: currentRefs.length > 0;
|
|
1906
|
+
const architectInvalidators = [];
|
|
1907
|
+
if (structureChanged) {
|
|
1908
|
+
if (meta._state !== undefined) {
|
|
1909
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
1910
|
+
phaseState = invalidateBuilder(phaseState);
|
|
2019
1911
|
}
|
|
2020
1912
|
else {
|
|
2021
|
-
|
|
2022
|
-
if (s === 'pending') {
|
|
2023
|
-
result[phase] = 'stale';
|
|
2024
|
-
}
|
|
2025
|
-
// running in non-first position would be a bug, but don't mask it
|
|
1913
|
+
architectInvalidators.push('structureHash');
|
|
2026
1914
|
}
|
|
2027
1915
|
}
|
|
2028
|
-
|
|
1916
|
+
if (steerChanged)
|
|
1917
|
+
architectInvalidators.push('steer');
|
|
1918
|
+
if (crossRefsDeclChanged)
|
|
1919
|
+
architectInvalidators.push('_crossRefs');
|
|
1920
|
+
if (effectiveSynthesisCount >= config.architectEvery) {
|
|
1921
|
+
architectInvalidators.push('architectEvery');
|
|
1922
|
+
}
|
|
1923
|
+
if (!meta._builder)
|
|
1924
|
+
architectInvalidators.push('firstRun');
|
|
1925
|
+
if (architectInvalidators.length > 0) {
|
|
1926
|
+
phaseState = invalidateArchitect(phaseState);
|
|
1927
|
+
}
|
|
1928
|
+
// ── Builder-level inputs ──
|
|
1929
|
+
// Cross-ref _content change (builder-invalidating)
|
|
1930
|
+
let crossRefContentChanged = false;
|
|
1931
|
+
return {
|
|
1932
|
+
phaseState,
|
|
1933
|
+
architectInvalidators,
|
|
1934
|
+
inputStatus: {
|
|
1935
|
+
structureHash,
|
|
1936
|
+
steerChanged,
|
|
1937
|
+
architectChanged,
|
|
1938
|
+
criticChanged,
|
|
1939
|
+
crossRefsDeclChanged,
|
|
1940
|
+
crossRefContentChanged,
|
|
1941
|
+
},
|
|
1942
|
+
};
|
|
2029
1943
|
}
|
|
2030
|
-
|
|
1944
|
+
|
|
2031
1945
|
/**
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2034
|
-
*
|
|
1946
|
+
* Weighted staleness formula for candidate selection.
|
|
1947
|
+
*
|
|
1948
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
1949
|
+
*
|
|
1950
|
+
* @module scheduling/weightedFormula
|
|
2035
1951
|
*/
|
|
2036
|
-
function invalidateArchitect(state) {
|
|
2037
|
-
return enforceInvariant({
|
|
2038
|
-
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
2039
|
-
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
2040
|
-
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
2041
|
-
});
|
|
2042
|
-
}
|
|
2043
1952
|
/**
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
1953
|
+
* Compute effective staleness for a set of candidates.
|
|
1954
|
+
*
|
|
1955
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
1956
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
1957
|
+
*
|
|
1958
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
1959
|
+
* metas to tune how much their tree position affects scheduling.
|
|
1960
|
+
*
|
|
1961
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
1962
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
1963
|
+
* @returns Same array with effectiveStaleness computed.
|
|
2047
1964
|
*/
|
|
2048
|
-
function
|
|
2049
|
-
if (
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
1965
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
1966
|
+
if (candidates.length === 0)
|
|
1967
|
+
return [];
|
|
1968
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
1969
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
1970
|
+
// Normalize: shift so minimum becomes 0
|
|
1971
|
+
const minDepth = Math.min(...depths);
|
|
1972
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
1973
|
+
return candidates.map((c, i) => {
|
|
1974
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
1975
|
+
return {
|
|
1976
|
+
...c,
|
|
1977
|
+
effectiveStaleness: c.actualStaleness *
|
|
1978
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
1979
|
+
};
|
|
2063
1980
|
});
|
|
2064
1981
|
}
|
|
2065
|
-
|
|
1982
|
+
|
|
2066
1983
|
/**
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
1984
|
+
* Corpus-wide phase scheduler.
|
|
1985
|
+
*
|
|
1986
|
+
* Selects the highest-priority ready phase across all metas.
|
|
1987
|
+
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
1988
|
+
* Tiebreak within band: weighted staleness (§3.9).
|
|
1989
|
+
*
|
|
1990
|
+
* @module phaseState/phaseScheduler
|
|
2069
1991
|
*/
|
|
2070
|
-
function architectSuccess(state) {
|
|
2071
|
-
return enforceInvariant({
|
|
2072
|
-
architect: 'fresh',
|
|
2073
|
-
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
2074
|
-
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
1992
|
/**
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
1993
|
+
* Build phase candidates from listMetas entries.
|
|
1994
|
+
*
|
|
1995
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
1996
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
1997
|
+
* Used by orchestratePhase, queue route, and status route.
|
|
2080
1998
|
*/
|
|
2081
|
-
function
|
|
2082
|
-
return
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
1999
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
2000
|
+
return entries.map((entry) => {
|
|
2001
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
2002
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
2003
|
+
if (entry.meta._phaseState) {
|
|
2004
|
+
const needsArchitect = !entry.meta._builder ||
|
|
2005
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
2006
|
+
if (needsArchitect && isFullyFresh(ps)) {
|
|
2007
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
node: entry.node,
|
|
2012
|
+
meta: entry.meta,
|
|
2013
|
+
phaseState: ps,
|
|
2014
|
+
actualStaleness: entry.stalenessSeconds,
|
|
2015
|
+
locked: entry.locked,
|
|
2016
|
+
disabled: entry.disabled,
|
|
2017
|
+
};
|
|
2086
2018
|
});
|
|
2087
2019
|
}
|
|
2088
2020
|
/**
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2021
|
+
* Rank all eligible phase candidates by priority.
|
|
2022
|
+
*
|
|
2023
|
+
* Filters to pending phases, computes effective staleness, and sorts by
|
|
2024
|
+
* band (ascending: critic first) then effective staleness (descending).
|
|
2025
|
+
*
|
|
2026
|
+
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
2091
2027
|
*/
|
|
2092
|
-
function
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2028
|
+
function rankPhaseCandidates(metas, depthWeight) {
|
|
2029
|
+
// Filter to metas with a pending (scheduler-eligible) phase
|
|
2030
|
+
const eligible = metas.filter((m) => {
|
|
2031
|
+
if (m.locked)
|
|
2032
|
+
return false;
|
|
2033
|
+
if (m.disabled && !m.isOverride)
|
|
2034
|
+
return false;
|
|
2035
|
+
const owed = getOwedPhase(m.phaseState);
|
|
2036
|
+
if (!owed)
|
|
2037
|
+
return false;
|
|
2038
|
+
return m.phaseState[owed] === 'pending';
|
|
2096
2039
|
});
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2040
|
+
if (eligible.length === 0)
|
|
2041
|
+
return [];
|
|
2042
|
+
// Compute effective staleness for tiebreaking
|
|
2043
|
+
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
2044
|
+
node: m.node,
|
|
2045
|
+
meta: m.meta,
|
|
2046
|
+
actualStaleness: m.actualStaleness,
|
|
2047
|
+
})), depthWeight);
|
|
2048
|
+
// Build candidates with band info
|
|
2049
|
+
const candidates = withStaleness.map((ws, i) => {
|
|
2050
|
+
const m = eligible[i];
|
|
2051
|
+
const owedPhase = getOwedPhase(m.phaseState);
|
|
2052
|
+
return {
|
|
2053
|
+
node: ws.node,
|
|
2054
|
+
meta: ws.meta,
|
|
2055
|
+
phaseState: m.phaseState,
|
|
2056
|
+
owedPhase,
|
|
2057
|
+
band: getPriorityBand(m.phaseState),
|
|
2058
|
+
actualStaleness: ws.actualStaleness,
|
|
2059
|
+
effectiveStaleness: ws.effectiveStaleness,
|
|
2060
|
+
};
|
|
2107
2061
|
});
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
*/
|
|
2114
|
-
function retryPhase(state, phase) {
|
|
2115
|
-
if (state[phase] !== 'failed')
|
|
2116
|
-
return state;
|
|
2117
|
-
return enforceInvariant({
|
|
2118
|
-
...state,
|
|
2119
|
-
[phase]: 'pending',
|
|
2062
|
+
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
2063
|
+
candidates.sort((a, b) => {
|
|
2064
|
+
if (a.band !== b.band)
|
|
2065
|
+
return a.band - b.band;
|
|
2066
|
+
return b.effectiveStaleness - a.effectiveStaleness;
|
|
2120
2067
|
});
|
|
2068
|
+
return candidates;
|
|
2121
2069
|
}
|
|
2122
2070
|
/**
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
for (const phase of ['architect', 'builder', 'critic']) {
|
|
2129
|
-
if (result[phase] === 'failed') {
|
|
2130
|
-
result = retryPhase(result, phase);
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
return result;
|
|
2134
|
-
}
|
|
2135
|
-
// ── Running transition ─────────────────────────────────────────────────
|
|
2136
|
-
/**
|
|
2137
|
-
* Mark a phase as running (scheduler picks it).
|
|
2071
|
+
* Select the best phase candidate across the corpus.
|
|
2072
|
+
*
|
|
2073
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
2074
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
2075
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
2138
2076
|
*/
|
|
2139
|
-
function
|
|
2140
|
-
return
|
|
2141
|
-
...state,
|
|
2142
|
-
[phase]: 'running',
|
|
2143
|
-
};
|
|
2077
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
2078
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
2144
2079
|
}
|
|
2145
|
-
// ── Query helpers ──────────────────────────────────────────────────────
|
|
2146
2080
|
/**
|
|
2147
|
-
*
|
|
2081
|
+
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
2082
|
+
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
2148
2083
|
*/
|
|
2149
|
-
function
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return null;
|
|
2084
|
+
function selectAllTier2Candidates(metas) {
|
|
2085
|
+
return metas
|
|
2086
|
+
.filter((m) => !m.locked && !m.disabled && isFullyFresh(m.phaseState))
|
|
2087
|
+
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
2088
|
+
.map((m) => ({ node: m.node, meta: m.meta }));
|
|
2155
2089
|
}
|
|
2090
|
+
|
|
2156
2091
|
/**
|
|
2157
|
-
*
|
|
2092
|
+
* Shared error utilities.
|
|
2093
|
+
*
|
|
2094
|
+
* @module errors
|
|
2158
2095
|
*/
|
|
2159
|
-
function isFullyFresh(state) {
|
|
2160
|
-
return (state.architect === 'fresh' &&
|
|
2161
|
-
state.builder === 'fresh' &&
|
|
2162
|
-
state.critic === 'fresh');
|
|
2163
|
-
}
|
|
2164
2096
|
/**
|
|
2165
|
-
*
|
|
2166
|
-
*
|
|
2097
|
+
* Wrap an unknown caught value into a MetaError.
|
|
2098
|
+
*
|
|
2099
|
+
* @param step - Which synthesis step failed.
|
|
2100
|
+
* @param err - The caught error value.
|
|
2101
|
+
* @param code - Error classification code.
|
|
2102
|
+
* @returns A structured MetaError.
|
|
2167
2103
|
*/
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
if (owed === 'builder')
|
|
2175
|
-
return 2;
|
|
2176
|
-
return 3;
|
|
2104
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
2105
|
+
return {
|
|
2106
|
+
step,
|
|
2107
|
+
code,
|
|
2108
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2109
|
+
};
|
|
2177
2110
|
}
|
|
2178
2111
|
|
|
2179
2112
|
/**
|
|
2180
|
-
*
|
|
2113
|
+
* Build the MetaContext for a synthesis cycle.
|
|
2181
2114
|
*
|
|
2182
|
-
*
|
|
2183
|
-
*
|
|
2184
|
-
* the architect-invalidating inputs.
|
|
2115
|
+
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
2116
|
+
* previous content/feedback, steer, and archive paths.
|
|
2185
2117
|
*
|
|
2186
|
-
* @module
|
|
2118
|
+
* @module orchestrator/contextPackage
|
|
2187
2119
|
*/
|
|
2188
2120
|
/**
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2191
|
-
* If the meta already has _phaseState, returns it as-is.
|
|
2192
|
-
*
|
|
2193
|
-
* Otherwise, reconstructs from available fields:
|
|
2194
|
-
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
2195
|
-
* - Errored meta: the failed phase is mapped from _error.step.
|
|
2196
|
-
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
2197
|
-
* - Fully-fresh meta: all phases fresh.
|
|
2198
|
-
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
2121
|
+
* Condense a file list into glob-like summaries.
|
|
2122
|
+
* Groups by directory + extension pattern.
|
|
2199
2123
|
*
|
|
2200
|
-
* @param
|
|
2201
|
-
* @param
|
|
2202
|
-
*
|
|
2203
|
-
* @returns The derived PhaseState.
|
|
2124
|
+
* @param files - Array of file paths.
|
|
2125
|
+
* @param maxIndividual - Show individual files up to this count.
|
|
2126
|
+
* @returns Condensed summary string.
|
|
2204
2127
|
*/
|
|
2205
|
-
function
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
const
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
if (failedPhase === 'architect') {
|
|
2216
|
-
if (!meta._builder) {
|
|
2217
|
-
state.builder = 'stale';
|
|
2218
|
-
state.critic = 'stale';
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
// If builder failed, critic is stale
|
|
2222
|
-
if (failedPhase === 'builder') {
|
|
2223
|
-
state.critic = 'stale';
|
|
2224
|
-
}
|
|
2225
|
-
return state;
|
|
2226
|
-
}
|
|
2227
|
-
// Never synthesized: no _content AND no _builder (and no error)
|
|
2228
|
-
if (!meta._content && !meta._builder) {
|
|
2229
|
-
return initialPhaseState();
|
|
2128
|
+
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
2129
|
+
if (files.length <= maxIndividual)
|
|
2130
|
+
return files.join('\n');
|
|
2131
|
+
// Group by dir + extension
|
|
2132
|
+
const groups = new Map();
|
|
2133
|
+
for (const f of files) {
|
|
2134
|
+
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
2135
|
+
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
2136
|
+
const key = dir + '*' + ext;
|
|
2137
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
2230
2138
|
}
|
|
2231
|
-
//
|
|
2232
|
-
|
|
2139
|
+
// Sort by count descending
|
|
2140
|
+
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
2141
|
+
return sorted
|
|
2142
|
+
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
2143
|
+
.join('\n');
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Read a meta.json file and extract `_content` and `_generatedAt`.
|
|
2147
|
+
*
|
|
2148
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
2149
|
+
* @returns Parsed fields, or defaults if missing/unreadable.
|
|
2150
|
+
*/
|
|
2151
|
+
async function readChildMeta(metaJsonPath) {
|
|
2152
|
+
try {
|
|
2153
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
2154
|
+
const meta = JSON.parse(raw);
|
|
2233
2155
|
return {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
critic: 'stale',
|
|
2156
|
+
content: meta._content ?? null,
|
|
2157
|
+
generatedAt: meta._generatedAt,
|
|
2237
2158
|
};
|
|
2238
2159
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
return {
|
|
2242
|
-
architect: 'fresh',
|
|
2243
|
-
builder: 'fresh',
|
|
2244
|
-
critic: 'pending',
|
|
2245
|
-
};
|
|
2160
|
+
catch {
|
|
2161
|
+
return { content: null, generatedAt: undefined };
|
|
2246
2162
|
}
|
|
2247
|
-
// Default: fully fresh
|
|
2248
|
-
return freshPhaseState();
|
|
2249
2163
|
}
|
|
2250
|
-
|
|
2251
|
-
/**
|
|
2252
|
-
* Compute a structure hash from a sorted file listing.
|
|
2253
|
-
*
|
|
2254
|
-
* Used to detect when directory structure changes, triggering
|
|
2255
|
-
* an architect re-run.
|
|
2256
|
-
*
|
|
2257
|
-
* @module structureHash
|
|
2258
|
-
*/
|
|
2259
2164
|
/**
|
|
2260
|
-
*
|
|
2165
|
+
* Read a meta.json file and extract its `_content` field.
|
|
2261
2166
|
*
|
|
2262
|
-
* @param
|
|
2263
|
-
* @returns
|
|
2167
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
2168
|
+
* @returns The `_content` string, or null if missing/unreadable.
|
|
2264
2169
|
*/
|
|
2265
|
-
function
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
return createHash('sha256').update(content).digest('hex');
|
|
2170
|
+
async function readMetaContent(metaJsonPath) {
|
|
2171
|
+
const info = await readChildMeta(metaJsonPath);
|
|
2172
|
+
return info.content;
|
|
2269
2173
|
}
|
|
2270
|
-
|
|
2271
2174
|
/**
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
2275
|
-
* then applies the cascade to update _phaseState.
|
|
2175
|
+
* Build the context package for a synthesis cycle.
|
|
2276
2176
|
*
|
|
2277
|
-
* @
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
*
|
|
2281
|
-
* Returns true when the snapshot exists and differs from the resolved prompt.
|
|
2282
|
-
* This is informational only — it does NOT trigger invalidation.
|
|
2177
|
+
* @param node - The meta node being synthesized.
|
|
2178
|
+
* @param meta - Current meta.json content.
|
|
2179
|
+
* @param watcher - WatcherClient for scope enumeration.
|
|
2180
|
+
* @returns The computed context package.
|
|
2283
2181
|
*/
|
|
2284
|
-
function
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
};
|
|
2304
|
-
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
// through real builder cycles, architect runs with the current prompt and
|
|
2314
|
-
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
2315
|
-
// corpus-wide synthesis storm (see #163).
|
|
2316
|
-
const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
|
|
2317
|
-
const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
|
|
2318
|
-
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
2319
|
-
// _crossRefs declaration change
|
|
2320
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
2321
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
2322
|
-
.slice()
|
|
2323
|
-
.sort()
|
|
2324
|
-
.join(',');
|
|
2325
|
-
const crossRefsDeclChanged = latestArchive
|
|
2326
|
-
? currentRefs !== archiveRefs
|
|
2327
|
-
: currentRefs.length > 0;
|
|
2328
|
-
const architectInvalidators = [];
|
|
2329
|
-
if (structureChanged) {
|
|
2330
|
-
if (meta._state !== undefined) {
|
|
2331
|
-
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
2332
|
-
phaseState = invalidateBuilder(phaseState);
|
|
2182
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
2183
|
+
// Scope and delta files via watcher walk
|
|
2184
|
+
const scopeStart = Date.now();
|
|
2185
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
2186
|
+
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
2187
|
+
logger?.debug({
|
|
2188
|
+
scopeFiles: scopeFiles.length,
|
|
2189
|
+
deltaFiles: deltaFiles.length,
|
|
2190
|
+
durationMs: Date.now() - scopeStart,
|
|
2191
|
+
}, 'scope and delta files computed');
|
|
2192
|
+
// Child meta outputs (parallel reads, delta-aware).
|
|
2193
|
+
// Only include full _content for children synthesized AFTER the parent's
|
|
2194
|
+
// _generatedAt. Non-delta children are already folded into the parent's
|
|
2195
|
+
// previous _content and are listed as path-only references (#169).
|
|
2196
|
+
const parentGeneratedAt = meta._generatedAt;
|
|
2197
|
+
const childMetas = {};
|
|
2198
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
2199
|
+
const info = await readChildMeta(join(child.metaPath, 'meta.json'));
|
|
2200
|
+
return [child.ownerPath, info];
|
|
2201
|
+
}));
|
|
2202
|
+
let deltaChildCount = 0;
|
|
2203
|
+
for (const [path, info] of childEntries) {
|
|
2204
|
+
// Delta child: no parent timestamp (first run), or child synthesized
|
|
2205
|
+
// after parent. Include full content.
|
|
2206
|
+
if (!parentGeneratedAt ||
|
|
2207
|
+
!info.generatedAt ||
|
|
2208
|
+
info.generatedAt > parentGeneratedAt) {
|
|
2209
|
+
childMetas[path] = info.content ?? undefined;
|
|
2210
|
+
deltaChildCount++;
|
|
2333
2211
|
}
|
|
2334
2212
|
else {
|
|
2335
|
-
|
|
2213
|
+
// Non-delta: already incorporated in previous synthesis.
|
|
2214
|
+
// Include path with null content so the builder knows it exists.
|
|
2215
|
+
childMetas[path] = null;
|
|
2336
2216
|
}
|
|
2337
2217
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2218
|
+
logger?.debug({
|
|
2219
|
+
totalChildren: childEntries.length,
|
|
2220
|
+
deltaChildren: deltaChildCount,
|
|
2221
|
+
}, 'child metas filtered by delta');
|
|
2222
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
2223
|
+
const crossRefMetas = {};
|
|
2224
|
+
const seen = new Set();
|
|
2225
|
+
const crossRefPaths = [];
|
|
2226
|
+
for (const refPath of meta._crossRefs ?? []) {
|
|
2227
|
+
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
2228
|
+
continue;
|
|
2229
|
+
if (seen.has(refPath))
|
|
2230
|
+
continue;
|
|
2231
|
+
seen.add(refPath);
|
|
2232
|
+
crossRefPaths.push(refPath);
|
|
2344
2233
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2234
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
2235
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
2236
|
+
return [refPath, content];
|
|
2237
|
+
}));
|
|
2238
|
+
for (const [path, content] of crossRefEntries) {
|
|
2239
|
+
crossRefMetas[path] = content;
|
|
2240
|
+
}
|
|
2241
|
+
// Archive paths
|
|
2242
|
+
const archives = listArchiveFiles(node.metaPath);
|
|
2243
|
+
// Nearest ancestor _builder output
|
|
2244
|
+
let ancestorBuilder;
|
|
2245
|
+
const ancestor = getAncestorMeta(node);
|
|
2246
|
+
if (ancestor) {
|
|
2247
|
+
try {
|
|
2248
|
+
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
2249
|
+
const ancestorMeta = JSON.parse(raw);
|
|
2250
|
+
if (ancestorMeta._builder) {
|
|
2251
|
+
ancestorBuilder = ancestorMeta._builder;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
// Ancestor meta.json unreadable — skip
|
|
2256
|
+
}
|
|
2349
2257
|
}
|
|
2350
|
-
// ── Builder-level inputs ──
|
|
2351
|
-
// Cross-ref _content change (builder-invalidating)
|
|
2352
|
-
let crossRefContentChanged = false;
|
|
2353
2258
|
return {
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2259
|
+
path: node.metaPath,
|
|
2260
|
+
scopeFiles,
|
|
2261
|
+
deltaFiles,
|
|
2262
|
+
childMetas,
|
|
2263
|
+
crossRefMetas,
|
|
2264
|
+
previousContent: meta._content ?? null,
|
|
2265
|
+
previousFeedback: meta._feedback ?? null,
|
|
2266
|
+
steer: meta._steer ?? null,
|
|
2267
|
+
previousState: meta._state ?? null,
|
|
2268
|
+
archives,
|
|
2269
|
+
ancestorBuilder,
|
|
2364
2270
|
};
|
|
2365
2271
|
}
|
|
2366
2272
|
|
|
2367
2273
|
/**
|
|
2368
|
-
*
|
|
2369
|
-
*
|
|
2370
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
2371
|
-
*
|
|
2372
|
-
* @module scheduling/weightedFormula
|
|
2373
|
-
*/
|
|
2374
|
-
/**
|
|
2375
|
-
* Compute effective staleness for a set of candidates.
|
|
2376
|
-
*
|
|
2377
|
-
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
2378
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
2274
|
+
* Build task prompts for each synthesis step.
|
|
2379
2275
|
*
|
|
2380
|
-
*
|
|
2381
|
-
*
|
|
2276
|
+
* Prompts are compiled as Handlebars templates with access to config,
|
|
2277
|
+
* meta, and scope context. The architect can write template expressions
|
|
2278
|
+
* into its _builder output; these resolve when the builder task is compiled.
|
|
2382
2279
|
*
|
|
2383
|
-
* @
|
|
2384
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
2385
|
-
* @returns Same array with effectiveStaleness computed.
|
|
2280
|
+
* @module orchestrator/buildTask
|
|
2386
2281
|
*/
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
2401
|
-
};
|
|
2402
|
-
});
|
|
2282
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
2283
|
+
/** Build the template context from synthesis inputs. */
|
|
2284
|
+
function buildTemplateContext(ctx, meta, config) {
|
|
2285
|
+
return {
|
|
2286
|
+
config,
|
|
2287
|
+
meta,
|
|
2288
|
+
scope: {
|
|
2289
|
+
fileCount: ctx.scopeFiles.length,
|
|
2290
|
+
deltaCount: ctx.deltaFiles.length,
|
|
2291
|
+
childCount: Object.keys(ctx.childMetas).length,
|
|
2292
|
+
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
2293
|
+
},
|
|
2294
|
+
};
|
|
2403
2295
|
}
|
|
2404
|
-
|
|
2405
2296
|
/**
|
|
2406
|
-
*
|
|
2407
|
-
*
|
|
2408
|
-
* Selects the highest-priority ready phase across all metas.
|
|
2409
|
-
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
2410
|
-
* Tiebreak within band: weighted staleness (§3.9).
|
|
2411
|
-
*
|
|
2412
|
-
* @module phaseState/phaseScheduler
|
|
2297
|
+
* Compile a string as a Handlebars template with the given context.
|
|
2298
|
+
* Returns the original string unchanged if compilation fails.
|
|
2413
2299
|
*/
|
|
2300
|
+
function compileTemplate(text, context) {
|
|
2301
|
+
try {
|
|
2302
|
+
return Handlebars.compile(text, { noEscape: true })(context);
|
|
2303
|
+
}
|
|
2304
|
+
catch {
|
|
2305
|
+
return text;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2414
2308
|
/**
|
|
2415
|
-
*
|
|
2309
|
+
* Append a keyed record of meta outputs as subsections, if non-empty.
|
|
2416
2310
|
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2311
|
+
* @param condenseMissing - When true, null entries are condensed into a
|
|
2312
|
+
* path listing (delta-aware child metas). When false, null entries get
|
|
2313
|
+
* individual subsections with "(not yet synthesized)" (cross-refs).
|
|
2420
2314
|
*/
|
|
2421
|
-
function
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2430
|
-
}
|
|
2315
|
+
function appendMetaSections(sections, heading, metas, condenseMissing = false) {
|
|
2316
|
+
if (Object.keys(metas).length === 0)
|
|
2317
|
+
return;
|
|
2318
|
+
if (!condenseMissing) {
|
|
2319
|
+
// Original behavior: every entry gets a subsection.
|
|
2320
|
+
sections.push('', heading);
|
|
2321
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
2322
|
+
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
2431
2323
|
}
|
|
2432
|
-
return
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
// Delta-aware: separate entries into three categories (#169).
|
|
2327
|
+
// - string content → full subsection (delta child with content)
|
|
2328
|
+
// - null → non-delta, already folded into previous synthesis
|
|
2329
|
+
// - undefined → delta child not yet synthesized
|
|
2330
|
+
const withContent = [];
|
|
2331
|
+
const unchanged = [];
|
|
2332
|
+
const notYetSynthesized = [];
|
|
2333
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
2334
|
+
if (typeof content === 'string') {
|
|
2335
|
+
withContent.push([path, content]);
|
|
2336
|
+
}
|
|
2337
|
+
else if (content === null) {
|
|
2338
|
+
unchanged.push(path);
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
notYetSynthesized.push(path);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
sections.push('', heading);
|
|
2345
|
+
for (const [path, content] of withContent) {
|
|
2346
|
+
sections.push(`### ${path}`, content);
|
|
2347
|
+
}
|
|
2348
|
+
if (notYetSynthesized.length > 0) {
|
|
2349
|
+
sections.push('', `### Not yet synthesized (${notYetSynthesized.length.toString()})`, ...notYetSynthesized.map((p) => `- ${p}`));
|
|
2350
|
+
}
|
|
2351
|
+
if (unchanged.length > 0) {
|
|
2352
|
+
sections.push('', `### Other children (${unchanged.length.toString()} \u2014 unchanged since last synthesis)`, ...unchanged.map((p) => `- ${p}`));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
/** Inject nearest ancestor's organizational context, if available. */
|
|
2356
|
+
function appendAncestorContext(sections, ctx) {
|
|
2357
|
+
if (ctx.ancestorBuilder) {
|
|
2358
|
+
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
/** Append optional context sections shared across all step prompts. */
|
|
2362
|
+
function appendSharedSections(sections, ctx, options) {
|
|
2363
|
+
const opts = {
|
|
2364
|
+
includeSteer: true,
|
|
2365
|
+
includePreviousContent: true,
|
|
2366
|
+
includePreviousFeedback: true,
|
|
2367
|
+
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
2368
|
+
includeChildMetas: true,
|
|
2369
|
+
includeCrossRefs: true,
|
|
2370
|
+
...options,
|
|
2371
|
+
};
|
|
2372
|
+
if (opts.includeSteer && ctx.steer) {
|
|
2373
|
+
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
2374
|
+
}
|
|
2375
|
+
if (opts.includePreviousContent && ctx.previousContent) {
|
|
2376
|
+
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
2377
|
+
}
|
|
2378
|
+
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
2379
|
+
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
2380
|
+
}
|
|
2381
|
+
if (opts.includeChildMetas) {
|
|
2382
|
+
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas, true);
|
|
2383
|
+
}
|
|
2384
|
+
if (opts.includeCrossRefs) {
|
|
2385
|
+
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
2386
|
+
}
|
|
2441
2387
|
}
|
|
2442
2388
|
/**
|
|
2443
|
-
*
|
|
2444
|
-
*
|
|
2445
|
-
* Filters to pending phases, computes effective staleness, and sorts by
|
|
2446
|
-
* band (ascending: critic first) then effective staleness (descending).
|
|
2389
|
+
* Build the architect task prompt.
|
|
2447
2390
|
*
|
|
2448
|
-
*
|
|
2391
|
+
* @param ctx - Synthesis context.
|
|
2392
|
+
* @param meta - Current meta.json.
|
|
2393
|
+
* @param config - Synthesis config.
|
|
2394
|
+
* @returns The architect task prompt string.
|
|
2449
2395
|
*/
|
|
2450
|
-
function
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
meta
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
return {
|
|
2475
|
-
node: ws.node,
|
|
2476
|
-
meta: ws.meta,
|
|
2477
|
-
phaseState: m.phaseState,
|
|
2478
|
-
owedPhase,
|
|
2479
|
-
band: getPriorityBand(m.phaseState),
|
|
2480
|
-
actualStaleness: ws.actualStaleness,
|
|
2481
|
-
effectiveStaleness: ws.effectiveStaleness,
|
|
2482
|
-
};
|
|
2483
|
-
});
|
|
2484
|
-
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
2485
|
-
candidates.sort((a, b) => {
|
|
2486
|
-
if (a.band !== b.band)
|
|
2487
|
-
return a.band - b.band;
|
|
2488
|
-
return b.effectiveStaleness - a.effectiveStaleness;
|
|
2489
|
-
});
|
|
2490
|
-
return candidates;
|
|
2396
|
+
function buildArchitectTask(ctx, meta, config) {
|
|
2397
|
+
const sections = [
|
|
2398
|
+
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
2399
|
+
'',
|
|
2400
|
+
getDefaultArchitectPrompt(),
|
|
2401
|
+
'',
|
|
2402
|
+
'## SCOPE',
|
|
2403
|
+
`Path: ${ctx.path}`,
|
|
2404
|
+
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
2405
|
+
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
2406
|
+
'',
|
|
2407
|
+
'### File listing (scope)',
|
|
2408
|
+
condenseScopeFiles(ctx.scopeFiles),
|
|
2409
|
+
];
|
|
2410
|
+
appendAncestorContext(sections, ctx);
|
|
2411
|
+
// Inject previous _builder so architect can see its own prior output
|
|
2412
|
+
if (meta._builder) {
|
|
2413
|
+
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
2414
|
+
}
|
|
2415
|
+
appendSharedSections(sections, ctx);
|
|
2416
|
+
if (ctx.archives.length > 0) {
|
|
2417
|
+
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.');
|
|
2418
|
+
}
|
|
2419
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2491
2420
|
}
|
|
2492
2421
|
/**
|
|
2493
|
-
*
|
|
2422
|
+
* Build the builder task prompt.
|
|
2494
2423
|
*
|
|
2495
|
-
* @param
|
|
2496
|
-
* @param
|
|
2497
|
-
* @
|
|
2498
|
-
|
|
2499
|
-
function selectPhaseCandidate(metas, depthWeight) {
|
|
2500
|
-
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
2501
|
-
}
|
|
2502
|
-
/**
|
|
2503
|
-
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
2504
|
-
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
2424
|
+
* @param ctx - Synthesis context.
|
|
2425
|
+
* @param meta - Current meta.json.
|
|
2426
|
+
* @param config - Synthesis config.
|
|
2427
|
+
* @returns The builder task prompt string.
|
|
2505
2428
|
*/
|
|
2506
|
-
function
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2429
|
+
function buildBuilderTask(ctx, meta, config) {
|
|
2430
|
+
const sections = [
|
|
2431
|
+
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
2432
|
+
'',
|
|
2433
|
+
'## TASK BRIEF (from Architect)',
|
|
2434
|
+
meta._builder ?? '(No architect brief available)',
|
|
2435
|
+
'',
|
|
2436
|
+
'## SCOPE',
|
|
2437
|
+
`Path: ${ctx.path}`,
|
|
2438
|
+
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
2439
|
+
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
2440
|
+
];
|
|
2441
|
+
appendAncestorContext(sections, ctx);
|
|
2442
|
+
if (ctx.previousState != null) {
|
|
2443
|
+
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), '```');
|
|
2444
|
+
}
|
|
2445
|
+
appendSharedSections(sections, ctx, {
|
|
2446
|
+
includeSteer: false,
|
|
2447
|
+
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
2448
|
+
});
|
|
2449
|
+
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.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
|
|
2450
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2511
2451
|
}
|
|
2512
|
-
|
|
2513
|
-
/**
|
|
2514
|
-
* Shared error utilities.
|
|
2515
|
-
*
|
|
2516
|
-
* @module errors
|
|
2517
|
-
*/
|
|
2518
2452
|
/**
|
|
2519
|
-
*
|
|
2453
|
+
* Build the critic task prompt.
|
|
2520
2454
|
*
|
|
2521
|
-
* @param
|
|
2522
|
-
* @param
|
|
2523
|
-
* @param
|
|
2524
|
-
* @returns
|
|
2455
|
+
* @param ctx - Synthesis context.
|
|
2456
|
+
* @param meta - Current meta.json (with _content already set by builder).
|
|
2457
|
+
* @param config - Synthesis config.
|
|
2458
|
+
* @returns The critic task prompt string.
|
|
2525
2459
|
*/
|
|
2526
|
-
function
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2460
|
+
function buildCriticTask(ctx, meta, config) {
|
|
2461
|
+
const sections = [
|
|
2462
|
+
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
2463
|
+
'',
|
|
2464
|
+
getDefaultCriticPrompt(),
|
|
2465
|
+
'',
|
|
2466
|
+
'## SYNTHESIS TO EVALUATE',
|
|
2467
|
+
meta._content ?? '(No content produced)',
|
|
2468
|
+
'',
|
|
2469
|
+
'## SCOPE',
|
|
2470
|
+
`Path: ${ctx.path}`,
|
|
2471
|
+
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
2472
|
+
];
|
|
2473
|
+
appendSharedSections(sections, ctx, {
|
|
2474
|
+
includePreviousContent: false,
|
|
2475
|
+
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
2476
|
+
includeChildMetas: false,
|
|
2477
|
+
includeCrossRefs: false,
|
|
2478
|
+
});
|
|
2479
|
+
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
2480
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2532
2481
|
}
|
|
2533
2482
|
|
|
2534
2483
|
/**
|
|
@@ -2723,7 +2672,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2723
2672
|
ps = architectSuccess(ps);
|
|
2724
2673
|
const architectUpdates = {
|
|
2725
2674
|
_builder: builderBrief,
|
|
2726
|
-
_architect:
|
|
2675
|
+
_architect: getDefaultArchitectPrompt(),
|
|
2727
2676
|
_synthesisCount: 0,
|
|
2728
2677
|
_architectTokens: architectTokens,
|
|
2729
2678
|
_generatedAt: new Date().toISOString(),
|
|
@@ -2876,7 +2825,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
2876
2825
|
const cycleComplete = isFullyFresh(ps);
|
|
2877
2826
|
const updates = {
|
|
2878
2827
|
_feedback: feedback,
|
|
2879
|
-
_critic:
|
|
2828
|
+
_critic: getDefaultCriticPrompt(),
|
|
2880
2829
|
_criticTokens: criticTokens,
|
|
2881
2830
|
_error: undefined,
|
|
2882
2831
|
};
|
|
@@ -4170,6 +4119,100 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4170
4119
|
});
|
|
4171
4120
|
}
|
|
4172
4121
|
|
|
4122
|
+
/**
|
|
4123
|
+
* Archive reading helpers — watcher scan with filesystem fallback.
|
|
4124
|
+
*
|
|
4125
|
+
* Used by the GET /metas/:path route to retrieve archive history.
|
|
4126
|
+
* Prefers watcher scan for performance; falls back to disk reads
|
|
4127
|
+
* when the watcher is unavailable.
|
|
4128
|
+
*
|
|
4129
|
+
* @module archive/readArchive
|
|
4130
|
+
*/
|
|
4131
|
+
/** Build a Qdrant filter for archive points under a given meta path. */
|
|
4132
|
+
function buildArchiveScanFilter(metaPath, metaArchiveProperty) {
|
|
4133
|
+
const must = [
|
|
4134
|
+
{
|
|
4135
|
+
key: 'file_path',
|
|
4136
|
+
match: { text: normalizePath(join(metaPath, 'archive')) },
|
|
4137
|
+
},
|
|
4138
|
+
];
|
|
4139
|
+
for (const [key, value] of Object.entries(metaArchiveProperty)) {
|
|
4140
|
+
must.push({ key, match: { value } });
|
|
4141
|
+
}
|
|
4142
|
+
return { must };
|
|
4143
|
+
}
|
|
4144
|
+
/** Extract file_path from a scan point payload. */
|
|
4145
|
+
function getArchiveFilePath(point) {
|
|
4146
|
+
const value = point.payload?.file_path;
|
|
4147
|
+
return typeof value === 'string' ? value : '';
|
|
4148
|
+
}
|
|
4149
|
+
/** Project archive metadata, stripping indexing-internal fields. */
|
|
4150
|
+
function projectArchivePayload(point, projectMeta) {
|
|
4151
|
+
const payload = point.payload ?? {};
|
|
4152
|
+
if (Object.keys(payload).length === 0)
|
|
4153
|
+
return null;
|
|
4154
|
+
const archiveMeta = { ...payload };
|
|
4155
|
+
delete archiveMeta.file_path;
|
|
4156
|
+
delete archiveMeta.chunk_text;
|
|
4157
|
+
delete archiveMeta.chunk_index;
|
|
4158
|
+
delete archiveMeta.total_chunks;
|
|
4159
|
+
delete archiveMeta.content_hash;
|
|
4160
|
+
delete archiveMeta.matched_rules;
|
|
4161
|
+
return projectMeta(archiveMeta);
|
|
4162
|
+
}
|
|
4163
|
+
/**
|
|
4164
|
+
* Read archive history via watcher scan.
|
|
4165
|
+
*
|
|
4166
|
+
* @param watcher - WatcherClient with scan support.
|
|
4167
|
+
* @param metaPath - Absolute path to the .meta/ directory.
|
|
4168
|
+
* @param metaArchiveProperty - Additional filter properties for archive points.
|
|
4169
|
+
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
4170
|
+
* @param projectMeta - Field projection function.
|
|
4171
|
+
* @returns Array of projected archive entries, newest first.
|
|
4172
|
+
*/
|
|
4173
|
+
async function readArchiveFromWatcher(watcher, metaPath, metaArchiveProperty, limit, projectMeta) {
|
|
4174
|
+
if (limit === 0)
|
|
4175
|
+
return [];
|
|
4176
|
+
if (!watcher.scan) {
|
|
4177
|
+
throw new Error('Watcher scan not available');
|
|
4178
|
+
}
|
|
4179
|
+
const points = [];
|
|
4180
|
+
let cursor;
|
|
4181
|
+
do {
|
|
4182
|
+
const result = await watcher.scan({
|
|
4183
|
+
filter: buildArchiveScanFilter(metaPath, metaArchiveProperty),
|
|
4184
|
+
limit: 100,
|
|
4185
|
+
cursor,
|
|
4186
|
+
});
|
|
4187
|
+
points.push(...result.points);
|
|
4188
|
+
cursor = result.cursor ?? undefined;
|
|
4189
|
+
} while (cursor);
|
|
4190
|
+
const sorted = points.sort((a, b) => getArchiveFilePath(a).localeCompare(getArchiveFilePath(b)));
|
|
4191
|
+
return sorted
|
|
4192
|
+
.slice(limit ? -limit : 0)
|
|
4193
|
+
.reverse()
|
|
4194
|
+
.map((point) => projectArchivePayload(point, projectMeta))
|
|
4195
|
+
.filter((value) => value !== null);
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* Read archive history from disk (fallback).
|
|
4199
|
+
*
|
|
4200
|
+
* @param metaPath - Absolute path to the .meta/ directory.
|
|
4201
|
+
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
4202
|
+
* @param projectMeta - Field projection function.
|
|
4203
|
+
* @returns Array of projected archive entries, newest first.
|
|
4204
|
+
*/
|
|
4205
|
+
async function readArchiveFromDisk(metaPath, limit, projectMeta) {
|
|
4206
|
+
if (limit === 0)
|
|
4207
|
+
return [];
|
|
4208
|
+
const archiveFiles = listArchiveFiles(metaPath);
|
|
4209
|
+
const selected = (limit ? archiveFiles.slice(-limit) : archiveFiles).reverse();
|
|
4210
|
+
return Promise.all(selected.map(async (archiveFile) => {
|
|
4211
|
+
const raw = await readFile(archiveFile, 'utf8');
|
|
4212
|
+
return projectMeta(JSON.parse(raw));
|
|
4213
|
+
}));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4173
4216
|
/**
|
|
4174
4217
|
* GET /metas — list metas with optional filters.
|
|
4175
4218
|
* GET /metas/:path — single meta detail.
|
|
@@ -5551,8 +5594,8 @@ function registerCustomCliCommands(program) {
|
|
|
5551
5594
|
/**
|
|
5552
5595
|
* Jeeves component descriptor for jeeves-meta.
|
|
5553
5596
|
*
|
|
5554
|
-
* Single source of truth consumed by the service CLI
|
|
5555
|
-
*
|
|
5597
|
+
* Single source of truth consumed by the service CLI and the config-apply
|
|
5598
|
+
* pipeline.
|
|
5556
5599
|
*
|
|
5557
5600
|
* @module descriptor
|
|
5558
5601
|
*/
|
|
@@ -5565,8 +5608,6 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
5565
5608
|
servicePackage: META_COMPONENT.servicePackage,
|
|
5566
5609
|
pluginPackage: META_COMPONENT.pluginPackage,
|
|
5567
5610
|
defaultPort: META_COMPONENT.defaultPort,
|
|
5568
|
-
// The runtime Zod custom validator only checks for a .parse() method.
|
|
5569
|
-
// Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
|
|
5570
5611
|
configSchema: serviceConfigSchema,
|
|
5571
5612
|
configFileName: 'config.json',
|
|
5572
5613
|
initTemplate: () => serviceConfigSchema.parse({
|
|
@@ -5588,10 +5629,6 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
5588
5629
|
'-c',
|
|
5589
5630
|
configPath,
|
|
5590
5631
|
],
|
|
5591
|
-
sectionId: 'Meta',
|
|
5592
|
-
refreshIntervalSeconds: 73,
|
|
5593
|
-
generateToolsContent: () => '',
|
|
5594
|
-
dependencies: { hard: ['watcher'], soft: [] },
|
|
5595
5632
|
customCliCommands: registerCustomCliCommands,
|
|
5596
5633
|
});
|
|
5597
5634
|
|