@karmaniverous/jeeves-meta 0.9.0 → 0.10.1
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 +5 -2
- package/dist/cli/jeeves-meta/architect.md +159 -0
- package/dist/cli/jeeves-meta/critic.md +104 -0
- package/dist/cli/jeeves-meta/index.js +478 -233
- package/dist/index.d.ts +90 -59
- package/dist/index.js +474 -233
- package/dist/prompts/architect.md +159 -0
- package/dist/prompts/critic.md +104 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import pino from 'pino';
|
|
10
|
+
import Handlebars from 'handlebars';
|
|
10
11
|
import { Cron } from 'croner';
|
|
11
12
|
import vm from 'vm';
|
|
12
13
|
import require$$0$3 from 'path';
|
|
@@ -56,14 +57,12 @@ function listArchiveFiles(metaPath) {
|
|
|
56
57
|
* @param maxArchive - Maximum snapshots to retain.
|
|
57
58
|
* @returns Number of files pruned.
|
|
58
59
|
*/
|
|
59
|
-
function pruneArchive(metaPath, maxArchive) {
|
|
60
|
+
async function pruneArchive(metaPath, maxArchive) {
|
|
60
61
|
const files = listArchiveFiles(metaPath);
|
|
61
62
|
const toRemove = files.length - maxArchive;
|
|
62
63
|
if (toRemove <= 0)
|
|
63
64
|
return 0;
|
|
64
|
-
|
|
65
|
-
unlinkSync(files[i]);
|
|
66
|
-
}
|
|
65
|
+
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
67
66
|
return toRemove;
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -78,11 +77,11 @@ function pruneArchive(metaPath, maxArchive) {
|
|
|
78
77
|
* @param metaPath - Absolute path to the .meta directory.
|
|
79
78
|
* @returns The latest archived meta, or null if no archives exist.
|
|
80
79
|
*/
|
|
81
|
-
function readLatestArchive(metaPath) {
|
|
80
|
+
async function readLatestArchive(metaPath) {
|
|
82
81
|
const files = listArchiveFiles(metaPath);
|
|
83
82
|
if (files.length === 0)
|
|
84
83
|
return null;
|
|
85
|
-
const raw =
|
|
84
|
+
const raw = await readFile(files[files.length - 1], 'utf8');
|
|
86
85
|
return JSON.parse(raw);
|
|
87
86
|
}
|
|
88
87
|
|
|
@@ -101,9 +100,9 @@ function readLatestArchive(metaPath) {
|
|
|
101
100
|
* @param meta - Current meta.json content.
|
|
102
101
|
* @returns The archive file path.
|
|
103
102
|
*/
|
|
104
|
-
function createSnapshot(metaPath, meta) {
|
|
103
|
+
async function createSnapshot(metaPath, meta) {
|
|
105
104
|
const archiveDir = join(metaPath, 'archive');
|
|
106
|
-
|
|
105
|
+
await mkdir(archiveDir, { recursive: true });
|
|
107
106
|
const now = new Date().toISOString().replace(/[:.]/g, '-');
|
|
108
107
|
const archiveFile = join(archiveDir, now + '.json');
|
|
109
108
|
const archived = {
|
|
@@ -111,7 +110,7 @@ function createSnapshot(metaPath, meta) {
|
|
|
111
110
|
_archived: true,
|
|
112
111
|
_archivedAt: new Date().toISOString(),
|
|
113
112
|
};
|
|
114
|
-
|
|
113
|
+
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
115
114
|
return archiveFile;
|
|
116
115
|
}
|
|
117
116
|
|
|
@@ -262,10 +261,10 @@ const metaConfigSchema = z.object({
|
|
|
262
261
|
criticTimeout: z.number().int().min(30).default(300),
|
|
263
262
|
/** Thinking level for spawned synthesis sessions. */
|
|
264
263
|
thinking: z.string().default('low'),
|
|
265
|
-
/** Resolved architect system prompt text. */
|
|
266
|
-
defaultArchitect: z.string(),
|
|
267
|
-
/** Resolved critic system prompt text. */
|
|
268
|
-
defaultCritic: z.string(),
|
|
264
|
+
/** Resolved architect system prompt text. Falls back to built-in default. */
|
|
265
|
+
defaultArchitect: z.string().optional(),
|
|
266
|
+
/** Resolved critic system prompt text. Falls back to built-in default. */
|
|
267
|
+
defaultCritic: z.string().optional(),
|
|
269
268
|
/** Skip unchanged candidates, bump _generatedAt. */
|
|
270
269
|
skipUnchanged: z.boolean().default(true),
|
|
271
270
|
/** Watcher metadata properties applied to live .meta/meta.json files. */
|
|
@@ -282,6 +281,15 @@ const loggingSchema = z.object({
|
|
|
282
281
|
/** Optional file path for log output. */
|
|
283
282
|
file: z.string().optional(),
|
|
284
283
|
});
|
|
284
|
+
/** Zod schema for a single auto-seed policy rule. */
|
|
285
|
+
const autoSeedRuleSchema = z.object({
|
|
286
|
+
/** Glob pattern matched against watcher walk results. */
|
|
287
|
+
match: z.string(),
|
|
288
|
+
/** Optional steering prompt for seeded metas. */
|
|
289
|
+
steer: z.string().optional(),
|
|
290
|
+
/** Optional cross-references for seeded metas. */
|
|
291
|
+
crossRefs: z.array(z.string()).optional(),
|
|
292
|
+
});
|
|
285
293
|
/** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
|
|
286
294
|
const serviceConfigSchema = metaConfigSchema.extend({
|
|
287
295
|
/** HTTP port for the service (default: 1938). */
|
|
@@ -298,6 +306,11 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
298
306
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
299
307
|
/** Logging configuration. */
|
|
300
308
|
logging: loggingSchema.default(() => loggingSchema.parse({})),
|
|
309
|
+
/**
|
|
310
|
+
* Auto-seed policy: declarative rules for auto-creating .meta/ directories.
|
|
311
|
+
* Rules are evaluated in order; last match wins for steer/crossRefs.
|
|
312
|
+
*/
|
|
313
|
+
autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
|
|
301
314
|
});
|
|
302
315
|
|
|
303
316
|
/**
|
|
@@ -389,6 +402,75 @@ function loadServiceConfig(configPath) {
|
|
|
389
402
|
return serviceConfigSchema.parse(raw);
|
|
390
403
|
}
|
|
391
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
407
|
+
*
|
|
408
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
409
|
+
*
|
|
410
|
+
* @module discovery/computeSummary
|
|
411
|
+
*/
|
|
412
|
+
/**
|
|
413
|
+
* Compute summary statistics from a list of meta entries.
|
|
414
|
+
*
|
|
415
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
416
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
417
|
+
* @returns Aggregated summary statistics.
|
|
418
|
+
*/
|
|
419
|
+
function computeSummary(entries, depthWeight) {
|
|
420
|
+
let staleCount = 0;
|
|
421
|
+
let errorCount = 0;
|
|
422
|
+
let lockedCount = 0;
|
|
423
|
+
let neverSynthesizedCount = 0;
|
|
424
|
+
let totalArchitectTokens = 0;
|
|
425
|
+
let totalBuilderTokens = 0;
|
|
426
|
+
let totalCriticTokens = 0;
|
|
427
|
+
let stalestPath = null;
|
|
428
|
+
let stalestEffective = -1;
|
|
429
|
+
let lastSynthesizedPath = null;
|
|
430
|
+
let lastSynthesizedAt = null;
|
|
431
|
+
for (const e of entries) {
|
|
432
|
+
if (e.stalenessSeconds > 0)
|
|
433
|
+
staleCount++;
|
|
434
|
+
if (e.hasError)
|
|
435
|
+
errorCount++;
|
|
436
|
+
if (e.locked)
|
|
437
|
+
lockedCount++;
|
|
438
|
+
if (e.lastSynthesized === null)
|
|
439
|
+
neverSynthesizedCount++;
|
|
440
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
441
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
442
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
443
|
+
// Track last synthesized
|
|
444
|
+
if (e.lastSynthesized &&
|
|
445
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
446
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
447
|
+
lastSynthesizedPath = e.path;
|
|
448
|
+
}
|
|
449
|
+
// Track stalest (effective staleness for scheduling)
|
|
450
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
451
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
452
|
+
if (effectiveStaleness > stalestEffective) {
|
|
453
|
+
stalestEffective = effectiveStaleness;
|
|
454
|
+
stalestPath = e.path;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
total: entries.length,
|
|
459
|
+
stale: staleCount,
|
|
460
|
+
errors: errorCount,
|
|
461
|
+
locked: lockedCount,
|
|
462
|
+
neverSynthesized: neverSynthesizedCount,
|
|
463
|
+
tokens: {
|
|
464
|
+
architect: totalArchitectTokens,
|
|
465
|
+
builder: totalBuilderTokens,
|
|
466
|
+
critic: totalCriticTokens,
|
|
467
|
+
},
|
|
468
|
+
stalestPath,
|
|
469
|
+
lastSynthesizedPath,
|
|
470
|
+
lastSynthesizedAt,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
392
474
|
/**
|
|
393
475
|
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
394
476
|
*
|
|
@@ -581,14 +663,15 @@ function cleanupStaleLocks(metaPaths, logger) {
|
|
|
581
663
|
* @module readMetaJson
|
|
582
664
|
*/
|
|
583
665
|
/**
|
|
584
|
-
* Read and parse a meta.json file from a `.meta/` directory path.
|
|
666
|
+
* Read and parse a meta.json file from a `.meta/` directory path (async).
|
|
585
667
|
*
|
|
586
668
|
* @param metaPath - Path to the `.meta/` directory.
|
|
587
669
|
* @returns Parsed meta.json content.
|
|
588
670
|
* @throws If the file doesn't exist or contains invalid JSON.
|
|
589
671
|
*/
|
|
590
|
-
function readMetaJson(metaPath) {
|
|
591
|
-
|
|
672
|
+
async function readMetaJson(metaPath) {
|
|
673
|
+
const raw = await readFile(join(metaPath, 'meta.json'), 'utf8');
|
|
674
|
+
return JSON.parse(raw);
|
|
592
675
|
}
|
|
593
676
|
|
|
594
677
|
/**
|
|
@@ -693,21 +776,10 @@ async function listMetas(config, watcher) {
|
|
|
693
776
|
const tree = buildOwnershipTree(metaPaths);
|
|
694
777
|
// Step 3: Read and enrich each meta from disk
|
|
695
778
|
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
779
|
for (const node of tree.nodes.values()) {
|
|
708
780
|
let meta;
|
|
709
781
|
try {
|
|
710
|
-
meta = readMetaJson(node.metaPath);
|
|
782
|
+
meta = await readMetaJson(node.metaPath);
|
|
711
783
|
}
|
|
712
784
|
catch {
|
|
713
785
|
// Skip unreadable metas
|
|
@@ -731,32 +803,6 @@ async function listMetas(config, watcher) {
|
|
|
731
803
|
const archTokens = meta._architectTokens ?? 0;
|
|
732
804
|
const buildTokens = meta._builderTokens ?? 0;
|
|
733
805
|
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
806
|
entries.push({
|
|
761
807
|
path: node.metaPath,
|
|
762
808
|
depth,
|
|
@@ -774,21 +820,7 @@ async function listMetas(config, watcher) {
|
|
|
774
820
|
});
|
|
775
821
|
}
|
|
776
822
|
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
|
-
},
|
|
823
|
+
summary: computeSummary(entries, config.depthWeight),
|
|
792
824
|
entries,
|
|
793
825
|
tree,
|
|
794
826
|
};
|
|
@@ -892,12 +924,18 @@ function filterInScope(node, files) {
|
|
|
892
924
|
/**
|
|
893
925
|
* Get all files in scope for a meta node via watcher walk.
|
|
894
926
|
*/
|
|
895
|
-
async function getScopeFiles(node, watcher) {
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
927
|
+
async function getScopeFiles(node, watcher, logger) {
|
|
928
|
+
const walkStart = Date.now();
|
|
929
|
+
const rawFiles = await watcher.walk([`${node.ownerPath}/**`]);
|
|
930
|
+
const allFiles = rawFiles.map(normalizePath);
|
|
931
|
+
const scopeFiles = filterInScope(node, allFiles);
|
|
932
|
+
logger?.debug({
|
|
933
|
+
ownerPath: node.ownerPath,
|
|
934
|
+
allFiles: allFiles.length,
|
|
935
|
+
scopeFiles: scopeFiles.length,
|
|
936
|
+
durationMs: Date.now() - walkStart,
|
|
937
|
+
}, 'scope files enumerated');
|
|
938
|
+
return { scopeFiles, allFiles };
|
|
901
939
|
}
|
|
902
940
|
/**
|
|
903
941
|
* Get files modified since a given timestamp within a meta node's scope.
|
|
@@ -1189,6 +1227,23 @@ function createLogger(config) {
|
|
|
1189
1227
|
return pino({ level });
|
|
1190
1228
|
}
|
|
1191
1229
|
|
|
1230
|
+
/**
|
|
1231
|
+
* Built-in default prompts for the synthesis pipeline.
|
|
1232
|
+
*
|
|
1233
|
+
* Prompts ship as .md files bundled into dist/prompts/ via rollup-plugin-copy.
|
|
1234
|
+
* Loaded at runtime relative to the compiled module location.
|
|
1235
|
+
*
|
|
1236
|
+
* Users can override via `defaultArchitect` / `defaultCritic` in the service
|
|
1237
|
+
* config. Most installations should use the built-in defaults.
|
|
1238
|
+
*
|
|
1239
|
+
* @module prompts
|
|
1240
|
+
*/
|
|
1241
|
+
const promptDir = dirname(fileURLToPath(import.meta.url));
|
|
1242
|
+
/** Built-in default architect prompt. */
|
|
1243
|
+
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
1244
|
+
/** Built-in default critic prompt. */
|
|
1245
|
+
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
1246
|
+
|
|
1192
1247
|
/**
|
|
1193
1248
|
* Build the MetaContext for a synthesis cycle.
|
|
1194
1249
|
*
|
|
@@ -1228,9 +1283,9 @@ function condenseScopeFiles(files, maxIndividual = 30) {
|
|
|
1228
1283
|
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1229
1284
|
* @returns The `_content` string, or null if missing/unreadable.
|
|
1230
1285
|
*/
|
|
1231
|
-
function readMetaContent(metaJsonPath) {
|
|
1286
|
+
async function readMetaContent(metaJsonPath) {
|
|
1232
1287
|
try {
|
|
1233
|
-
const raw =
|
|
1288
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
1234
1289
|
const meta = JSON.parse(raw);
|
|
1235
1290
|
return meta._content ?? null;
|
|
1236
1291
|
}
|
|
@@ -1246,25 +1301,43 @@ function readMetaContent(metaJsonPath) {
|
|
|
1246
1301
|
* @param watcher - WatcherClient for scope enumeration.
|
|
1247
1302
|
* @returns The computed context package.
|
|
1248
1303
|
*/
|
|
1249
|
-
async function buildContextPackage(node, meta, watcher) {
|
|
1304
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
1250
1305
|
// Scope and delta files via watcher walk
|
|
1251
|
-
const
|
|
1306
|
+
const scopeStart = Date.now();
|
|
1307
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
1252
1308
|
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
1253
|
-
|
|
1309
|
+
logger?.debug({
|
|
1310
|
+
scopeFiles: scopeFiles.length,
|
|
1311
|
+
deltaFiles: deltaFiles.length,
|
|
1312
|
+
durationMs: Date.now() - scopeStart,
|
|
1313
|
+
}, 'scope and delta files computed');
|
|
1314
|
+
// Child meta outputs (parallel reads)
|
|
1254
1315
|
const childMetas = {};
|
|
1255
|
-
|
|
1256
|
-
|
|
1316
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
1317
|
+
const content = await readMetaContent(join(child.metaPath, 'meta.json'));
|
|
1318
|
+
return [child.ownerPath, content];
|
|
1319
|
+
}));
|
|
1320
|
+
for (const [path, content] of childEntries) {
|
|
1321
|
+
childMetas[path] = content;
|
|
1257
1322
|
}
|
|
1258
|
-
// Cross-referenced meta outputs
|
|
1323
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
1259
1324
|
const crossRefMetas = {};
|
|
1260
1325
|
const seen = new Set();
|
|
1326
|
+
const crossRefPaths = [];
|
|
1261
1327
|
for (const refPath of meta._crossRefs ?? []) {
|
|
1262
1328
|
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
1263
1329
|
continue;
|
|
1264
1330
|
if (seen.has(refPath))
|
|
1265
1331
|
continue;
|
|
1266
1332
|
seen.add(refPath);
|
|
1267
|
-
|
|
1333
|
+
crossRefPaths.push(refPath);
|
|
1334
|
+
}
|
|
1335
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
1336
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
1337
|
+
return [refPath, content];
|
|
1338
|
+
}));
|
|
1339
|
+
for (const [path, content] of crossRefEntries) {
|
|
1340
|
+
crossRefMetas[path] = content;
|
|
1268
1341
|
}
|
|
1269
1342
|
// Archive paths
|
|
1270
1343
|
const archives = listArchiveFiles(node.metaPath);
|
|
@@ -1285,8 +1358,37 @@ async function buildContextPackage(node, meta, watcher) {
|
|
|
1285
1358
|
/**
|
|
1286
1359
|
* Build task prompts for each synthesis step.
|
|
1287
1360
|
*
|
|
1361
|
+
* Prompts are compiled as Handlebars templates with access to config,
|
|
1362
|
+
* meta, and scope context. The architect can write template expressions
|
|
1363
|
+
* into its _builder output; these resolve when the builder task is compiled.
|
|
1364
|
+
*
|
|
1288
1365
|
* @module orchestrator/buildTask
|
|
1289
1366
|
*/
|
|
1367
|
+
/** Build the template context from synthesis inputs. */
|
|
1368
|
+
function buildTemplateContext(ctx, meta, config) {
|
|
1369
|
+
return {
|
|
1370
|
+
config,
|
|
1371
|
+
meta,
|
|
1372
|
+
scope: {
|
|
1373
|
+
fileCount: ctx.scopeFiles.length,
|
|
1374
|
+
deltaCount: ctx.deltaFiles.length,
|
|
1375
|
+
childCount: Object.keys(ctx.childMetas).length,
|
|
1376
|
+
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Compile a string as a Handlebars template with the given context.
|
|
1382
|
+
* Returns the original string unchanged if compilation fails.
|
|
1383
|
+
*/
|
|
1384
|
+
function compileTemplate(text, context) {
|
|
1385
|
+
try {
|
|
1386
|
+
return Handlebars.compile(text, { noEscape: true })(context);
|
|
1387
|
+
}
|
|
1388
|
+
catch {
|
|
1389
|
+
return text;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1290
1392
|
/** Append a keyed record of meta outputs as subsections, if non-empty. */
|
|
1291
1393
|
function appendMetaSections(sections, heading, metas) {
|
|
1292
1394
|
if (Object.keys(metas).length === 0)
|
|
@@ -1333,7 +1435,7 @@ function appendSharedSections(sections, ctx, options) {
|
|
|
1333
1435
|
*/
|
|
1334
1436
|
function buildArchitectTask(ctx, meta, config) {
|
|
1335
1437
|
const sections = [
|
|
1336
|
-
meta._architect ?? config.defaultArchitect,
|
|
1438
|
+
meta._architect ?? config.defaultArchitect ?? DEFAULT_ARCHITECT_PROMPT,
|
|
1337
1439
|
'',
|
|
1338
1440
|
'## SCOPE',
|
|
1339
1441
|
`Path: ${ctx.path}`,
|
|
@@ -1351,7 +1453,7 @@ function buildArchitectTask(ctx, meta, config) {
|
|
|
1351
1453
|
if (ctx.archives.length > 0) {
|
|
1352
1454
|
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.');
|
|
1353
1455
|
}
|
|
1354
|
-
return sections.join('\n');
|
|
1456
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1355
1457
|
}
|
|
1356
1458
|
/**
|
|
1357
1459
|
* Build the builder task prompt.
|
|
@@ -1379,7 +1481,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
1379
1481
|
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
1380
1482
|
});
|
|
1381
1483
|
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.');
|
|
1382
|
-
return sections.join('\n');
|
|
1484
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1383
1485
|
}
|
|
1384
1486
|
/**
|
|
1385
1487
|
* Build the critic task prompt.
|
|
@@ -1391,7 +1493,7 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
1391
1493
|
*/
|
|
1392
1494
|
function buildCriticTask(ctx, meta, config) {
|
|
1393
1495
|
const sections = [
|
|
1394
|
-
meta._critic ?? config.defaultCritic,
|
|
1496
|
+
meta._critic ?? config.defaultCritic ?? DEFAULT_CRITIC_PROMPT,
|
|
1395
1497
|
'',
|
|
1396
1498
|
'## SYNTHESIS TO EVALUATE',
|
|
1397
1499
|
meta._content ?? '(No content produced)',
|
|
@@ -1407,7 +1509,7 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
1407
1509
|
includeCrossRefs: false,
|
|
1408
1510
|
});
|
|
1409
1511
|
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
1410
|
-
return sections.join('\n');
|
|
1512
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1411
1513
|
}
|
|
1412
1514
|
|
|
1413
1515
|
/**
|
|
@@ -1436,8 +1538,8 @@ const metaErrorSchema = z.object({
|
|
|
1436
1538
|
/** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
|
|
1437
1539
|
const metaJsonSchema = z
|
|
1438
1540
|
.object({
|
|
1439
|
-
/** Stable identity.
|
|
1440
|
-
_id: z.uuid(),
|
|
1541
|
+
/** Stable identity. Auto-generated on first synthesis if not provided. */
|
|
1542
|
+
_id: z.uuid().optional(),
|
|
1441
1543
|
/** Human-provided steering prompt. Optional. */
|
|
1442
1544
|
_steer: z.string().optional(),
|
|
1443
1545
|
/**
|
|
@@ -1530,10 +1632,10 @@ const metaJsonSchema = z
|
|
|
1530
1632
|
* @returns The updated MetaJson.
|
|
1531
1633
|
* @throws If validation fails (malformed output).
|
|
1532
1634
|
*/
|
|
1533
|
-
function mergeAndWrite(options) {
|
|
1635
|
+
async function mergeAndWrite(options) {
|
|
1534
1636
|
const merged = {
|
|
1535
|
-
// Preserve human-set fields
|
|
1536
|
-
_id: options.current._id,
|
|
1637
|
+
// Preserve human-set fields (auto-generate _id on first synthesis)
|
|
1638
|
+
_id: options.current._id ?? randomUUID(),
|
|
1537
1639
|
_steer: options.current._steer,
|
|
1538
1640
|
_depth: options.current._depth,
|
|
1539
1641
|
_emphasis: options.current._emphasis,
|
|
@@ -1606,7 +1708,7 @@ function mergeAndWrite(options) {
|
|
|
1606
1708
|
}
|
|
1607
1709
|
// Write to specified path (lock staging) or default meta.json
|
|
1608
1710
|
const filePath = options.outputPath ?? join(options.metaPath, 'meta.json');
|
|
1609
|
-
|
|
1711
|
+
await writeFile(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
1610
1712
|
return result.data;
|
|
1611
1713
|
}
|
|
1612
1714
|
|
|
@@ -1846,11 +1948,11 @@ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
|
1846
1948
|
* @module orchestrator/finalizeCycle
|
|
1847
1949
|
*/
|
|
1848
1950
|
/** Finalize a cycle using lock staging: write to .lock → copy to meta.json + archive → delete .lock. */
|
|
1849
|
-
function finalizeCycle(opts) {
|
|
1951
|
+
async function finalizeCycle(opts) {
|
|
1850
1952
|
const lockPath = join(opts.metaPath, '.lock');
|
|
1851
1953
|
const metaJsonPath = join(opts.metaPath, 'meta.json');
|
|
1852
|
-
// Stage: write merged result to .lock
|
|
1853
|
-
const updated = mergeAndWrite({
|
|
1954
|
+
// Stage: write merged result to .lock (sequential — ordering matters)
|
|
1955
|
+
const updated = await mergeAndWrite({
|
|
1854
1956
|
metaPath: opts.metaPath,
|
|
1855
1957
|
current: opts.current,
|
|
1856
1958
|
architect: opts.architect,
|
|
@@ -1869,10 +1971,10 @@ function finalizeCycle(opts) {
|
|
|
1869
1971
|
stateOnly: opts.stateOnly,
|
|
1870
1972
|
});
|
|
1871
1973
|
// Commit: copy .lock → meta.json
|
|
1872
|
-
|
|
1873
|
-
// Archive + prune from the committed meta.json
|
|
1874
|
-
createSnapshot(opts.metaPath, updated);
|
|
1875
|
-
pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
1974
|
+
await copyFile(lockPath, metaJsonPath);
|
|
1975
|
+
// Archive + prune from the committed meta.json (sequential)
|
|
1976
|
+
await createSnapshot(opts.metaPath, updated);
|
|
1977
|
+
await pruneArchive(opts.metaPath, opts.config.maxArchive);
|
|
1876
1978
|
// .lock is cleaned up by the finally block (releaseLock)
|
|
1877
1979
|
return updated;
|
|
1878
1980
|
}
|
|
@@ -1985,14 +2087,12 @@ function parseCriticOutput(output) {
|
|
|
1985
2087
|
* Returns an {@link OrchestrateResult} if state was salvaged, or `null`
|
|
1986
2088
|
* if the caller should fall through to a hard failure.
|
|
1987
2089
|
*/
|
|
1988
|
-
function attemptTimeoutRecovery(opts) {
|
|
2090
|
+
async function attemptTimeoutRecovery(opts) {
|
|
1989
2091
|
const { err, currentMeta, metaPath, config, builderBrief, structureHash, synthesisCount, } = opts;
|
|
1990
2092
|
let partialOutput = null;
|
|
1991
2093
|
try {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
partialOutput = parseBuilderOutput(raw);
|
|
1995
|
-
}
|
|
2094
|
+
const raw = await readFile(err.outputPath, 'utf8');
|
|
2095
|
+
partialOutput = parseBuilderOutput(raw);
|
|
1996
2096
|
}
|
|
1997
2097
|
catch {
|
|
1998
2098
|
// Could not read partial output — fall through to hard failure
|
|
@@ -2006,7 +2106,7 @@ function attemptTimeoutRecovery(opts) {
|
|
|
2006
2106
|
code: 'TIMEOUT',
|
|
2007
2107
|
message: err.message,
|
|
2008
2108
|
};
|
|
2009
|
-
finalizeCycle({
|
|
2109
|
+
await finalizeCycle({
|
|
2010
2110
|
metaPath,
|
|
2011
2111
|
current: currentMeta,
|
|
2012
2112
|
config,
|
|
@@ -2037,12 +2137,12 @@ function attemptTimeoutRecovery(opts) {
|
|
|
2037
2137
|
* @module orchestrator/synthesizeNode
|
|
2038
2138
|
*/
|
|
2039
2139
|
/** Run the architect/builder/critic pipeline on a single node. */
|
|
2040
|
-
async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress) {
|
|
2140
|
+
async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger) {
|
|
2041
2141
|
// Step 5-6: Steer change detection
|
|
2042
|
-
const latestArchive = readLatestArchive(node.metaPath);
|
|
2142
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
2043
2143
|
const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
2044
2144
|
// Step 7: Compute context (includes scope files and delta files)
|
|
2045
|
-
const ctx = await buildContextPackage(node, currentMeta, watcher);
|
|
2145
|
+
const ctx = await buildContextPackage(node, currentMeta, watcher, logger);
|
|
2046
2146
|
// Step 5 (deferred): Structure hash from context scope files
|
|
2047
2147
|
const newStructureHash = computeStructureHash(ctx.scopeFiles);
|
|
2048
2148
|
const structureChanged = newStructureHash !== currentMeta._structureHash;
|
|
@@ -2054,6 +2154,16 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2054
2154
|
let architectTokens;
|
|
2055
2155
|
let builderTokens;
|
|
2056
2156
|
let criticTokens;
|
|
2157
|
+
// Shared base options for all finalizeCycle calls.
|
|
2158
|
+
// Note: synthesisCount is excluded because it mutates during the pipeline.
|
|
2159
|
+
const baseFinalizeOptions = {
|
|
2160
|
+
metaPath: node.metaPath,
|
|
2161
|
+
current: currentMeta,
|
|
2162
|
+
config,
|
|
2163
|
+
architect: currentMeta._architect ?? '',
|
|
2164
|
+
critic: currentMeta._critic ?? '',
|
|
2165
|
+
structureHash: newStructureHash,
|
|
2166
|
+
};
|
|
2057
2167
|
if (architectTriggered) {
|
|
2058
2168
|
try {
|
|
2059
2169
|
await onProgress?.({
|
|
@@ -2082,16 +2192,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2082
2192
|
stepError = toMetaError('architect', err);
|
|
2083
2193
|
if (!currentMeta._builder) {
|
|
2084
2194
|
// No cached builder — cycle fails
|
|
2085
|
-
finalizeCycle({
|
|
2086
|
-
|
|
2087
|
-
current: currentMeta,
|
|
2088
|
-
config,
|
|
2089
|
-
architect: currentMeta._architect ?? '',
|
|
2195
|
+
await finalizeCycle({
|
|
2196
|
+
...baseFinalizeOptions,
|
|
2090
2197
|
builder: '',
|
|
2091
|
-
critic: currentMeta._critic ?? '',
|
|
2092
2198
|
builderOutput: null,
|
|
2093
2199
|
feedback: null,
|
|
2094
|
-
structureHash: newStructureHash,
|
|
2095
2200
|
synthesisCount,
|
|
2096
2201
|
error: stepError,
|
|
2097
2202
|
architectTokens,
|
|
@@ -2133,7 +2238,7 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2133
2238
|
}
|
|
2134
2239
|
catch (err) {
|
|
2135
2240
|
if (err instanceof SpawnTimeoutError) {
|
|
2136
|
-
const recovered = attemptTimeoutRecovery({
|
|
2241
|
+
const recovered = await attemptTimeoutRecovery({
|
|
2137
2242
|
err,
|
|
2138
2243
|
currentMeta,
|
|
2139
2244
|
metaPath: node.metaPath,
|
|
@@ -2146,16 +2251,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2146
2251
|
return recovered;
|
|
2147
2252
|
}
|
|
2148
2253
|
stepError = toMetaError('builder', err);
|
|
2149
|
-
finalizeCycle({
|
|
2150
|
-
|
|
2151
|
-
current: currentMeta,
|
|
2152
|
-
config,
|
|
2153
|
-
architect: currentMeta._architect ?? '',
|
|
2254
|
+
await finalizeCycle({
|
|
2255
|
+
...baseFinalizeOptions,
|
|
2154
2256
|
builder: builderBrief,
|
|
2155
|
-
critic: currentMeta._critic ?? '',
|
|
2156
2257
|
builderOutput: null,
|
|
2157
2258
|
feedback: null,
|
|
2158
|
-
structureHash: newStructureHash,
|
|
2159
2259
|
synthesisCount,
|
|
2160
2260
|
error: stepError,
|
|
2161
2261
|
});
|
|
@@ -2194,16 +2294,11 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
|
|
|
2194
2294
|
stepError = stepError ?? toMetaError('critic', err);
|
|
2195
2295
|
}
|
|
2196
2296
|
// Steps 11-12: Merge, archive, prune
|
|
2197
|
-
finalizeCycle({
|
|
2198
|
-
|
|
2199
|
-
current: currentMeta,
|
|
2200
|
-
config,
|
|
2201
|
-
architect: currentMeta._architect ?? '',
|
|
2297
|
+
await finalizeCycle({
|
|
2298
|
+
...baseFinalizeOptions,
|
|
2202
2299
|
builder: builderBrief,
|
|
2203
|
-
critic: currentMeta._critic ?? '',
|
|
2204
2300
|
builderOutput,
|
|
2205
2301
|
feedback,
|
|
2206
|
-
structureHash: newStructureHash,
|
|
2207
2302
|
synthesisCount,
|
|
2208
2303
|
error: stepError,
|
|
2209
2304
|
architectTokens,
|
|
@@ -2235,8 +2330,8 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2235
2330
|
if (!acquireLock(node.metaPath))
|
|
2236
2331
|
return { synthesized: false };
|
|
2237
2332
|
try {
|
|
2238
|
-
const currentMeta = readMetaJson(normalizedTarget);
|
|
2239
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
|
|
2333
|
+
const currentMeta = await readMetaJson(normalizedTarget);
|
|
2334
|
+
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
2240
2335
|
}
|
|
2241
2336
|
finally {
|
|
2242
2337
|
releaseLock(node.metaPath);
|
|
@@ -2253,7 +2348,7 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2253
2348
|
const metas = new Map();
|
|
2254
2349
|
for (const mp of metaPaths) {
|
|
2255
2350
|
try {
|
|
2256
|
-
metas.set(normalizePath(mp), readMetaJson(mp));
|
|
2351
|
+
metas.set(normalizePath(mp), await readMetaJson(mp));
|
|
2257
2352
|
}
|
|
2258
2353
|
catch {
|
|
2259
2354
|
// Skip metas with unreadable meta.json
|
|
@@ -2289,9 +2384,9 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2289
2384
|
const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
|
|
2290
2385
|
if (!verifiedStale && candidate.meta._generatedAt) {
|
|
2291
2386
|
// Bump _generatedAt so it doesn't win next cycle
|
|
2292
|
-
const freshMeta = readMetaJson(candidate.node.metaPath);
|
|
2387
|
+
const freshMeta = await readMetaJson(candidate.node.metaPath);
|
|
2293
2388
|
freshMeta._generatedAt = new Date().toISOString();
|
|
2294
|
-
|
|
2389
|
+
await writeFile(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
|
|
2295
2390
|
releaseLock(candidate.node.metaPath);
|
|
2296
2391
|
if (config.skipUnchanged)
|
|
2297
2392
|
continue;
|
|
@@ -2304,8 +2399,8 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
|
|
|
2304
2399
|
return { synthesized: false };
|
|
2305
2400
|
const node = winner.node;
|
|
2306
2401
|
try {
|
|
2307
|
-
const currentMeta = readMetaJson(node.metaPath);
|
|
2308
|
-
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
|
|
2402
|
+
const currentMeta = await readMetaJson(node.metaPath);
|
|
2403
|
+
return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress, logger);
|
|
2309
2404
|
}
|
|
2310
2405
|
finally {
|
|
2311
2406
|
// Step 13: Release lock
|
|
@@ -2344,14 +2439,15 @@ function formatSeconds(durationMs) {
|
|
|
2344
2439
|
function titleCasePhase(phase) {
|
|
2345
2440
|
return phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
2346
2441
|
}
|
|
2347
|
-
/** Build a link
|
|
2442
|
+
/** Build a link to the entity's meta.json output file. */
|
|
2348
2443
|
function buildEntityLink(path, serverBaseUrl) {
|
|
2444
|
+
// Normalize backslashes, then convert drive letter to URL path segment
|
|
2445
|
+
const normalized = normalizePath(path).replace(/^([A-Za-z]):/, '/$1');
|
|
2446
|
+
const metaJsonPath = `${normalized}/.meta/meta.json`;
|
|
2349
2447
|
if (!serverBaseUrl)
|
|
2350
|
-
return
|
|
2448
|
+
return metaJsonPath;
|
|
2351
2449
|
const base = serverBaseUrl.replace(/\/+$/, '');
|
|
2352
|
-
|
|
2353
|
-
const normalized = path.replace(/^([A-Za-z]):/, '/$1').replace(/\\/g, '/');
|
|
2354
|
-
return `${base}/path${normalized}`;
|
|
2450
|
+
return `${base}/path${metaJsonPath}`;
|
|
2355
2451
|
}
|
|
2356
2452
|
function formatProgressEvent(event, serverBaseUrl) {
|
|
2357
2453
|
const pathDisplay = buildEntityLink(event.path, serverBaseUrl);
|
|
@@ -2430,6 +2526,123 @@ class ProgressReporter {
|
|
|
2430
2526
|
}
|
|
2431
2527
|
}
|
|
2432
2528
|
|
|
2529
|
+
/**
|
|
2530
|
+
* Core seed logic — create a .meta/ directory with initial meta.json.
|
|
2531
|
+
*
|
|
2532
|
+
* Shared between the POST /seed route handler and the auto-seed pass.
|
|
2533
|
+
*
|
|
2534
|
+
* @module seed/createMeta
|
|
2535
|
+
*/
|
|
2536
|
+
/**
|
|
2537
|
+
* Create a .meta/ directory with an initial meta.json.
|
|
2538
|
+
*
|
|
2539
|
+
* Does NOT check for existing .meta/ — caller is responsible for that guard.
|
|
2540
|
+
*
|
|
2541
|
+
* @param ownerPath - The owner directory path.
|
|
2542
|
+
* @param options - Optional cross-refs and steering prompt.
|
|
2543
|
+
* @returns The meta directory path and generated ID.
|
|
2544
|
+
*/
|
|
2545
|
+
async function createMeta(ownerPath, options) {
|
|
2546
|
+
const metaDir = resolveMetaDir(ownerPath);
|
|
2547
|
+
await mkdir(metaDir, { recursive: true });
|
|
2548
|
+
const _id = randomUUID();
|
|
2549
|
+
const metaJson = { _id };
|
|
2550
|
+
if (options?.crossRefs !== undefined)
|
|
2551
|
+
metaJson._crossRefs = options.crossRefs;
|
|
2552
|
+
if (options?.steer !== undefined)
|
|
2553
|
+
metaJson._steer = options.steer;
|
|
2554
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
2555
|
+
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
2556
|
+
return { metaDir, _id };
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Check if a .meta/ directory already exists for an owner path.
|
|
2560
|
+
*
|
|
2561
|
+
* @param ownerPath - The owner directory path.
|
|
2562
|
+
* @returns True if .meta/ already exists.
|
|
2563
|
+
*/
|
|
2564
|
+
function metaExists(ownerPath) {
|
|
2565
|
+
return existsSync(resolveMetaDir(ownerPath));
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/**
|
|
2569
|
+
* Auto-seed pass — scan for directories matching policy rules and seed them.
|
|
2570
|
+
*
|
|
2571
|
+
* Runs before discovery in each scheduler tick. For each auto-seed rule,
|
|
2572
|
+
* walks matching directories via the watcher and creates .meta/ directories
|
|
2573
|
+
* for those that don't already have one.
|
|
2574
|
+
*
|
|
2575
|
+
* Rules are processed in array order; last match wins for steer/crossRefs.
|
|
2576
|
+
*
|
|
2577
|
+
* @module seed/autoSeed
|
|
2578
|
+
*/
|
|
2579
|
+
/**
|
|
2580
|
+
* Extract parent directory paths from watcher walk results.
|
|
2581
|
+
*
|
|
2582
|
+
* Walk returns file paths; we need the unique set of immediate parent
|
|
2583
|
+
* directories that could be owners.
|
|
2584
|
+
*/
|
|
2585
|
+
function extractDirectories(filePaths) {
|
|
2586
|
+
const dirs = new Set();
|
|
2587
|
+
for (const fp of filePaths) {
|
|
2588
|
+
const dir = posix.dirname(fp);
|
|
2589
|
+
if (dir !== '.' && dir !== '/') {
|
|
2590
|
+
dirs.add(dir);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
return [...dirs];
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
2596
|
+
* Run the auto-seed pass: apply policy rules and create missing metas.
|
|
2597
|
+
*
|
|
2598
|
+
* @param rules - Auto-seed policy rules from config.
|
|
2599
|
+
* @param watcher - Watcher client for filesystem enumeration.
|
|
2600
|
+
* @param logger - Logger for reporting seed actions.
|
|
2601
|
+
* @returns Summary of what was seeded.
|
|
2602
|
+
*/
|
|
2603
|
+
async function autoSeedPass(rules, watcher, logger) {
|
|
2604
|
+
if (rules.length === 0)
|
|
2605
|
+
return { seeded: 0, paths: [] };
|
|
2606
|
+
// Build a map of ownerPath → effective options (last match wins)
|
|
2607
|
+
const candidates = new Map();
|
|
2608
|
+
for (const rule of rules) {
|
|
2609
|
+
const files = await watcher.walk([rule.match]);
|
|
2610
|
+
const dirs = extractDirectories(files);
|
|
2611
|
+
for (const dir of dirs) {
|
|
2612
|
+
candidates.set(dir, {
|
|
2613
|
+
steer: rule.steer,
|
|
2614
|
+
crossRefs: rule.crossRefs,
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
// Filter out paths that already have .meta/meta.json
|
|
2619
|
+
const toSeed = [];
|
|
2620
|
+
for (const [path, opts] of candidates) {
|
|
2621
|
+
if (!metaExists(path)) {
|
|
2622
|
+
toSeed.push({ path, ...opts });
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
// Seed remaining
|
|
2626
|
+
const seededPaths = [];
|
|
2627
|
+
for (const candidate of toSeed) {
|
|
2628
|
+
try {
|
|
2629
|
+
await createMeta(candidate.path, {
|
|
2630
|
+
steer: candidate.steer,
|
|
2631
|
+
crossRefs: candidate.crossRefs,
|
|
2632
|
+
});
|
|
2633
|
+
seededPaths.push(candidate.path);
|
|
2634
|
+
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
2635
|
+
}
|
|
2636
|
+
catch (err) {
|
|
2637
|
+
logger?.warn({
|
|
2638
|
+
path: candidate.path,
|
|
2639
|
+
err: err instanceof Error ? err.message : String(err),
|
|
2640
|
+
}, 'auto-seed failed for path');
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return { seeded: seededPaths.length, paths: seededPaths };
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2433
2646
|
/**
|
|
2434
2647
|
* Croner-based scheduler that discovers the stalest meta candidate each tick
|
|
2435
2648
|
* and enqueues it for synthesis.
|
|
@@ -2527,6 +2740,18 @@ class Scheduler {
|
|
|
2527
2740
|
}, 'Skipping tick (backoff)');
|
|
2528
2741
|
return;
|
|
2529
2742
|
}
|
|
2743
|
+
// Auto-seed pass: create .meta/ for matching directories
|
|
2744
|
+
if (this.config.autoSeed.length > 0) {
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await autoSeedPass(this.config.autoSeed, this.watcher, this.logger);
|
|
2747
|
+
if (result.seeded > 0) {
|
|
2748
|
+
this.logger.info({ seeded: result.seeded }, 'Auto-seed pass completed');
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
catch (err) {
|
|
2752
|
+
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2530
2755
|
const candidate = await this.discoverStalest();
|
|
2531
2756
|
if (!candidate) {
|
|
2532
2757
|
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
@@ -9782,53 +10007,6 @@ const metaDetailQuerySchema = z.object({
|
|
|
9782
10007
|
])
|
|
9783
10008
|
.optional(),
|
|
9784
10009
|
});
|
|
9785
|
-
/** Compute summary stats from a filtered set of MetaEntries. */
|
|
9786
|
-
function computeFilteredSummary(entries) {
|
|
9787
|
-
let staleCount = 0;
|
|
9788
|
-
let errorCount = 0;
|
|
9789
|
-
let neverSynthCount = 0;
|
|
9790
|
-
let stalestPath = null;
|
|
9791
|
-
let stalestSeconds = -1;
|
|
9792
|
-
let lastSynthesizedPath = null;
|
|
9793
|
-
let lastSynthesizedAt = null;
|
|
9794
|
-
let totalArchitectTokens = 0;
|
|
9795
|
-
let totalBuilderTokens = 0;
|
|
9796
|
-
let totalCriticTokens = 0;
|
|
9797
|
-
for (const e of entries) {
|
|
9798
|
-
if (e.stalenessSeconds > 0)
|
|
9799
|
-
staleCount++;
|
|
9800
|
-
if (e.hasError)
|
|
9801
|
-
errorCount++;
|
|
9802
|
-
if (e.stalenessSeconds === Infinity)
|
|
9803
|
-
neverSynthCount++;
|
|
9804
|
-
if (e.stalenessSeconds > stalestSeconds) {
|
|
9805
|
-
stalestSeconds = e.stalenessSeconds;
|
|
9806
|
-
stalestPath = e.path;
|
|
9807
|
-
}
|
|
9808
|
-
if (e.lastSynthesized &&
|
|
9809
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
9810
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
9811
|
-
lastSynthesizedPath = e.path;
|
|
9812
|
-
}
|
|
9813
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
9814
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
9815
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
9816
|
-
}
|
|
9817
|
-
return {
|
|
9818
|
-
total: entries.length,
|
|
9819
|
-
stale: staleCount,
|
|
9820
|
-
errors: errorCount,
|
|
9821
|
-
neverSynthesized: neverSynthCount,
|
|
9822
|
-
stalestPath,
|
|
9823
|
-
lastSynthesizedPath,
|
|
9824
|
-
lastSynthesizedAt,
|
|
9825
|
-
tokens: {
|
|
9826
|
-
architect: totalArchitectTokens,
|
|
9827
|
-
builder: totalBuilderTokens,
|
|
9828
|
-
critic: totalCriticTokens,
|
|
9829
|
-
},
|
|
9830
|
-
};
|
|
9831
|
-
}
|
|
9832
10010
|
function registerMetasRoutes(app, deps) {
|
|
9833
10011
|
app.get('/metas', async (request) => {
|
|
9834
10012
|
const query = metasQuerySchema.parse(request.query);
|
|
@@ -9843,7 +10021,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9843
10021
|
entries = entries.filter((e) => e.hasError === query.hasError);
|
|
9844
10022
|
}
|
|
9845
10023
|
if (query.neverSynthesized !== undefined) {
|
|
9846
|
-
entries = entries.filter((e) => (e.
|
|
10024
|
+
entries = entries.filter((e) => (e.lastSynthesized === null) === query.neverSynthesized);
|
|
9847
10025
|
}
|
|
9848
10026
|
if (query.locked !== undefined) {
|
|
9849
10027
|
entries = entries.filter((e) => e.locked === query.locked);
|
|
@@ -9852,7 +10030,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9852
10030
|
entries = entries.filter((e) => e.stalenessSeconds >= query.staleHours * 3600);
|
|
9853
10031
|
}
|
|
9854
10032
|
// Summary (computed from filtered entries)
|
|
9855
|
-
const summary =
|
|
10033
|
+
const summary = computeSummary(entries, config.depthWeight);
|
|
9856
10034
|
// Field projection
|
|
9857
10035
|
const fieldList = query.fields?.split(',');
|
|
9858
10036
|
const defaultFields = [
|
|
@@ -9904,7 +10082,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9904
10082
|
message: 'Meta path not found: ' + targetPath,
|
|
9905
10083
|
});
|
|
9906
10084
|
}
|
|
9907
|
-
const meta = JSON.parse(
|
|
10085
|
+
const meta = JSON.parse(await readFile(join(targetNode.metaPath, 'meta.json'), 'utf8'));
|
|
9908
10086
|
// Field projection
|
|
9909
10087
|
const defaultExclude = new Set([
|
|
9910
10088
|
'_architect',
|
|
@@ -9952,13 +10130,11 @@ function registerMetasRoutes(app, deps) {
|
|
|
9952
10130
|
// Cross-refs status
|
|
9953
10131
|
const crossRefsRaw = meta._crossRefs;
|
|
9954
10132
|
if (Array.isArray(crossRefsRaw) && crossRefsRaw.length > 0) {
|
|
9955
|
-
response.crossRefs = crossRefsRaw.map((refPath) => {
|
|
10133
|
+
response.crossRefs = await Promise.all(crossRefsRaw.map(async (refPath) => {
|
|
9956
10134
|
const rp = String(refPath);
|
|
9957
10135
|
const refMetaFile = join(rp, '.meta', 'meta.json');
|
|
9958
|
-
if (!existsSync(refMetaFile))
|
|
9959
|
-
return { path: rp, status: 'missing' };
|
|
9960
10136
|
try {
|
|
9961
|
-
const refMeta = JSON.parse(
|
|
10137
|
+
const refMeta = JSON.parse(await readFile(refMetaFile, 'utf8'));
|
|
9962
10138
|
return {
|
|
9963
10139
|
path: rp,
|
|
9964
10140
|
status: 'resolved',
|
|
@@ -9968,7 +10144,7 @@ function registerMetasRoutes(app, deps) {
|
|
|
9968
10144
|
catch {
|
|
9969
10145
|
return { path: rp, status: 'missing' };
|
|
9970
10146
|
}
|
|
9971
|
-
});
|
|
10147
|
+
}));
|
|
9972
10148
|
}
|
|
9973
10149
|
// Archive
|
|
9974
10150
|
if (query.includeArchive) {
|
|
@@ -9977,10 +10153,10 @@ function registerMetasRoutes(app, deps) {
|
|
|
9977
10153
|
? query.includeArchive
|
|
9978
10154
|
: archiveFiles.length;
|
|
9979
10155
|
const selected = archiveFiles.slice(-limit).reverse();
|
|
9980
|
-
response.archive = selected.map((af) => {
|
|
9981
|
-
const raw =
|
|
10156
|
+
response.archive = await Promise.all(selected.map(async (af) => {
|
|
10157
|
+
const raw = await readFile(af, 'utf8');
|
|
9982
10158
|
return projectMeta(JSON.parse(raw));
|
|
9983
|
-
});
|
|
10159
|
+
}));
|
|
9984
10160
|
}
|
|
9985
10161
|
return response;
|
|
9986
10162
|
});
|
|
@@ -10031,12 +10207,12 @@ function registerPreviewRoute(app, deps) {
|
|
|
10031
10207
|
}
|
|
10032
10208
|
targetNode = findNode(result.tree, stalestPath);
|
|
10033
10209
|
}
|
|
10034
|
-
const meta = readMetaJson(targetNode.metaPath);
|
|
10210
|
+
const meta = await readMetaJson(targetNode.metaPath);
|
|
10035
10211
|
// Scope files
|
|
10036
10212
|
const { scopeFiles } = await getScopeFiles(targetNode, watcher);
|
|
10037
10213
|
const structureHash = computeStructureHash(scopeFiles);
|
|
10038
10214
|
const structureChanged = structureHash !== meta._structureHash;
|
|
10039
|
-
const latestArchive = readLatestArchive(targetNode.metaPath);
|
|
10215
|
+
const latestArchive = await readLatestArchive(targetNode.metaPath);
|
|
10040
10216
|
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
10041
10217
|
const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
|
|
10042
10218
|
// Delta files
|
|
@@ -10088,30 +10264,27 @@ function registerPreviewRoute(app, deps) {
|
|
|
10088
10264
|
const seedBodySchema = z.object({
|
|
10089
10265
|
path: z.string().min(1),
|
|
10090
10266
|
crossRefs: z.array(z.string()).optional(),
|
|
10267
|
+
steer: z.string().optional(),
|
|
10091
10268
|
});
|
|
10092
10269
|
function registerSeedRoute(app, deps) {
|
|
10093
|
-
app.post('/seed', (request, reply) => {
|
|
10270
|
+
app.post('/seed', async (request, reply) => {
|
|
10094
10271
|
const body = seedBodySchema.parse(request.body);
|
|
10095
|
-
|
|
10096
|
-
if (existsSync(metaDir)) {
|
|
10272
|
+
if (metaExists(body.path)) {
|
|
10097
10273
|
return reply.status(409).send({
|
|
10098
10274
|
error: 'CONFLICT',
|
|
10099
10275
|
message: `.meta directory already exists at ${body.path}`,
|
|
10100
10276
|
});
|
|
10101
10277
|
}
|
|
10102
|
-
deps.logger.info({
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
const metaJsonPath = join(metaDir, 'meta.json');
|
|
10108
|
-
deps.logger.info({ metaJsonPath }, 'writing meta.json');
|
|
10109
|
-
writeFileSync(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
10278
|
+
deps.logger.info({ path: body.path }, 'seeding .meta directory');
|
|
10279
|
+
const result = await createMeta(body.path, {
|
|
10280
|
+
crossRefs: body.crossRefs,
|
|
10281
|
+
steer: body.steer,
|
|
10282
|
+
});
|
|
10110
10283
|
return reply.status(201).send({
|
|
10111
10284
|
status: 'created',
|
|
10112
10285
|
path: body.path,
|
|
10113
|
-
metaDir,
|
|
10114
|
-
_id:
|
|
10286
|
+
metaDir: result.metaDir,
|
|
10287
|
+
_id: result._id,
|
|
10115
10288
|
});
|
|
10116
10289
|
});
|
|
10117
10290
|
}
|
|
@@ -10560,6 +10733,44 @@ class RuleRegistrar {
|
|
|
10560
10733
|
}
|
|
10561
10734
|
}
|
|
10562
10735
|
|
|
10736
|
+
/**
|
|
10737
|
+
* Post-registration verification of virtual rule application.
|
|
10738
|
+
*
|
|
10739
|
+
* After rules are registered with the watcher, verifies that .meta/meta.json
|
|
10740
|
+
* files are discoverable via watcher walk (which depends on virtual rules
|
|
10741
|
+
* being applied). Logs a warning if expected metas are not found.
|
|
10742
|
+
*
|
|
10743
|
+
* @module rules/verify
|
|
10744
|
+
*/
|
|
10745
|
+
/**
|
|
10746
|
+
* Verify that virtual rules are applied to indexed .meta/meta.json files.
|
|
10747
|
+
*
|
|
10748
|
+
* Runs a discovery pass and logs the result. If no metas are found but
|
|
10749
|
+
* the filesystem likely has some, logs a warning suggesting reindex.
|
|
10750
|
+
*
|
|
10751
|
+
* @param watcher - WatcherClient for discovery.
|
|
10752
|
+
* @param logger - Logger for reporting results.
|
|
10753
|
+
* @returns Number of metas discovered.
|
|
10754
|
+
*/
|
|
10755
|
+
async function verifyRuleApplication(watcher, logger) {
|
|
10756
|
+
try {
|
|
10757
|
+
const metaPaths = await discoverMetas(watcher);
|
|
10758
|
+
if (metaPaths.length === 0) {
|
|
10759
|
+
logger.warn({ count: 0 }, 'Post-registration verification: no .meta/meta.json files found via watcher walk. ' +
|
|
10760
|
+
'Virtual rules may not be applied to indexed files. ' +
|
|
10761
|
+
'If metas exist, a path-scoped reindex may be needed.');
|
|
10762
|
+
}
|
|
10763
|
+
else {
|
|
10764
|
+
logger.info({ count: metaPaths.length }, 'Post-registration verification: metas discoverable');
|
|
10765
|
+
}
|
|
10766
|
+
return metaPaths.length;
|
|
10767
|
+
}
|
|
10768
|
+
catch (err) {
|
|
10769
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'Post-registration verification failed (watcher may be unavailable)');
|
|
10770
|
+
return 0;
|
|
10771
|
+
}
|
|
10772
|
+
}
|
|
10773
|
+
|
|
10563
10774
|
/**
|
|
10564
10775
|
* Minimal Fastify HTTP server for jeeves-meta service.
|
|
10565
10776
|
*
|
|
@@ -10855,7 +11066,7 @@ async function startService(config, configPath) {
|
|
|
10855
11066
|
cycleTokens += evt.tokens;
|
|
10856
11067
|
}
|
|
10857
11068
|
await progress.report(evt);
|
|
10858
|
-
});
|
|
11069
|
+
}, logger);
|
|
10859
11070
|
// orchestrate() always returns exactly one result
|
|
10860
11071
|
const result = results[0];
|
|
10861
11072
|
const durationMs = Date.now() - startMs;
|
|
@@ -10907,11 +11118,15 @@ async function startService(config, configPath) {
|
|
|
10907
11118
|
}
|
|
10908
11119
|
// Start scheduler
|
|
10909
11120
|
scheduler.start();
|
|
10910
|
-
// Rule registration (fire-and-forget with retries)
|
|
11121
|
+
// Rule registration (fire-and-forget with retries) + post-registration verification
|
|
10911
11122
|
const registrar = new RuleRegistrar(config, logger, watcher);
|
|
10912
11123
|
scheduler.setRegistrar(registrar);
|
|
10913
11124
|
routeDeps.registrar = registrar;
|
|
10914
|
-
void registrar.register()
|
|
11125
|
+
void registrar.register().then(() => {
|
|
11126
|
+
if (registrar.isRegistered) {
|
|
11127
|
+
void verifyRuleApplication(watcher, logger);
|
|
11128
|
+
}
|
|
11129
|
+
});
|
|
10915
11130
|
// Periodic watcher health check (independent of scheduler)
|
|
10916
11131
|
const healthCheck = new WatcherHealthCheck({
|
|
10917
11132
|
watcherUrl: config.watcherUrl,
|
|
@@ -10920,26 +11135,52 @@ async function startService(config, configPath) {
|
|
|
10920
11135
|
logger,
|
|
10921
11136
|
});
|
|
10922
11137
|
healthCheck.start();
|
|
10923
|
-
// Config hot-reload (gap #12)
|
|
11138
|
+
// Config hot-reload (gap #12, expanded #32)
|
|
11139
|
+
// Fields requiring a service restart to take effect
|
|
11140
|
+
const restartRequiredFields = [
|
|
11141
|
+
'port',
|
|
11142
|
+
'host',
|
|
11143
|
+
'watcherUrl',
|
|
11144
|
+
'gatewayUrl',
|
|
11145
|
+
'gatewayApiKey',
|
|
11146
|
+
'defaultArchitect',
|
|
11147
|
+
'defaultCritic',
|
|
11148
|
+
];
|
|
10924
11149
|
if (configPath) {
|
|
10925
11150
|
watchFile(configPath, { interval: 5000 }, () => {
|
|
10926
11151
|
try {
|
|
10927
11152
|
const newConfig = loadServiceConfig(configPath);
|
|
10928
|
-
//
|
|
11153
|
+
// Warn about restart-required field changes
|
|
11154
|
+
for (const field of restartRequiredFields) {
|
|
11155
|
+
const oldVal = config[field];
|
|
11156
|
+
const newVal = newConfig[field];
|
|
11157
|
+
if (oldVal !== newVal) {
|
|
11158
|
+
logger.warn({ field, oldValue: oldVal, newValue: newVal }, 'Config field changed but requires restart to take effect');
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
// Hot-reload schedule
|
|
10929
11162
|
if (newConfig.schedule !== config.schedule) {
|
|
10930
11163
|
scheduler.updateSchedule(newConfig.schedule);
|
|
10931
11164
|
logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
|
|
10932
11165
|
}
|
|
10933
|
-
|
|
10934
|
-
// Mutate shared config reference for progress reporter
|
|
10935
|
-
config.reportChannel =
|
|
10936
|
-
newConfig.reportChannel;
|
|
10937
|
-
logger.info({ reportChannel: newConfig.reportChannel }, 'reportChannel hot-reloaded');
|
|
10938
|
-
}
|
|
11166
|
+
// Hot-reload logging level
|
|
10939
11167
|
if (newConfig.logging.level !== config.logging.level) {
|
|
10940
11168
|
logger.level = newConfig.logging.level;
|
|
10941
11169
|
logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
|
|
10942
11170
|
}
|
|
11171
|
+
// Merge all non-restart-required fields into shared config ref.
|
|
11172
|
+
// newConfig is Zod-parsed, so removed fields get defaults — no deletion needed.
|
|
11173
|
+
const restartSet = new Set(restartRequiredFields);
|
|
11174
|
+
for (const key of Object.keys(newConfig)) {
|
|
11175
|
+
if (restartSet.has(key) || key === 'logging')
|
|
11176
|
+
continue;
|
|
11177
|
+
const oldVal = config[key];
|
|
11178
|
+
const newVal = newConfig[key];
|
|
11179
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
11180
|
+
config[key] = newVal;
|
|
11181
|
+
logger.info({ field: key }, 'Config field hot-reloaded');
|
|
11182
|
+
}
|
|
11183
|
+
}
|
|
10943
11184
|
}
|
|
10944
11185
|
catch (err) {
|
|
10945
11186
|
logger.warn({ err }, 'Config hot-reload failed');
|
|
@@ -10960,4 +11201,4 @@ async function startService(config, configPath) {
|
|
|
10960
11201
|
logger.info('Service fully initialized');
|
|
10961
11202
|
}
|
|
10962
11203
|
|
|
10963
|
-
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 };
|
|
11204
|
+
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 };
|