@karmaniverous/jeeves-meta 0.16.3 → 0.18.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/cli/jeeves-meta/index.js +1386 -1350
- package/dist/prompts/architect.md +31 -59
- package/dist/prompts/critic.md +18 -33
- package/package.json +14 -22
- package/dist/archive/index.d.ts +0 -10
- package/dist/archive/listArchive.d.ts +0 -12
- package/dist/archive/prune.d.ts +0 -14
- package/dist/archive/readArchive.d.ts +0 -30
- package/dist/archive/readLatest.d.ts +0 -13
- package/dist/archive/snapshot.d.ts +0 -17
- package/dist/bootstrap.d.ts +0 -26
- package/dist/cache.d.ts +0 -22
- package/dist/cli/jeeves-meta/architect.md +0 -173
- package/dist/cli/jeeves-meta/critic.md +0 -104
- package/dist/cli.d.ts +0 -10
- package/dist/configHotReload.d.ts +0 -30
- package/dist/configLoader.d.ts +0 -36
- package/dist/constants.d.ts +0 -13
- package/dist/customCliCommands.d.ts +0 -13
- package/dist/descriptor.d.ts +0 -19
- package/dist/discovery/buildMinimalNode.d.ts +0 -22
- package/dist/discovery/computeSummary.d.ts +0 -17
- package/dist/discovery/discoverMetas.d.ts +0 -19
- package/dist/discovery/getAncestorMeta.d.ts +0 -16
- package/dist/discovery/index.d.ts +0 -12
- package/dist/discovery/listMetas.d.ts +0 -63
- package/dist/discovery/ownershipTree.d.ts +0 -25
- package/dist/discovery/scope.d.ts +0 -47
- package/dist/discovery/types.d.ts +0 -25
- package/dist/ema.d.ts +0 -14
- package/dist/errors.d.ts +0 -15
- package/dist/escapeGlob.d.ts +0 -23
- package/dist/executor/GatewayExecutor.d.ts +0 -79
- package/dist/executor/SpawnAbortedError.d.ts +0 -9
- package/dist/executor/SpawnTimeoutError.d.ts +0 -13
- package/dist/executor/index.d.ts +0 -8
- package/dist/index.d.ts +0 -35
- package/dist/index.js +0 -5798
- package/dist/interfaces/MetaContext.d.ts +0 -38
- package/dist/interfaces/MetaExecutor.d.ts +0 -44
- package/dist/interfaces/WatcherClient.d.ts +0 -75
- package/dist/interfaces/index.d.ts +0 -8
- package/dist/lock.d.ts +0 -70
- package/dist/logger/index.d.ts +0 -27
- package/dist/mtimeFilter.d.ts +0 -26
- package/dist/normalizePath.d.ts +0 -6
- package/dist/orchestrator/buildTask.d.ts +0 -38
- package/dist/orchestrator/contextPackage.d.ts +0 -30
- package/dist/orchestrator/index.d.ts +0 -10
- package/dist/orchestrator/orchestratePhase.d.ts +0 -44
- package/dist/orchestrator/parseOutput.d.ts +0 -42
- package/dist/orchestrator/runPhase.d.ts +0 -40
- package/dist/phaseState/derivePhaseState.d.ts +0 -39
- package/dist/phaseState/index.d.ts +0 -9
- package/dist/phaseState/invalidate.d.ts +0 -39
- package/dist/phaseState/phaseScheduler.d.ts +0 -76
- package/dist/phaseState/phaseTransitions.d.ts +0 -83
- package/dist/progress/index.d.ts +0 -44
- package/dist/prompts/index.d.ts +0 -12
- package/dist/queue/index.d.ts +0 -90
- package/dist/readMetaJson.d.ts +0 -17
- package/dist/routes/__testUtils.d.ts +0 -37
- package/dist/routes/config.d.ts +0 -11
- package/dist/routes/configApply.d.ts +0 -13
- package/dist/routes/index.d.ts +0 -50
- package/dist/routes/metas.d.ts +0 -9
- package/dist/routes/metasUpdate.d.ts +0 -11
- package/dist/routes/preview.d.ts +0 -8
- package/dist/routes/queue.d.ts +0 -13
- package/dist/routes/seed.d.ts +0 -8
- package/dist/routes/status.d.ts +0 -13
- package/dist/routes/synthesize.d.ts +0 -12
- package/dist/routes/unlock.d.ts +0 -8
- package/dist/rules/healthCheck.d.ts +0 -36
- package/dist/rules/index.d.ts +0 -37
- package/dist/rules/verify.d.ts +0 -22
- package/dist/scheduler/index.d.ts +0 -71
- package/dist/scheduling/index.d.ts +0 -7
- package/dist/scheduling/staleness.d.ts +0 -68
- package/dist/scheduling/weightedFormula.d.ts +0 -38
- package/dist/schema/config.d.ts +0 -57
- package/dist/schema/error.d.ts +0 -6
- package/dist/schema/index.d.ts +0 -8
- package/dist/schema/meta.d.ts +0 -72
- package/dist/seed/autoSeed.d.ts +0 -30
- package/dist/seed/createMeta.d.ts +0 -38
- package/dist/seed/index.d.ts +0 -7
- package/dist/server.d.ts +0 -24
- package/dist/shutdown/index.d.ts +0 -33
- package/dist/structureHash.d.ts +0 -15
- package/dist/watcher-client/HttpWatcherClient.d.ts +0 -38
- package/dist/watcher-client/index.d.ts +0 -6
|
@@ -1,126 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { sleepAsync, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
|
|
2
|
+
import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
|
|
3
3
|
import { normalizePath, metaConfigSchema, getEndpoint, META_COMPONENT } from '@karmaniverous/jeeves-meta-core';
|
|
4
4
|
import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
|
|
5
|
-
import path, { join, dirname, relative, posix, resolve } from 'node:path';
|
|
5
|
+
import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
|
|
6
6
|
import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { randomUUID, createHash } from 'node:crypto';
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import Handlebars from 'handlebars';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
13
12
|
import process$1 from 'node:process';
|
|
13
|
+
import Handlebars from 'handlebars';
|
|
14
14
|
import { Cron } from 'croner';
|
|
15
15
|
import Fastify from 'fastify';
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Compute summary statistics from an array of MetaEntry objects.
|
|
19
|
-
*
|
|
20
|
-
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
21
|
-
*
|
|
22
|
-
* @module discovery/computeSummary
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* Compute summary statistics from a list of meta entries.
|
|
26
|
-
*
|
|
27
|
-
* @param entries - Enriched meta entries (full or filtered).
|
|
28
|
-
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
29
|
-
* @returns Aggregated summary statistics.
|
|
30
|
-
*/
|
|
31
|
-
function computeSummary(entries, depthWeight) {
|
|
32
|
-
let staleCount = 0;
|
|
33
|
-
let errorCount = 0;
|
|
34
|
-
let lockedCount = 0;
|
|
35
|
-
let disabledCount = 0;
|
|
36
|
-
let neverSynthesizedCount = 0;
|
|
37
|
-
let totalArchitectTokens = 0;
|
|
38
|
-
let totalBuilderTokens = 0;
|
|
39
|
-
let totalCriticTokens = 0;
|
|
40
|
-
let stalestPath = null;
|
|
41
|
-
let stalestEffective = -1;
|
|
42
|
-
let lastSynthesizedPath = null;
|
|
43
|
-
let lastSynthesizedAt = null;
|
|
44
|
-
for (const e of entries) {
|
|
45
|
-
if (e.stalenessSeconds > 0)
|
|
46
|
-
staleCount++;
|
|
47
|
-
if (e.hasError)
|
|
48
|
-
errorCount++;
|
|
49
|
-
if (e.locked)
|
|
50
|
-
lockedCount++;
|
|
51
|
-
if (e.disabled)
|
|
52
|
-
disabledCount++;
|
|
53
|
-
if (e.lastSynthesized === null)
|
|
54
|
-
neverSynthesizedCount++;
|
|
55
|
-
totalArchitectTokens += e.architectTokens ?? 0;
|
|
56
|
-
totalBuilderTokens += e.builderTokens ?? 0;
|
|
57
|
-
totalCriticTokens += e.criticTokens ?? 0;
|
|
58
|
-
// Track last synthesized
|
|
59
|
-
if (e.lastSynthesized &&
|
|
60
|
-
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
61
|
-
lastSynthesizedAt = e.lastSynthesized;
|
|
62
|
-
lastSynthesizedPath = e.path;
|
|
63
|
-
}
|
|
64
|
-
// Track stalest (effective staleness for scheduling)
|
|
65
|
-
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
66
|
-
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
67
|
-
if (effectiveStaleness > stalestEffective) {
|
|
68
|
-
stalestEffective = effectiveStaleness;
|
|
69
|
-
stalestPath = e.path;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
total: entries.length,
|
|
74
|
-
stale: staleCount,
|
|
75
|
-
errors: errorCount,
|
|
76
|
-
locked: lockedCount,
|
|
77
|
-
disabled: disabledCount,
|
|
78
|
-
neverSynthesized: neverSynthesizedCount,
|
|
79
|
-
tokens: {
|
|
80
|
-
architect: totalArchitectTokens,
|
|
81
|
-
builder: totalBuilderTokens,
|
|
82
|
-
critic: totalCriticTokens,
|
|
83
|
-
},
|
|
84
|
-
stalestPath,
|
|
85
|
-
lastSynthesizedPath,
|
|
86
|
-
lastSynthesizedAt,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
92
|
-
*
|
|
93
|
-
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
94
|
-
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
95
|
-
*
|
|
96
|
-
* @module discovery/discoverMetas
|
|
97
|
-
*/
|
|
98
|
-
/**
|
|
99
|
-
* Discover all .meta/ directories via watcher walk.
|
|
100
|
-
*
|
|
101
|
-
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
102
|
-
* and returns deduplicated meta directory paths.
|
|
103
|
-
*
|
|
104
|
-
* @param watcher - WatcherClient for walk queries.
|
|
105
|
-
* @returns Array of normalized .meta/ directory paths.
|
|
106
|
-
*/
|
|
107
|
-
async function discoverMetas(watcher) {
|
|
108
|
-
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
109
|
-
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
110
|
-
const seen = new Set();
|
|
111
|
-
const metaPaths = [];
|
|
112
|
-
for (const filePath of allPaths) {
|
|
113
|
-
const fp = normalizePath(filePath);
|
|
114
|
-
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
115
|
-
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
116
|
-
if (seen.has(metaPath))
|
|
117
|
-
continue;
|
|
118
|
-
seen.add(metaPath);
|
|
119
|
-
metaPaths.push(metaPath);
|
|
120
|
-
}
|
|
121
|
-
return metaPaths;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
17
|
/**
|
|
125
18
|
* Retrieve the nearest ancestor meta node from the ownership tree.
|
|
126
19
|
*
|
|
@@ -426,6 +319,113 @@ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
|
426
319
|
return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
|
|
427
320
|
}
|
|
428
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Compute summary statistics from an array of MetaEntry objects.
|
|
324
|
+
*
|
|
325
|
+
* Shared between listMetas() (full list) and route handlers (filtered lists).
|
|
326
|
+
*
|
|
327
|
+
* @module discovery/computeSummary
|
|
328
|
+
*/
|
|
329
|
+
/**
|
|
330
|
+
* Compute summary statistics from a list of meta entries.
|
|
331
|
+
*
|
|
332
|
+
* @param entries - Enriched meta entries (full or filtered).
|
|
333
|
+
* @param depthWeight - Config depth weight for effective staleness calculation.
|
|
334
|
+
* @returns Aggregated summary statistics.
|
|
335
|
+
*/
|
|
336
|
+
function computeSummary(entries, depthWeight) {
|
|
337
|
+
let staleCount = 0;
|
|
338
|
+
let errorCount = 0;
|
|
339
|
+
let lockedCount = 0;
|
|
340
|
+
let disabledCount = 0;
|
|
341
|
+
let neverSynthesizedCount = 0;
|
|
342
|
+
let totalArchitectTokens = 0;
|
|
343
|
+
let totalBuilderTokens = 0;
|
|
344
|
+
let totalCriticTokens = 0;
|
|
345
|
+
let stalestPath = null;
|
|
346
|
+
let stalestEffective = -1;
|
|
347
|
+
let lastSynthesizedPath = null;
|
|
348
|
+
let lastSynthesizedAt = null;
|
|
349
|
+
for (const e of entries) {
|
|
350
|
+
if (e.stalenessSeconds > 0)
|
|
351
|
+
staleCount++;
|
|
352
|
+
if (e.hasError)
|
|
353
|
+
errorCount++;
|
|
354
|
+
if (e.locked)
|
|
355
|
+
lockedCount++;
|
|
356
|
+
if (e.disabled)
|
|
357
|
+
disabledCount++;
|
|
358
|
+
if (e.lastSynthesized === null)
|
|
359
|
+
neverSynthesizedCount++;
|
|
360
|
+
totalArchitectTokens += e.architectTokens ?? 0;
|
|
361
|
+
totalBuilderTokens += e.builderTokens ?? 0;
|
|
362
|
+
totalCriticTokens += e.criticTokens ?? 0;
|
|
363
|
+
// Track last synthesized
|
|
364
|
+
if (e.lastSynthesized &&
|
|
365
|
+
(!lastSynthesizedAt || e.lastSynthesized > lastSynthesizedAt)) {
|
|
366
|
+
lastSynthesizedAt = e.lastSynthesized;
|
|
367
|
+
lastSynthesizedPath = e.path;
|
|
368
|
+
}
|
|
369
|
+
// Track stalest (effective staleness for scheduling)
|
|
370
|
+
const depthFactor = Math.pow(1 + depthWeight, e.depth);
|
|
371
|
+
const effectiveStaleness = e.stalenessSeconds * depthFactor * e.emphasis;
|
|
372
|
+
if (effectiveStaleness > stalestEffective) {
|
|
373
|
+
stalestEffective = effectiveStaleness;
|
|
374
|
+
stalestPath = e.path;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
total: entries.length,
|
|
379
|
+
stale: staleCount,
|
|
380
|
+
errors: errorCount,
|
|
381
|
+
locked: lockedCount,
|
|
382
|
+
disabled: disabledCount,
|
|
383
|
+
neverSynthesized: neverSynthesizedCount,
|
|
384
|
+
tokens: {
|
|
385
|
+
architect: totalArchitectTokens,
|
|
386
|
+
builder: totalBuilderTokens,
|
|
387
|
+
critic: totalCriticTokens,
|
|
388
|
+
},
|
|
389
|
+
stalestPath,
|
|
390
|
+
lastSynthesizedPath,
|
|
391
|
+
lastSynthesizedAt,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Discover .meta/ directories via watcher `/walk` endpoint.
|
|
397
|
+
*
|
|
398
|
+
* Uses filesystem enumeration through the watcher (not Qdrant) to find
|
|
399
|
+
* all `.meta/meta.json` files and returns deduplicated meta directory paths.
|
|
400
|
+
*
|
|
401
|
+
* @module discovery/discoverMetas
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Discover all .meta/ directories via watcher walk.
|
|
405
|
+
*
|
|
406
|
+
* Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
|
|
407
|
+
* and returns deduplicated meta directory paths.
|
|
408
|
+
*
|
|
409
|
+
* @param watcher - WatcherClient for walk queries.
|
|
410
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
411
|
+
*/
|
|
412
|
+
async function discoverMetas(watcher) {
|
|
413
|
+
const allPaths = await watcher.walk(['**/.meta/meta.json']);
|
|
414
|
+
// Deduplicate by .meta/ directory path (handles multi-chunk files)
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
const metaPaths = [];
|
|
417
|
+
for (const filePath of allPaths) {
|
|
418
|
+
const fp = normalizePath(filePath);
|
|
419
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
420
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
421
|
+
if (seen.has(metaPath))
|
|
422
|
+
continue;
|
|
423
|
+
seen.add(metaPath);
|
|
424
|
+
metaPaths.push(metaPath);
|
|
425
|
+
}
|
|
426
|
+
return metaPaths;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
429
|
/**
|
|
430
430
|
* Build the ownership tree from discovered .meta/ paths.
|
|
431
431
|
*
|
|
@@ -763,6 +763,12 @@ function applyHotReloadedConfig(newConfig) {
|
|
|
763
763
|
*
|
|
764
764
|
* @module schema/config
|
|
765
765
|
*/
|
|
766
|
+
/** Default Handlebars template strings for progress reporting. Single source of truth. */
|
|
767
|
+
const DEFAULT_TEMPLATE_STRINGS = {
|
|
768
|
+
phaseStart: ':gear: Started meta synthesis {{phase}} phase of <{{dirLink}}>',
|
|
769
|
+
phaseEnd: ':white_check_mark: Completed meta synthesis {{phase}} phase ({{tokens}} tokens / {{seconds}}s) at <{{metaLink}}>',
|
|
770
|
+
phaseError: ':x: Meta synthesis {{phase}} phase failed at <{{dirLink}}>\n Error: {{error}}',
|
|
771
|
+
};
|
|
766
772
|
/** Zod schema for logging configuration. */
|
|
767
773
|
const loggingSchema = z.object({
|
|
768
774
|
/** Log level. */
|
|
@@ -791,14 +797,24 @@ const serviceConfigSchema = metaConfigSchema.extend({
|
|
|
791
797
|
reportChannel: z.string().optional(),
|
|
792
798
|
/** Channel/user ID to send progress messages to. */
|
|
793
799
|
reportTarget: z.string().optional(),
|
|
794
|
-
/** Optional base URL for the service, used to construct entity links in progress reports. */
|
|
795
|
-
serverBaseUrl: z.string().optional(),
|
|
796
800
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
801
|
+
* URL of the local jeeves-server instance, used by the ProgressReporter
|
|
802
|
+
* to resolve filesystem paths to browse links via the resolve-path API.
|
|
803
|
+
* Default: http://127.0.0.1:1934
|
|
804
|
+
*/
|
|
805
|
+
serverUrl: z.string().default('http://127.0.0.1:1934'),
|
|
806
|
+
/**
|
|
807
|
+
* Handlebars templates for progress reporting messages.
|
|
808
|
+
* Each template receives a standard set of data keys (dirLink, metaLink,
|
|
809
|
+
* phase, tokens, seconds, error).
|
|
800
810
|
*/
|
|
801
|
-
|
|
811
|
+
templates: z
|
|
812
|
+
.object({
|
|
813
|
+
phaseStart: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseStart),
|
|
814
|
+
phaseEnd: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseEnd),
|
|
815
|
+
phaseError: z.string().default(DEFAULT_TEMPLATE_STRINGS.phaseError),
|
|
816
|
+
})
|
|
817
|
+
.default(() => ({ ...DEFAULT_TEMPLATE_STRINGS })),
|
|
802
818
|
/** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
|
|
803
819
|
watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
|
|
804
820
|
/** Logging configuration. */
|
|
@@ -1235,1284 +1251,1233 @@ function createLogger(config) {
|
|
|
1235
1251
|
return pino({ level });
|
|
1236
1252
|
}
|
|
1237
1253
|
|
|
1238
|
-
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
1239
|
-
|
|
1240
|
-
function findUpSync(name, {
|
|
1241
|
-
cwd = process$1.cwd(),
|
|
1242
|
-
type = 'file',
|
|
1243
|
-
stopAt,
|
|
1244
|
-
} = {}) {
|
|
1245
|
-
let directory = path.resolve(toPath(cwd) ?? '');
|
|
1246
|
-
const {root} = path.parse(directory);
|
|
1247
|
-
stopAt = path.resolve(directory, toPath(stopAt) ?? root);
|
|
1248
|
-
const isAbsoluteName = path.isAbsolute(name);
|
|
1249
|
-
|
|
1250
|
-
while (directory) {
|
|
1251
|
-
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
1252
|
-
|
|
1253
|
-
try {
|
|
1254
|
-
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
|
|
1255
|
-
if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
|
|
1256
|
-
return filePath;
|
|
1257
|
-
}
|
|
1258
|
-
} catch {}
|
|
1259
|
-
|
|
1260
|
-
if (directory === stopAt || directory === root) {
|
|
1261
|
-
break;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
directory = path.dirname(directory);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
const isTypeOnlyPackageJsonData = packageData => {
|
|
1269
|
-
if (!packageData || typeof packageData !== 'object' || Array.isArray(packageData)) {
|
|
1270
|
-
return false;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
const keys = Object.keys(packageData);
|
|
1274
|
-
return keys.length === 1 && keys[0] === 'type' && typeof packageData.type === 'string';
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
const isTypeOnlyPackageJsonSync = filePath => {
|
|
1278
|
-
let fileContents;
|
|
1279
|
-
|
|
1280
|
-
try {
|
|
1281
|
-
fileContents = fs.readFileSync(filePath, 'utf8');
|
|
1282
|
-
} catch {
|
|
1283
|
-
return false;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
try {
|
|
1287
|
-
return isTypeOnlyPackageJsonData(JSON.parse(fileContents));
|
|
1288
|
-
} catch {
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
const getNextSearchDirectory = filePath => {
|
|
1294
|
-
const packageDirectoryPath = path.dirname(filePath);
|
|
1295
|
-
const parentDirectoryPath = path.dirname(packageDirectoryPath);
|
|
1296
|
-
return parentDirectoryPath === packageDirectoryPath ? undefined : parentDirectoryPath;
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
const findPackageDirectorySync = (directory, ignoreTypeOnlyPackageJson) => {
|
|
1300
|
-
const filePath = findUpSync('package.json', {cwd: directory});
|
|
1301
|
-
if (!filePath) {
|
|
1302
|
-
return undefined;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const packageDirectoryPath = path.dirname(filePath);
|
|
1306
|
-
if (!ignoreTypeOnlyPackageJson) {
|
|
1307
|
-
return packageDirectoryPath;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (!isTypeOnlyPackageJsonSync(filePath)) {
|
|
1311
|
-
return packageDirectoryPath;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const nextDirectory = getNextSearchDirectory(filePath);
|
|
1315
|
-
if (!nextDirectory) {
|
|
1316
|
-
return undefined;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
return findPackageDirectorySync(nextDirectory, ignoreTypeOnlyPackageJson);
|
|
1320
|
-
};
|
|
1321
|
-
|
|
1322
|
-
function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
1323
|
-
return findPackageDirectorySync(cwd ?? process$1.cwd(), ignoreTypeOnlyPackageJson);
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
1254
|
/**
|
|
1327
|
-
*
|
|
1255
|
+
* Build a minimal MetaNode from a known meta path using watcher walk.
|
|
1328
1256
|
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1257
|
+
* Used for targeted synthesis (when a specific path is requested) to avoid
|
|
1258
|
+
* the full discovery + ownership tree build. Discovers only immediate child
|
|
1259
|
+
* `.meta/` directories.
|
|
1331
1260
|
*
|
|
1332
|
-
* @module
|
|
1261
|
+
* @module discovery/buildMinimalNode
|
|
1333
1262
|
*/
|
|
1334
|
-
const packageRoot = packageDirectorySync({
|
|
1335
|
-
cwd: fileURLToPath(import.meta.url),
|
|
1336
|
-
});
|
|
1337
|
-
const promptDir = join(packageRoot, 'dist', 'prompts');
|
|
1338
|
-
/** Built-in default architect prompt. */
|
|
1339
|
-
const DEFAULT_ARCHITECT_PROMPT = readFileSync(join(promptDir, 'architect.md'), 'utf8');
|
|
1340
|
-
/** Built-in default critic prompt. */
|
|
1341
|
-
const DEFAULT_CRITIC_PROMPT = readFileSync(join(promptDir, 'critic.md'), 'utf8');
|
|
1342
|
-
|
|
1343
1263
|
/**
|
|
1344
|
-
*
|
|
1264
|
+
* Build a minimal MetaNode for a known meta path.
|
|
1345
1265
|
*
|
|
1346
|
-
*
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* List archive .json files sorted chronologically (oldest first).
|
|
1266
|
+
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
1267
|
+
* a shallow ownership tree (self + direct children only).
|
|
1350
1268
|
*
|
|
1351
|
-
* @param metaPath - Absolute path to the
|
|
1352
|
-
* @
|
|
1269
|
+
* @param metaPath - Absolute path to the `.meta/` directory.
|
|
1270
|
+
* @param watcher - WatcherClient for filesystem enumeration.
|
|
1271
|
+
* @returns MetaNode with direct children wired.
|
|
1353
1272
|
*/
|
|
1354
|
-
function
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1273
|
+
async function buildMinimalNode(metaPath, watcher) {
|
|
1274
|
+
const normalized = normalizePath(metaPath);
|
|
1275
|
+
const ownerPath = posix.dirname(normalized);
|
|
1276
|
+
// Find child metas using watcher walk.
|
|
1277
|
+
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1278
|
+
// to match the ownership semantics used elsewhere.
|
|
1279
|
+
const rawMetaJsonPaths = await watcher.walk([
|
|
1280
|
+
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1281
|
+
]);
|
|
1282
|
+
const candidateMetaPaths = [
|
|
1283
|
+
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
1284
|
+
].filter((p) => p !== normalized);
|
|
1285
|
+
const candidates = candidateMetaPaths
|
|
1286
|
+
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
1287
|
+
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
1288
|
+
const directChildren = [];
|
|
1289
|
+
for (const c of candidates) {
|
|
1290
|
+
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
1291
|
+
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
1292
|
+
if (!nestedUnderExisting)
|
|
1293
|
+
directChildren.push(c);
|
|
1361
1294
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1295
|
+
const children = directChildren.map((c) => ({
|
|
1296
|
+
metaPath: c.metaPath,
|
|
1297
|
+
ownerPath: c.ownerPath,
|
|
1298
|
+
treeDepth: 1,
|
|
1299
|
+
children: [],
|
|
1300
|
+
parent: null,
|
|
1301
|
+
}));
|
|
1302
|
+
const node = {
|
|
1303
|
+
metaPath: normalized,
|
|
1304
|
+
ownerPath,
|
|
1305
|
+
treeDepth: 0,
|
|
1306
|
+
children,
|
|
1307
|
+
parent: null,
|
|
1308
|
+
};
|
|
1309
|
+
for (const child of children) {
|
|
1310
|
+
child.parent = node;
|
|
1364
1311
|
}
|
|
1312
|
+
return node;
|
|
1365
1313
|
}
|
|
1366
1314
|
|
|
1367
1315
|
/**
|
|
1368
|
-
*
|
|
1316
|
+
* Pure phase-state transition functions.
|
|
1369
1317
|
*
|
|
1370
|
-
*
|
|
1318
|
+
* Implements every row of the §8 "Transitions and invalidation cascade" table.
|
|
1319
|
+
* No I/O — pure functions over PhaseState and documented inputs.
|
|
1320
|
+
*
|
|
1321
|
+
* @module phaseState/phaseTransitions
|
|
1371
1322
|
*/
|
|
1372
1323
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
* Removes the oldest files.
|
|
1375
|
-
*
|
|
1376
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1377
|
-
* @param maxArchive - Maximum snapshots to retain.
|
|
1378
|
-
* @returns Number of files pruned.
|
|
1324
|
+
* Create a fresh (fully-complete) phase state.
|
|
1379
1325
|
*/
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const toRemove = files.length - maxArchive;
|
|
1383
|
-
if (toRemove <= 0)
|
|
1384
|
-
return 0;
|
|
1385
|
-
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
1386
|
-
return toRemove;
|
|
1326
|
+
function freshPhaseState() {
|
|
1327
|
+
return { architect: 'fresh', builder: 'fresh', critic: 'fresh' };
|
|
1387
1328
|
}
|
|
1388
|
-
|
|
1389
1329
|
/**
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
* Used by the GET /metas/:path route to retrieve archive history.
|
|
1393
|
-
* Prefers watcher scan for performance; falls back to disk reads
|
|
1394
|
-
* when the watcher is unavailable.
|
|
1395
|
-
*
|
|
1396
|
-
* @module archive/readArchive
|
|
1330
|
+
* Create a phase state for a never-synthesized meta (all pending from architect).
|
|
1397
1331
|
*/
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const must = [
|
|
1401
|
-
{
|
|
1402
|
-
key: 'file_path',
|
|
1403
|
-
match: { text: normalizePath(join(metaPath, 'archive')) },
|
|
1404
|
-
},
|
|
1405
|
-
];
|
|
1406
|
-
for (const [key, value] of Object.entries(metaArchiveProperty)) {
|
|
1407
|
-
must.push({ key, match: { value } });
|
|
1408
|
-
}
|
|
1409
|
-
return { must };
|
|
1410
|
-
}
|
|
1411
|
-
/** Extract file_path from a scan point payload. */
|
|
1412
|
-
function getArchiveFilePath(point) {
|
|
1413
|
-
const value = point.payload?.file_path;
|
|
1414
|
-
return typeof value === 'string' ? value : '';
|
|
1415
|
-
}
|
|
1416
|
-
/** Project archive metadata, stripping indexing-internal fields. */
|
|
1417
|
-
function projectArchivePayload(point, projectMeta) {
|
|
1418
|
-
const payload = point.payload ?? {};
|
|
1419
|
-
if (Object.keys(payload).length === 0)
|
|
1420
|
-
return null;
|
|
1421
|
-
const archiveMeta = { ...payload };
|
|
1422
|
-
delete archiveMeta.file_path;
|
|
1423
|
-
delete archiveMeta.chunk_text;
|
|
1424
|
-
delete archiveMeta.chunk_index;
|
|
1425
|
-
delete archiveMeta.total_chunks;
|
|
1426
|
-
delete archiveMeta.content_hash;
|
|
1427
|
-
delete archiveMeta.matched_rules;
|
|
1428
|
-
return projectMeta(archiveMeta);
|
|
1332
|
+
function initialPhaseState() {
|
|
1333
|
+
return { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
1429
1334
|
}
|
|
1430
1335
|
/**
|
|
1431
|
-
*
|
|
1336
|
+
* Enforce the per-meta invariant: at most one phase is pending or running,
|
|
1337
|
+
* and it is the first non-fresh phase in pipeline order.
|
|
1432
1338
|
*
|
|
1433
|
-
*
|
|
1434
|
-
* @param metaPath - Absolute path to the .meta/ directory.
|
|
1435
|
-
* @param metaArchiveProperty - Additional filter properties for archive points.
|
|
1436
|
-
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
1437
|
-
* @param projectMeta - Field projection function.
|
|
1438
|
-
* @returns Array of projected archive entries, newest first.
|
|
1339
|
+
* Stale phases that become the first non-fresh phase are promoted to pending.
|
|
1439
1340
|
*/
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1341
|
+
function enforceInvariant(state) {
|
|
1342
|
+
const result = { ...state };
|
|
1343
|
+
let foundNonFresh = false;
|
|
1344
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1345
|
+
const s = result[phase];
|
|
1346
|
+
if (s === 'fresh')
|
|
1347
|
+
continue;
|
|
1348
|
+
if (!foundNonFresh) {
|
|
1349
|
+
foundNonFresh = true;
|
|
1350
|
+
// First non-fresh: if stale, promote to pending
|
|
1351
|
+
if (s === 'stale') {
|
|
1352
|
+
result[phase] = 'pending';
|
|
1353
|
+
}
|
|
1354
|
+
// pending, running, failed stay as-is
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
// Subsequent non-fresh: must not be pending or running
|
|
1358
|
+
if (s === 'pending') {
|
|
1359
|
+
result[phase] = 'stale';
|
|
1360
|
+
}
|
|
1361
|
+
// running in non-first position would be a bug, but don't mask it
|
|
1362
|
+
}
|
|
1445
1363
|
}
|
|
1446
|
-
|
|
1447
|
-
let cursor;
|
|
1448
|
-
do {
|
|
1449
|
-
const result = await watcher.scan({
|
|
1450
|
-
filter: buildArchiveScanFilter(metaPath, metaArchiveProperty),
|
|
1451
|
-
limit: 100,
|
|
1452
|
-
cursor,
|
|
1453
|
-
});
|
|
1454
|
-
points.push(...result.points);
|
|
1455
|
-
cursor = result.cursor ?? undefined;
|
|
1456
|
-
} while (cursor);
|
|
1457
|
-
const sorted = points.sort((a, b) => getArchiveFilePath(a).localeCompare(getArchiveFilePath(b)));
|
|
1458
|
-
return sorted
|
|
1459
|
-
.slice(limit ? -limit : 0)
|
|
1460
|
-
.reverse()
|
|
1461
|
-
.map((point) => projectArchivePayload(point, projectMeta))
|
|
1462
|
-
.filter((value) => value !== null);
|
|
1364
|
+
return result;
|
|
1463
1365
|
}
|
|
1366
|
+
// ── Invalidation cascades ──────────────────────────────────────────────
|
|
1464
1367
|
/**
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
1469
|
-
* @param projectMeta - Field projection function.
|
|
1470
|
-
* @returns Array of projected archive entries, newest first.
|
|
1368
|
+
* Architect invalidated: architect → pending; builder, critic → stale.
|
|
1369
|
+
* Triggers: first run, _structureHash change, _steer change,
|
|
1370
|
+
* _crossRefs declaration change, _synthesisCount \>= architectEvery.
|
|
1471
1371
|
*/
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const raw = await readFile(archiveFile, 'utf8');
|
|
1479
|
-
return projectMeta(JSON.parse(raw));
|
|
1480
|
-
}));
|
|
1372
|
+
function invalidateArchitect(state) {
|
|
1373
|
+
return enforceInvariant({
|
|
1374
|
+
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
1375
|
+
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
1376
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1377
|
+
});
|
|
1481
1378
|
}
|
|
1482
|
-
|
|
1483
1379
|
/**
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1380
|
+
* Builder invalidated (scope mtime or cross-ref _content change):
|
|
1381
|
+
* builder → pending; critic → stale.
|
|
1382
|
+
* Only applies when architect is fresh; otherwise, builder stays stale.
|
|
1487
1383
|
*/
|
|
1384
|
+
function invalidateBuilder(state) {
|
|
1385
|
+
if (state.architect !== 'fresh') {
|
|
1386
|
+
// Architect is not fresh — builder stays stale (or whatever it is)
|
|
1387
|
+
return enforceInvariant({
|
|
1388
|
+
...state,
|
|
1389
|
+
builder: state.builder === 'fresh' || state.builder === 'stale'
|
|
1390
|
+
? 'stale'
|
|
1391
|
+
: state.builder,
|
|
1392
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return enforceInvariant({
|
|
1396
|
+
...state,
|
|
1397
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
1398
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
// ── Phase success transitions ──────────────────────────────────────────
|
|
1488
1402
|
/**
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1492
|
-
* @returns The latest archived meta, or null if no archives exist.
|
|
1403
|
+
* Architect completes successfully.
|
|
1404
|
+
* architect → fresh; builder → pending; critic → stale.
|
|
1493
1405
|
*/
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1406
|
+
function architectSuccess(state) {
|
|
1407
|
+
return enforceInvariant({
|
|
1408
|
+
architect: 'fresh',
|
|
1409
|
+
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
1410
|
+
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
1411
|
+
});
|
|
1500
1412
|
}
|
|
1501
|
-
|
|
1502
1413
|
/**
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1505
|
-
* Copies current meta.json to archive/\{ISO-timestamp\}.json with
|
|
1506
|
-
* _archived: true and _archivedAt added.
|
|
1507
|
-
*
|
|
1508
|
-
* @module archive/snapshot
|
|
1414
|
+
* Builder completes successfully.
|
|
1415
|
+
* builder → fresh; critic → pending.
|
|
1509
1416
|
*/
|
|
1417
|
+
function builderSuccess(state) {
|
|
1418
|
+
return enforceInvariant({
|
|
1419
|
+
...state,
|
|
1420
|
+
builder: 'fresh',
|
|
1421
|
+
critic: state.critic === 'failed' ? 'failed' : 'pending',
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1510
1424
|
/**
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
1514
|
-
* @param meta - Current meta.json content.
|
|
1515
|
-
* @returns The archive file path.
|
|
1425
|
+
* Critic completes successfully.
|
|
1426
|
+
* critic → fresh. Meta becomes fully fresh.
|
|
1516
1427
|
*/
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const archived = {
|
|
1523
|
-
...meta,
|
|
1524
|
-
_archived: true,
|
|
1525
|
-
_archivedAt: new Date().toISOString(),
|
|
1526
|
-
};
|
|
1527
|
-
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
1528
|
-
return archiveFile;
|
|
1428
|
+
function criticSuccess(state) {
|
|
1429
|
+
return enforceInvariant({
|
|
1430
|
+
...state,
|
|
1431
|
+
critic: 'fresh',
|
|
1432
|
+
});
|
|
1529
1433
|
}
|
|
1530
|
-
|
|
1434
|
+
// ── Failure transition ─────────────────────────────────────────────────
|
|
1531
1435
|
/**
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
1535
|
-
* previous content/feedback, steer, and archive paths.
|
|
1536
|
-
*
|
|
1537
|
-
* @module orchestrator/contextPackage
|
|
1436
|
+
* A phase fails (error, timeout, or abort).
|
|
1437
|
+
* Target phase → failed; upstream and downstream unchanged.
|
|
1538
1438
|
*/
|
|
1439
|
+
function phaseFailed(state, phase) {
|
|
1440
|
+
return enforceInvariant({
|
|
1441
|
+
...state,
|
|
1442
|
+
[phase]: 'failed',
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
// ── Surgical retry ─────────────────────────────────────────────────────
|
|
1539
1446
|
/**
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
* @param files - Array of file paths.
|
|
1544
|
-
* @param maxIndividual - Show individual files up to this count.
|
|
1545
|
-
* @returns Condensed summary string.
|
|
1447
|
+
* Retry a failed phase: failed → pending.
|
|
1448
|
+
* Only valid when the phase is currently failed.
|
|
1546
1449
|
*/
|
|
1547
|
-
function
|
|
1548
|
-
if (
|
|
1549
|
-
return
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
1555
|
-
const key = dir + '*' + ext;
|
|
1556
|
-
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
1557
|
-
}
|
|
1558
|
-
// Sort by count descending
|
|
1559
|
-
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
1560
|
-
return sorted
|
|
1561
|
-
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
1562
|
-
.join('\n');
|
|
1450
|
+
function retryPhase(state, phase) {
|
|
1451
|
+
if (state[phase] !== 'failed')
|
|
1452
|
+
return state;
|
|
1453
|
+
return enforceInvariant({
|
|
1454
|
+
...state,
|
|
1455
|
+
[phase]: 'pending',
|
|
1456
|
+
});
|
|
1563
1457
|
}
|
|
1564
1458
|
/**
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1567
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1568
|
-
* @returns Parsed fields, or defaults if missing/unreadable.
|
|
1459
|
+
* Retry all failed phases: each failed phase → pending.
|
|
1460
|
+
* Used by scheduler ticks and queue reads to auto-promote failed phases.
|
|
1569
1461
|
*/
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
generatedAt: meta._generatedAt,
|
|
1577
|
-
};
|
|
1462
|
+
function retryAllFailed(state) {
|
|
1463
|
+
let result = state;
|
|
1464
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1465
|
+
if (result[phase] === 'failed') {
|
|
1466
|
+
result = retryPhase(result, phase);
|
|
1467
|
+
}
|
|
1578
1468
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1469
|
+
return result;
|
|
1470
|
+
}
|
|
1471
|
+
// ── Running transition ─────────────────────────────────────────────────
|
|
1472
|
+
/**
|
|
1473
|
+
* Mark a phase as running (scheduler picks it).
|
|
1474
|
+
*/
|
|
1475
|
+
function phaseRunning(state, phase) {
|
|
1476
|
+
return {
|
|
1477
|
+
...state,
|
|
1478
|
+
[phase]: 'running',
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
// ── Query helpers ──────────────────────────────────────────────────────
|
|
1482
|
+
/**
|
|
1483
|
+
* Get the owed phase: first non-fresh phase in pipeline order, or null.
|
|
1484
|
+
*/
|
|
1485
|
+
function getOwedPhase(state) {
|
|
1486
|
+
for (const phase of ['architect', 'builder', 'critic']) {
|
|
1487
|
+
if (state[phase] !== 'fresh')
|
|
1488
|
+
return phase;
|
|
1581
1489
|
}
|
|
1490
|
+
return null;
|
|
1582
1491
|
}
|
|
1583
1492
|
/**
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
1587
|
-
* @returns The `_content` string, or null if missing/unreadable.
|
|
1493
|
+
* Check if a meta is fully fresh (all phases fresh).
|
|
1588
1494
|
*/
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1495
|
+
function isFullyFresh(state) {
|
|
1496
|
+
return (state.architect === 'fresh' &&
|
|
1497
|
+
state.builder === 'fresh' &&
|
|
1498
|
+
state.critic === 'fresh');
|
|
1592
1499
|
}
|
|
1593
1500
|
/**
|
|
1594
|
-
*
|
|
1501
|
+
* Get the scheduler priority band for a meta's owed phase.
|
|
1502
|
+
* 1 = critic (highest), 2 = builder, 3 = architect, null = fully fresh.
|
|
1503
|
+
*/
|
|
1504
|
+
function getPriorityBand(state) {
|
|
1505
|
+
const owed = getOwedPhase(state);
|
|
1506
|
+
if (!owed)
|
|
1507
|
+
return null;
|
|
1508
|
+
if (owed === 'critic')
|
|
1509
|
+
return 1;
|
|
1510
|
+
if (owed === 'builder')
|
|
1511
|
+
return 2;
|
|
1512
|
+
return 3;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* Backward-compatible derivation of _phaseState from existing meta fields.
|
|
1595
1517
|
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1518
|
+
* When a meta is loaded from disk without _phaseState, this reconstructs
|
|
1519
|
+
* the phase state from _content, _builder, _state, _error.step, and
|
|
1520
|
+
* the architect-invalidating inputs.
|
|
1521
|
+
*
|
|
1522
|
+
* @module phaseState/derivePhaseState
|
|
1600
1523
|
*/
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
for
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
// Non-delta: already incorporated in previous synthesis.
|
|
1633
|
-
// Include path with null content so the builder knows it exists.
|
|
1634
|
-
childMetas[path] = null;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
logger?.debug({
|
|
1638
|
-
totalChildren: childEntries.length,
|
|
1639
|
-
deltaChildren: deltaChildCount,
|
|
1640
|
-
}, 'child metas filtered by delta');
|
|
1641
|
-
// Cross-referenced meta outputs (parallel reads)
|
|
1642
|
-
const crossRefMetas = {};
|
|
1643
|
-
const seen = new Set();
|
|
1644
|
-
const crossRefPaths = [];
|
|
1645
|
-
for (const refPath of meta._crossRefs ?? []) {
|
|
1646
|
-
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
1647
|
-
continue;
|
|
1648
|
-
if (seen.has(refPath))
|
|
1649
|
-
continue;
|
|
1650
|
-
seen.add(refPath);
|
|
1651
|
-
crossRefPaths.push(refPath);
|
|
1652
|
-
}
|
|
1653
|
-
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
1654
|
-
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
1655
|
-
return [refPath, content];
|
|
1656
|
-
}));
|
|
1657
|
-
for (const [path, content] of crossRefEntries) {
|
|
1658
|
-
crossRefMetas[path] = content;
|
|
1659
|
-
}
|
|
1660
|
-
// Archive paths
|
|
1661
|
-
const archives = listArchiveFiles(node.metaPath);
|
|
1662
|
-
// Nearest ancestor _builder output
|
|
1663
|
-
let ancestorBuilder;
|
|
1664
|
-
const ancestor = getAncestorMeta(node);
|
|
1665
|
-
if (ancestor) {
|
|
1666
|
-
try {
|
|
1667
|
-
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
1668
|
-
const ancestorMeta = JSON.parse(raw);
|
|
1669
|
-
if (ancestorMeta._builder) {
|
|
1670
|
-
ancestorBuilder = ancestorMeta._builder;
|
|
1524
|
+
/**
|
|
1525
|
+
* Derive _phaseState from existing meta fields.
|
|
1526
|
+
*
|
|
1527
|
+
* If the meta already has _phaseState, returns it as-is.
|
|
1528
|
+
*
|
|
1529
|
+
* Otherwise, reconstructs from available fields:
|
|
1530
|
+
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
1531
|
+
* - Errored meta: the failed phase is mapped from _error.step.
|
|
1532
|
+
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
1533
|
+
* - Fully-fresh meta: all phases fresh.
|
|
1534
|
+
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
1535
|
+
*
|
|
1536
|
+
* @param meta - The meta.json content.
|
|
1537
|
+
* @param inputs - Optional derivation inputs. If not provided, a simpler
|
|
1538
|
+
* heuristic is used (no architect invalidation check).
|
|
1539
|
+
* @returns The derived PhaseState.
|
|
1540
|
+
*/
|
|
1541
|
+
function derivePhaseState(meta, inputs) {
|
|
1542
|
+
// Already has _phaseState — use it
|
|
1543
|
+
if (meta._phaseState)
|
|
1544
|
+
return meta._phaseState;
|
|
1545
|
+
// Check for errors first — _error.step maps directly to failed phase
|
|
1546
|
+
if (meta._error) {
|
|
1547
|
+
const failedPhase = meta._error.step;
|
|
1548
|
+
const state = freshPhaseState();
|
|
1549
|
+
state[failedPhase] = 'failed';
|
|
1550
|
+
// If architect failed and no _builder, downstream is stale
|
|
1551
|
+
if (failedPhase === 'architect') {
|
|
1552
|
+
if (!meta._builder) {
|
|
1553
|
+
state.builder = 'stale';
|
|
1554
|
+
state.critic = 'stale';
|
|
1671
1555
|
}
|
|
1672
1556
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1557
|
+
// If builder failed, critic is stale
|
|
1558
|
+
if (failedPhase === 'builder') {
|
|
1559
|
+
state.critic = 'stale';
|
|
1675
1560
|
}
|
|
1561
|
+
return state;
|
|
1676
1562
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1563
|
+
// Never synthesized: no _content AND no _builder (and no error)
|
|
1564
|
+
if (!meta._content && !meta._builder) {
|
|
1565
|
+
return initialPhaseState();
|
|
1566
|
+
}
|
|
1567
|
+
// Has _builder but no _content: builder is pending
|
|
1568
|
+
if (meta._builder && !meta._content) {
|
|
1569
|
+
return {
|
|
1570
|
+
architect: 'fresh',
|
|
1571
|
+
builder: 'pending',
|
|
1572
|
+
critic: 'stale',
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
// Has _content but no _feedback: critic is pending
|
|
1576
|
+
if (meta._content && !meta._feedback) {
|
|
1577
|
+
return {
|
|
1578
|
+
architect: 'fresh',
|
|
1579
|
+
builder: 'fresh',
|
|
1580
|
+
critic: 'pending',
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
// Default: fully fresh
|
|
1584
|
+
return freshPhaseState();
|
|
1690
1585
|
}
|
|
1691
1586
|
|
|
1692
1587
|
/**
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
* Prompts are compiled as Handlebars templates with access to config,
|
|
1696
|
-
* meta, and scope context. The architect can write template expressions
|
|
1697
|
-
* into its _builder output; these resolve when the builder task is compiled.
|
|
1588
|
+
* List archive snapshot files in chronological order.
|
|
1698
1589
|
*
|
|
1699
|
-
* @module
|
|
1590
|
+
* @module archive/listArchive
|
|
1700
1591
|
*/
|
|
1701
|
-
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
1702
|
-
/** Build the template context from synthesis inputs. */
|
|
1703
|
-
function buildTemplateContext(ctx, meta, config) {
|
|
1704
|
-
return {
|
|
1705
|
-
config,
|
|
1706
|
-
meta,
|
|
1707
|
-
scope: {
|
|
1708
|
-
fileCount: ctx.scopeFiles.length,
|
|
1709
|
-
deltaCount: ctx.deltaFiles.length,
|
|
1710
|
-
childCount: Object.keys(ctx.childMetas).length,
|
|
1711
|
-
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
1712
|
-
},
|
|
1713
|
-
};
|
|
1714
|
-
}
|
|
1715
1592
|
/**
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1593
|
+
* List archive .json files sorted chronologically (oldest first).
|
|
1594
|
+
*
|
|
1595
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1596
|
+
* @returns Array of absolute paths to archive files, or empty if none.
|
|
1718
1597
|
*/
|
|
1719
|
-
function
|
|
1598
|
+
function listArchiveFiles(metaPath) {
|
|
1599
|
+
const archiveDir = join(metaPath, 'archive');
|
|
1720
1600
|
try {
|
|
1721
|
-
return
|
|
1601
|
+
return readdirSync(archiveDir)
|
|
1602
|
+
.filter((f) => f.endsWith('.json'))
|
|
1603
|
+
.sort()
|
|
1604
|
+
.map((f) => join(archiveDir, f));
|
|
1722
1605
|
}
|
|
1723
1606
|
catch {
|
|
1724
|
-
return
|
|
1607
|
+
return [];
|
|
1725
1608
|
}
|
|
1726
1609
|
}
|
|
1610
|
+
|
|
1727
1611
|
/**
|
|
1728
|
-
*
|
|
1612
|
+
* Prune old archive snapshots beyond maxArchive.
|
|
1729
1613
|
*
|
|
1730
|
-
* @
|
|
1731
|
-
* path listing (delta-aware child metas). When false, null entries get
|
|
1732
|
-
* individual subsections with "(not yet synthesized)" (cross-refs).
|
|
1614
|
+
* @module archive/prune
|
|
1733
1615
|
*/
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const withContent = [];
|
|
1750
|
-
const unchanged = [];
|
|
1751
|
-
const notYetSynthesized = [];
|
|
1752
|
-
for (const [path, content] of Object.entries(metas)) {
|
|
1753
|
-
if (typeof content === 'string') {
|
|
1754
|
-
withContent.push([path, content]);
|
|
1755
|
-
}
|
|
1756
|
-
else if (content === null) {
|
|
1757
|
-
unchanged.push(path);
|
|
1758
|
-
}
|
|
1759
|
-
else {
|
|
1760
|
-
notYetSynthesized.push(path);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
sections.push('', heading);
|
|
1764
|
-
for (const [path, content] of withContent) {
|
|
1765
|
-
sections.push(`### ${path}`, content);
|
|
1766
|
-
}
|
|
1767
|
-
if (notYetSynthesized.length > 0) {
|
|
1768
|
-
sections.push('', `### Not yet synthesized (${notYetSynthesized.length.toString()})`, ...notYetSynthesized.map((p) => `- ${p}`));
|
|
1769
|
-
}
|
|
1770
|
-
if (unchanged.length > 0) {
|
|
1771
|
-
sections.push('', `### Other children (${unchanged.length.toString()} \u2014 unchanged since last synthesis)`, ...unchanged.map((p) => `- ${p}`));
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
/** Inject nearest ancestor's organizational context, if available. */
|
|
1775
|
-
function appendAncestorContext(sections, ctx) {
|
|
1776
|
-
if (ctx.ancestorBuilder) {
|
|
1777
|
-
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
/** Append optional context sections shared across all step prompts. */
|
|
1781
|
-
function appendSharedSections(sections, ctx, options) {
|
|
1782
|
-
const opts = {
|
|
1783
|
-
includeSteer: true,
|
|
1784
|
-
includePreviousContent: true,
|
|
1785
|
-
includePreviousFeedback: true,
|
|
1786
|
-
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
1787
|
-
includeChildMetas: true,
|
|
1788
|
-
includeCrossRefs: true,
|
|
1789
|
-
...options,
|
|
1790
|
-
};
|
|
1791
|
-
if (opts.includeSteer && ctx.steer) {
|
|
1792
|
-
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
1793
|
-
}
|
|
1794
|
-
if (opts.includePreviousContent && ctx.previousContent) {
|
|
1795
|
-
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
1796
|
-
}
|
|
1797
|
-
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
1798
|
-
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
1799
|
-
}
|
|
1800
|
-
if (opts.includeChildMetas) {
|
|
1801
|
-
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas, true);
|
|
1802
|
-
}
|
|
1803
|
-
if (opts.includeCrossRefs) {
|
|
1804
|
-
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
1805
|
-
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Prune archive directory to keep at most maxArchive snapshots.
|
|
1618
|
+
* Removes the oldest files.
|
|
1619
|
+
*
|
|
1620
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1621
|
+
* @param maxArchive - Maximum snapshots to retain.
|
|
1622
|
+
* @returns Number of files pruned.
|
|
1623
|
+
*/
|
|
1624
|
+
async function pruneArchive(metaPath, maxArchive) {
|
|
1625
|
+
const files = listArchiveFiles(metaPath);
|
|
1626
|
+
const toRemove = files.length - maxArchive;
|
|
1627
|
+
if (toRemove <= 0)
|
|
1628
|
+
return 0;
|
|
1629
|
+
await Promise.all(files.slice(0, toRemove).map(unlink));
|
|
1630
|
+
return toRemove;
|
|
1806
1631
|
}
|
|
1632
|
+
|
|
1807
1633
|
/**
|
|
1808
|
-
*
|
|
1634
|
+
* Read the latest archive snapshot for steer change detection.
|
|
1809
1635
|
*
|
|
1810
|
-
* @
|
|
1811
|
-
* @param meta - Current meta.json.
|
|
1812
|
-
* @param config - Synthesis config.
|
|
1813
|
-
* @returns The architect task prompt string.
|
|
1636
|
+
* @module archive/readLatest
|
|
1814
1637
|
*/
|
|
1815
|
-
function buildArchitectTask(ctx, meta, config) {
|
|
1816
|
-
const sections = [
|
|
1817
|
-
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
1818
|
-
'',
|
|
1819
|
-
DEFAULT_ARCHITECT_PROMPT,
|
|
1820
|
-
'',
|
|
1821
|
-
'## SCOPE',
|
|
1822
|
-
`Path: ${ctx.path}`,
|
|
1823
|
-
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
1824
|
-
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
1825
|
-
'',
|
|
1826
|
-
'### File listing (scope)',
|
|
1827
|
-
condenseScopeFiles(ctx.scopeFiles),
|
|
1828
|
-
];
|
|
1829
|
-
appendAncestorContext(sections, ctx);
|
|
1830
|
-
// Inject previous _builder so architect can see its own prior output
|
|
1831
|
-
if (meta._builder) {
|
|
1832
|
-
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
1833
|
-
}
|
|
1834
|
-
appendSharedSections(sections, ctx);
|
|
1835
|
-
if (ctx.archives.length > 0) {
|
|
1836
|
-
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.');
|
|
1837
|
-
}
|
|
1838
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1839
|
-
}
|
|
1840
1638
|
/**
|
|
1841
|
-
*
|
|
1639
|
+
* Read the most recent archive snapshot.
|
|
1842
1640
|
*
|
|
1843
|
-
* @param
|
|
1844
|
-
* @
|
|
1845
|
-
* @param config - Synthesis config.
|
|
1846
|
-
* @returns The builder task prompt string.
|
|
1641
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1642
|
+
* @returns The latest archived meta, or null if no archives exist.
|
|
1847
1643
|
*/
|
|
1848
|
-
function
|
|
1849
|
-
const
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
'',
|
|
1855
|
-
'## SCOPE',
|
|
1856
|
-
`Path: ${ctx.path}`,
|
|
1857
|
-
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
1858
|
-
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
1859
|
-
];
|
|
1860
|
-
appendAncestorContext(sections, ctx);
|
|
1861
|
-
if (ctx.previousState != null) {
|
|
1862
|
-
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
1863
|
-
}
|
|
1864
|
-
appendSharedSections(sections, ctx, {
|
|
1865
|
-
includeSteer: false,
|
|
1866
|
-
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
1867
|
-
});
|
|
1868
|
-
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
|
|
1869
|
-
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
1644
|
+
async function readLatestArchive(metaPath) {
|
|
1645
|
+
const files = listArchiveFiles(metaPath);
|
|
1646
|
+
if (files.length === 0)
|
|
1647
|
+
return null;
|
|
1648
|
+
const raw = await readFile(files[files.length - 1], 'utf8');
|
|
1649
|
+
return JSON.parse(raw);
|
|
1870
1650
|
}
|
|
1651
|
+
|
|
1871
1652
|
/**
|
|
1872
|
-
*
|
|
1653
|
+
* Create archive snapshots of meta.json.
|
|
1873
1654
|
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
* @
|
|
1655
|
+
* Copies current meta.json to archive/\{ISO-timestamp\}.json with
|
|
1656
|
+
* _archived: true and _archivedAt added.
|
|
1657
|
+
*
|
|
1658
|
+
* @module archive/snapshot
|
|
1878
1659
|
*/
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1660
|
+
/**
|
|
1661
|
+
* Create an archive snapshot of the current meta.json.
|
|
1662
|
+
*
|
|
1663
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
1664
|
+
* @param meta - Current meta.json content.
|
|
1665
|
+
* @returns The archive file path.
|
|
1666
|
+
*/
|
|
1667
|
+
async function createSnapshot(metaPath, meta) {
|
|
1668
|
+
const archiveDir = join(metaPath, 'archive');
|
|
1669
|
+
await mkdir(archiveDir, { recursive: true });
|
|
1670
|
+
const now = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1671
|
+
const archiveFile = join(archiveDir, now + '.json');
|
|
1672
|
+
const archived = {
|
|
1673
|
+
...meta,
|
|
1674
|
+
_archived: true,
|
|
1675
|
+
_archivedAt: new Date().toISOString(),
|
|
1676
|
+
};
|
|
1677
|
+
await writeFile(archiveFile, JSON.stringify(archived, null, 2) + '\n');
|
|
1678
|
+
return archiveFile;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
|
|
1682
|
+
|
|
1683
|
+
function findUpSync(name, {
|
|
1684
|
+
cwd = process$1.cwd(),
|
|
1685
|
+
type = 'file',
|
|
1686
|
+
stopAt,
|
|
1687
|
+
} = {}) {
|
|
1688
|
+
let directory = path.resolve(toPath(cwd) ?? '');
|
|
1689
|
+
const {root} = path.parse(directory);
|
|
1690
|
+
stopAt = path.resolve(directory, toPath(stopAt) ?? root);
|
|
1691
|
+
const isAbsoluteName = path.isAbsolute(name);
|
|
1692
|
+
|
|
1693
|
+
while (directory) {
|
|
1694
|
+
const filePath = isAbsoluteName ? name : path.join(directory, name);
|
|
1695
|
+
|
|
1696
|
+
try {
|
|
1697
|
+
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
|
|
1698
|
+
if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
|
|
1699
|
+
return filePath;
|
|
1700
|
+
}
|
|
1701
|
+
} catch {}
|
|
1702
|
+
|
|
1703
|
+
if (directory === stopAt || directory === root) {
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
directory = path.dirname(directory);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const isTypeOnlyPackageJsonData = packageData => {
|
|
1712
|
+
if (!packageData || typeof packageData !== 'object' || Array.isArray(packageData)) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const keys = Object.keys(packageData);
|
|
1717
|
+
return keys.length === 1 && keys[0] === 'type' && typeof packageData.type === 'string';
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
const isTypeOnlyPackageJsonSync = filePath => {
|
|
1721
|
+
let fileContents;
|
|
1722
|
+
|
|
1723
|
+
try {
|
|
1724
|
+
fileContents = fs.readFileSync(filePath, 'utf8');
|
|
1725
|
+
} catch {
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
try {
|
|
1730
|
+
return isTypeOnlyPackageJsonData(JSON.parse(fileContents));
|
|
1731
|
+
} catch {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
const getNextSearchDirectory = filePath => {
|
|
1737
|
+
const packageDirectoryPath = path.dirname(filePath);
|
|
1738
|
+
const parentDirectoryPath = path.dirname(packageDirectoryPath);
|
|
1739
|
+
return parentDirectoryPath === packageDirectoryPath ? undefined : parentDirectoryPath;
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const findPackageDirectorySync = (directory, ignoreTypeOnlyPackageJson) => {
|
|
1743
|
+
const filePath = findUpSync('package.json', {cwd: directory});
|
|
1744
|
+
if (!filePath) {
|
|
1745
|
+
return undefined;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const packageDirectoryPath = path.dirname(filePath);
|
|
1749
|
+
if (!ignoreTypeOnlyPackageJson) {
|
|
1750
|
+
return packageDirectoryPath;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (!isTypeOnlyPackageJsonSync(filePath)) {
|
|
1754
|
+
return packageDirectoryPath;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const nextDirectory = getNextSearchDirectory(filePath);
|
|
1758
|
+
if (!nextDirectory) {
|
|
1759
|
+
return undefined;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return findPackageDirectorySync(nextDirectory, ignoreTypeOnlyPackageJson);
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
|
|
1766
|
+
return findPackageDirectorySync(cwd ?? process$1.cwd(), ignoreTypeOnlyPackageJson);
|
|
1900
1767
|
}
|
|
1901
1768
|
|
|
1902
1769
|
/**
|
|
1903
|
-
*
|
|
1770
|
+
* Built-in default prompts for the synthesis pipeline.
|
|
1904
1771
|
*
|
|
1905
|
-
*
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1772
|
+
* Prompts live as `.md` files next to this module in `src/prompts/`. The
|
|
1773
|
+
* CLI build copies them into `dist/prompts/` (rollup-plugin-copy), and a
|
|
1774
|
+
* build-time assertion verifies the copy.
|
|
1908
1775
|
*
|
|
1909
|
-
*
|
|
1776
|
+
* The prompt directory is resolved relative to the executing module, so
|
|
1777
|
+
* running from source (e.g. vitest) reads `src/prompts/` and the published
|
|
1778
|
+
* bundle reads `dist/prompts/`. Prompts are read lazily on first use, so a
|
|
1779
|
+
* missing file surfaces as one clear error instead of an import-time crash.
|
|
1780
|
+
*
|
|
1781
|
+
* @module prompts
|
|
1910
1782
|
*/
|
|
1911
1783
|
/**
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
* Walks the owner directory for child `.meta/meta.json` files and constructs
|
|
1915
|
-
* a shallow ownership tree (self + direct children only).
|
|
1784
|
+
* Resolve the directory holding the built-in prompt files for a module.
|
|
1916
1785
|
*
|
|
1917
|
-
* @param
|
|
1918
|
-
* @
|
|
1919
|
-
*
|
|
1786
|
+
* @param moduleFile - Absolute path of the executing module.
|
|
1787
|
+
* @returns `<package root>/src/prompts` when the module lives under
|
|
1788
|
+
* `src/`, otherwise `<package root>/dist/prompts` (published bundle).
|
|
1920
1789
|
*/
|
|
1921
|
-
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
// We include only *direct* children (nearest descendants in the ownership tree)
|
|
1926
|
-
// to match the ownership semantics used elsewhere.
|
|
1927
|
-
const rawMetaJsonPaths = await watcher.walk([
|
|
1928
|
-
`${escapeGlob(ownerPath)}/**/.meta/meta.json`,
|
|
1929
|
-
]);
|
|
1930
|
-
const candidateMetaPaths = [
|
|
1931
|
-
...new Set(rawMetaJsonPaths.map((p) => posix.dirname(normalizePath(p)))),
|
|
1932
|
-
].filter((p) => p !== normalized);
|
|
1933
|
-
const candidates = candidateMetaPaths
|
|
1934
|
-
.map((mp) => ({ metaPath: mp, ownerPath: posix.dirname(mp) }))
|
|
1935
|
-
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
1936
|
-
const directChildren = [];
|
|
1937
|
-
for (const c of candidates) {
|
|
1938
|
-
const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
|
|
1939
|
-
c.ownerPath.startsWith(d.ownerPath + '/'));
|
|
1940
|
-
if (!nestedUnderExisting)
|
|
1941
|
-
directChildren.push(c);
|
|
1790
|
+
function resolvePromptDir(moduleFile) {
|
|
1791
|
+
const packageRoot = packageDirectorySync({ cwd: dirname(moduleFile) });
|
|
1792
|
+
if (!packageRoot) {
|
|
1793
|
+
throw new Error(`jeeves-meta: cannot locate package root for prompt module ${moduleFile}`);
|
|
1942
1794
|
}
|
|
1943
|
-
const
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
parent: null,
|
|
1956
|
-
};
|
|
1957
|
-
for (const child of children) {
|
|
1958
|
-
child.parent = node;
|
|
1795
|
+
const [topLevel] = relative(packageRoot, moduleFile).split(sep);
|
|
1796
|
+
return join(packageRoot, topLevel === 'src' ? 'src' : 'dist', 'prompts');
|
|
1797
|
+
}
|
|
1798
|
+
const cache = new Map();
|
|
1799
|
+
function loadPrompt(file) {
|
|
1800
|
+
const cached = cache.get(file);
|
|
1801
|
+
if (cached !== undefined)
|
|
1802
|
+
return cached;
|
|
1803
|
+
const path = join(getPromptDir(), file);
|
|
1804
|
+
let content;
|
|
1805
|
+
try {
|
|
1806
|
+
content = readFileSync(path, 'utf8');
|
|
1959
1807
|
}
|
|
1960
|
-
|
|
1808
|
+
catch (err) {
|
|
1809
|
+
throw new Error(`jeeves-meta: built-in prompt not found at ${path}`, {
|
|
1810
|
+
cause: err,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
cache.set(file, content);
|
|
1814
|
+
return content;
|
|
1815
|
+
}
|
|
1816
|
+
/** Directory the built-in prompts are read from for this module. */
|
|
1817
|
+
function getPromptDir() {
|
|
1818
|
+
return resolvePromptDir(fileURLToPath(import.meta.url));
|
|
1819
|
+
}
|
|
1820
|
+
/** Built-in default architect prompt (read on first use, then cached). */
|
|
1821
|
+
function getDefaultArchitectPrompt() {
|
|
1822
|
+
return loadPrompt('architect.md');
|
|
1823
|
+
}
|
|
1824
|
+
/** Built-in default critic prompt (read on first use, then cached). */
|
|
1825
|
+
function getDefaultCriticPrompt() {
|
|
1826
|
+
return loadPrompt('critic.md');
|
|
1961
1827
|
}
|
|
1962
1828
|
|
|
1963
1829
|
/**
|
|
1964
|
-
*
|
|
1830
|
+
* Compute a structure hash from a sorted file listing.
|
|
1965
1831
|
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1832
|
+
* Used to detect when directory structure changes, triggering
|
|
1833
|
+
* an architect re-run.
|
|
1968
1834
|
*
|
|
1969
|
-
* @module
|
|
1835
|
+
* @module structureHash
|
|
1970
1836
|
*/
|
|
1971
1837
|
/**
|
|
1972
|
-
*
|
|
1838
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
1839
|
+
*
|
|
1840
|
+
* @param filePaths - Array of file paths in scope.
|
|
1841
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
1973
1842
|
*/
|
|
1974
|
-
function
|
|
1975
|
-
|
|
1843
|
+
function computeStructureHash(filePaths) {
|
|
1844
|
+
const sorted = [...filePaths].sort();
|
|
1845
|
+
const content = sorted.join('\n');
|
|
1846
|
+
return createHash('sha256').update(content).digest('hex');
|
|
1976
1847
|
}
|
|
1848
|
+
|
|
1977
1849
|
/**
|
|
1978
|
-
*
|
|
1850
|
+
* Per-tick invalidation pass.
|
|
1851
|
+
*
|
|
1852
|
+
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
1853
|
+
* then applies the cascade to update _phaseState.
|
|
1854
|
+
*
|
|
1855
|
+
* @module phaseState/invalidate
|
|
1979
1856
|
*/
|
|
1980
|
-
|
|
1981
|
-
|
|
1857
|
+
/**
|
|
1858
|
+
* Check whether a persisted prompt snapshot mismatches the currently-resolved prompt.
|
|
1859
|
+
* Returns true when the snapshot exists and differs from the resolved prompt.
|
|
1860
|
+
* This is informational only — it does NOT trigger invalidation.
|
|
1861
|
+
*/
|
|
1862
|
+
function isPromptStale(snapshot, resolved) {
|
|
1863
|
+
return snapshot !== undefined && snapshot !== resolved;
|
|
1982
1864
|
}
|
|
1983
1865
|
/**
|
|
1984
|
-
*
|
|
1985
|
-
* and it is the first non-fresh phase in pipeline order.
|
|
1866
|
+
* Compute invalidation inputs and apply cascade for a single meta.
|
|
1986
1867
|
*
|
|
1987
|
-
*
|
|
1868
|
+
* @param meta - Current meta.json content with existing _phaseState.
|
|
1869
|
+
* @param scopeFiles - Sorted file list from scope.
|
|
1870
|
+
* @param config - MetaConfig for architectEvery.
|
|
1871
|
+
* @param node - MetaNode for archive access.
|
|
1872
|
+
* @param crossRefMetas - Map of cross-ref owner paths to their current _content.
|
|
1873
|
+
* @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
|
|
1874
|
+
* @returns Updated phase state and invalidation details.
|
|
1988
1875
|
*/
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1876
|
+
async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
|
|
1877
|
+
let phaseState = meta._phaseState ?? {
|
|
1878
|
+
architect: 'fresh',
|
|
1879
|
+
builder: 'fresh',
|
|
1880
|
+
critic: 'fresh',
|
|
1881
|
+
};
|
|
1882
|
+
// ── Architect-level inputs ──
|
|
1883
|
+
const structureHash = computeStructureHash(scopeFiles);
|
|
1884
|
+
const structureChanged = structureHash !== meta._structureHash;
|
|
1885
|
+
const latestArchive = await readLatestArchive(node.metaPath);
|
|
1886
|
+
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
1887
|
+
// Prompt staleness detection: compare persisted prompt snapshots against
|
|
1888
|
+
// currently-resolved prompts. This is INFORMATIONAL ONLY — reported via
|
|
1889
|
+
// inputStatus so /preview can surface it, but it must NEVER feed into
|
|
1890
|
+
// the invalidation cascade. When a meta naturally reaches architectEvery
|
|
1891
|
+
// through real builder cycles, architect runs with the current prompt and
|
|
1892
|
+
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
1893
|
+
// corpus-wide synthesis storm (see #163).
|
|
1894
|
+
const architectChanged = isPromptStale(meta._architect, getDefaultArchitectPrompt());
|
|
1895
|
+
const criticChanged = isPromptStale(meta._critic, getDefaultCriticPrompt());
|
|
1896
|
+
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
1897
|
+
// _crossRefs declaration change
|
|
1898
|
+
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
1899
|
+
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
1900
|
+
.slice()
|
|
1901
|
+
.sort()
|
|
1902
|
+
.join(',');
|
|
1903
|
+
const crossRefsDeclChanged = latestArchive
|
|
1904
|
+
? currentRefs !== archiveRefs
|
|
1905
|
+
: currentRefs.length > 0;
|
|
1906
|
+
const architectInvalidators = [];
|
|
1907
|
+
if (structureChanged) {
|
|
1908
|
+
if (meta._state !== undefined) {
|
|
1909
|
+
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
1910
|
+
phaseState = invalidateBuilder(phaseState);
|
|
2003
1911
|
}
|
|
2004
1912
|
else {
|
|
2005
|
-
|
|
2006
|
-
if (s === 'pending') {
|
|
2007
|
-
result[phase] = 'stale';
|
|
2008
|
-
}
|
|
2009
|
-
// running in non-first position would be a bug, but don't mask it
|
|
1913
|
+
architectInvalidators.push('structureHash');
|
|
2010
1914
|
}
|
|
2011
1915
|
}
|
|
2012
|
-
|
|
1916
|
+
if (steerChanged)
|
|
1917
|
+
architectInvalidators.push('steer');
|
|
1918
|
+
if (crossRefsDeclChanged)
|
|
1919
|
+
architectInvalidators.push('_crossRefs');
|
|
1920
|
+
if (effectiveSynthesisCount >= config.architectEvery) {
|
|
1921
|
+
architectInvalidators.push('architectEvery');
|
|
1922
|
+
}
|
|
1923
|
+
if (!meta._builder)
|
|
1924
|
+
architectInvalidators.push('firstRun');
|
|
1925
|
+
if (architectInvalidators.length > 0) {
|
|
1926
|
+
phaseState = invalidateArchitect(phaseState);
|
|
1927
|
+
}
|
|
1928
|
+
// ── Builder-level inputs ──
|
|
1929
|
+
// Cross-ref _content change (builder-invalidating)
|
|
1930
|
+
let crossRefContentChanged = false;
|
|
1931
|
+
return {
|
|
1932
|
+
phaseState,
|
|
1933
|
+
architectInvalidators,
|
|
1934
|
+
inputStatus: {
|
|
1935
|
+
structureHash,
|
|
1936
|
+
steerChanged,
|
|
1937
|
+
architectChanged,
|
|
1938
|
+
criticChanged,
|
|
1939
|
+
crossRefsDeclChanged,
|
|
1940
|
+
crossRefContentChanged,
|
|
1941
|
+
},
|
|
1942
|
+
};
|
|
2013
1943
|
}
|
|
2014
|
-
|
|
1944
|
+
|
|
2015
1945
|
/**
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
2018
|
-
*
|
|
1946
|
+
* Weighted staleness formula for candidate selection.
|
|
1947
|
+
*
|
|
1948
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
1949
|
+
*
|
|
1950
|
+
* @module scheduling/weightedFormula
|
|
2019
1951
|
*/
|
|
2020
|
-
function invalidateArchitect(state) {
|
|
2021
|
-
return enforceInvariant({
|
|
2022
|
-
architect: state.architect === 'failed' ? 'failed' : 'pending',
|
|
2023
|
-
builder: state.builder === 'fresh' ? 'stale' : state.builder,
|
|
2024
|
-
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2027
1952
|
/**
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
1953
|
+
* Compute effective staleness for a set of candidates.
|
|
1954
|
+
*
|
|
1955
|
+
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
1956
|
+
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
1957
|
+
*
|
|
1958
|
+
* Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
|
|
1959
|
+
* metas to tune how much their tree position affects scheduling.
|
|
1960
|
+
*
|
|
1961
|
+
* @param candidates - Array of \{ node, meta, actualStaleness \}.
|
|
1962
|
+
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
1963
|
+
* @returns Same array with effectiveStaleness computed.
|
|
2031
1964
|
*/
|
|
2032
|
-
function
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
1965
|
+
function computeEffectiveStaleness(candidates, depthWeight) {
|
|
1966
|
+
if (candidates.length === 0)
|
|
1967
|
+
return [];
|
|
1968
|
+
// Get depth for each candidate: use _depth override or tree depth
|
|
1969
|
+
const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
|
|
1970
|
+
// Normalize: shift so minimum becomes 0
|
|
1971
|
+
const minDepth = Math.min(...depths);
|
|
1972
|
+
const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
|
|
1973
|
+
return candidates.map((c, i) => {
|
|
1974
|
+
const emphasis = c.meta._emphasis ?? 1;
|
|
1975
|
+
return {
|
|
1976
|
+
...c,
|
|
1977
|
+
effectiveStaleness: c.actualStaleness *
|
|
1978
|
+
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
1979
|
+
};
|
|
2047
1980
|
});
|
|
2048
1981
|
}
|
|
2049
|
-
|
|
1982
|
+
|
|
2050
1983
|
/**
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
1984
|
+
* Corpus-wide phase scheduler.
|
|
1985
|
+
*
|
|
1986
|
+
* Selects the highest-priority ready phase across all metas.
|
|
1987
|
+
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
1988
|
+
* Tiebreak within band: weighted staleness (§3.9).
|
|
1989
|
+
*
|
|
1990
|
+
* @module phaseState/phaseScheduler
|
|
2053
1991
|
*/
|
|
2054
|
-
function architectSuccess(state) {
|
|
2055
|
-
return enforceInvariant({
|
|
2056
|
-
architect: 'fresh',
|
|
2057
|
-
builder: state.builder === 'failed' ? 'failed' : 'pending',
|
|
2058
|
-
critic: state.critic === 'fresh' ? 'stale' : state.critic,
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
1992
|
/**
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
1993
|
+
* Build phase candidates from listMetas entries.
|
|
1994
|
+
*
|
|
1995
|
+
* Derives phase state, auto-retries failed phases, and applies Tier 1
|
|
1996
|
+
* cheap-invalidation (no I/O) for metas with persisted _phaseState.
|
|
1997
|
+
* Used by orchestratePhase, queue route, and status route.
|
|
2064
1998
|
*/
|
|
2065
|
-
function
|
|
2066
|
-
return
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
1999
|
+
function buildPhaseCandidates(entries, architectEvery) {
|
|
2000
|
+
return entries.map((entry) => {
|
|
2001
|
+
let ps = retryAllFailed(derivePhaseState(entry.meta));
|
|
2002
|
+
// Tier 1 cheap invalidation for metas with persisted _phaseState
|
|
2003
|
+
if (entry.meta._phaseState) {
|
|
2004
|
+
const needsArchitect = !entry.meta._builder ||
|
|
2005
|
+
(entry.meta._synthesisCount ?? 0) >= architectEvery;
|
|
2006
|
+
if (needsArchitect && isFullyFresh(ps)) {
|
|
2007
|
+
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return {
|
|
2011
|
+
node: entry.node,
|
|
2012
|
+
meta: entry.meta,
|
|
2013
|
+
phaseState: ps,
|
|
2014
|
+
actualStaleness: entry.stalenessSeconds,
|
|
2015
|
+
locked: entry.locked,
|
|
2016
|
+
disabled: entry.disabled,
|
|
2017
|
+
};
|
|
2070
2018
|
});
|
|
2071
2019
|
}
|
|
2072
2020
|
/**
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2021
|
+
* Rank all eligible phase candidates by priority.
|
|
2022
|
+
*
|
|
2023
|
+
* Filters to pending phases, computes effective staleness, and sorts by
|
|
2024
|
+
* band (ascending: critic first) then effective staleness (descending).
|
|
2025
|
+
*
|
|
2026
|
+
* Used by selectPhaseCandidate (returns first) and the queue route (returns all).
|
|
2075
2027
|
*/
|
|
2076
|
-
function
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2028
|
+
function rankPhaseCandidates(metas, depthWeight) {
|
|
2029
|
+
// Filter to metas with a pending (scheduler-eligible) phase
|
|
2030
|
+
const eligible = metas.filter((m) => {
|
|
2031
|
+
if (m.locked)
|
|
2032
|
+
return false;
|
|
2033
|
+
if (m.disabled && !m.isOverride)
|
|
2034
|
+
return false;
|
|
2035
|
+
const owed = getOwedPhase(m.phaseState);
|
|
2036
|
+
if (!owed)
|
|
2037
|
+
return false;
|
|
2038
|
+
return m.phaseState[owed] === 'pending';
|
|
2080
2039
|
});
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2040
|
+
if (eligible.length === 0)
|
|
2041
|
+
return [];
|
|
2042
|
+
// Compute effective staleness for tiebreaking
|
|
2043
|
+
const withStaleness = computeEffectiveStaleness(eligible.map((m) => ({
|
|
2044
|
+
node: m.node,
|
|
2045
|
+
meta: m.meta,
|
|
2046
|
+
actualStaleness: m.actualStaleness,
|
|
2047
|
+
})), depthWeight);
|
|
2048
|
+
// Build candidates with band info
|
|
2049
|
+
const candidates = withStaleness.map((ws, i) => {
|
|
2050
|
+
const m = eligible[i];
|
|
2051
|
+
const owedPhase = getOwedPhase(m.phaseState);
|
|
2052
|
+
return {
|
|
2053
|
+
node: ws.node,
|
|
2054
|
+
meta: ws.meta,
|
|
2055
|
+
phaseState: m.phaseState,
|
|
2056
|
+
owedPhase,
|
|
2057
|
+
band: getPriorityBand(m.phaseState),
|
|
2058
|
+
actualStaleness: ws.actualStaleness,
|
|
2059
|
+
effectiveStaleness: ws.effectiveStaleness,
|
|
2060
|
+
};
|
|
2091
2061
|
});
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
*/
|
|
2098
|
-
function retryPhase(state, phase) {
|
|
2099
|
-
if (state[phase] !== 'failed')
|
|
2100
|
-
return state;
|
|
2101
|
-
return enforceInvariant({
|
|
2102
|
-
...state,
|
|
2103
|
-
[phase]: 'pending',
|
|
2062
|
+
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
2063
|
+
candidates.sort((a, b) => {
|
|
2064
|
+
if (a.band !== b.band)
|
|
2065
|
+
return a.band - b.band;
|
|
2066
|
+
return b.effectiveStaleness - a.effectiveStaleness;
|
|
2104
2067
|
});
|
|
2068
|
+
return candidates;
|
|
2105
2069
|
}
|
|
2106
2070
|
/**
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
for (const phase of ['architect', 'builder', 'critic']) {
|
|
2113
|
-
if (result[phase] === 'failed') {
|
|
2114
|
-
result = retryPhase(result, phase);
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
return result;
|
|
2118
|
-
}
|
|
2119
|
-
// ── Running transition ─────────────────────────────────────────────────
|
|
2120
|
-
/**
|
|
2121
|
-
* Mark a phase as running (scheduler picks it).
|
|
2071
|
+
* Select the best phase candidate across the corpus.
|
|
2072
|
+
*
|
|
2073
|
+
* @param metas - Array of (node, meta, phaseState, stalenessSeconds) tuples.
|
|
2074
|
+
* @param depthWeight - Config depthWeight for staleness tiebreak.
|
|
2075
|
+
* @returns The winning candidate, or null if no phase is ready.
|
|
2122
2076
|
*/
|
|
2123
|
-
function
|
|
2124
|
-
return
|
|
2125
|
-
...state,
|
|
2126
|
-
[phase]: 'running',
|
|
2127
|
-
};
|
|
2077
|
+
function selectPhaseCandidate(metas, depthWeight) {
|
|
2078
|
+
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
2128
2079
|
}
|
|
2129
|
-
// ── Query helpers ──────────────────────────────────────────────────────
|
|
2130
2080
|
/**
|
|
2131
|
-
*
|
|
2081
|
+
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
2082
|
+
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
2132
2083
|
*/
|
|
2133
|
-
function
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
return null;
|
|
2084
|
+
function selectAllTier2Candidates(metas) {
|
|
2085
|
+
return metas
|
|
2086
|
+
.filter((m) => !m.locked && !m.disabled && isFullyFresh(m.phaseState))
|
|
2087
|
+
.sort((a, b) => b.actualStaleness - a.actualStaleness)
|
|
2088
|
+
.map((m) => ({ node: m.node, meta: m.meta }));
|
|
2139
2089
|
}
|
|
2090
|
+
|
|
2140
2091
|
/**
|
|
2141
|
-
*
|
|
2092
|
+
* Shared error utilities.
|
|
2093
|
+
*
|
|
2094
|
+
* @module errors
|
|
2142
2095
|
*/
|
|
2143
|
-
function isFullyFresh(state) {
|
|
2144
|
-
return (state.architect === 'fresh' &&
|
|
2145
|
-
state.builder === 'fresh' &&
|
|
2146
|
-
state.critic === 'fresh');
|
|
2147
|
-
}
|
|
2148
2096
|
/**
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2097
|
+
* Wrap an unknown caught value into a MetaError.
|
|
2098
|
+
*
|
|
2099
|
+
* @param step - Which synthesis step failed.
|
|
2100
|
+
* @param err - The caught error value.
|
|
2101
|
+
* @param code - Error classification code.
|
|
2102
|
+
* @returns A structured MetaError.
|
|
2151
2103
|
*/
|
|
2152
|
-
function
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
if (owed === 'builder')
|
|
2159
|
-
return 2;
|
|
2160
|
-
return 3;
|
|
2104
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
2105
|
+
return {
|
|
2106
|
+
step,
|
|
2107
|
+
code,
|
|
2108
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2109
|
+
};
|
|
2161
2110
|
}
|
|
2162
2111
|
|
|
2163
2112
|
/**
|
|
2164
|
-
*
|
|
2113
|
+
* Build the MetaContext for a synthesis cycle.
|
|
2165
2114
|
*
|
|
2166
|
-
*
|
|
2167
|
-
*
|
|
2168
|
-
* the architect-invalidating inputs.
|
|
2115
|
+
* Computes shared inputs once: scope files, delta files, child meta outputs,
|
|
2116
|
+
* previous content/feedback, steer, and archive paths.
|
|
2169
2117
|
*
|
|
2170
|
-
* @module
|
|
2118
|
+
* @module orchestrator/contextPackage
|
|
2171
2119
|
*/
|
|
2172
2120
|
/**
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
* If the meta already has _phaseState, returns it as-is.
|
|
2176
|
-
*
|
|
2177
|
-
* Otherwise, reconstructs from available fields:
|
|
2178
|
-
* - Never-synthesized meta (no _content, no _builder): all phases start pending/stale.
|
|
2179
|
-
* - Errored meta: the failed phase is mapped from _error.step.
|
|
2180
|
-
* - Mid-cycle meta with cached _builder but no _content: builder pending.
|
|
2181
|
-
* - Fully-fresh meta: all phases fresh.
|
|
2182
|
-
* - Meta with stale architect inputs: architect pending, downstream stale.
|
|
2121
|
+
* Condense a file list into glob-like summaries.
|
|
2122
|
+
* Groups by directory + extension pattern.
|
|
2183
2123
|
*
|
|
2184
|
-
* @param
|
|
2185
|
-
* @param
|
|
2186
|
-
*
|
|
2187
|
-
* @returns The derived PhaseState.
|
|
2124
|
+
* @param files - Array of file paths.
|
|
2125
|
+
* @param maxIndividual - Show individual files up to this count.
|
|
2126
|
+
* @returns Condensed summary string.
|
|
2188
2127
|
*/
|
|
2189
|
-
function
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
const
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
if (failedPhase === 'architect') {
|
|
2200
|
-
if (!meta._builder) {
|
|
2201
|
-
state.builder = 'stale';
|
|
2202
|
-
state.critic = 'stale';
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
// If builder failed, critic is stale
|
|
2206
|
-
if (failedPhase === 'builder') {
|
|
2207
|
-
state.critic = 'stale';
|
|
2208
|
-
}
|
|
2209
|
-
return state;
|
|
2210
|
-
}
|
|
2211
|
-
// Never synthesized: no _content AND no _builder (and no error)
|
|
2212
|
-
if (!meta._content && !meta._builder) {
|
|
2213
|
-
return initialPhaseState();
|
|
2128
|
+
function condenseScopeFiles(files, maxIndividual = 30) {
|
|
2129
|
+
if (files.length <= maxIndividual)
|
|
2130
|
+
return files.join('\n');
|
|
2131
|
+
// Group by dir + extension
|
|
2132
|
+
const groups = new Map();
|
|
2133
|
+
for (const f of files) {
|
|
2134
|
+
const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
|
|
2135
|
+
const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
|
|
2136
|
+
const key = dir + '*' + ext;
|
|
2137
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
2214
2138
|
}
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2139
|
+
// Sort by count descending
|
|
2140
|
+
const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
|
|
2141
|
+
return sorted
|
|
2142
|
+
.map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
|
|
2143
|
+
.join('\n');
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Read a meta.json file and extract `_content` and `_generatedAt`.
|
|
2147
|
+
*
|
|
2148
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
2149
|
+
* @returns Parsed fields, or defaults if missing/unreadable.
|
|
2150
|
+
*/
|
|
2151
|
+
async function readChildMeta(metaJsonPath) {
|
|
2152
|
+
try {
|
|
2153
|
+
const raw = await readFile(metaJsonPath, 'utf8');
|
|
2154
|
+
const meta = JSON.parse(raw);
|
|
2217
2155
|
return {
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
critic: 'stale',
|
|
2156
|
+
content: meta._content ?? null,
|
|
2157
|
+
generatedAt: meta._generatedAt,
|
|
2221
2158
|
};
|
|
2222
2159
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
return {
|
|
2226
|
-
architect: 'fresh',
|
|
2227
|
-
builder: 'fresh',
|
|
2228
|
-
critic: 'pending',
|
|
2229
|
-
};
|
|
2160
|
+
catch {
|
|
2161
|
+
return { content: null, generatedAt: undefined };
|
|
2230
2162
|
}
|
|
2231
|
-
// Default: fully fresh
|
|
2232
|
-
return freshPhaseState();
|
|
2233
2163
|
}
|
|
2234
|
-
|
|
2235
|
-
/**
|
|
2236
|
-
* Compute a structure hash from a sorted file listing.
|
|
2237
|
-
*
|
|
2238
|
-
* Used to detect when directory structure changes, triggering
|
|
2239
|
-
* an architect re-run.
|
|
2240
|
-
*
|
|
2241
|
-
* @module structureHash
|
|
2242
|
-
*/
|
|
2243
2164
|
/**
|
|
2244
|
-
*
|
|
2165
|
+
* Read a meta.json file and extract its `_content` field.
|
|
2245
2166
|
*
|
|
2246
|
-
* @param
|
|
2247
|
-
* @returns
|
|
2167
|
+
* @param metaJsonPath - Absolute path to a meta.json file.
|
|
2168
|
+
* @returns The `_content` string, or null if missing/unreadable.
|
|
2248
2169
|
*/
|
|
2249
|
-
function
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
return createHash('sha256').update(content).digest('hex');
|
|
2170
|
+
async function readMetaContent(metaJsonPath) {
|
|
2171
|
+
const info = await readChildMeta(metaJsonPath);
|
|
2172
|
+
return info.content;
|
|
2253
2173
|
}
|
|
2254
|
-
|
|
2255
2174
|
/**
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
2258
|
-
* Computes architect-invalidating and builder-invalidating inputs for a meta,
|
|
2259
|
-
* then applies the cascade to update _phaseState.
|
|
2175
|
+
* Build the context package for a synthesis cycle.
|
|
2260
2176
|
*
|
|
2261
|
-
* @
|
|
2177
|
+
* @param node - The meta node being synthesized.
|
|
2178
|
+
* @param meta - Current meta.json content.
|
|
2179
|
+
* @param watcher - WatcherClient for scope enumeration.
|
|
2180
|
+
* @returns The computed context package.
|
|
2262
2181
|
*/
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
let
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
|
|
2293
|
-
// Prompt staleness detection: compare persisted prompt snapshots against
|
|
2294
|
-
// currently-resolved prompts. This is INFORMATIONAL ONLY — reported via
|
|
2295
|
-
// inputStatus so /preview can surface it, but it must NEVER feed into
|
|
2296
|
-
// the invalidation cascade. When a meta naturally reaches architectEvery
|
|
2297
|
-
// through real builder cycles, architect runs with the current prompt and
|
|
2298
|
-
// the snapshot updates. Coupling prompt changes to invalidation causes a
|
|
2299
|
-
// corpus-wide synthesis storm (see #163).
|
|
2300
|
-
const architectChanged = isPromptStale(meta._architect, DEFAULT_ARCHITECT_PROMPT);
|
|
2301
|
-
const criticChanged = isPromptStale(meta._critic, DEFAULT_CRITIC_PROMPT);
|
|
2302
|
-
const effectiveSynthesisCount = meta._synthesisCount ?? 0;
|
|
2303
|
-
// _crossRefs declaration change
|
|
2304
|
-
const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
|
|
2305
|
-
const archiveRefs = (latestArchive?._crossRefs ?? [])
|
|
2306
|
-
.slice()
|
|
2307
|
-
.sort()
|
|
2308
|
-
.join(',');
|
|
2309
|
-
const crossRefsDeclChanged = latestArchive
|
|
2310
|
-
? currentRefs !== archiveRefs
|
|
2311
|
-
: currentRefs.length > 0;
|
|
2312
|
-
const architectInvalidators = [];
|
|
2313
|
-
if (structureChanged) {
|
|
2314
|
-
if (meta._state !== undefined) {
|
|
2315
|
-
// Progressive entity: new files → builder only (cursor handles incremental)
|
|
2316
|
-
phaseState = invalidateBuilder(phaseState);
|
|
2182
|
+
async function buildContextPackage(node, meta, watcher, logger) {
|
|
2183
|
+
// Scope and delta files via watcher walk
|
|
2184
|
+
const scopeStart = Date.now();
|
|
2185
|
+
const { scopeFiles } = await getScopeFiles(node, watcher, logger);
|
|
2186
|
+
const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
|
|
2187
|
+
logger?.debug({
|
|
2188
|
+
scopeFiles: scopeFiles.length,
|
|
2189
|
+
deltaFiles: deltaFiles.length,
|
|
2190
|
+
durationMs: Date.now() - scopeStart,
|
|
2191
|
+
}, 'scope and delta files computed');
|
|
2192
|
+
// Child meta outputs (parallel reads, delta-aware).
|
|
2193
|
+
// Only include full _content for children synthesized AFTER the parent's
|
|
2194
|
+
// _generatedAt. Non-delta children are already folded into the parent's
|
|
2195
|
+
// previous _content and are listed as path-only references (#169).
|
|
2196
|
+
const parentGeneratedAt = meta._generatedAt;
|
|
2197
|
+
const childMetas = {};
|
|
2198
|
+
const childEntries = await Promise.all(node.children.map(async (child) => {
|
|
2199
|
+
const info = await readChildMeta(join(child.metaPath, 'meta.json'));
|
|
2200
|
+
return [child.ownerPath, info];
|
|
2201
|
+
}));
|
|
2202
|
+
let deltaChildCount = 0;
|
|
2203
|
+
for (const [path, info] of childEntries) {
|
|
2204
|
+
// Delta child: no parent timestamp (first run), or child synthesized
|
|
2205
|
+
// after parent. Include full content.
|
|
2206
|
+
if (!parentGeneratedAt ||
|
|
2207
|
+
!info.generatedAt ||
|
|
2208
|
+
info.generatedAt > parentGeneratedAt) {
|
|
2209
|
+
childMetas[path] = info.content ?? undefined;
|
|
2210
|
+
deltaChildCount++;
|
|
2317
2211
|
}
|
|
2318
2212
|
else {
|
|
2319
|
-
|
|
2213
|
+
// Non-delta: already incorporated in previous synthesis.
|
|
2214
|
+
// Include path with null content so the builder knows it exists.
|
|
2215
|
+
childMetas[path] = null;
|
|
2320
2216
|
}
|
|
2321
2217
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2218
|
+
logger?.debug({
|
|
2219
|
+
totalChildren: childEntries.length,
|
|
2220
|
+
deltaChildren: deltaChildCount,
|
|
2221
|
+
}, 'child metas filtered by delta');
|
|
2222
|
+
// Cross-referenced meta outputs (parallel reads)
|
|
2223
|
+
const crossRefMetas = {};
|
|
2224
|
+
const seen = new Set();
|
|
2225
|
+
const crossRefPaths = [];
|
|
2226
|
+
for (const refPath of meta._crossRefs ?? []) {
|
|
2227
|
+
if (refPath === node.ownerPath || refPath === node.metaPath)
|
|
2228
|
+
continue;
|
|
2229
|
+
if (seen.has(refPath))
|
|
2230
|
+
continue;
|
|
2231
|
+
seen.add(refPath);
|
|
2232
|
+
crossRefPaths.push(refPath);
|
|
2328
2233
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2234
|
+
const crossRefEntries = await Promise.all(crossRefPaths.map(async (refPath) => {
|
|
2235
|
+
const content = await readMetaContent(join(refPath, '.meta', 'meta.json'));
|
|
2236
|
+
return [refPath, content];
|
|
2237
|
+
}));
|
|
2238
|
+
for (const [path, content] of crossRefEntries) {
|
|
2239
|
+
crossRefMetas[path] = content;
|
|
2240
|
+
}
|
|
2241
|
+
// Archive paths
|
|
2242
|
+
const archives = listArchiveFiles(node.metaPath);
|
|
2243
|
+
// Nearest ancestor _builder output
|
|
2244
|
+
let ancestorBuilder;
|
|
2245
|
+
const ancestor = getAncestorMeta(node);
|
|
2246
|
+
if (ancestor) {
|
|
2247
|
+
try {
|
|
2248
|
+
const raw = await readFile(join(ancestor.metaPath, 'meta.json'), 'utf8');
|
|
2249
|
+
const ancestorMeta = JSON.parse(raw);
|
|
2250
|
+
if (ancestorMeta._builder) {
|
|
2251
|
+
ancestorBuilder = ancestorMeta._builder;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
// Ancestor meta.json unreadable — skip
|
|
2256
|
+
}
|
|
2333
2257
|
}
|
|
2334
|
-
// ── Builder-level inputs ──
|
|
2335
|
-
// Cross-ref _content change (builder-invalidating)
|
|
2336
|
-
let crossRefContentChanged = false;
|
|
2337
2258
|
return {
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2259
|
+
path: node.metaPath,
|
|
2260
|
+
scopeFiles,
|
|
2261
|
+
deltaFiles,
|
|
2262
|
+
childMetas,
|
|
2263
|
+
crossRefMetas,
|
|
2264
|
+
previousContent: meta._content ?? null,
|
|
2265
|
+
previousFeedback: meta._feedback ?? null,
|
|
2266
|
+
steer: meta._steer ?? null,
|
|
2267
|
+
previousState: meta._state ?? null,
|
|
2268
|
+
archives,
|
|
2269
|
+
ancestorBuilder,
|
|
2348
2270
|
};
|
|
2349
2271
|
}
|
|
2350
2272
|
|
|
2351
2273
|
/**
|
|
2352
|
-
*
|
|
2353
|
-
*
|
|
2354
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
2355
|
-
*
|
|
2356
|
-
* @module scheduling/weightedFormula
|
|
2357
|
-
*/
|
|
2358
|
-
/**
|
|
2359
|
-
* Compute effective staleness for a set of candidates.
|
|
2360
|
-
*
|
|
2361
|
-
* Normalizes depths so the minimum becomes 0, then applies the formula:
|
|
2362
|
-
* effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
|
|
2274
|
+
* Build task prompts for each synthesis step.
|
|
2363
2275
|
*
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
2276
|
+
* Prompts are compiled as Handlebars templates with access to config,
|
|
2277
|
+
* meta, and scope context. The architect can write template expressions
|
|
2278
|
+
* into its _builder output; these resolve when the builder task is compiled.
|
|
2366
2279
|
*
|
|
2367
|
-
* @
|
|
2368
|
-
* @param depthWeight - Exponent for depth weighting (0 = pure staleness).
|
|
2369
|
-
* @returns Same array with effectiveStaleness computed.
|
|
2280
|
+
* @module orchestrator/buildTask
|
|
2370
2281
|
*/
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
|
|
2385
|
-
};
|
|
2386
|
-
});
|
|
2282
|
+
Handlebars.registerHelper('gt', (a, b) => a > b);
|
|
2283
|
+
/** Build the template context from synthesis inputs. */
|
|
2284
|
+
function buildTemplateContext(ctx, meta, config) {
|
|
2285
|
+
return {
|
|
2286
|
+
config,
|
|
2287
|
+
meta,
|
|
2288
|
+
scope: {
|
|
2289
|
+
fileCount: ctx.scopeFiles.length,
|
|
2290
|
+
deltaCount: ctx.deltaFiles.length,
|
|
2291
|
+
childCount: Object.keys(ctx.childMetas).length,
|
|
2292
|
+
crossRefCount: Object.keys(ctx.crossRefMetas).length,
|
|
2293
|
+
},
|
|
2294
|
+
};
|
|
2387
2295
|
}
|
|
2388
|
-
|
|
2389
2296
|
/**
|
|
2390
|
-
*
|
|
2391
|
-
*
|
|
2392
|
-
* Selects the highest-priority ready phase across all metas.
|
|
2393
|
-
* Priority: critic (band 1) \> builder (band 2) \> architect (band 3).
|
|
2394
|
-
* Tiebreak within band: weighted staleness (§3.9).
|
|
2395
|
-
*
|
|
2396
|
-
* @module phaseState/phaseScheduler
|
|
2297
|
+
* Compile a string as a Handlebars template with the given context.
|
|
2298
|
+
* Returns the original string unchanged if compilation fails.
|
|
2397
2299
|
*/
|
|
2300
|
+
function compileTemplate(text, context) {
|
|
2301
|
+
try {
|
|
2302
|
+
return Handlebars.compile(text, { noEscape: true })(context);
|
|
2303
|
+
}
|
|
2304
|
+
catch {
|
|
2305
|
+
return text;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2398
2308
|
/**
|
|
2399
|
-
*
|
|
2309
|
+
* Append a keyed record of meta outputs as subsections, if non-empty.
|
|
2400
2310
|
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2311
|
+
* @param condenseMissing - When true, null entries are condensed into a
|
|
2312
|
+
* path listing (delta-aware child metas). When false, null entries get
|
|
2313
|
+
* individual subsections with "(not yet synthesized)" (cross-refs).
|
|
2404
2314
|
*/
|
|
2405
|
-
function
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
|
|
2414
|
-
}
|
|
2315
|
+
function appendMetaSections(sections, heading, metas, condenseMissing = false) {
|
|
2316
|
+
if (Object.keys(metas).length === 0)
|
|
2317
|
+
return;
|
|
2318
|
+
if (!condenseMissing) {
|
|
2319
|
+
// Original behavior: every entry gets a subsection.
|
|
2320
|
+
sections.push('', heading);
|
|
2321
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
2322
|
+
sections.push(`### ${path}`, typeof content === 'string' ? content : '(not yet synthesized)');
|
|
2415
2323
|
}
|
|
2416
|
-
return
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
// Delta-aware: separate entries into three categories (#169).
|
|
2327
|
+
// - string content → full subsection (delta child with content)
|
|
2328
|
+
// - null → non-delta, already folded into previous synthesis
|
|
2329
|
+
// - undefined → delta child not yet synthesized
|
|
2330
|
+
const withContent = [];
|
|
2331
|
+
const unchanged = [];
|
|
2332
|
+
const notYetSynthesized = [];
|
|
2333
|
+
for (const [path, content] of Object.entries(metas)) {
|
|
2334
|
+
if (typeof content === 'string') {
|
|
2335
|
+
withContent.push([path, content]);
|
|
2336
|
+
}
|
|
2337
|
+
else if (content === null) {
|
|
2338
|
+
unchanged.push(path);
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
notYetSynthesized.push(path);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
sections.push('', heading);
|
|
2345
|
+
for (const [path, content] of withContent) {
|
|
2346
|
+
sections.push(`### ${path}`, content);
|
|
2347
|
+
}
|
|
2348
|
+
if (notYetSynthesized.length > 0) {
|
|
2349
|
+
sections.push('', `### Not yet synthesized (${notYetSynthesized.length.toString()})`, ...notYetSynthesized.map((p) => `- ${p}`));
|
|
2350
|
+
}
|
|
2351
|
+
if (unchanged.length > 0) {
|
|
2352
|
+
sections.push('', `### Other children (${unchanged.length.toString()} \u2014 unchanged since last synthesis)`, ...unchanged.map((p) => `- ${p}`));
|
|
2353
|
+
}
|
|
2425
2354
|
}
|
|
2426
|
-
/**
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
}
|
|
2446
|
-
if (
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
return {
|
|
2459
|
-
node: ws.node,
|
|
2460
|
-
meta: ws.meta,
|
|
2461
|
-
phaseState: m.phaseState,
|
|
2462
|
-
owedPhase,
|
|
2463
|
-
band: getPriorityBand(m.phaseState),
|
|
2464
|
-
actualStaleness: ws.actualStaleness,
|
|
2465
|
-
effectiveStaleness: ws.effectiveStaleness,
|
|
2466
|
-
};
|
|
2467
|
-
});
|
|
2468
|
-
// Sort by band (ascending = critic first) then effective staleness (descending)
|
|
2469
|
-
candidates.sort((a, b) => {
|
|
2470
|
-
if (a.band !== b.band)
|
|
2471
|
-
return a.band - b.band;
|
|
2472
|
-
return b.effectiveStaleness - a.effectiveStaleness;
|
|
2473
|
-
});
|
|
2474
|
-
return candidates;
|
|
2355
|
+
/** Inject nearest ancestor's organizational context, if available. */
|
|
2356
|
+
function appendAncestorContext(sections, ctx) {
|
|
2357
|
+
if (ctx.ancestorBuilder) {
|
|
2358
|
+
sections.push('', '## PARENT ORGANIZATIONAL CONTEXT', ctx.ancestorBuilder);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
/** Append optional context sections shared across all step prompts. */
|
|
2362
|
+
function appendSharedSections(sections, ctx, options) {
|
|
2363
|
+
const opts = {
|
|
2364
|
+
includeSteer: true,
|
|
2365
|
+
includePreviousContent: true,
|
|
2366
|
+
includePreviousFeedback: true,
|
|
2367
|
+
feedbackHeading: '## PREVIOUS FEEDBACK',
|
|
2368
|
+
includeChildMetas: true,
|
|
2369
|
+
includeCrossRefs: true,
|
|
2370
|
+
...options,
|
|
2371
|
+
};
|
|
2372
|
+
if (opts.includeSteer && ctx.steer) {
|
|
2373
|
+
sections.push('', '## STEERING PROMPT', ctx.steer);
|
|
2374
|
+
}
|
|
2375
|
+
if (opts.includePreviousContent && ctx.previousContent) {
|
|
2376
|
+
sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
|
|
2377
|
+
}
|
|
2378
|
+
if (opts.includePreviousFeedback && ctx.previousFeedback) {
|
|
2379
|
+
sections.push('', opts.feedbackHeading, ctx.previousFeedback);
|
|
2380
|
+
}
|
|
2381
|
+
if (opts.includeChildMetas) {
|
|
2382
|
+
appendMetaSections(sections, '## CHILD META OUTPUTS', ctx.childMetas, true);
|
|
2383
|
+
}
|
|
2384
|
+
if (opts.includeCrossRefs) {
|
|
2385
|
+
appendMetaSections(sections, '## CROSS-REFERENCED METAS', ctx.crossRefMetas);
|
|
2386
|
+
}
|
|
2475
2387
|
}
|
|
2476
2388
|
/**
|
|
2477
|
-
*
|
|
2389
|
+
* Build the architect task prompt.
|
|
2478
2390
|
*
|
|
2479
|
-
* @param
|
|
2480
|
-
* @param
|
|
2481
|
-
* @
|
|
2482
|
-
|
|
2483
|
-
function selectPhaseCandidate(metas, depthWeight) {
|
|
2484
|
-
return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
|
|
2485
|
-
}
|
|
2486
|
-
/**
|
|
2487
|
-
* Select all fully-fresh, non-disabled, non-locked metas sorted by staleness
|
|
2488
|
-
* (descending — stalest first) for Tier 2 invalidation scanning.
|
|
2391
|
+
* @param ctx - Synthesis context.
|
|
2392
|
+
* @param meta - Current meta.json.
|
|
2393
|
+
* @param config - Synthesis config.
|
|
2394
|
+
* @returns The architect task prompt string.
|
|
2489
2395
|
*/
|
|
2490
|
-
function
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2396
|
+
function buildArchitectTask(ctx, meta, config) {
|
|
2397
|
+
const sections = [
|
|
2398
|
+
`# jeeves-meta · ARCHITECT · ${ctx.path}`,
|
|
2399
|
+
'',
|
|
2400
|
+
getDefaultArchitectPrompt(),
|
|
2401
|
+
'',
|
|
2402
|
+
'## SCOPE',
|
|
2403
|
+
`Path: ${ctx.path}`,
|
|
2404
|
+
`Total files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
2405
|
+
`Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
|
|
2406
|
+
'',
|
|
2407
|
+
'### File listing (scope)',
|
|
2408
|
+
condenseScopeFiles(ctx.scopeFiles),
|
|
2409
|
+
];
|
|
2410
|
+
appendAncestorContext(sections, ctx);
|
|
2411
|
+
// Inject previous _builder so architect can see its own prior output
|
|
2412
|
+
if (meta._builder) {
|
|
2413
|
+
sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
|
|
2414
|
+
}
|
|
2415
|
+
appendSharedSections(sections, ctx);
|
|
2416
|
+
if (ctx.archives.length > 0) {
|
|
2417
|
+
sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
|
|
2418
|
+
}
|
|
2419
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2495
2420
|
}
|
|
2496
|
-
|
|
2497
2421
|
/**
|
|
2498
|
-
*
|
|
2422
|
+
* Build the builder task prompt.
|
|
2499
2423
|
*
|
|
2500
|
-
* @
|
|
2424
|
+
* @param ctx - Synthesis context.
|
|
2425
|
+
* @param meta - Current meta.json.
|
|
2426
|
+
* @param config - Synthesis config.
|
|
2427
|
+
* @returns The builder task prompt string.
|
|
2501
2428
|
*/
|
|
2429
|
+
function buildBuilderTask(ctx, meta, config) {
|
|
2430
|
+
const sections = [
|
|
2431
|
+
`# jeeves-meta · BUILDER · ${ctx.path}`,
|
|
2432
|
+
'',
|
|
2433
|
+
'## TASK BRIEF (from Architect)',
|
|
2434
|
+
meta._builder ?? '(No architect brief available)',
|
|
2435
|
+
'',
|
|
2436
|
+
'## SCOPE',
|
|
2437
|
+
`Path: ${ctx.path}`,
|
|
2438
|
+
`Delta files (${ctx.deltaFiles.length.toString()} changed):`,
|
|
2439
|
+
...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
|
|
2440
|
+
];
|
|
2441
|
+
appendAncestorContext(sections, ctx);
|
|
2442
|
+
if (ctx.previousState != null) {
|
|
2443
|
+
sections.push('', '## PREVIOUS STATE', 'The following opaque state was returned by the previous synthesis cycle.', 'Use it to continue progressive work. Update `_state` in your output to', 'reflect your progress.', '', '```json', JSON.stringify(ctx.previousState, null, 2), '```');
|
|
2444
|
+
}
|
|
2445
|
+
appendSharedSections(sections, ctx, {
|
|
2446
|
+
includeSteer: false,
|
|
2447
|
+
feedbackHeading: '## FEEDBACK FROM CRITIC',
|
|
2448
|
+
});
|
|
2449
|
+
sections.push('', '## OUTPUT FORMAT', '', 'Respond with ONLY a JSON object. No explanation, no markdown fences, no text before or after.', '', 'Required schema:', '{', ' "type": "object",', ' "required": ["_content"],', ' "properties": {', ' "_content": { "type": "string", "description": "Markdown narrative synthesis" },', ' "_state": { "description": "Opaque state object for progressive work across cycles" }', ' },', ' "additionalProperties": true', '}', '', 'Add any structured fields that capture important facts about this entity', '(e.g. status, risks, dependencies, metrics). Use descriptive key names without underscore prefix.', 'The _content field is the only required key — everything else is domain-driven.', '_state is optional: set it to carry state across synthesis cycles for progressive work.', '', 'DIAGRAMS: When diagrams would aid understanding, use PlantUML in fenced code blocks (```plantuml).', 'PlantUML is rendered natively by the serving infrastructure. NEVER use ASCII art diagrams.', '', 'IMPORTANT: Do not call sessions_spawn or sessions_yield. Read all files directly using the tools', 'available in your session. Do not attempt to parallelize work by spawning sub-agents.');
|
|
2450
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2451
|
+
}
|
|
2502
2452
|
/**
|
|
2503
|
-
*
|
|
2453
|
+
* Build the critic task prompt.
|
|
2504
2454
|
*
|
|
2505
|
-
* @param
|
|
2506
|
-
* @param
|
|
2507
|
-
* @param
|
|
2508
|
-
* @returns
|
|
2455
|
+
* @param ctx - Synthesis context.
|
|
2456
|
+
* @param meta - Current meta.json (with _content already set by builder).
|
|
2457
|
+
* @param config - Synthesis config.
|
|
2458
|
+
* @returns The critic task prompt string.
|
|
2509
2459
|
*/
|
|
2510
|
-
function
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2460
|
+
function buildCriticTask(ctx, meta, config) {
|
|
2461
|
+
const sections = [
|
|
2462
|
+
`# jeeves-meta · CRITIC · ${ctx.path}`,
|
|
2463
|
+
'',
|
|
2464
|
+
getDefaultCriticPrompt(),
|
|
2465
|
+
'',
|
|
2466
|
+
'## SYNTHESIS TO EVALUATE',
|
|
2467
|
+
meta._content ?? '(No content produced)',
|
|
2468
|
+
'',
|
|
2469
|
+
'## SCOPE',
|
|
2470
|
+
`Path: ${ctx.path}`,
|
|
2471
|
+
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
2472
|
+
];
|
|
2473
|
+
appendSharedSections(sections, ctx, {
|
|
2474
|
+
includePreviousContent: false,
|
|
2475
|
+
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
2476
|
+
includeChildMetas: false,
|
|
2477
|
+
includeCrossRefs: false,
|
|
2478
|
+
});
|
|
2479
|
+
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
2480
|
+
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
2516
2481
|
}
|
|
2517
2482
|
|
|
2518
2483
|
/**
|
|
@@ -2707,7 +2672,7 @@ async function runArchitect(node, currentMeta, phaseState, config, executor, wat
|
|
|
2707
2672
|
ps = architectSuccess(ps);
|
|
2708
2673
|
const architectUpdates = {
|
|
2709
2674
|
_builder: builderBrief,
|
|
2710
|
-
_architect:
|
|
2675
|
+
_architect: getDefaultArchitectPrompt(),
|
|
2711
2676
|
_synthesisCount: 0,
|
|
2712
2677
|
_architectTokens: architectTokens,
|
|
2713
2678
|
_generatedAt: new Date().toISOString(),
|
|
@@ -2860,7 +2825,7 @@ async function runCritic(node, currentMeta, phaseState, config, executor, watche
|
|
|
2860
2825
|
const cycleComplete = isFullyFresh(ps);
|
|
2861
2826
|
const updates = {
|
|
2862
2827
|
_feedback: feedback,
|
|
2863
|
-
_critic:
|
|
2828
|
+
_critic: getDefaultCriticPrompt(),
|
|
2864
2829
|
_criticTokens: criticTokens,
|
|
2865
2830
|
_error: undefined,
|
|
2866
2831
|
};
|
|
@@ -3055,128 +3020,118 @@ async function executePhase(node, currentMeta, phaseState, phase, config, execut
|
|
|
3055
3020
|
/**
|
|
3056
3021
|
* Progress reporting via OpenClaw gateway `/tools/invoke` → `message` tool.
|
|
3057
3022
|
*
|
|
3023
|
+
* Progress events are rendered using Handlebars templates. Owner-path links
|
|
3024
|
+
* are resolved via the jeeves-server resolve-path API, with graceful
|
|
3025
|
+
* fallback to raw filesystem paths when the server is unreachable.
|
|
3026
|
+
*
|
|
3058
3027
|
* @module progress
|
|
3059
3028
|
*/
|
|
3029
|
+
/** Compile raw template strings into Handlebars delegates. */
|
|
3030
|
+
function compileTemplates(raw) {
|
|
3031
|
+
const merged = {
|
|
3032
|
+
phaseStart: raw?.phaseStart ?? DEFAULT_TEMPLATE_STRINGS.phaseStart,
|
|
3033
|
+
phaseEnd: raw?.phaseEnd ?? DEFAULT_TEMPLATE_STRINGS.phaseEnd,
|
|
3034
|
+
phaseError: raw?.phaseError ?? DEFAULT_TEMPLATE_STRINGS.phaseError,
|
|
3035
|
+
};
|
|
3036
|
+
return {
|
|
3037
|
+
phaseStart: Handlebars.compile(merged.phaseStart),
|
|
3038
|
+
phaseEnd: Handlebars.compile(merged.phaseEnd),
|
|
3039
|
+
phaseError: Handlebars.compile(merged.phaseError),
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3060
3042
|
function formatNumber(n) {
|
|
3061
3043
|
return n.toLocaleString('en-US');
|
|
3062
3044
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
function
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
.
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
/**
|
|
3086
|
-
* Resolve a filesystem path to a jeeves-server browse path segment.
|
|
3087
|
-
*
|
|
3088
|
-
* - Windows: `j:/domains/foo` → `/j/domains/foo` (drive-letter transform).
|
|
3089
|
-
* - Linux with serverDriveRoots: `/opt/jeeves/content/slack` + roots
|
|
3090
|
-
* `{ content: "/opt/jeeves/content" }` → `/content/slack`.
|
|
3091
|
-
* - Linux without serverDriveRoots: returns the normalized path unchanged
|
|
3092
|
-
* (browse link will be invalid, but this is the pre-existing behavior).
|
|
3093
|
-
*/
|
|
3094
|
-
function resolveServerPath(path, serverDriveRoots) {
|
|
3095
|
-
const normalized = normalizePath(path);
|
|
3096
|
-
// Windows: drive letter present — use existing transform
|
|
3097
|
-
if (/^[A-Za-z]:/.test(normalized)) {
|
|
3098
|
-
return normalized.replace(/^([A-Za-z]):/, '/$1');
|
|
3099
|
-
}
|
|
3100
|
-
// Linux: attempt serverDriveRoots resolution (longest/most-specific root wins)
|
|
3101
|
-
if (serverDriveRoots) {
|
|
3102
|
-
const sorted = Object.entries(serverDriveRoots)
|
|
3103
|
-
.map(([label, root]) => [label, normalizePath(root).replace(/\/+$/, '')])
|
|
3104
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
3105
|
-
for (const [label, normalizedRoot] of sorted) {
|
|
3106
|
-
if (normalized === normalizedRoot ||
|
|
3107
|
-
normalized.startsWith(normalizedRoot + '/')) {
|
|
3108
|
-
const relative = normalized
|
|
3109
|
-
.slice(normalizedRoot.length)
|
|
3110
|
-
.replace(/^\//, '');
|
|
3111
|
-
return `/${label}${relative ? '/' + relative : ''}`;
|
|
3112
|
-
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Resolve a filesystem path to a browse URL via the jeeves-server
|
|
3047
|
+
* resolve-path API. Returns the raw path as fallback on any failure.
|
|
3048
|
+
*
|
|
3049
|
+
* On HTTP 200, uses publicUrl if present, otherwise prefixes serverUrl
|
|
3050
|
+
* to browseUrl. On any error or non-200 response, returns fsPath as-is.
|
|
3051
|
+
*/
|
|
3052
|
+
/** Timeout for resolve-path API calls (ms). */
|
|
3053
|
+
const RESOLVE_PATH_TIMEOUT_MS = 3000;
|
|
3054
|
+
async function resolveLink(fsPath, serverUrl) {
|
|
3055
|
+
try {
|
|
3056
|
+
const url = new URL('/api/resolve-path', serverUrl);
|
|
3057
|
+
url.searchParams.set('fsPath', fsPath);
|
|
3058
|
+
const res = await fetchWithTimeout(url.toString(), RESOLVE_PATH_TIMEOUT_MS);
|
|
3059
|
+
if (!res.ok)
|
|
3060
|
+
return fsPath;
|
|
3061
|
+
const data = (await res.json());
|
|
3062
|
+
if (data.publicUrl)
|
|
3063
|
+
return data.publicUrl;
|
|
3064
|
+
if (data.browseUrl) {
|
|
3065
|
+
const base = serverUrl.replace(/\/+$/, '');
|
|
3066
|
+
return base + data.browseUrl;
|
|
3113
3067
|
}
|
|
3068
|
+
return fsPath;
|
|
3069
|
+
}
|
|
3070
|
+
catch {
|
|
3071
|
+
return fsPath;
|
|
3114
3072
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
const
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
const
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3073
|
+
}
|
|
3074
|
+
/** Resolve browse links for both the owner directory and its meta.json. */
|
|
3075
|
+
async function resolveLinks(ownerPath, serverUrl) {
|
|
3076
|
+
const dirLink = await resolveLink(ownerPath, serverUrl);
|
|
3077
|
+
// Derive metaLink by appending /.meta/meta.json to the resolved dir link
|
|
3078
|
+
const metaLink = dirLink.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3079
|
+
return { dirLink, metaLink };
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Render the appropriate template for a progress event.
|
|
3083
|
+
*
|
|
3084
|
+
* @param event - The progress event to render.
|
|
3085
|
+
* @param templates - Compiled Handlebars templates.
|
|
3086
|
+
* @param serverUrl - jeeves-server URL for resolve-path API.
|
|
3087
|
+
* @returns Rendered message string.
|
|
3088
|
+
*/
|
|
3089
|
+
async function renderProgressEvent(event, templates, serverUrl) {
|
|
3090
|
+
const ownerPath = event.path;
|
|
3091
|
+
const metaPath = ownerPath.replace(/\/+$/, '') + '/.meta/meta.json';
|
|
3092
|
+
const phase = (event.phase ?? 'unknown').toUpperCase();
|
|
3093
|
+
const { dirLink, metaLink } = await resolveLinks(ownerPath, serverUrl);
|
|
3094
|
+
const base = {
|
|
3095
|
+
dirPath: ownerPath,
|
|
3096
|
+
metaPath,
|
|
3097
|
+
dirLink,
|
|
3098
|
+
metaLink,
|
|
3099
|
+
phase,
|
|
3100
|
+
};
|
|
3138
3101
|
switch (event.type) {
|
|
3139
|
-
case '
|
|
3140
|
-
|
|
3141
|
-
return `🔬 Started meta synthesis: ${dirLink}`;
|
|
3142
|
-
}
|
|
3143
|
-
case 'phase_start': {
|
|
3144
|
-
if (!event.phase) {
|
|
3145
|
-
return ' ⚙️ Phase started';
|
|
3146
|
-
}
|
|
3147
|
-
return ` ⚙️ ${titleCasePhase(event.phase)} phase started`;
|
|
3148
|
-
}
|
|
3102
|
+
case 'phase_start':
|
|
3103
|
+
return templates.phaseStart(base);
|
|
3149
3104
|
case 'phase_complete': {
|
|
3150
|
-
const
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
case 'synthesis_complete': {
|
|
3156
|
-
const metaLink = buildMetaJsonLink(event.path, serverBaseUrl, serverDriveRoots);
|
|
3157
|
-
const tokenStr = formatTokens(event.tokens);
|
|
3158
|
-
const duration = event.durationMs !== undefined
|
|
3159
|
-
? formatSeconds(event.durationMs)
|
|
3160
|
-
: '0.0s';
|
|
3161
|
-
return `✅ Completed: ${metaLink} (${tokenStr} / ${duration})`;
|
|
3105
|
+
const seconds = event.durationMs !== undefined
|
|
3106
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3107
|
+
: '0';
|
|
3108
|
+
const tokens = event.tokens !== undefined ? formatNumber(event.tokens) : 'unknown';
|
|
3109
|
+
return templates.phaseEnd({ ...base, seconds, tokens });
|
|
3162
3110
|
}
|
|
3163
3111
|
case 'error': {
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3112
|
+
const seconds = event.durationMs !== undefined
|
|
3113
|
+
? String(Math.round(event.durationMs / 1000))
|
|
3114
|
+
: '0';
|
|
3115
|
+
const errorMsg = event.error ?? 'Unknown error';
|
|
3116
|
+
return templates.phaseError({ ...base, seconds, error: errorMsg });
|
|
3168
3117
|
}
|
|
3169
|
-
default:
|
|
3118
|
+
default:
|
|
3170
3119
|
return 'Unknown progress event';
|
|
3171
|
-
}
|
|
3172
3120
|
}
|
|
3173
3121
|
}
|
|
3174
3122
|
class ProgressReporter {
|
|
3175
3123
|
config;
|
|
3176
3124
|
logger;
|
|
3125
|
+
templates;
|
|
3126
|
+
/** Snapshot of template source strings used to compile `this.templates`. */
|
|
3127
|
+
lastTemplateSource;
|
|
3128
|
+
serverUrl;
|
|
3177
3129
|
constructor(config, logger) {
|
|
3178
3130
|
this.config = config;
|
|
3179
3131
|
this.logger = logger;
|
|
3132
|
+
this.templates = compileTemplates(config.templates);
|
|
3133
|
+
this.lastTemplateSource = JSON.stringify(config.templates);
|
|
3134
|
+
this.serverUrl = config.serverUrl ?? 'http://127.0.0.1:1934';
|
|
3180
3135
|
}
|
|
3181
3136
|
async report(event) {
|
|
3182
3137
|
// Multi-channel mode: reportTarget is the destination, reportChannel is the platform.
|
|
@@ -3184,7 +3139,27 @@ class ProgressReporter {
|
|
|
3184
3139
|
const target = this.config.reportTarget ?? this.config.reportChannel;
|
|
3185
3140
|
if (!target)
|
|
3186
3141
|
return;
|
|
3187
|
-
|
|
3142
|
+
// Detect template hot-reload: config.templates may be mutated by
|
|
3143
|
+
// applyHotReloadedConfig; recompile when the source strings change.
|
|
3144
|
+
const currentSource = JSON.stringify(this.config.templates);
|
|
3145
|
+
if (currentSource !== this.lastTemplateSource) {
|
|
3146
|
+
try {
|
|
3147
|
+
this.templates = compileTemplates(this.config.templates);
|
|
3148
|
+
this.lastTemplateSource = currentSource;
|
|
3149
|
+
this.logger.info('Progress templates recompiled (hot-reload)');
|
|
3150
|
+
}
|
|
3151
|
+
catch (err) {
|
|
3152
|
+
this.logger.warn({ err }, 'Failed to compile hot-reloaded templates; keeping previous templates');
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
let message;
|
|
3156
|
+
try {
|
|
3157
|
+
message = await renderProgressEvent(event, this.templates, this.serverUrl);
|
|
3158
|
+
}
|
|
3159
|
+
catch (err) {
|
|
3160
|
+
this.logger.warn({ err }, 'Progress event rendering failed');
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3188
3163
|
const url = new URL('/tools/invoke', this.config.gatewayUrl);
|
|
3189
3164
|
const args = {
|
|
3190
3165
|
action: 'send',
|
|
@@ -4144,6 +4119,100 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4144
4119
|
});
|
|
4145
4120
|
}
|
|
4146
4121
|
|
|
4122
|
+
/**
|
|
4123
|
+
* Archive reading helpers — watcher scan with filesystem fallback.
|
|
4124
|
+
*
|
|
4125
|
+
* Used by the GET /metas/:path route to retrieve archive history.
|
|
4126
|
+
* Prefers watcher scan for performance; falls back to disk reads
|
|
4127
|
+
* when the watcher is unavailable.
|
|
4128
|
+
*
|
|
4129
|
+
* @module archive/readArchive
|
|
4130
|
+
*/
|
|
4131
|
+
/** Build a Qdrant filter for archive points under a given meta path. */
|
|
4132
|
+
function buildArchiveScanFilter(metaPath, metaArchiveProperty) {
|
|
4133
|
+
const must = [
|
|
4134
|
+
{
|
|
4135
|
+
key: 'file_path',
|
|
4136
|
+
match: { text: normalizePath(join(metaPath, 'archive')) },
|
|
4137
|
+
},
|
|
4138
|
+
];
|
|
4139
|
+
for (const [key, value] of Object.entries(metaArchiveProperty)) {
|
|
4140
|
+
must.push({ key, match: { value } });
|
|
4141
|
+
}
|
|
4142
|
+
return { must };
|
|
4143
|
+
}
|
|
4144
|
+
/** Extract file_path from a scan point payload. */
|
|
4145
|
+
function getArchiveFilePath(point) {
|
|
4146
|
+
const value = point.payload?.file_path;
|
|
4147
|
+
return typeof value === 'string' ? value : '';
|
|
4148
|
+
}
|
|
4149
|
+
/** Project archive metadata, stripping indexing-internal fields. */
|
|
4150
|
+
function projectArchivePayload(point, projectMeta) {
|
|
4151
|
+
const payload = point.payload ?? {};
|
|
4152
|
+
if (Object.keys(payload).length === 0)
|
|
4153
|
+
return null;
|
|
4154
|
+
const archiveMeta = { ...payload };
|
|
4155
|
+
delete archiveMeta.file_path;
|
|
4156
|
+
delete archiveMeta.chunk_text;
|
|
4157
|
+
delete archiveMeta.chunk_index;
|
|
4158
|
+
delete archiveMeta.total_chunks;
|
|
4159
|
+
delete archiveMeta.content_hash;
|
|
4160
|
+
delete archiveMeta.matched_rules;
|
|
4161
|
+
return projectMeta(archiveMeta);
|
|
4162
|
+
}
|
|
4163
|
+
/**
|
|
4164
|
+
* Read archive history via watcher scan.
|
|
4165
|
+
*
|
|
4166
|
+
* @param watcher - WatcherClient with scan support.
|
|
4167
|
+
* @param metaPath - Absolute path to the .meta/ directory.
|
|
4168
|
+
* @param metaArchiveProperty - Additional filter properties for archive points.
|
|
4169
|
+
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
4170
|
+
* @param projectMeta - Field projection function.
|
|
4171
|
+
* @returns Array of projected archive entries, newest first.
|
|
4172
|
+
*/
|
|
4173
|
+
async function readArchiveFromWatcher(watcher, metaPath, metaArchiveProperty, limit, projectMeta) {
|
|
4174
|
+
if (limit === 0)
|
|
4175
|
+
return [];
|
|
4176
|
+
if (!watcher.scan) {
|
|
4177
|
+
throw new Error('Watcher scan not available');
|
|
4178
|
+
}
|
|
4179
|
+
const points = [];
|
|
4180
|
+
let cursor;
|
|
4181
|
+
do {
|
|
4182
|
+
const result = await watcher.scan({
|
|
4183
|
+
filter: buildArchiveScanFilter(metaPath, metaArchiveProperty),
|
|
4184
|
+
limit: 100,
|
|
4185
|
+
cursor,
|
|
4186
|
+
});
|
|
4187
|
+
points.push(...result.points);
|
|
4188
|
+
cursor = result.cursor ?? undefined;
|
|
4189
|
+
} while (cursor);
|
|
4190
|
+
const sorted = points.sort((a, b) => getArchiveFilePath(a).localeCompare(getArchiveFilePath(b)));
|
|
4191
|
+
return sorted
|
|
4192
|
+
.slice(limit ? -limit : 0)
|
|
4193
|
+
.reverse()
|
|
4194
|
+
.map((point) => projectArchivePayload(point, projectMeta))
|
|
4195
|
+
.filter((value) => value !== null);
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* Read archive history from disk (fallback).
|
|
4199
|
+
*
|
|
4200
|
+
* @param metaPath - Absolute path to the .meta/ directory.
|
|
4201
|
+
* @param limit - Max entries to return (newest first). Undefined = all.
|
|
4202
|
+
* @param projectMeta - Field projection function.
|
|
4203
|
+
* @returns Array of projected archive entries, newest first.
|
|
4204
|
+
*/
|
|
4205
|
+
async function readArchiveFromDisk(metaPath, limit, projectMeta) {
|
|
4206
|
+
if (limit === 0)
|
|
4207
|
+
return [];
|
|
4208
|
+
const archiveFiles = listArchiveFiles(metaPath);
|
|
4209
|
+
const selected = (limit ? archiveFiles.slice(-limit) : archiveFiles).reverse();
|
|
4210
|
+
return Promise.all(selected.map(async (archiveFile) => {
|
|
4211
|
+
const raw = await readFile(archiveFile, 'utf8');
|
|
4212
|
+
return projectMeta(JSON.parse(raw));
|
|
4213
|
+
}));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4147
4216
|
/**
|
|
4148
4217
|
* GET /metas — list metas with optional filters.
|
|
4149
4218
|
* GET /metas/:path — single meta detail.
|
|
@@ -4762,6 +4831,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4762
4831
|
critic: emptyPhaseCounts(),
|
|
4763
4832
|
};
|
|
4764
4833
|
let nextPhase = null;
|
|
4834
|
+
let metaCounts = null;
|
|
4765
4835
|
try {
|
|
4766
4836
|
const metaResult = await cache.get(config, watcher);
|
|
4767
4837
|
// Count raw phase states (before retry) for display
|
|
@@ -4783,6 +4853,17 @@ function registerStatusRoute(app, deps) {
|
|
|
4783
4853
|
staleness: winner.effectiveStaleness,
|
|
4784
4854
|
};
|
|
4785
4855
|
}
|
|
4856
|
+
// Meta counts summary
|
|
4857
|
+
const summary = computeSummary(metaResult.entries, config.depthWeight);
|
|
4858
|
+
metaCounts = {
|
|
4859
|
+
total: summary.total,
|
|
4860
|
+
enabled: summary.total - summary.disabled,
|
|
4861
|
+
disabled: summary.disabled,
|
|
4862
|
+
neverSynthesized: summary.neverSynthesized,
|
|
4863
|
+
stale: summary.stale,
|
|
4864
|
+
errors: summary.errors,
|
|
4865
|
+
locked: summary.locked,
|
|
4866
|
+
};
|
|
4786
4867
|
}
|
|
4787
4868
|
catch {
|
|
4788
4869
|
// Watcher unreachable — phase summary unavailable
|
|
@@ -4811,6 +4892,7 @@ function registerStatusRoute(app, deps) {
|
|
|
4811
4892
|
},
|
|
4812
4893
|
phaseStateSummary,
|
|
4813
4894
|
nextPhase,
|
|
4895
|
+
metaCounts,
|
|
4814
4896
|
};
|
|
4815
4897
|
},
|
|
4816
4898
|
});
|
|
@@ -5120,11 +5202,10 @@ class HttpWatcherClient {
|
|
|
5120
5202
|
async post(endpoint, body) {
|
|
5121
5203
|
const url = this.baseUrl + endpoint;
|
|
5122
5204
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
5123
|
-
const res = await
|
|
5205
|
+
const res = await fetchWithTimeout(url, this.timeoutMs, {
|
|
5124
5206
|
method: 'POST',
|
|
5125
5207
|
headers: { 'Content-Type': 'application/json' },
|
|
5126
5208
|
body: JSON.stringify(body),
|
|
5127
|
-
signal: AbortSignal.timeout(this.timeoutMs),
|
|
5128
5209
|
});
|
|
5129
5210
|
if (res.ok) {
|
|
5130
5211
|
return res.json();
|
|
@@ -5166,25 +5247,6 @@ class HttpWatcherClient {
|
|
|
5166
5247
|
*
|
|
5167
5248
|
* @module bootstrap
|
|
5168
5249
|
*/
|
|
5169
|
-
/**
|
|
5170
|
-
* Compute per-cycle token total from a completed meta.
|
|
5171
|
-
*
|
|
5172
|
-
* Exported for testing.
|
|
5173
|
-
*
|
|
5174
|
-
* Uses `_synthesisCount` as a discriminator: after increment by `runCritic`,
|
|
5175
|
-
* a value of 1 means architect ran this cycle (was 0 pre-increment),
|
|
5176
|
-
* so all three phase token fields are summed. A value \> 1 means architect
|
|
5177
|
-
* was skipped (cached brief reused), so only builder + critic are summed.
|
|
5178
|
-
*/
|
|
5179
|
-
function computeCycleTokens(meta) {
|
|
5180
|
-
const builderTokens = meta._builderTokens ?? 0;
|
|
5181
|
-
const criticTokens = meta._criticTokens ?? 0;
|
|
5182
|
-
const architectRan = (meta._synthesisCount ?? 1) === 1;
|
|
5183
|
-
const architectTokens = architectRan
|
|
5184
|
-
? (meta._architectTokens ?? 0)
|
|
5185
|
-
: 0;
|
|
5186
|
-
return architectTokens + builderTokens + criticTokens;
|
|
5187
|
-
}
|
|
5188
5250
|
/**
|
|
5189
5251
|
* Bootstrap the service: create logger, build server, start listening,
|
|
5190
5252
|
* wire scheduler, queue processing, rule registration, config hot-reload,
|
|
@@ -5261,16 +5323,7 @@ async function startService(config, configPath) {
|
|
|
5261
5323
|
// Track whether synthesis_start has been emitted (deferred until
|
|
5262
5324
|
// a phase actually begins, avoiding orphan "Started" messages
|
|
5263
5325
|
// when the meta is skipped/locked/fresh). See #165.
|
|
5264
|
-
let synthesisStartEmitted = false;
|
|
5265
5326
|
const result = await orchestratePhase(config, executor, watcher, path, async (evt) => {
|
|
5266
|
-
// Emit synthesis_start on first phase_start (deferred guard)
|
|
5267
|
-
if (evt.type === 'phase_start' && !synthesisStartEmitted) {
|
|
5268
|
-
synthesisStartEmitted = true;
|
|
5269
|
-
await progress.report({
|
|
5270
|
-
type: 'synthesis_start',
|
|
5271
|
-
path: ownerPath,
|
|
5272
|
-
});
|
|
5273
|
-
}
|
|
5274
5327
|
// Wire current-phase tracking for GET /queue and POST /synthesize/abort
|
|
5275
5328
|
if (evt.type === 'phase_start' && evt.phase) {
|
|
5276
5329
|
queue.setCurrentPhase(ownerPath, evt.phase);
|
|
@@ -5312,19 +5365,8 @@ async function startService(config, configPath) {
|
|
|
5312
5365
|
// Task #9: Reset backoff on ANY successful phase execution
|
|
5313
5366
|
scheduler.resetBackoff();
|
|
5314
5367
|
}
|
|
5315
|
-
//
|
|
5316
|
-
|
|
5317
|
-
const updatedMeta = result.phaseResult?.updatedMeta;
|
|
5318
|
-
const tokens = updatedMeta
|
|
5319
|
-
? computeCycleTokens(updatedMeta)
|
|
5320
|
-
: undefined;
|
|
5321
|
-
await progress.report({
|
|
5322
|
-
type: 'synthesis_complete',
|
|
5323
|
-
path: ownerPath,
|
|
5324
|
-
durationMs,
|
|
5325
|
-
tokens,
|
|
5326
|
-
});
|
|
5327
|
-
}
|
|
5368
|
+
// Note: synthesis_complete is no longer emitted; the phaseEnd template
|
|
5369
|
+
// for the final critic phase serves as the completion signal (#129).
|
|
5328
5370
|
}
|
|
5329
5371
|
catch (err) {
|
|
5330
5372
|
stats.totalErrors++;
|
|
@@ -5552,8 +5594,8 @@ function registerCustomCliCommands(program) {
|
|
|
5552
5594
|
/**
|
|
5553
5595
|
* Jeeves component descriptor for jeeves-meta.
|
|
5554
5596
|
*
|
|
5555
|
-
* Single source of truth consumed by the service CLI
|
|
5556
|
-
*
|
|
5597
|
+
* Single source of truth consumed by the service CLI and the config-apply
|
|
5598
|
+
* pipeline.
|
|
5557
5599
|
*
|
|
5558
5600
|
* @module descriptor
|
|
5559
5601
|
*/
|
|
@@ -5566,8 +5608,6 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
5566
5608
|
servicePackage: META_COMPONENT.servicePackage,
|
|
5567
5609
|
pluginPackage: META_COMPONENT.pluginPackage,
|
|
5568
5610
|
defaultPort: META_COMPONENT.defaultPort,
|
|
5569
|
-
// The runtime Zod custom validator only checks for a .parse() method.
|
|
5570
|
-
// Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
|
|
5571
5611
|
configSchema: serviceConfigSchema,
|
|
5572
5612
|
configFileName: 'config.json',
|
|
5573
5613
|
initTemplate: () => serviceConfigSchema.parse({
|
|
@@ -5589,10 +5629,6 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
5589
5629
|
'-c',
|
|
5590
5630
|
configPath,
|
|
5591
5631
|
],
|
|
5592
|
-
sectionId: 'Meta',
|
|
5593
|
-
refreshIntervalSeconds: 73,
|
|
5594
|
-
generateToolsContent: () => '',
|
|
5595
|
-
dependencies: { hard: ['watcher'], soft: [] },
|
|
5596
5632
|
customCliCommands: registerCustomCliCommands,
|
|
5597
5633
|
});
|
|
5598
5634
|
|