@karmaniverous/jeeves-meta 0.8.0 → 0.10.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 +4 -2
- package/dist/cli/jeeves-meta/index.js +482 -225
- package/dist/index.d.ts +85 -55
- package/dist/index.js +478 -225
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs, { readdirSync,
|
|
2
|
-
import path, { join, dirname, resolve, relative } from 'node:path';
|
|
1
|
+
import fs, { readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, statSync, mkdirSync, watchFile } from 'node:fs';
|
|
2
|
+
import path, { join, dirname, resolve, relative, posix } from 'node:path';
|
|
3
|
+
import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import 'node:fs/promises';
|
|
5
5
|
import process$1 from 'node:process';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -56,14 +56,12 @@ function listArchiveFiles(metaPath) {
|
|
|
56
56
|
* @param maxArchive - Maximum snapshots to retain.
|
|
57
57
|
* @returns Number of files pruned.
|
|
58
58
|
*/
|
|
59
|
-
function pruneArchive(metaPath, maxArchive) {
|
|
59
|
+
async function pruneArchive(metaPath, maxArchive) {
|
|
60
60
|
const files = listArchiveFiles(metaPath);
|
|
61
61
|
const toRemove = files.length - maxArchive;
|
|
62
62
|
if (toRemove <= 0)
|
|
63
63
|
return 0;
|
|
64
|
-
|
|
65
|
-
unlinkSync(files[i]);
|
|
66
|
-
}
|
|
64
|
+
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
67
65
|
return toRemove;
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -78,11 +76,11 @@ function pruneArchive(metaPath, maxArchive) {
|
|
|
78
76
|
* @param metaPath - Absolute path to the .meta directory.
|
|
79
77
|
* @returns The latest archived meta, or null if no archives exist.
|
|
80
78
|
*/
|
|
81
|
-
function readLatestArchive(metaPath) {
|
|
79
|
+
async function readLatestArchive(metaPath) {
|
|
82
80
|
const files = listArchiveFiles(metaPath);
|
|
83
81
|
if (files.length === 0)
|
|
84
82
|
return null;
|
|
85
|
-
const raw =
|
|
83
|
+
const raw = await readFile(files[files.length - 1], 'utf8');
|
|
86
84
|
return JSON.parse(raw);
|
|
87
85
|
}
|
|
88
86
|
|
|
@@ -101,9 +99,9 @@ function readLatestArchive(metaPath) {
|
|
|
101
99
|
* @param meta - Current meta.json content.
|
|
102
100
|
* @returns The archive file path.
|
|
103
101
|
*/
|
|
104
|
-
function createSnapshot(metaPath, meta) {
|
|
102
|
+
async function createSnapshot(metaPath, meta) {
|
|
105
103
|
const archiveDir = join(metaPath, 'archive');
|
|
106
|
-
|
|
104
|
+
await mkdir(archiveDir, { recursive: true });
|
|
107
105
|
const now = new Date().toISOString().replace(/[:.]/g, '-');
|
|
108
106
|
const archiveFile = join(archiveDir, now + '.json');
|
|
109
107
|
const archived = {
|
|
@@ -111,7 +109,7 @@ function createSnapshot(metaPath, meta) {
|
|
|
111
109
|
_archived: true,
|
|
112
110
|
_archivedAt: new Date().toISOString(),
|
|
113
111
|
};
|
|
114
|
-
|
|
112
|
+
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
115
113
|
return archiveFile;
|
|
116
114
|
}
|
|
117
115
|
|
|
@@ -282,6 +280,15 @@ const loggingSchema = z.object({
|
|
|
282
280
|
/** Optional file path for log output. */
|
|
283
281
|
file: z.string().optional(),
|
|
284
282
|
});
|
|
283
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
284
|
+
const autoSeedRuleSchema = z.object({
|
|
285
|
+
/** Glob pattern matched against watcher walk results. */
|
|
286
|
+
match: z.string(),
|
|
287
|
+
/** Optional steering prompt for seeded metas. */
|
|
288
|
+
steer: z.string().optional(),
|
|
289
|
+
/** Optional cross-references for seeded metas. */
|
|
290
|
+
crossRefs: z.array(z.string()).optional(),
|
|
291
|
+
});
|
|
285
292
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
286
293
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
287
294
|
/** HTTP port for the service (default: 1938). */
|
|
@@ -298,6 +305,11 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
298
305
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
299
306
|
/** Logging configuration. */
|
|
300
307
|
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
308
|
+
/**
|
|
309
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
310
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
311
|
+
*/
|
|
312
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
301
313
|
});
|
|
302
314
|
|
|
303
315
|
/**
|
|
@@ -389,6 +401,75 @@ function loadServiceConfig(configPath) {
|
|
|
389
401
|
return serviceConfigSchema.parse(raw);
|
|
390
402
|
}
|
|
391
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
406
|
+
*
|
|
407
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
408
|
+
*
|
|
409
|
+
* @module discovery/computeSummary
|
|
410
|
+
*/
|
|
411
|
+
/**
|
|
412
|
+
* Compute summary statistics from a list of meta entries.
|
|
413
|
+
*
|
|
414
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
415
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
416
|
+
* @returns Aggregated summary statistics.
|
|
417
|
+
*/
|
|
418
|
+
function computeSummary(entries, depthWeight) {
|
|
419
|
+
let staleCount = 0;
|
|
420
|
+
let errorCount = 0;
|
|
421
|
+
let lockedCount = 0;
|
|
422
|
+
let neverSynthesizedCount = 0;
|
|
423
|
+
let totalArchitectTokens = 0;
|
|
424
|
+
let totalBuilderTokens = 0;
|
|
425
|
+
let totalCriticTokens = 0;
|
|
426
|
+
let stalestPath = null;
|
|
427
|
+
let stalestEffective = -1;
|
|
428
|
+
let lastSynthesizedPath = null;
|
|
429
|
+
let lastSynthesizedAt = null;
|
|
430
|
+
for (const e of entries) {
|
|
431
|
+
if (e.stalenessSeconds > 0)
|
|
432
|
+
staleCount++;
|
|
433
|
+
if (e.hasError)
|
|
434
|
+
errorCount++;
|
|
435
|
+
if (e.locked)
|
|
436
|
+
lockedCount++;
|
|
437
|
+
if (e.lastSynthesized === null)
|
|
438
|
+
neverSynthesizedCount++;
|
|
439
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
440
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
441
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
442
|
+
// Track last synthesized
|
|
443
|
+
if (e.lastSynthesized &&
|
|
444
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
445
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
446
|
+
lastSynthesizedPath = e.path;
|
|
447
|
+
}
|
|
448
|
+
// Track stalest (effective staleness for scheduling)
|
|
449
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
450
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
451
|
+
if (effectiveStaleness > stalestEffective) {
|
|
452
|
+
stalestEffective = effectiveStaleness;
|
|
453
|
+
stalestPath = e.path;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
total: entries.length,
|
|
458
|
+
stale: staleCount,
|
|
459
|
+
errors: errorCount,
|
|
460
|
+
locked: lockedCount,
|
|
461
|
+
neverSynthesized: neverSynthesizedCount,
|
|
462
|
+
tokens: {
|
|
463
|
+
architect: totalArchitectTokens,
|
|
464
|
+
builder: totalBuilderTokens,
|
|
465
|
+
critic: totalCriticTokens,
|
|
466
|
+
},
|
|
467
|
+
stalestPath,
|
|
468
|
+
lastSynthesizedPath,
|
|
469
|
+
lastSynthesizedAt,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
392
473
|
/**
|
|
393
474
|
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
394
475
|
*
|
|
@@ -581,14 +662,15 @@ function cleanupStaleLocks(metaPaths, logger) {
|
|
|
581
662
|
* @module readMetaJson
|
|
582
663
|
*/
|
|
583
664
|
/**
|
|
584
|
-
* Read and parse a meta.json file from a `.meta/` directory path.
|
|
665
|
+
* Read and parse a meta.json file from a `.meta/` directory path (async).
|
|
585
666
|
*
|
|
586
667
|
* @param metaPath - Path to the `.meta/` directory.
|
|
587
668
|
* @returns Parsed meta.json content.
|
|
588
669
|
* @throws If the file doesn't exist or contains invalid JSON.
|
|
589
670
|
*/
|
|
590
|
-
function readMetaJson(metaPath) {
|
|
591
|
-
|
|
671
|
+
async function readMetaJson(metaPath) {
|
|
672
|
+
const raw = await readFile(join(metaPath, 'meta.json'), 'utf8');
|
|
673
|
+
return JSON.parse(raw);
|
|
592
674
|
}
|
|
593
675
|
|
|
594
676
|
/**
|
|
@@ -693,21 +775,10 @@ async function listMetas(config, watcher) {
|
|
|
693
775
|
const tree = buildOwnershipTree(metaPaths);
|
|
694
776
|
// Step 3: Read and enrich each meta from disk
|
|
695
777
|
const entries = [];
|
|
696
|
-
let staleCount = 0;
|
|
697
|
-
let errorCount = 0;
|
|
698
|
-
let lockedCount = 0;
|
|
699
|
-
let neverSynthesizedCount = 0;
|
|
700
|
-
let totalArchTokens = 0;
|
|
701
|
-
let totalBuilderTokens = 0;
|
|
702
|
-
let totalCriticTokens = 0;
|
|
703
|
-
let lastSynthPath = null;
|
|
704
|
-
let lastSynthAt = null;
|
|
705
|
-
let stalestPath = null;
|
|
706
|
-
let stalestEffective = -1;
|
|
707
778
|
for (const node of tree.nodes.values()) {
|
|
708
779
|
let meta;
|
|
709
780
|
try {
|
|
710
|
-
meta = readMetaJson(node.metaPath);
|
|
781
|
+
meta = await readMetaJson(node.metaPath);
|
|
711
782
|
}
|
|
712
783
|
catch {
|
|
713
784
|
// Skip unreadable metas
|
|
@@ -731,32 +802,6 @@ async function listMetas(config, watcher) {
|
|
|
731
802
|
const archTokens = meta._architectTokens ?? 0;
|
|
732
803
|
const buildTokens = meta._builderTokens ?? 0;
|
|
733
804
|
const critTokens = meta._criticTokens ?? 0;
|
|
734
|
-
// Accumulate summary stats
|
|
735
|
-
if (stalenessSeconds > 0)
|
|
736
|
-
staleCount++;
|
|
737
|
-
if (hasError)
|
|
738
|
-
errorCount++;
|
|
739
|
-
if (locked)
|
|
740
|
-
lockedCount++;
|
|
741
|
-
if (neverSynth)
|
|
742
|
-
neverSynthesizedCount++;
|
|
743
|
-
totalArchTokens += archTokens;
|
|
744
|
-
totalBuilderTokens += buildTokens;
|
|
745
|
-
totalCriticTokens += critTokens;
|
|
746
|
-
// Track last synthesized
|
|
747
|
-
if (meta._generatedAt) {
|
|
748
|
-
if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
|
|
749
|
-
lastSynthAt = meta._generatedAt;
|
|
750
|
-
lastSynthPath = node.metaPath;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
// Track stalest (effective staleness for scheduling)
|
|
754
|
-
const depthFactor = Math.pow(1 + config.depthWeight, depth);
|
|
755
|
-
const effectiveStaleness = stalenessSeconds * depthFactor * emphasis;
|
|
756
|
-
if (effectiveStaleness > stalestEffective) {
|
|
757
|
-
stalestEffective = effectiveStaleness;
|
|
758
|
-
stalestPath = node.metaPath;
|
|
759
|
-
}
|
|
760
805
|
entries.push({
|
|
761
806
|
path: node.metaPath,
|
|
762
807
|
depth,
|
|
@@ -774,21 +819,7 @@ async function listMetas(config, watcher) {
|
|
|
774
819
|
});
|
|
775
820
|
}
|
|
776
821
|
return {
|
|
777
|
-
summary:
|
|
778
|
-
total: entries.length,
|
|
779
|
-
stale: staleCount,
|
|
780
|
-
errors: errorCount,
|
|
781
|
-
locked: lockedCount,
|
|
782
|
-
neverSynthesized: neverSynthesizedCount,
|
|
783
|
-
tokens: {
|
|
784
|
-
architect: totalArchTokens,
|
|
785
|
-
builder: totalBuilderTokens,
|
|
786
|
-
critic: totalCriticTokens,
|
|
787
|
-
},
|
|
788
|
-
stalestPath,
|
|
789
|
-
lastSynthesizedPath: lastSynthPath,
|
|
790
|
-
lastSynthesizedAt: lastSynthAt,
|
|
791
|
-
},
|
|
822
|
+
summary: computeSummary(entries, config.depthWeight),
|
|
792
823
|
entries,
|
|
793
824
|
tree,
|
|
794
825
|
};
|
|
@@ -892,12 +923,17 @@ function filterInScope(node, files) {
|
|
|
892
923
|
/**
|
|
893
924
|
* Get all files in scope for a meta node via watcher walk.
|
|
894
925
|
*/
|
|
895
|
-
async function getScopeFiles(node, watcher) {
|
|
926
|
+
async function getScopeFiles(node, watcher, logger) {
|
|
927
|
+
const walkStart = Date.now();
|
|
896
928
|
const allFiles = await watcher.walk([`${node.ownerPath}/**`]);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
929
|
+
const scopeFiles = filterInScope(node, allFiles);
|
|
930
|
+
logger?.debug({
|
|
931
|
+
ownerPath: node.ownerPath,
|
|
932
|
+
allFiles: allFiles.length,
|
|
933
|
+
scopeFiles: scopeFiles.length,
|
|
934
|
+
durationMs: Date.now() - walkStart,
|
|
935
|
+
}, 'scope files enumerated');
|
|
936
|
+
return { scopeFiles, allFiles };
|
|
901
937
|
}
|
|
902
938
|
/**
|
|
903
939
|
* Get files modified since a given timestamp within a meta node's scope.
|
|
@@ -1222,6 +1258,22 @@ function condenseScopeFiles(files, maxIndividual = 30) {
|
|
|
1222
1258
|
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
1223
1259
|
.join('\n');
|
|
1224
1260
|
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Read a meta.json file and extract its `_content` field.
|
|
1263
|
+
*
|
|
1264
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1265
|
+
* @returns The `_content` string, or null if missing/unreadable.
|
|
1266
|
+
*/
|
|
1267
|
+
async function readMetaContent(metaJsonPath) {
|
|
1268
|
+
try {
|
|
1269
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
1270
|
+
const meta = JSON.parse(raw);
|
|
1271
|
+
return meta._content ?? null;
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1225
1277
|
/**
|
|
1226
1278
|
* Build the context package for a synthesis cycle.
|
|
1227
1279
|
*
|
|
@@ -1230,22 +1282,43 @@ function condenseScopeFiles(files, maxIndividual = 30) {
|
|
|
1230
1282
|
* @param watcher - WatcherClient for scope enumeration.
|
|
1231
1283
|
* @returns The computed context package.
|
|
1232
1284
|
*/
|
|
1233
|
-
async function buildContextPackage(node, meta, watcher) {
|
|
1285
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
1234
1286
|
// Scope and delta files via watcher walk
|
|
1235
|
-
const
|
|
1287
|
+
const scopeStart = Date.now();
|
|
1288
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
1236
1289
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
1237
|
-
|
|
1290
|
+
logger?.debug({
|
|
1291
|
+
scopeFiles: scopeFiles.length,
|
|
1292
|
+
deltaFiles: deltaFiles.length,
|
|
1293
|
+
durationMs: Date.now() - scopeStart,
|
|
1294
|
+
}, 'scope and delta files computed');
|
|
1295
|
+
// Child meta outputs (parallel reads)
|
|
1238
1296
|
const childMetas = {};
|
|
1239
|
-
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1297
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
1298
|
+
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
1299
|
+
return [child.ownerPath, content];
|
|
1300
|
+
}));
|
|
1301
|
+
for (const [path, content] of childEntries) {
|
|
1302
|
+
childMetas[path] = content;
|
|
1303
|
+
}
|
|
1304
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
1305
|
+
const crossRefMetas = {};
|
|
1306
|
+
const seen = new Set();
|
|
1307
|
+
const crossRefPaths = [];
|
|
1308
|
+
for (const refPath of meta._crossRefs ?? []) {
|
|
1309
|
+
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
1310
|
+
continue;
|
|
1311
|
+
if (seen.has(refPath))
|
|
1312
|
+
continue;
|
|
1313
|
+
seen.add(refPath);
|
|
1314
|
+
crossRefPaths.push(refPath);
|
|
1315
|
+
}
|
|
1316
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
1317
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
1318
|
+
return [refPath, content];
|
|
1319
|
+
}));
|
|
1320
|
+
for (const [path, content] of crossRefEntries) {
|
|
1321
|
+
crossRefMetas[path] = content;
|
|
1249
1322
|
}
|
|
1250
1323
|
// Archive paths
|
|
1251
1324
|
const archives = listArchiveFiles(node.metaPath);
|
|
@@ -1254,6 +1327,7 @@ async function buildContextPackage(node, meta, watcher) {
|
|
|
1254
1327
|
scopeFiles,
|
|
1255
1328
|
deltaFiles,
|
|
1256
1329
|
childMetas,
|
|
1330
|
+
crossRefMetas,
|
|
1257
1331
|
previousContent: meta._content ?? null,
|
|
1258
1332
|
previousFeedback: meta._feedback ?? null,
|
|
1259
1333
|
steer: meta._steer ?? null,
|
|
@@ -1267,6 +1341,15 @@ async function buildContextPackage(node, meta, watcher) {
|
|
|
1267
1341
|
*
|
|
1268
1342
|
* @module orchestrator/buildTask
|
|
1269
1343
|
*/
|
|
1344
|
+
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
1345
|
+
function appendMetaSections(sections, heading, metas) {
|
|
1346
|
+
if (Object.keys(metas).length === 0)
|
|
1347
|
+
return;
|
|
1348
|
+
sections.push('', heading);
|
|
1349
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
1350
|
+
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1270
1353
|
/** Append optional context sections shared across all step prompts. */
|
|
1271
1354
|
function appendSharedSections(sections, ctx, options) {
|
|
1272
1355
|
const opts = {
|
|
@@ -1275,6 +1358,7 @@ function appendSharedSections(sections, ctx, options) {
|
|
|
1275
1358
|
includePreviousFeedback: true,
|
|
1276
1359
|
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
1277
1360
|
includeChildMetas: true,
|
|
1361
|
+
includeCrossRefs: true,
|
|
1278
1362
|
...options,
|
|
1279
1363
|
};
|
|
1280
1364
|
if (opts.includeSteer && ctx.steer) {
|
|
@@ -1286,11 +1370,11 @@ function appendSharedSections(sections, ctx, options) {
|
|
|
1286
1370
|
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
1287
1371
|
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
1288
1372
|
}
|
|
1289
|
-
if (opts.includeChildMetas
|
|
1290
|
-
sections
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1373
|
+
if (opts.includeChildMetas) {
|
|
1374
|
+
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas);
|
|
1375
|
+
}
|
|
1376
|
+
if (opts.includeCrossRefs) {
|
|
1377
|
+
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
1294
1378
|
}
|
|
1295
1379
|
}
|
|
1296
1380
|
/**
|
|
@@ -1374,6 +1458,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
1374
1458
|
includePreviousContent: false,
|
|
1375
1459
|
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
1376
1460
|
includeChildMetas: false,
|
|
1461
|
+
includeCrossRefs: false,
|
|
1377
1462
|
});
|
|
1378
1463
|
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
1379
1464
|
return sections.join('\n');
|
|
@@ -1405,10 +1490,15 @@ const metaErrorSchema = z.object({
|
|
|
1405
1490
|
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
1406
1491
|
const metaJsonSchema = z
|
|
1407
1492
|
.object({
|
|
1408
|
-
/** Stable identity.
|
|
1409
|
-
_id: z.uuid(),
|
|
1493
|
+
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
1494
|
+
_id: z.uuid().optional(),
|
|
1410
1495
|
/** Human-provided steering prompt. Optional. */
|
|
1411
1496
|
_steer: z.string().optional(),
|
|
1497
|
+
/**
|
|
1498
|
+
* Explicit cross-references to other meta owner paths.
|
|
1499
|
+
* Referenced metas' _content is included as architect/builder context.
|
|
1500
|
+
*/
|
|
1501
|
+
_crossRefs: z.array(z.string()).optional(),
|
|
1412
1502
|
/** Architect system prompt used this turn. Defaults from config. */
|
|
1413
1503
|
_architect: z.string().optional(),
|
|
1414
1504
|
/**
|
|
@@ -1494,10 +1584,10 @@ const metaJsonSchema = z
|
|
|
1494
1584
|
* @returns The updated MetaJson.
|
|
1495
1585
|
* @throws If validation fails (malformed output).
|
|
1496
1586
|
*/
|
|
1497
|
-
function mergeAndWrite(options) {
|
|
1587
|
+
async function mergeAndWrite(options) {
|
|
1498
1588
|
const merged = {
|
|
1499
|
-
// Preserve human-set fields
|
|
1500
|
-
_id: options.current._id,
|
|
1589
|
+
// Preserve human-set fields (auto-generate _id on first synthesis)
|
|
1590
|
+
_id: options.current._id ?? randomUUID(),
|
|
1501
1591
|
_steer: options.current._steer,
|
|
1502
1592
|
_depth: options.current._depth,
|
|
1503
1593
|
_emphasis: options.current._emphasis,
|
|
@@ -1570,7 +1660,7 @@ function mergeAndWrite(options) {
|
|
|
1570
1660
|
}
|
|
1571
1661
|
// Write to specified path (lock staging) or default meta.json
|
|
1572
1662
|
const filePath = options.outputPath ?? join(options.metaPath, 'meta.json');
|
|
1573
|
-
|
|
1663
|
+
await writeFile(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
1574
1664
|
return result.data;
|
|
1575
1665
|
}
|
|
1576
1666
|
|
|
@@ -1810,11 +1900,11 @@ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
|
1810
1900
|
* @module orchestrator/finalizeCycle
|
|
1811
1901
|
*/
|
|
1812
1902
|
/** Finalize a cycle using lock staging: write to .lock → copy to meta.json + archive → delete .lock. */
|
|
1813
|
-
function finalizeCycle(opts) {
|
|
1903
|
+
async function finalizeCycle(opts) {
|
|
1814
1904
|
const lockPath = join(opts.metaPath, '.lock');
|
|
1815
1905
|
const metaJsonPath = join(opts.metaPath, 'meta.json');
|
|
1816
|
-
// Stage: write merged result to .lock
|
|
1817
|
-
const updated = mergeAndWrite({
|
|
1906
|
+
// Stage: write merged result to .lock (sequential — ordering matters)
|
|
1907
|
+
const updated = await mergeAndWrite({
|
|
1818
1908
|
metaPath: opts.metaPath,
|
|
1819
1909
|
current: opts.current,
|
|
1820
1910
|
architect: opts.architect,
|
|
@@ -1833,10 +1923,10 @@ function finalizeCycle(opts) {
|
|
|
1833
1923
|
stateOnly: opts.stateOnly,
|
|
1834
1924
|
});
|
|
1835
1925
|
// Commit: copy .lock → meta.json
|
|
1836
|
-
|
|
1837
|
-
// Archive + prune from the committed meta.json
|
|
1838
|
-
createSnapshot(opts.metaPath, updated);
|
|
1839
|
-
pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
1926
|
+
await copyFile(lockPath, metaJsonPath);
|
|
1927
|
+
// Archive + prune from the committed meta.json (sequential)
|
|
1928
|
+
await createSnapshot(opts.metaPath, updated);
|
|
1929
|
+
await pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
1840
1930
|
// .lock is cleaned up by the finally block (releaseLock)
|
|
1841
1931
|
return updated;
|
|
1842
1932
|
}
|
|
@@ -1949,14 +2039,12 @@ function parseCriticOutput(output) {
|
|
|
1949
2039
|
* Returns an {@link OrchestrateResult} if state was salvaged, or `null`
|
|
1950
2040
|
* if the caller should fall through to a hard failure.
|
|
1951
2041
|
*/
|
|
1952
|
-
function attemptTimeoutRecovery(opts) {
|
|
2042
|
+
async function attemptTimeoutRecovery(opts) {
|
|
1953
2043
|
const { err, currentMeta, metaPath, config, builderBrief, structureHash, synthesisCount, } = opts;
|
|
1954
2044
|
let partialOutput = null;
|
|
1955
2045
|
try {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
partialOutput = parseBuilderOutput(raw);
|
|
1959
|
-
}
|
|
2046
|
+
const raw = await readFile(err.outputPath, 'utf8');
|
|
2047
|
+
partialOutput = parseBuilderOutput(raw);
|
|
1960
2048
|
}
|
|
1961
2049
|
catch {
|
|
1962
2050
|
// Could not read partial output — fall through to hard failure
|
|
@@ -1970,7 +2058,7 @@ function attemptTimeoutRecovery(opts) {
|
|
|
1970
2058
|
code: 'TIMEOUT',
|
|
1971
2059
|
message: err.message,
|
|
1972
2060
|
};
|
|
1973
|
-
finalizeCycle({
|
|
2061
|
+
await finalizeCycle({
|
|
1974
2062
|
metaPath,
|
|
1975
2063
|
current: currentMeta,
|
|
1976
2064
|
config,
|
|
@@ -2001,12 +2089,12 @@ function attemptTimeoutRecovery(opts) {
|
|
|
2001
2089
|
* @module orchestrator/synthesizeNode
|
|
2002
2090
|
*/
|
|
2003
2091
|
/** Run the architect/builder/critic pipeline on a single node. */
|
|
2004
|
-
async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress) {
|
|
2092
|
+
async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger) {
|
|
2005
2093
|
// Step 5-6: Steer change detection
|
|
2006
|
-
const latestArchive = readLatestArchive(node.metaPath);
|
|
2094
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
2007
2095
|
const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
2008
2096
|
// Step 7: Compute context (includes scope files and delta files)
|
|
2009
|
-
const ctx = await buildContextPackage(node, currentMeta, watcher);
|
|
2097
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
2010
2098
|
// Step 5 (deferred): Structure hash from context scope files
|
|
2011
2099
|
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
2012
2100
|
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
@@ -2018,6 +2106,16 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2018
2106
|
let architectTokens;
|
|
2019
2107
|
let builderTokens;
|
|
2020
2108
|
let criticTokens;
|
|
2109
|
+
// Shared base options for all finalizeCycle calls.
|
|
2110
|
+
// Note: synthesisCount is excluded because it mutates during the pipeline.
|
|
2111
|
+
const baseFinalizeOptions = {
|
|
2112
|
+
metaPath: node.metaPath,
|
|
2113
|
+
current: currentMeta,
|
|
2114
|
+
config,
|
|
2115
|
+
architect: currentMeta._architect ?? '',
|
|
2116
|
+
critic: currentMeta._critic ?? '',
|
|
2117
|
+
structureHash: newStructureHash,
|
|
2118
|
+
};
|
|
2021
2119
|
if (architectTriggered) {
|
|
2022
2120
|
try {
|
|
2023
2121
|
await onProgress?.({
|
|
@@ -2046,16 +2144,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2046
2144
|
stepError = toMetaError('architect', err);
|
|
2047
2145
|
if (!currentMeta._builder) {
|
|
2048
2146
|
// No cached builder — cycle fails
|
|
2049
|
-
finalizeCycle({
|
|
2050
|
-
|
|
2051
|
-
current: currentMeta,
|
|
2052
|
-
config,
|
|
2053
|
-
architect: currentMeta._architect ?? '',
|
|
2147
|
+
await finalizeCycle({
|
|
2148
|
+
...baseFinalizeOptions,
|
|
2054
2149
|
builder: '',
|
|
2055
|
-
critic: currentMeta._critic ?? '',
|
|
2056
2150
|
builderOutput: null,
|
|
2057
2151
|
feedback: null,
|
|
2058
|
-
structureHash: newStructureHash,
|
|
2059
2152
|
synthesisCount,
|
|
2060
2153
|
error: stepError,
|
|
2061
2154
|
architectTokens,
|
|
@@ -2097,7 +2190,7 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2097
2190
|
}
|
|
2098
2191
|
catch (err) {
|
|
2099
2192
|
if (err instanceof SpawnTimeoutError) {
|
|
2100
|
-
const recovered = attemptTimeoutRecovery({
|
|
2193
|
+
const recovered = await attemptTimeoutRecovery({
|
|
2101
2194
|
err,
|
|
2102
2195
|
currentMeta,
|
|
2103
2196
|
metaPath: node.metaPath,
|
|
@@ -2110,16 +2203,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2110
2203
|
return recovered;
|
|
2111
2204
|
}
|
|
2112
2205
|
stepError = toMetaError('builder', err);
|
|
2113
|
-
finalizeCycle({
|
|
2114
|
-
|
|
2115
|
-
current: currentMeta,
|
|
2116
|
-
config,
|
|
2117
|
-
architect: currentMeta._architect ?? '',
|
|
2206
|
+
await finalizeCycle({
|
|
2207
|
+
...baseFinalizeOptions,
|
|
2118
2208
|
builder: builderBrief,
|
|
2119
|
-
critic: currentMeta._critic ?? '',
|
|
2120
2209
|
builderOutput: null,
|
|
2121
2210
|
feedback: null,
|
|
2122
|
-
structureHash: newStructureHash,
|
|
2123
2211
|
synthesisCount,
|
|
2124
2212
|
error: stepError,
|
|
2125
2213
|
});
|
|
@@ -2158,16 +2246,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2158
2246
|
stepError = stepError ?? toMetaError('critic', err);
|
|
2159
2247
|
}
|
|
2160
2248
|
// Steps 11-12: Merge, archive, prune
|
|
2161
|
-
finalizeCycle({
|
|
2162
|
-
|
|
2163
|
-
current: currentMeta,
|
|
2164
|
-
config,
|
|
2165
|
-
architect: currentMeta._architect ?? '',
|
|
2249
|
+
await finalizeCycle({
|
|
2250
|
+
...baseFinalizeOptions,
|
|
2166
2251
|
builder: builderBrief,
|
|
2167
|
-
critic: currentMeta._critic ?? '',
|
|
2168
2252
|
builderOutput,
|
|
2169
2253
|
feedback,
|
|
2170
|
-
structureHash: newStructureHash,
|
|
2171
2254
|
synthesisCount,
|
|
2172
2255
|
error: stepError,
|
|
2173
2256
|
architectTokens,
|
|
@@ -2199,8 +2282,8 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2199
2282
|
if (!acquireLock(node.metaPath))
|
|
2200
2283
|
return { synthesized: false };
|
|
2201
2284
|
try {
|
|
2202
|
-
const currentMeta = readMetaJson(normalizedTarget);
|
|
2203
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
|
|
2285
|
+
const currentMeta = await readMetaJson(normalizedTarget);
|
|
2286
|
+
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
2204
2287
|
}
|
|
2205
2288
|
finally {
|
|
2206
2289
|
releaseLock(node.metaPath);
|
|
@@ -2217,7 +2300,7 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2217
2300
|
const metas = new Map();
|
|
2218
2301
|
for (const mp of metaPaths) {
|
|
2219
2302
|
try {
|
|
2220
|
-
metas.set(normalizePath(mp), readMetaJson(mp));
|
|
2303
|
+
metas.set(normalizePath(mp), await readMetaJson(mp));
|
|
2221
2304
|
}
|
|
2222
2305
|
catch {
|
|
2223
2306
|
// Skip metas with unreadable meta.json
|
|
@@ -2253,9 +2336,9 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2253
2336
|
const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
|
|
2254
2337
|
if (!verifiedStale && candidate.meta._generatedAt) {
|
|
2255
2338
|
// Bump _generatedAt so it doesn't win next cycle
|
|
2256
|
-
const freshMeta = readMetaJson(candidate.node.metaPath);
|
|
2339
|
+
const freshMeta = await readMetaJson(candidate.node.metaPath);
|
|
2257
2340
|
freshMeta._generatedAt = new Date().toISOString();
|
|
2258
|
-
|
|
2341
|
+
await writeFile(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
|
|
2259
2342
|
releaseLock(candidate.node.metaPath);
|
|
2260
2343
|
if (config.skipUnchanged)
|
|
2261
2344
|
continue;
|
|
@@ -2268,8 +2351,8 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2268
2351
|
return { synthesized: false };
|
|
2269
2352
|
const node = winner.node;
|
|
2270
2353
|
try {
|
|
2271
|
-
const currentMeta = readMetaJson(node.metaPath);
|
|
2272
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
|
|
2354
|
+
const currentMeta = await readMetaJson(node.metaPath);
|
|
2355
|
+
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
2273
2356
|
}
|
|
2274
2357
|
finally {
|
|
2275
2358
|
// Step 13: Release lock
|
|
@@ -2308,14 +2391,15 @@ function formatSeconds(durationMs) {
|
|
|
2308
2391
|
function titleCasePhase(phase) {
|
|
2309
2392
|
return phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
2310
2393
|
}
|
|
2311
|
-
/** Build a link
|
|
2394
|
+
/** Build a link to the entity's meta.json output file. */
|
|
2312
2395
|
function buildEntityLink(path, serverBaseUrl) {
|
|
2396
|
+
// Normalize backslashes, then convert drive letter to URL path segment
|
|
2397
|
+
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2398
|
+
const metaJsonPath = `${normalized}/.meta/meta.json`;
|
|
2313
2399
|
if (!serverBaseUrl)
|
|
2314
|
-
return
|
|
2400
|
+
return metaJsonPath;
|
|
2315
2401
|
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2316
|
-
|
|
2317
|
-
const normalized = path.replace(/^([A-Za-z]):/, '/$1').replace(/\\/g, '/');
|
|
2318
|
-
return `${base}/path${normalized}`;
|
|
2402
|
+
return `${base}/path${metaJsonPath}`;
|
|
2319
2403
|
}
|
|
2320
2404
|
function formatProgressEvent(event, serverBaseUrl) {
|
|
2321
2405
|
const pathDisplay = buildEntityLink(event.path, serverBaseUrl);
|
|
@@ -2394,6 +2478,123 @@ class ProgressReporter {
|
|
|
2394
2478
|
}
|
|
2395
2479
|
}
|
|
2396
2480
|
|
|
2481
|
+
/**
|
|
2482
|
+
* Core seed logic — create a .meta/ directory with initial meta.json.
|
|
2483
|
+
*
|
|
2484
|
+
* Shared between the POST /seed route handler and the auto-seed pass.
|
|
2485
|
+
*
|
|
2486
|
+
* @module seed/createMeta
|
|
2487
|
+
*/
|
|
2488
|
+
/**
|
|
2489
|
+
* Create a .meta/ directory with an initial meta.json.
|
|
2490
|
+
*
|
|
2491
|
+
* Does NOT check for existing .meta/ — caller is responsible for that guard.
|
|
2492
|
+
*
|
|
2493
|
+
* @param ownerPath - The owner directory path.
|
|
2494
|
+
* @param options - Optional cross-refs and steering prompt.
|
|
2495
|
+
* @returns The meta directory path and generated ID.
|
|
2496
|
+
*/
|
|
2497
|
+
async function createMeta(ownerPath, options) {
|
|
2498
|
+
const metaDir = resolveMetaDir(ownerPath);
|
|
2499
|
+
await mkdir(metaDir, { recursive: true });
|
|
2500
|
+
const _id = randomUUID();
|
|
2501
|
+
const metaJson = { _id };
|
|
2502
|
+
if (options?.crossRefs !== undefined)
|
|
2503
|
+
metaJson._crossRefs = options.crossRefs;
|
|
2504
|
+
if (options?.steer !== undefined)
|
|
2505
|
+
metaJson._steer = options.steer;
|
|
2506
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
2507
|
+
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
2508
|
+
return { metaDir, _id };
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Check if a .meta/ directory already exists for an owner path.
|
|
2512
|
+
*
|
|
2513
|
+
* @param ownerPath - The owner directory path.
|
|
2514
|
+
* @returns True if .meta/ already exists.
|
|
2515
|
+
*/
|
|
2516
|
+
function metaExists(ownerPath) {
|
|
2517
|
+
return existsSync(resolveMetaDir(ownerPath));
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Auto-seed pass — scan for directories matching policy rules and seed them.
|
|
2522
|
+
*
|
|
2523
|
+
* Runs before discovery in each scheduler tick. For each auto-seed rule,
|
|
2524
|
+
* walks matching directories via the watcher and creates .meta/ directories
|
|
2525
|
+
* for those that don't already have one.
|
|
2526
|
+
*
|
|
2527
|
+
* Rules are processed in array order; last match wins for steer/crossRefs.
|
|
2528
|
+
*
|
|
2529
|
+
* @module seed/autoSeed
|
|
2530
|
+
*/
|
|
2531
|
+
/**
|
|
2532
|
+
* Extract parent directory paths from watcher walk results.
|
|
2533
|
+
*
|
|
2534
|
+
* Walk returns file paths; we need the unique set of immediate parent
|
|
2535
|
+
* directories that could be owners.
|
|
2536
|
+
*/
|
|
2537
|
+
function extractDirectories(filePaths) {
|
|
2538
|
+
const dirs = new Set();
|
|
2539
|
+
for (const fp of filePaths) {
|
|
2540
|
+
const dir = posix.dirname(fp);
|
|
2541
|
+
if (dir !== '.' && dir !== '/') {
|
|
2542
|
+
dirs.add(dir);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
return [...dirs];
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Run the auto-seed pass: apply policy rules and create missing metas.
|
|
2549
|
+
*
|
|
2550
|
+
* @param rules - Auto-seed policy rules from config.
|
|
2551
|
+
* @param watcher - Watcher client for filesystem enumeration.
|
|
2552
|
+
* @param logger - Logger for reporting seed actions.
|
|
2553
|
+
* @returns Summary of what was seeded.
|
|
2554
|
+
*/
|
|
2555
|
+
async function autoSeedPass(rules, watcher, logger) {
|
|
2556
|
+
if (rules.length === 0)
|
|
2557
|
+
return { seeded: 0, paths: [] };
|
|
2558
|
+
// Build a map of ownerPath → effective options (last match wins)
|
|
2559
|
+
const candidates = new Map();
|
|
2560
|
+
for (const rule of rules) {
|
|
2561
|
+
const files = await watcher.walk([rule.match]);
|
|
2562
|
+
const dirs = extractDirectories(files);
|
|
2563
|
+
for (const dir of dirs) {
|
|
2564
|
+
candidates.set(dir, {
|
|
2565
|
+
steer: rule.steer,
|
|
2566
|
+
crossRefs: rule.crossRefs,
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// Filter out paths that already have .meta/meta.json
|
|
2571
|
+
const toSeed = [];
|
|
2572
|
+
for (const [path, opts] of candidates) {
|
|
2573
|
+
if (!metaExists(path)) {
|
|
2574
|
+
toSeed.push({ path, ...opts });
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
// Seed remaining
|
|
2578
|
+
const seededPaths = [];
|
|
2579
|
+
for (const candidate of toSeed) {
|
|
2580
|
+
try {
|
|
2581
|
+
await createMeta(candidate.path, {
|
|
2582
|
+
steer: candidate.steer,
|
|
2583
|
+
crossRefs: candidate.crossRefs,
|
|
2584
|
+
});
|
|
2585
|
+
seededPaths.push(candidate.path);
|
|
2586
|
+
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
2587
|
+
}
|
|
2588
|
+
catch (err) {
|
|
2589
|
+
logger?.warn({
|
|
2590
|
+
path: candidate.path,
|
|
2591
|
+
err: err instanceof Error ? err.message : String(err),
|
|
2592
|
+
}, 'auto-seed failed for path');
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return { seeded: seededPaths.length, paths: seededPaths };
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2397
2598
|
/**
|
|
2398
2599
|
* Croner-based scheduler that discovers the stalest meta candidate each tick
|
|
2399
2600
|
* and enqueues it for synthesis.
|
|
@@ -2491,6 +2692,18 @@ class Scheduler {
|
|
|
2491
2692
|
}, 'Skipping tick (backoff)');
|
|
2492
2693
|
return;
|
|
2493
2694
|
}
|
|
2695
|
+
// Auto-seed pass: create .meta/ for matching directories
|
|
2696
|
+
if (this.config.autoSeed.length > 0) {
|
|
2697
|
+
try {
|
|
2698
|
+
const result = await autoSeedPass(this.config.autoSeed, this.watcher, this.logger);
|
|
2699
|
+
if (result.seeded > 0) {
|
|
2700
|
+
this.logger.info({ seeded: result.seeded }, 'Auto-seed pass completed');
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
catch (err) {
|
|
2704
|
+
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2494
2707
|
const candidate = await this.discoverStalest();
|
|
2495
2708
|
if (!candidate) {
|
|
2496
2709
|
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
@@ -9746,53 +9959,6 @@ const metaDetailQuerySchema = z.object({
|
|
|
9746
9959
|
])
|
|
9747
9960
|
.optional(),
|
|
9748
9961
|
});
|
|
9749
|
-
/** Compute summary stats from a filtered set of MetaEntries. */
|
|
9750
|
-
function computeFilteredSummary(entries) {
|
|
9751
|
-
let staleCount = 0;
|
|
9752
|
-
let errorCount = 0;
|
|
9753
|
-
let neverSynthCount = 0;
|
|
9754
|
-
let stalestPath = null;
|
|
9755
|
-
let stalestSeconds = -1;
|
|
9756
|
-
let lastSynthesizedPath = null;
|
|
9757
|
-
let lastSynthesizedAt = null;
|
|
9758
|
-
let totalArchitectTokens = 0;
|
|
9759
|
-
let totalBuilderTokens = 0;
|
|
9760
|
-
let totalCriticTokens = 0;
|
|
9761
|
-
for (const e of entries) {
|
|
9762
|
-
if (e.stalenessSeconds > 0)
|
|
9763
|
-
staleCount++;
|
|
9764
|
-
if (e.hasError)
|
|
9765
|
-
errorCount++;
|
|
9766
|
-
if (e.stalenessSeconds === Infinity)
|
|
9767
|
-
neverSynthCount++;
|
|
9768
|
-
if (e.stalenessSeconds > stalestSeconds) {
|
|
9769
|
-
stalestSeconds = e.stalenessSeconds;
|
|
9770
|
-
stalestPath = e.path;
|
|
9771
|
-
}
|
|
9772
|
-
if (e.lastSynthesized &&
|
|
9773
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
9774
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
9775
|
-
lastSynthesizedPath = e.path;
|
|
9776
|
-
}
|
|
9777
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
9778
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
9779
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
9780
|
-
}
|
|
9781
|
-
return {
|
|
9782
|
-
total: entries.length,
|
|
9783
|
-
stale: staleCount,
|
|
9784
|
-
errors: errorCount,
|
|
9785
|
-
neverSynthesized: neverSynthCount,
|
|
9786
|
-
stalestPath,
|
|
9787
|
-
lastSynthesizedPath,
|
|
9788
|
-
lastSynthesizedAt,
|
|
9789
|
-
tokens: {
|
|
9790
|
-
architect: totalArchitectTokens,
|
|
9791
|
-
builder: totalBuilderTokens,
|
|
9792
|
-
critic: totalCriticTokens,
|
|
9793
|
-
},
|
|
9794
|
-
};
|
|
9795
|
-
}
|
|
9796
9962
|
function registerMetasRoutes(app, deps) {
|
|
9797
9963
|
app.get('/metas', async (request) => {
|
|
9798
9964
|
const query = metasQuerySchema.parse(request.query);
|
|
@@ -9807,7 +9973,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9807
9973
|
entries = entries.filter((e) => e.hasError === query.hasError);
|
|
9808
9974
|
}
|
|
9809
9975
|
if (query.neverSynthesized !== undefined) {
|
|
9810
|
-
entries = entries.filter((e) => (e.
|
|
9976
|
+
entries = entries.filter((e) => (e.lastSynthesized === null) === query.neverSynthesized);
|
|
9811
9977
|
}
|
|
9812
9978
|
if (query.locked !== undefined) {
|
|
9813
9979
|
entries = entries.filter((e) => e.locked === query.locked);
|
|
@@ -9816,7 +9982,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9816
9982
|
entries = entries.filter((e) => e.stalenessSeconds >= query.staleHours * 3600);
|
|
9817
9983
|
}
|
|
9818
9984
|
// Summary (computed from filtered entries)
|
|
9819
|
-
const summary =
|
|
9985
|
+
const summary = computeSummary(entries, config.depthWeight);
|
|
9820
9986
|
// Field projection
|
|
9821
9987
|
const fieldList = query.fields?.split(',');
|
|
9822
9988
|
const defaultFields = [
|
|
@@ -9868,7 +10034,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9868
10034
|
message: 'Meta path not found: ' + targetPath,
|
|
9869
10035
|
});
|
|
9870
10036
|
}
|
|
9871
|
-
const meta = JSON.parse(
|
|
10037
|
+
const meta = JSON.parse(await readFile(join(targetNode.metaPath, 'meta.json'), 'utf8'));
|
|
9872
10038
|
// Field projection
|
|
9873
10039
|
const defaultExclude = new Set([
|
|
9874
10040
|
'_architect',
|
|
@@ -9913,6 +10079,25 @@ function registerMetasRoutes(app, deps) {
|
|
|
9913
10079
|
score: Math.round(score * 100) / 100,
|
|
9914
10080
|
},
|
|
9915
10081
|
};
|
|
10082
|
+
// Cross-refs status
|
|
10083
|
+
const crossRefsRaw = meta._crossRefs;
|
|
10084
|
+
if (Array.isArray(crossRefsRaw) && crossRefsRaw.length > 0) {
|
|
10085
|
+
response.crossRefs = await Promise.all(crossRefsRaw.map(async (refPath) => {
|
|
10086
|
+
const rp = String(refPath);
|
|
10087
|
+
const refMetaFile = join(rp, '.meta', 'meta.json');
|
|
10088
|
+
try {
|
|
10089
|
+
const refMeta = JSON.parse(await readFile(refMetaFile, 'utf8'));
|
|
10090
|
+
return {
|
|
10091
|
+
path: rp,
|
|
10092
|
+
status: 'resolved',
|
|
10093
|
+
hasContent: Boolean(refMeta._content),
|
|
10094
|
+
};
|
|
10095
|
+
}
|
|
10096
|
+
catch {
|
|
10097
|
+
return { path: rp, status: 'missing' };
|
|
10098
|
+
}
|
|
10099
|
+
}));
|
|
10100
|
+
}
|
|
9916
10101
|
// Archive
|
|
9917
10102
|
if (query.includeArchive) {
|
|
9918
10103
|
const archiveFiles = listArchiveFiles(targetNode.metaPath);
|
|
@@ -9920,10 +10105,10 @@ function registerMetasRoutes(app, deps) {
|
|
|
9920
10105
|
? query.includeArchive
|
|
9921
10106
|
: archiveFiles.length;
|
|
9922
10107
|
const selected = archiveFiles.slice(-limit).reverse();
|
|
9923
|
-
response.archive = selected.map((af) => {
|
|
9924
|
-
const raw =
|
|
10108
|
+
response.archive = await Promise.all(selected.map(async (af) => {
|
|
10109
|
+
const raw = await readFile(af, 'utf8');
|
|
9925
10110
|
return projectMeta(JSON.parse(raw));
|
|
9926
|
-
});
|
|
10111
|
+
}));
|
|
9927
10112
|
}
|
|
9928
10113
|
return response;
|
|
9929
10114
|
});
|
|
@@ -9974,12 +10159,12 @@ function registerPreviewRoute(app, deps) {
|
|
|
9974
10159
|
}
|
|
9975
10160
|
targetNode = findNode(result.tree, stalestPath);
|
|
9976
10161
|
}
|
|
9977
|
-
const meta = readMetaJson(targetNode.metaPath);
|
|
10162
|
+
const meta = await readMetaJson(targetNode.metaPath);
|
|
9978
10163
|
// Scope files
|
|
9979
10164
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
9980
10165
|
const structureHash = computeStructureHash(scopeFiles);
|
|
9981
10166
|
const structureChanged = structureHash !== meta._structureHash;
|
|
9982
|
-
const latestArchive = readLatestArchive(targetNode.metaPath);
|
|
10167
|
+
const latestArchive = await readLatestArchive(targetNode.metaPath);
|
|
9983
10168
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
9984
10169
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
9985
10170
|
// Delta files
|
|
@@ -10030,28 +10215,28 @@ function registerPreviewRoute(app, deps) {
|
|
|
10030
10215
|
*/
|
|
10031
10216
|
const seedBodySchema = z.object({
|
|
10032
10217
|
path: z.string().min(1),
|
|
10218
|
+
crossRefs: z.array(z.string()).optional(),
|
|
10219
|
+
steer: z.string().optional(),
|
|
10033
10220
|
});
|
|
10034
10221
|
function registerSeedRoute(app, deps) {
|
|
10035
|
-
app.post('/seed', (request, reply) => {
|
|
10222
|
+
app.post('/seed', async (request, reply) => {
|
|
10036
10223
|
const body = seedBodySchema.parse(request.body);
|
|
10037
|
-
|
|
10038
|
-
if (existsSync(metaDir)) {
|
|
10224
|
+
if (metaExists(body.path)) {
|
|
10039
10225
|
return reply.status(409).send({
|
|
10040
10226
|
error: 'CONFLICT',
|
|
10041
10227
|
message: `.meta directory already exists at ${body.path}`,
|
|
10042
10228
|
});
|
|
10043
10229
|
}
|
|
10044
|
-
deps.logger.info({
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
writeFileSync(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
10230
|
+
deps.logger.info({ path: body.path }, 'seeding .meta directory');
|
|
10231
|
+
const result = await createMeta(body.path, {
|
|
10232
|
+
crossRefs: body.crossRefs,
|
|
10233
|
+
steer: body.steer,
|
|
10234
|
+
});
|
|
10050
10235
|
return reply.status(201).send({
|
|
10051
10236
|
status: 'created',
|
|
10052
10237
|
path: body.path,
|
|
10053
|
-
metaDir,
|
|
10054
|
-
_id:
|
|
10238
|
+
metaDir: result.metaDir,
|
|
10239
|
+
_id: result._id,
|
|
10055
10240
|
});
|
|
10056
10241
|
});
|
|
10057
10242
|
}
|
|
@@ -10500,6 +10685,44 @@ class RuleRegistrar {
|
|
|
10500
10685
|
}
|
|
10501
10686
|
}
|
|
10502
10687
|
|
|
10688
|
+
/**
|
|
10689
|
+
* Post-registration verification of virtual rule application.
|
|
10690
|
+
*
|
|
10691
|
+
* After rules are registered with the watcher, verifies that .meta/meta.json
|
|
10692
|
+
* files are discoverable via watcher walk (which depends on virtual rules
|
|
10693
|
+
* being applied). Logs a warning if expected metas are not found.
|
|
10694
|
+
*
|
|
10695
|
+
* @module rules/verify
|
|
10696
|
+
*/
|
|
10697
|
+
/**
|
|
10698
|
+
* Verify that virtual rules are applied to indexed .meta/meta.json files.
|
|
10699
|
+
*
|
|
10700
|
+
* Runs a discovery pass and logs the result. If no metas are found but
|
|
10701
|
+
* the filesystem likely has some, logs a warning suggesting reindex.
|
|
10702
|
+
*
|
|
10703
|
+
* @param watcher - WatcherClient for discovery.
|
|
10704
|
+
* @param logger - Logger for reporting results.
|
|
10705
|
+
* @returns Number of metas discovered.
|
|
10706
|
+
*/
|
|
10707
|
+
async function verifyRuleApplication(watcher, logger) {
|
|
10708
|
+
try {
|
|
10709
|
+
const metaPaths = await discoverMetas(watcher);
|
|
10710
|
+
if (metaPaths.length === 0) {
|
|
10711
|
+
logger.warn({ count: 0 }, 'Post-registration verification: no .meta/meta.json files found via watcher walk. ' +
|
|
10712
|
+
'Virtual rules may not be applied to indexed files. ' +
|
|
10713
|
+
'If metas exist, a path-scoped reindex may be needed.');
|
|
10714
|
+
}
|
|
10715
|
+
else {
|
|
10716
|
+
logger.info({ count: metaPaths.length }, 'Post-registration verification: metas discoverable');
|
|
10717
|
+
}
|
|
10718
|
+
return metaPaths.length;
|
|
10719
|
+
}
|
|
10720
|
+
catch (err) {
|
|
10721
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'Post-registration verification failed (watcher may be unavailable)');
|
|
10722
|
+
return 0;
|
|
10723
|
+
}
|
|
10724
|
+
}
|
|
10725
|
+
|
|
10503
10726
|
/**
|
|
10504
10727
|
* Minimal Fastify HTTP server for jeeves-meta service.
|
|
10505
10728
|
*
|
|
@@ -10795,7 +11018,7 @@ async function startService(config, configPath) {
|
|
|
10795
11018
|
cycleTokens += evt.tokens;
|
|
10796
11019
|
}
|
|
10797
11020
|
await progress.report(evt);
|
|
10798
|
-
});
|
|
11021
|
+
}, logger);
|
|
10799
11022
|
// orchestrate() always returns exactly one result
|
|
10800
11023
|
const result = results[0];
|
|
10801
11024
|
const durationMs = Date.now() - startMs;
|
|
@@ -10847,11 +11070,15 @@ async function startService(config, configPath) {
|
|
|
10847
11070
|
}
|
|
10848
11071
|
// Start scheduler
|
|
10849
11072
|
scheduler.start();
|
|
10850
|
-
// Rule registration (fire-and-forget with retries)
|
|
11073
|
+
// Rule registration (fire-and-forget with retries) + post-registration verification
|
|
10851
11074
|
const registrar = new RuleRegistrar(config, logger, watcher);
|
|
10852
11075
|
scheduler.setRegistrar(registrar);
|
|
10853
11076
|
routeDeps.registrar = registrar;
|
|
10854
|
-
void registrar.register()
|
|
11077
|
+
void registrar.register().then(() => {
|
|
11078
|
+
if (registrar.isRegistered) {
|
|
11079
|
+
void verifyRuleApplication(watcher, logger);
|
|
11080
|
+
}
|
|
11081
|
+
});
|
|
10855
11082
|
// Periodic watcher health check (independent of scheduler)
|
|
10856
11083
|
const healthCheck = new WatcherHealthCheck({
|
|
10857
11084
|
watcherUrl: config.watcherUrl,
|
|
@@ -10860,26 +11087,52 @@ async function startService(config, configPath) {
|
|
|
10860
11087
|
logger,
|
|
10861
11088
|
});
|
|
10862
11089
|
healthCheck.start();
|
|
10863
|
-
// Config hot-reload (gap #12)
|
|
11090
|
+
// Config hot-reload (gap #12, expanded #32)
|
|
11091
|
+
// Fields requiring a service restart to take effect
|
|
11092
|
+
const restartRequiredFields = [
|
|
11093
|
+
'port',
|
|
11094
|
+
'host',
|
|
11095
|
+
'watcherUrl',
|
|
11096
|
+
'gatewayUrl',
|
|
11097
|
+
'gatewayApiKey',
|
|
11098
|
+
'defaultArchitect',
|
|
11099
|
+
'defaultCritic',
|
|
11100
|
+
];
|
|
10864
11101
|
if (configPath) {
|
|
10865
11102
|
watchFile(configPath, { interval: 5000 }, () => {
|
|
10866
11103
|
try {
|
|
10867
11104
|
const newConfig = loadServiceConfig(configPath);
|
|
10868
|
-
//
|
|
11105
|
+
// Warn about restart-required field changes
|
|
11106
|
+
for (const field of restartRequiredFields) {
|
|
11107
|
+
const oldVal = config[field];
|
|
11108
|
+
const newVal = newConfig[field];
|
|
11109
|
+
if (oldVal !== newVal) {
|
|
11110
|
+
logger.warn({ field, oldValue: oldVal, newValue: newVal }, 'Config field changed but requires restart to take effect');
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
// Hot-reload schedule
|
|
10869
11114
|
if (newConfig.schedule !== config.schedule) {
|
|
10870
11115
|
scheduler.updateSchedule(newConfig.schedule);
|
|
10871
11116
|
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
10872
11117
|
}
|
|
10873
|
-
|
|
10874
|
-
// Mutate shared config reference for progress reporter
|
|
10875
|
-
config.reportChannel =
|
|
10876
|
-
newConfig.reportChannel;
|
|
10877
|
-
logger.info({ reportChannel: newConfig.reportChannel }, 'reportChannel hot-reloaded');
|
|
10878
|
-
}
|
|
11118
|
+
// Hot-reload logging level
|
|
10879
11119
|
if (newConfig.logging.level !== config.logging.level) {
|
|
10880
11120
|
logger.level = newConfig.logging.level;
|
|
10881
11121
|
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
10882
11122
|
}
|
|
11123
|
+
// Merge all non-restart-required fields into shared config ref.
|
|
11124
|
+
// newConfig is Zod-parsed, so removed fields get defaults — no deletion needed.
|
|
11125
|
+
const restartSet = new Set(restartRequiredFields);
|
|
11126
|
+
for (const key of Object.keys(newConfig)) {
|
|
11127
|
+
if (restartSet.has(key) || key === 'logging')
|
|
11128
|
+
continue;
|
|
11129
|
+
const oldVal = config[key];
|
|
11130
|
+
const newVal = newConfig[key];
|
|
11131
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
11132
|
+
config[key] = newVal;
|
|
11133
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
11134
|
+
}
|
|
11135
|
+
}
|
|
10883
11136
|
}
|
|
10884
11137
|
catch (err) {
|
|
10885
11138
|
logger.warn({ err }, 'Config hot-reload failed');
|
|
@@ -10900,4 +11153,4 @@ async function startService(config, configPath) {
|
|
|
10900
11153
|
logger.info('Service fully initialized');
|
|
10901
11154
|
}
|
|
10902
11155
|
|
|
10903
|
-
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaErrorSchema, metaJsonSchema, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError };
|
|
11156
|
+
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaErrorSchema, metaJsonSchema, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
|