@karmaniverous/jeeves-meta 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/jeeves-meta/index.js +5570 -100
- package/dist/index.js +1140 -1131
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13,9 +13,9 @@ import require$$2 from 'events';
|
|
|
13
13
|
import vm from 'vm';
|
|
14
14
|
import * as commander from 'commander';
|
|
15
15
|
import { tmpdir } from 'node:os';
|
|
16
|
-
import 'node:child_process';
|
|
17
16
|
import { z } from 'zod';
|
|
18
|
-
import
|
|
17
|
+
import 'node:child_process';
|
|
18
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
19
19
|
import pino from 'pino';
|
|
20
20
|
import Handlebars from 'handlebars';
|
|
21
21
|
import { Cron } from 'croner';
|
|
@@ -7684,12 +7684,17 @@ const jeevesComponentDescriptorSchema = z.object({
|
|
|
7684
7684
|
.optional(),
|
|
7685
7685
|
/**
|
|
7686
7686
|
* Returns command + args for launching the service process.
|
|
7687
|
-
* Consumed by `
|
|
7687
|
+
* Consumed by `service install`.
|
|
7688
7688
|
*/
|
|
7689
7689
|
startCommand: z.function({
|
|
7690
7690
|
input: [z.string()],
|
|
7691
7691
|
output: z.array(z.string()),
|
|
7692
7692
|
}),
|
|
7693
|
+
/** In-process service entry point for the CLI `start` command. */
|
|
7694
|
+
run: z.function({
|
|
7695
|
+
input: [z.string()],
|
|
7696
|
+
output: z.promise(z.void()),
|
|
7697
|
+
}),
|
|
7693
7698
|
/** TOOLS.md section name (e.g., 'Watcher'). */
|
|
7694
7699
|
sectionId: z.string().min(1, 'sectionId must be a non-empty string'),
|
|
7695
7700
|
/** Refresh interval in seconds (must be a prime number). */
|
|
@@ -8265,49 +8270,6 @@ function loadServiceConfig(configPath) {
|
|
|
8265
8270
|
return serviceConfigSchema.parse(raw);
|
|
8266
8271
|
}
|
|
8267
8272
|
|
|
8268
|
-
/**
|
|
8269
|
-
* Jeeves component descriptor for jeeves-meta.
|
|
8270
|
-
*
|
|
8271
|
-
* Single source of truth consumed by the service CLI, plugin writer, and
|
|
8272
|
-
* config-apply pipeline.
|
|
8273
|
-
*
|
|
8274
|
-
* @module descriptor
|
|
8275
|
-
*/
|
|
8276
|
-
/**
|
|
8277
|
-
* Parsed jeeves-meta component descriptor.
|
|
8278
|
-
*/
|
|
8279
|
-
const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
8280
|
-
name: 'meta',
|
|
8281
|
-
version: SERVICE_VERSION,
|
|
8282
|
-
servicePackage: '@karmaniverous/jeeves-meta',
|
|
8283
|
-
pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
|
|
8284
|
-
defaultPort: 1938,
|
|
8285
|
-
// The runtime Zod custom validator only checks for a .parse() method.
|
|
8286
|
-
// Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
|
|
8287
|
-
configSchema: serviceConfigSchema,
|
|
8288
|
-
configFileName: 'config.json',
|
|
8289
|
-
initTemplate: () => serviceConfigSchema.parse({
|
|
8290
|
-
watcherUrl: 'http://127.0.0.1:1936',
|
|
8291
|
-
}),
|
|
8292
|
-
onConfigApply: (merged) => {
|
|
8293
|
-
const parsed = serviceConfigSchema.parse(merged);
|
|
8294
|
-
applyHotReloadedConfig(parsed);
|
|
8295
|
-
return Promise.resolve();
|
|
8296
|
-
},
|
|
8297
|
-
startCommand: (configPath) => [
|
|
8298
|
-
'node',
|
|
8299
|
-
'dist/cli.js',
|
|
8300
|
-
'start',
|
|
8301
|
-
'-c',
|
|
8302
|
-
configPath,
|
|
8303
|
-
],
|
|
8304
|
-
sectionId: 'Meta',
|
|
8305
|
-
refreshIntervalSeconds: 73,
|
|
8306
|
-
generateToolsContent: () => '',
|
|
8307
|
-
dependencies: { hard: ['watcher'], soft: [] },
|
|
8308
|
-
customCliCommands: registerCustomCliCommands,
|
|
8309
|
-
});
|
|
8310
|
-
|
|
8311
8273
|
/**
|
|
8312
8274
|
* Compute summary statistics from an array of MetaEntry objects.
|
|
8313
8275
|
*
|
|
@@ -8862,67 +8824,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
|
|
|
8862
8824
|
return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
|
|
8863
8825
|
}
|
|
8864
8826
|
|
|
8865
|
-
/**
|
|
8866
|
-
* Exponential moving average helper for token tracking.
|
|
8867
|
-
*
|
|
8868
|
-
* @module ema
|
|
8869
|
-
*/
|
|
8870
|
-
const DEFAULT_DECAY = 0.3;
|
|
8871
|
-
/**
|
|
8872
|
-
* Compute exponential moving average.
|
|
8873
|
-
*
|
|
8874
|
-
* @param current - New observation.
|
|
8875
|
-
* @param previous - Previous EMA value, or undefined for first observation.
|
|
8876
|
-
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
8877
|
-
* @returns Updated EMA.
|
|
8878
|
-
*/
|
|
8879
|
-
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
8880
|
-
if (previous === undefined)
|
|
8881
|
-
return current;
|
|
8882
|
-
return decay * current + (1 - decay) * previous;
|
|
8883
|
-
}
|
|
8884
|
-
|
|
8885
|
-
/**
|
|
8886
|
-
* Shared error utilities.
|
|
8887
|
-
*
|
|
8888
|
-
* @module errors
|
|
8889
|
-
*/
|
|
8890
|
-
/**
|
|
8891
|
-
* Wrap an unknown caught value into a MetaError.
|
|
8892
|
-
*
|
|
8893
|
-
* @param step - Which synthesis step failed.
|
|
8894
|
-
* @param err - The caught error value.
|
|
8895
|
-
* @param code - Error classification code.
|
|
8896
|
-
* @returns A structured MetaError.
|
|
8897
|
-
*/
|
|
8898
|
-
function toMetaError(step, err, code = 'FAILED') {
|
|
8899
|
-
return {
|
|
8900
|
-
step,
|
|
8901
|
-
code,
|
|
8902
|
-
message: err instanceof Error ? err.message : String(err),
|
|
8903
|
-
};
|
|
8904
|
-
}
|
|
8905
|
-
|
|
8906
|
-
/**
|
|
8907
|
-
* Compute a structure hash from a sorted file listing.
|
|
8908
|
-
*
|
|
8909
|
-
* Used to detect when directory structure changes, triggering
|
|
8910
|
-
* an architect re-run.
|
|
8911
|
-
*
|
|
8912
|
-
* @module structureHash
|
|
8913
|
-
*/
|
|
8914
|
-
/**
|
|
8915
|
-
* Compute a SHA-256 hash of a sorted file listing.
|
|
8916
|
-
*
|
|
8917
|
-
* @param filePaths - Array of file paths in scope.
|
|
8918
|
-
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
8919
|
-
*/
|
|
8920
|
-
function computeStructureHash(filePaths) {
|
|
8921
|
-
const sorted = [...filePaths].sort();
|
|
8922
|
-
const content = sorted.join('\n');
|
|
8923
|
-
return createHash('sha256').update(content).digest('hex');
|
|
8924
|
-
}
|
|
8925
|
-
|
|
8926
8827
|
/** Sleep for a given number of milliseconds. */
|
|
8927
8828
|
function sleep(ms) {
|
|
8928
8829
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -9460,6 +9361,26 @@ function buildCriticTask(ctx, meta, config) {
|
|
|
9460
9361
|
return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
|
|
9461
9362
|
}
|
|
9462
9363
|
|
|
9364
|
+
/**
|
|
9365
|
+
* Exponential moving average helper for token tracking.
|
|
9366
|
+
*
|
|
9367
|
+
* @module ema
|
|
9368
|
+
*/
|
|
9369
|
+
const DEFAULT_DECAY = 0.3;
|
|
9370
|
+
/**
|
|
9371
|
+
* Compute exponential moving average.
|
|
9372
|
+
*
|
|
9373
|
+
* @param current - New observation.
|
|
9374
|
+
* @param previous - Previous EMA value, or undefined for first observation.
|
|
9375
|
+
* @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
|
|
9376
|
+
* @returns Updated EMA.
|
|
9377
|
+
*/
|
|
9378
|
+
function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
9379
|
+
if (previous === undefined)
|
|
9380
|
+
return current;
|
|
9381
|
+
return decay * current + (1 - decay) * previous;
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9463
9384
|
/**
|
|
9464
9385
|
* Structured error from a synthesis step failure.
|
|
9465
9386
|
*
|
|
@@ -9890,6 +9811,47 @@ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
|
|
|
9890
9811
|
return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
|
|
9891
9812
|
}
|
|
9892
9813
|
|
|
9814
|
+
/**
|
|
9815
|
+
* Shared error utilities.
|
|
9816
|
+
*
|
|
9817
|
+
* @module errors
|
|
9818
|
+
*/
|
|
9819
|
+
/**
|
|
9820
|
+
* Wrap an unknown caught value into a MetaError.
|
|
9821
|
+
*
|
|
9822
|
+
* @param step - Which synthesis step failed.
|
|
9823
|
+
* @param err - The caught error value.
|
|
9824
|
+
* @param code - Error classification code.
|
|
9825
|
+
* @returns A structured MetaError.
|
|
9826
|
+
*/
|
|
9827
|
+
function toMetaError(step, err, code = 'FAILED') {
|
|
9828
|
+
return {
|
|
9829
|
+
step,
|
|
9830
|
+
code,
|
|
9831
|
+
message: err instanceof Error ? err.message : String(err),
|
|
9832
|
+
};
|
|
9833
|
+
}
|
|
9834
|
+
|
|
9835
|
+
/**
|
|
9836
|
+
* Compute a structure hash from a sorted file listing.
|
|
9837
|
+
*
|
|
9838
|
+
* Used to detect when directory structure changes, triggering
|
|
9839
|
+
* an architect re-run.
|
|
9840
|
+
*
|
|
9841
|
+
* @module structureHash
|
|
9842
|
+
*/
|
|
9843
|
+
/**
|
|
9844
|
+
* Compute a SHA-256 hash of a sorted file listing.
|
|
9845
|
+
*
|
|
9846
|
+
* @param filePaths - Array of file paths in scope.
|
|
9847
|
+
* @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
|
|
9848
|
+
*/
|
|
9849
|
+
function computeStructureHash(filePaths) {
|
|
9850
|
+
const sorted = [...filePaths].sort();
|
|
9851
|
+
const content = sorted.join('\n');
|
|
9852
|
+
return createHash('sha256').update(content).digest('hex');
|
|
9853
|
+
}
|
|
9854
|
+
|
|
9893
9855
|
/**
|
|
9894
9856
|
* Lock-staged cycle finalization: write to .lock, copy to meta.json, archive, prune.
|
|
9895
9857
|
*
|
|
@@ -10516,309 +10478,35 @@ class ProgressReporter {
|
|
|
10516
10478
|
}
|
|
10517
10479
|
|
|
10518
10480
|
/**
|
|
10519
|
-
*
|
|
10520
|
-
*
|
|
10521
|
-
* Shared between the POST /seed route handler and the auto-seed pass.
|
|
10522
|
-
*
|
|
10523
|
-
* @module seed/createMeta
|
|
10524
|
-
*/
|
|
10525
|
-
/**
|
|
10526
|
-
* Create a .meta/ directory with an initial meta.json.
|
|
10527
|
-
*
|
|
10528
|
-
* Does NOT check for existing .meta/ — caller is responsible for that guard.
|
|
10529
|
-
*
|
|
10530
|
-
* @param ownerPath - The owner directory path.
|
|
10531
|
-
* @param options - Optional cross-refs and steering prompt.
|
|
10532
|
-
* @returns The meta directory path and generated ID.
|
|
10533
|
-
*/
|
|
10534
|
-
async function createMeta(ownerPath, options) {
|
|
10535
|
-
const metaDir = resolveMetaDir(ownerPath);
|
|
10536
|
-
await mkdir(metaDir, { recursive: true });
|
|
10537
|
-
const _id = randomUUID();
|
|
10538
|
-
const metaJson = { _id };
|
|
10539
|
-
if (options?.crossRefs !== undefined)
|
|
10540
|
-
metaJson._crossRefs = options.crossRefs;
|
|
10541
|
-
if (options?.steer !== undefined)
|
|
10542
|
-
metaJson._steer = options.steer;
|
|
10543
|
-
const metaJsonPath = join(metaDir, 'meta.json');
|
|
10544
|
-
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
10545
|
-
return { metaDir, _id };
|
|
10546
|
-
}
|
|
10547
|
-
/**
|
|
10548
|
-
* Check if a .meta/ directory already exists for an owner path.
|
|
10549
|
-
*
|
|
10550
|
-
* @param ownerPath - The owner directory path.
|
|
10551
|
-
* @returns True if .meta/ already exists.
|
|
10552
|
-
*/
|
|
10553
|
-
function metaExists(ownerPath) {
|
|
10554
|
-
return existsSync(resolveMetaDir(ownerPath));
|
|
10555
|
-
}
|
|
10556
|
-
|
|
10557
|
-
/**
|
|
10558
|
-
* Auto-seed pass — scan for directories matching policy rules and seed them.
|
|
10559
|
-
*
|
|
10560
|
-
* Runs before discovery in each scheduler tick. For each auto-seed rule,
|
|
10561
|
-
* walks matching directories via the watcher and creates .meta/ directories
|
|
10562
|
-
* for those that don't already have one.
|
|
10481
|
+
* Single-threaded synthesis queue with priority support and deduplication.
|
|
10563
10482
|
*
|
|
10564
|
-
*
|
|
10483
|
+
* The scheduler enqueues the stalest candidate each tick. HTTP-triggered
|
|
10484
|
+
* synthesis requests get priority (inserted at front). A path appears at
|
|
10485
|
+
* most once in the queue; re-triggering returns the current position.
|
|
10565
10486
|
*
|
|
10566
|
-
* @module
|
|
10487
|
+
* @module queue
|
|
10567
10488
|
*/
|
|
10489
|
+
const DEPTH_WARNING_THRESHOLD = 3;
|
|
10568
10490
|
/**
|
|
10569
|
-
*
|
|
10491
|
+
* Single-threaded synthesis queue.
|
|
10570
10492
|
*
|
|
10571
|
-
*
|
|
10572
|
-
*
|
|
10493
|
+
* Only one synthesis runs at a time. Priority items are inserted at the
|
|
10494
|
+
* front of the queue. Duplicate paths are rejected with their current
|
|
10495
|
+
* position returned.
|
|
10573
10496
|
*/
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
* @param rules - Auto-seed policy rules from config.
|
|
10588
|
-
* @param watcher - Watcher client for filesystem enumeration.
|
|
10589
|
-
* @param logger - Logger for reporting seed actions.
|
|
10590
|
-
* @returns Summary of what was seeded.
|
|
10591
|
-
*/
|
|
10592
|
-
async function autoSeedPass(rules, watcher, logger) {
|
|
10593
|
-
if (rules.length === 0)
|
|
10594
|
-
return { seeded: 0, paths: [] };
|
|
10595
|
-
// Build a map of ownerPath → effective options (last match wins)
|
|
10596
|
-
const candidates = new Map();
|
|
10597
|
-
for (const rule of rules) {
|
|
10598
|
-
const files = await watcher.walk([rule.match]);
|
|
10599
|
-
const dirs = extractDirectories(files);
|
|
10600
|
-
for (const dir of dirs) {
|
|
10601
|
-
candidates.set(dir, {
|
|
10602
|
-
steer: rule.steer,
|
|
10603
|
-
crossRefs: rule.crossRefs,
|
|
10604
|
-
});
|
|
10605
|
-
}
|
|
10606
|
-
}
|
|
10607
|
-
// Filter out paths that already have .meta/meta.json
|
|
10608
|
-
const toSeed = [];
|
|
10609
|
-
for (const [path, opts] of candidates) {
|
|
10610
|
-
if (!metaExists(path)) {
|
|
10611
|
-
toSeed.push({ path, ...opts });
|
|
10612
|
-
}
|
|
10613
|
-
}
|
|
10614
|
-
// Seed remaining
|
|
10615
|
-
const seededPaths = [];
|
|
10616
|
-
for (const candidate of toSeed) {
|
|
10617
|
-
try {
|
|
10618
|
-
await createMeta(candidate.path, {
|
|
10619
|
-
steer: candidate.steer,
|
|
10620
|
-
crossRefs: candidate.crossRefs,
|
|
10621
|
-
});
|
|
10622
|
-
seededPaths.push(candidate.path);
|
|
10623
|
-
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
10624
|
-
}
|
|
10625
|
-
catch (err) {
|
|
10626
|
-
logger?.warn({
|
|
10627
|
-
path: candidate.path,
|
|
10628
|
-
err: err instanceof Error ? err.message : String(err),
|
|
10629
|
-
}, 'auto-seed failed for path');
|
|
10630
|
-
}
|
|
10631
|
-
}
|
|
10632
|
-
return { seeded: seededPaths.length, paths: seededPaths };
|
|
10633
|
-
}
|
|
10634
|
-
|
|
10635
|
-
/**
|
|
10636
|
-
* Croner-based scheduler that discovers the stalest meta candidate each tick
|
|
10637
|
-
* and enqueues it for synthesis.
|
|
10638
|
-
*
|
|
10639
|
-
* @module scheduler
|
|
10640
|
-
*/
|
|
10641
|
-
const MAX_BACKOFF_MULTIPLIER = 4;
|
|
10642
|
-
/**
|
|
10643
|
-
* Periodic scheduler that discovers stale meta candidates and enqueues them.
|
|
10644
|
-
*
|
|
10645
|
-
* Supports adaptive backoff when no candidates are found and hot-reloadable
|
|
10646
|
-
* cron expressions via {@link Scheduler.updateSchedule}.
|
|
10647
|
-
*/
|
|
10648
|
-
class Scheduler {
|
|
10649
|
-
job = null;
|
|
10650
|
-
backoffMultiplier = 1;
|
|
10651
|
-
tickCount = 0;
|
|
10652
|
-
config;
|
|
10653
|
-
queue;
|
|
10654
|
-
logger;
|
|
10655
|
-
watcher;
|
|
10656
|
-
registrar = null;
|
|
10657
|
-
currentExpression;
|
|
10658
|
-
constructor(config, queue, logger, watcher) {
|
|
10659
|
-
this.config = config;
|
|
10660
|
-
this.queue = queue;
|
|
10661
|
-
this.logger = logger;
|
|
10662
|
-
this.watcher = watcher;
|
|
10663
|
-
this.currentExpression = config.schedule;
|
|
10664
|
-
}
|
|
10665
|
-
/** Set the rule registrar for watcher restart detection. */
|
|
10666
|
-
setRegistrar(registrar) {
|
|
10667
|
-
this.registrar = registrar;
|
|
10668
|
-
}
|
|
10669
|
-
/** Start the cron job. */
|
|
10670
|
-
start() {
|
|
10671
|
-
if (this.job)
|
|
10672
|
-
return;
|
|
10673
|
-
this.job = new Cron(this.currentExpression, () => {
|
|
10674
|
-
void this.tick();
|
|
10675
|
-
});
|
|
10676
|
-
this.logger.info({ schedule: this.currentExpression }, 'Scheduler started');
|
|
10677
|
-
}
|
|
10678
|
-
/** Stop the cron job. */
|
|
10679
|
-
stop() {
|
|
10680
|
-
if (!this.job)
|
|
10681
|
-
return;
|
|
10682
|
-
this.job.stop();
|
|
10683
|
-
this.job = null;
|
|
10684
|
-
this.backoffMultiplier = 1;
|
|
10685
|
-
this.logger.info('Scheduler stopped');
|
|
10686
|
-
}
|
|
10687
|
-
/** Hot-reload the cron schedule expression. */
|
|
10688
|
-
updateSchedule(expression) {
|
|
10689
|
-
this.currentExpression = expression;
|
|
10690
|
-
if (this.job) {
|
|
10691
|
-
this.job.stop();
|
|
10692
|
-
this.job = new Cron(expression, () => {
|
|
10693
|
-
void this.tick();
|
|
10694
|
-
});
|
|
10695
|
-
this.logger.info({ schedule: expression }, 'Schedule updated');
|
|
10696
|
-
}
|
|
10697
|
-
}
|
|
10698
|
-
/** Reset backoff multiplier (call after successful synthesis). */
|
|
10699
|
-
resetBackoff() {
|
|
10700
|
-
if (this.backoffMultiplier > 1) {
|
|
10701
|
-
this.logger.debug('Backoff reset after successful synthesis');
|
|
10702
|
-
}
|
|
10703
|
-
this.backoffMultiplier = 1;
|
|
10704
|
-
}
|
|
10705
|
-
/** Whether the scheduler is currently running. */
|
|
10706
|
-
get isRunning() {
|
|
10707
|
-
return this.job !== null;
|
|
10708
|
-
}
|
|
10709
|
-
/** Next scheduled tick time, or null if not running. */
|
|
10710
|
-
get nextRunAt() {
|
|
10711
|
-
if (!this.job)
|
|
10712
|
-
return null;
|
|
10713
|
-
return this.job.nextRun() ?? null;
|
|
10714
|
-
}
|
|
10715
|
-
/**
|
|
10716
|
-
* Single tick: discover stalest candidate and enqueue it.
|
|
10717
|
-
*
|
|
10718
|
-
* Skips if the queue is currently processing. Applies adaptive backoff
|
|
10719
|
-
* when no candidates are found.
|
|
10720
|
-
*/
|
|
10721
|
-
async tick() {
|
|
10722
|
-
this.tickCount++;
|
|
10723
|
-
// Apply backoff: skip ticks when backing off
|
|
10724
|
-
if (this.backoffMultiplier > 1 &&
|
|
10725
|
-
this.tickCount % this.backoffMultiplier !== 0) {
|
|
10726
|
-
this.logger.trace({
|
|
10727
|
-
backoffMultiplier: this.backoffMultiplier,
|
|
10728
|
-
tickCount: this.tickCount,
|
|
10729
|
-
}, 'Skipping tick (backoff)');
|
|
10730
|
-
return;
|
|
10731
|
-
}
|
|
10732
|
-
// Auto-seed pass: create .meta/ for matching directories
|
|
10733
|
-
if (this.config.autoSeed.length > 0) {
|
|
10734
|
-
try {
|
|
10735
|
-
const result = await autoSeedPass(this.config.autoSeed, this.watcher, this.logger);
|
|
10736
|
-
if (result.seeded > 0) {
|
|
10737
|
-
this.logger.info({ seeded: result.seeded }, 'Auto-seed pass completed');
|
|
10738
|
-
}
|
|
10739
|
-
}
|
|
10740
|
-
catch (err) {
|
|
10741
|
-
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
10742
|
-
}
|
|
10743
|
-
}
|
|
10744
|
-
const candidate = await this.discoverStalest();
|
|
10745
|
-
if (!candidate) {
|
|
10746
|
-
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
10747
|
-
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No stale candidates found, increasing backoff');
|
|
10748
|
-
return;
|
|
10749
|
-
}
|
|
10750
|
-
this.queue.enqueue(candidate);
|
|
10751
|
-
this.logger.info({ path: candidate }, 'Enqueued stale candidate');
|
|
10752
|
-
// Opportunistic watcher restart detection
|
|
10753
|
-
if (this.registrar) {
|
|
10754
|
-
try {
|
|
10755
|
-
const statusRes = await fetch(new URL('/status', this.config.watcherUrl), {
|
|
10756
|
-
signal: AbortSignal.timeout(3000),
|
|
10757
|
-
});
|
|
10758
|
-
if (statusRes.ok) {
|
|
10759
|
-
const status = (await statusRes.json());
|
|
10760
|
-
if (typeof status.uptime === 'number') {
|
|
10761
|
-
await this.registrar.checkAndReregister(status.uptime);
|
|
10762
|
-
}
|
|
10763
|
-
}
|
|
10764
|
-
}
|
|
10765
|
-
catch {
|
|
10766
|
-
// Watcher unreachable — skip uptime check
|
|
10767
|
-
}
|
|
10768
|
-
}
|
|
10769
|
-
}
|
|
10770
|
-
/**
|
|
10771
|
-
* Discover the stalest meta candidate via watcher.
|
|
10772
|
-
*/
|
|
10773
|
-
async discoverStalest() {
|
|
10774
|
-
try {
|
|
10775
|
-
const result = await listMetas(this.config, this.watcher);
|
|
10776
|
-
const stale = result.entries
|
|
10777
|
-
.filter((e) => e.stalenessSeconds > 0)
|
|
10778
|
-
.map((e) => ({
|
|
10779
|
-
node: e.node,
|
|
10780
|
-
meta: e.meta,
|
|
10781
|
-
actualStaleness: e.stalenessSeconds,
|
|
10782
|
-
}));
|
|
10783
|
-
return discoverStalestPath(stale, this.config.depthWeight);
|
|
10784
|
-
}
|
|
10785
|
-
catch (err) {
|
|
10786
|
-
this.logger.warn({ err }, 'Failed to discover stalest candidate');
|
|
10787
|
-
return null;
|
|
10788
|
-
}
|
|
10789
|
-
}
|
|
10790
|
-
}
|
|
10791
|
-
|
|
10792
|
-
/**
|
|
10793
|
-
* Single-threaded synthesis queue with priority support and deduplication.
|
|
10794
|
-
*
|
|
10795
|
-
* The scheduler enqueues the stalest candidate each tick. HTTP-triggered
|
|
10796
|
-
* synthesis requests get priority (inserted at front). A path appears at
|
|
10797
|
-
* most once in the queue; re-triggering returns the current position.
|
|
10798
|
-
*
|
|
10799
|
-
* @module queue
|
|
10800
|
-
*/
|
|
10801
|
-
const DEPTH_WARNING_THRESHOLD = 3;
|
|
10802
|
-
/**
|
|
10803
|
-
* Single-threaded synthesis queue.
|
|
10804
|
-
*
|
|
10805
|
-
* Only one synthesis runs at a time. Priority items are inserted at the
|
|
10806
|
-
* front of the queue. Duplicate paths are rejected with their current
|
|
10807
|
-
* position returned.
|
|
10808
|
-
*/
|
|
10809
|
-
class SynthesisQueue {
|
|
10810
|
-
queue = [];
|
|
10811
|
-
currentItem = null;
|
|
10812
|
-
processing = false;
|
|
10813
|
-
logger;
|
|
10814
|
-
onEnqueueCallback = null;
|
|
10815
|
-
/**
|
|
10816
|
-
* Create a new SynthesisQueue.
|
|
10817
|
-
*
|
|
10818
|
-
* @param logger - Pino logger instance.
|
|
10819
|
-
*/
|
|
10820
|
-
constructor(logger) {
|
|
10821
|
-
this.logger = logger;
|
|
10497
|
+
class SynthesisQueue {
|
|
10498
|
+
queue = [];
|
|
10499
|
+
currentItem = null;
|
|
10500
|
+
processing = false;
|
|
10501
|
+
logger;
|
|
10502
|
+
onEnqueueCallback = null;
|
|
10503
|
+
/**
|
|
10504
|
+
* Create a new SynthesisQueue.
|
|
10505
|
+
*
|
|
10506
|
+
* @param logger - Pino logger instance.
|
|
10507
|
+
*/
|
|
10508
|
+
constructor(logger) {
|
|
10509
|
+
this.logger = logger;
|
|
10822
10510
|
}
|
|
10823
10511
|
/**
|
|
10824
10512
|
* Set a callback to invoke when a new (non-duplicate) item is enqueued.
|
|
@@ -10973,242 +10661,856 @@ class SynthesisQueue {
|
|
|
10973
10661
|
}
|
|
10974
10662
|
|
|
10975
10663
|
/**
|
|
10976
|
-
*
|
|
10664
|
+
* Periodic watcher health check for rule registration resilience.
|
|
10977
10665
|
*
|
|
10978
|
-
*
|
|
10979
|
-
*
|
|
10666
|
+
* Pings watcher `/status` on a configurable interval, detects restarts
|
|
10667
|
+
* (uptime decrease), and re-registers virtual rules automatically.
|
|
10668
|
+
* Independent of the synthesis scheduler.
|
|
10980
10669
|
*
|
|
10981
|
-
* @module
|
|
10670
|
+
* @module rules/healthCheck
|
|
10982
10671
|
*/
|
|
10983
|
-
/** Return a sanitized copy of the config (redact gatewayApiKey). */
|
|
10984
|
-
function sanitizeConfig(config) {
|
|
10985
|
-
return {
|
|
10986
|
-
...config,
|
|
10987
|
-
gatewayApiKey: config.gatewayApiKey ? '[REDACTED]' : undefined,
|
|
10988
|
-
};
|
|
10989
|
-
}
|
|
10990
|
-
function registerConfigRoute(app, deps) {
|
|
10991
|
-
const configHandler = createConfigQueryHandler(() => sanitizeConfig(deps.config));
|
|
10992
|
-
app.get('/config', async (request, reply) => {
|
|
10993
|
-
const { path } = request.query;
|
|
10994
|
-
const result = await configHandler({ path });
|
|
10995
|
-
return reply.status(result.status).send(result.body);
|
|
10996
|
-
});
|
|
10997
|
-
}
|
|
10998
|
-
|
|
10999
|
-
/**
|
|
11000
|
-
* POST /config/apply — apply a config patch via the core SDK handler.
|
|
11001
|
-
*
|
|
11002
|
-
* @module routes/configApply
|
|
11003
|
-
*/
|
|
11004
|
-
/** Register the POST /config/apply route. */
|
|
11005
|
-
function registerConfigApplyRoute(app) {
|
|
11006
|
-
const handler = createConfigApplyHandler(metaDescriptor);
|
|
11007
|
-
app.post('/config/apply', async (request, reply) => {
|
|
11008
|
-
const result = await handler(request.body);
|
|
11009
|
-
return reply.status(result.status).send(result.body);
|
|
11010
|
-
});
|
|
11011
|
-
}
|
|
11012
|
-
|
|
11013
10672
|
/**
|
|
11014
|
-
*
|
|
11015
|
-
* GET /metas/:path — single meta detail.
|
|
10673
|
+
* Manages the periodic watcher health check loop.
|
|
11016
10674
|
*
|
|
11017
|
-
*
|
|
10675
|
+
* Starts a `setInterval` that pings the watcher and delegates
|
|
10676
|
+
* restart detection to `RuleRegistrar.checkAndReregister()`.
|
|
11018
10677
|
*/
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
.
|
|
11027
|
-
.
|
|
11028
|
-
.
|
|
11029
|
-
.
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
.
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
.transform((v) => v === 'true')
|
|
11037
|
-
.optional(),
|
|
11038
|
-
fields: z.string().optional(),
|
|
11039
|
-
});
|
|
11040
|
-
const metaDetailQuerySchema = z.object({
|
|
11041
|
-
fields: z.string().optional(),
|
|
11042
|
-
includeArchive: z
|
|
11043
|
-
.union([
|
|
11044
|
-
z.enum(['true', 'false']).transform((v) => v === 'true'),
|
|
11045
|
-
z.string().transform(Number).pipe(z.number().int().nonnegative()),
|
|
11046
|
-
])
|
|
11047
|
-
.optional(),
|
|
11048
|
-
});
|
|
11049
|
-
function registerMetasRoutes(app, deps) {
|
|
11050
|
-
app.get('/metas', async (request) => {
|
|
11051
|
-
const query = metasQuerySchema.parse(request.query);
|
|
11052
|
-
const { config, watcher } = deps;
|
|
11053
|
-
const result = await listMetas(config, watcher);
|
|
11054
|
-
let entries = result.entries;
|
|
11055
|
-
// Apply filters
|
|
11056
|
-
if (query.pathPrefix) {
|
|
11057
|
-
entries = entries.filter((e) => e.path.includes(query.pathPrefix));
|
|
11058
|
-
}
|
|
11059
|
-
if (query.hasError !== undefined) {
|
|
11060
|
-
entries = entries.filter((e) => e.hasError === query.hasError);
|
|
11061
|
-
}
|
|
11062
|
-
if (query.neverSynthesized !== undefined) {
|
|
11063
|
-
entries = entries.filter((e) => (e.lastSynthesized === null) === query.neverSynthesized);
|
|
10678
|
+
class WatcherHealthCheck {
|
|
10679
|
+
watcherUrl;
|
|
10680
|
+
intervalMs;
|
|
10681
|
+
registrar;
|
|
10682
|
+
logger;
|
|
10683
|
+
handle = null;
|
|
10684
|
+
constructor(opts) {
|
|
10685
|
+
this.watcherUrl = opts.watcherUrl.replace(/\/+$/, '');
|
|
10686
|
+
this.intervalMs = opts.intervalMs;
|
|
10687
|
+
this.registrar = opts.registrar;
|
|
10688
|
+
this.logger = opts.logger;
|
|
10689
|
+
}
|
|
10690
|
+
/** Start the periodic health check. No-op if intervalMs is 0. */
|
|
10691
|
+
start() {
|
|
10692
|
+
if (this.intervalMs <= 0) {
|
|
10693
|
+
this.logger.info('Watcher health check disabled (interval = 0)');
|
|
10694
|
+
return;
|
|
11064
10695
|
}
|
|
11065
|
-
|
|
11066
|
-
|
|
10696
|
+
this.handle = setInterval(() => {
|
|
10697
|
+
void this.check();
|
|
10698
|
+
}, this.intervalMs);
|
|
10699
|
+
// Don't prevent process exit
|
|
10700
|
+
if (typeof this.handle === 'object' && 'unref' in this.handle) {
|
|
10701
|
+
this.handle.unref();
|
|
11067
10702
|
}
|
|
11068
|
-
|
|
11069
|
-
|
|
10703
|
+
this.logger.info({ intervalMs: this.intervalMs }, 'Watcher health check started');
|
|
10704
|
+
}
|
|
10705
|
+
/** Stop the periodic health check. */
|
|
10706
|
+
stop() {
|
|
10707
|
+
if (this.handle) {
|
|
10708
|
+
clearInterval(this.handle);
|
|
10709
|
+
this.handle = null;
|
|
11070
10710
|
}
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
'depth',
|
|
11078
|
-
'emphasis',
|
|
11079
|
-
'stalenessSeconds',
|
|
11080
|
-
'lastSynthesized',
|
|
11081
|
-
'hasError',
|
|
11082
|
-
'locked',
|
|
11083
|
-
'architectTokens',
|
|
11084
|
-
'builderTokens',
|
|
11085
|
-
'criticTokens',
|
|
11086
|
-
];
|
|
11087
|
-
const projectedFields = fieldList ?? defaultFields;
|
|
11088
|
-
const metas = entries.map((e) => {
|
|
11089
|
-
const full = {
|
|
11090
|
-
path: e.path,
|
|
11091
|
-
depth: e.depth,
|
|
11092
|
-
emphasis: e.emphasis,
|
|
11093
|
-
stalenessSeconds: e.stalenessSeconds === Infinity
|
|
11094
|
-
? null
|
|
11095
|
-
: Math.round(e.stalenessSeconds),
|
|
11096
|
-
lastSynthesized: e.lastSynthesized,
|
|
11097
|
-
hasError: e.hasError,
|
|
11098
|
-
locked: e.locked,
|
|
11099
|
-
architectTokens: e.architectTokens,
|
|
11100
|
-
builderTokens: e.builderTokens,
|
|
11101
|
-
criticTokens: e.criticTokens,
|
|
11102
|
-
};
|
|
11103
|
-
const projected = {};
|
|
11104
|
-
for (const f of projectedFields) {
|
|
11105
|
-
if (f in full)
|
|
11106
|
-
projected[f] = full[f];
|
|
11107
|
-
}
|
|
11108
|
-
return projected;
|
|
11109
|
-
});
|
|
11110
|
-
return { summary, metas };
|
|
11111
|
-
});
|
|
11112
|
-
app.get('/metas/:path', async (request, reply) => {
|
|
11113
|
-
const query = metaDetailQuerySchema.parse(request.query);
|
|
11114
|
-
const { config, watcher } = deps;
|
|
11115
|
-
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
11116
|
-
const result = await listMetas(config, watcher);
|
|
11117
|
-
const targetNode = findNode(result.tree, targetPath);
|
|
11118
|
-
if (!targetNode) {
|
|
11119
|
-
return reply.status(404).send({
|
|
11120
|
-
error: 'NOT_FOUND',
|
|
11121
|
-
message: 'Meta path not found: ' + targetPath,
|
|
10711
|
+
}
|
|
10712
|
+
/** Single health check iteration. */
|
|
10713
|
+
async check() {
|
|
10714
|
+
try {
|
|
10715
|
+
const res = await fetch(this.watcherUrl + '/status', {
|
|
10716
|
+
signal: AbortSignal.timeout(5000),
|
|
11122
10717
|
});
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
const defaultExclude = new Set([
|
|
11127
|
-
'_architect',
|
|
11128
|
-
'_builder',
|
|
11129
|
-
'_critic',
|
|
11130
|
-
'_content',
|
|
11131
|
-
'_feedback',
|
|
11132
|
-
]);
|
|
11133
|
-
const fieldList = query.fields?.split(',');
|
|
11134
|
-
const projectMeta = (m) => {
|
|
11135
|
-
if (fieldList) {
|
|
11136
|
-
const r = {};
|
|
11137
|
-
for (const f of fieldList)
|
|
11138
|
-
r[f] = m[f];
|
|
11139
|
-
return r;
|
|
10718
|
+
if (!res.ok) {
|
|
10719
|
+
this.logger.warn({ status: res.status }, 'Watcher health check: non-OK response');
|
|
10720
|
+
return;
|
|
11140
10721
|
}
|
|
11141
|
-
const
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
10722
|
+
const data = (await res.json());
|
|
10723
|
+
// If rules were never successfully registered (startup failure),
|
|
10724
|
+
// attempt registration now that the watcher is reachable.
|
|
10725
|
+
if (!this.registrar.isRegistered) {
|
|
10726
|
+
this.logger.info('Rules not registered — attempting registration');
|
|
10727
|
+
await this.registrar.register();
|
|
11145
10728
|
}
|
|
11146
|
-
|
|
11147
|
-
};
|
|
11148
|
-
// Compute scope
|
|
11149
|
-
const { scopeFiles, allFiles } = await getScopeFiles(targetNode, watcher);
|
|
11150
|
-
// Compute staleness
|
|
11151
|
-
const metaTyped = meta;
|
|
11152
|
-
const staleSeconds = metaTyped._generatedAt
|
|
11153
|
-
? Math.round((Date.now() - new Date(metaTyped._generatedAt).getTime()) / 1000)
|
|
11154
|
-
: null;
|
|
11155
|
-
const score = computeStalenessScore(staleSeconds, metaTyped._depth ?? 0, metaTyped._emphasis ?? 1, config.depthWeight);
|
|
11156
|
-
const response = {
|
|
11157
|
-
path: targetNode.metaPath,
|
|
11158
|
-
meta: projectMeta(meta),
|
|
11159
|
-
scope: {
|
|
11160
|
-
ownedFiles: scopeFiles.length,
|
|
11161
|
-
childMetas: targetNode.children.length,
|
|
11162
|
-
totalFiles: allFiles.length,
|
|
11163
|
-
},
|
|
11164
|
-
staleness: {
|
|
11165
|
-
seconds: staleSeconds,
|
|
11166
|
-
score: Math.round(score * 100) / 100,
|
|
11167
|
-
},
|
|
11168
|
-
};
|
|
11169
|
-
// Cross-refs status
|
|
11170
|
-
const crossRefsRaw = meta._crossRefs;
|
|
11171
|
-
if (Array.isArray(crossRefsRaw) && crossRefsRaw.length > 0) {
|
|
11172
|
-
response.crossRefs = await Promise.all(crossRefsRaw.map(async (refPath) => {
|
|
11173
|
-
const rp = String(refPath);
|
|
11174
|
-
const refMetaFile = join(rp, '.meta', 'meta.json');
|
|
11175
|
-
try {
|
|
11176
|
-
const refMeta = JSON.parse(await readFile(refMetaFile, 'utf8'));
|
|
11177
|
-
return {
|
|
11178
|
-
path: rp,
|
|
11179
|
-
status: 'resolved',
|
|
11180
|
-
hasContent: Boolean(refMeta._content),
|
|
11181
|
-
};
|
|
11182
|
-
}
|
|
11183
|
-
catch {
|
|
11184
|
-
return { path: rp, status: 'missing' };
|
|
11185
|
-
}
|
|
11186
|
-
}));
|
|
10729
|
+
await this.registrar.checkAndReregister(data.uptime);
|
|
11187
10730
|
}
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
const limit = typeof query.includeArchive === 'number'
|
|
11191
|
-
? query.includeArchive
|
|
11192
|
-
: undefined;
|
|
11193
|
-
try {
|
|
11194
|
-
response.archive = await readArchiveFromWatcher(watcher, targetNode.metaPath, config.metaArchiveProperty, limit, projectMeta);
|
|
11195
|
-
}
|
|
11196
|
-
catch {
|
|
11197
|
-
response.archive = await readArchiveFromDisk(targetNode.metaPath, limit, projectMeta);
|
|
11198
|
-
}
|
|
10731
|
+
catch (err) {
|
|
10732
|
+
this.logger.debug({ err }, 'Watcher health check: unreachable (expected during startup)');
|
|
11199
10733
|
}
|
|
11200
|
-
|
|
11201
|
-
});
|
|
10734
|
+
}
|
|
11202
10735
|
}
|
|
11203
10736
|
|
|
11204
10737
|
/**
|
|
11205
|
-
*
|
|
10738
|
+
* Virtual rule registration with jeeves-watcher.
|
|
11206
10739
|
*
|
|
11207
|
-
*
|
|
10740
|
+
* Service registers inference rules at startup (with retry) and
|
|
10741
|
+
* re-registers opportunistically when watcher restart is detected.
|
|
10742
|
+
*
|
|
10743
|
+
* @module rules
|
|
11208
10744
|
*/
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
10745
|
+
const SOURCE = 'jeeves-meta';
|
|
10746
|
+
const MAX_RETRIES = 10;
|
|
10747
|
+
const RETRY_BASE_MS = 2000;
|
|
10748
|
+
/**
|
|
10749
|
+
* Convert a `Record<string, unknown>` config property into watcher
|
|
10750
|
+
* schema `set` directives: `{ key: { set: value } }` per entry.
|
|
10751
|
+
*/
|
|
10752
|
+
function toSchemaSetDirectives(props) {
|
|
10753
|
+
return Object.fromEntries(Object.entries(props).map(([k, v]) => [k, { set: v }]));
|
|
10754
|
+
}
|
|
10755
|
+
/** Build the three virtual rule definitions. */
|
|
10756
|
+
function buildMetaRules(config) {
|
|
10757
|
+
return [
|
|
10758
|
+
{
|
|
10759
|
+
name: 'meta-current',
|
|
10760
|
+
description: 'Live jeeves-meta .meta/meta.json files',
|
|
10761
|
+
match: {
|
|
10762
|
+
properties: {
|
|
10763
|
+
file: {
|
|
10764
|
+
properties: {
|
|
10765
|
+
path: { type: 'string', glob: '**/.meta/meta.json' },
|
|
10766
|
+
},
|
|
10767
|
+
},
|
|
10768
|
+
},
|
|
10769
|
+
},
|
|
10770
|
+
schema: [
|
|
10771
|
+
'base',
|
|
10772
|
+
{
|
|
10773
|
+
properties: {
|
|
10774
|
+
...toSchemaSetDirectives(config.metaProperty),
|
|
10775
|
+
meta_id: { type: 'string', set: '{{json._id}}' },
|
|
10776
|
+
meta_steer: { type: 'string', set: '{{json._steer}}' },
|
|
10777
|
+
meta_depth: { type: 'number', set: '{{json._depth}}' },
|
|
10778
|
+
meta_emphasis: { type: 'number', set: '{{json._emphasis}}' },
|
|
10779
|
+
meta_synthesis_count: {
|
|
10780
|
+
type: 'integer',
|
|
10781
|
+
set: '{{json._synthesisCount}}',
|
|
10782
|
+
},
|
|
10783
|
+
meta_structure_hash: {
|
|
10784
|
+
type: 'string',
|
|
10785
|
+
set: '{{json._structureHash}}',
|
|
10786
|
+
},
|
|
10787
|
+
meta_architect_tokens: {
|
|
10788
|
+
type: 'integer',
|
|
10789
|
+
set: '{{json._architectTokens}}',
|
|
10790
|
+
},
|
|
10791
|
+
meta_builder_tokens: {
|
|
10792
|
+
type: 'integer',
|
|
10793
|
+
set: '{{json._builderTokens}}',
|
|
10794
|
+
},
|
|
10795
|
+
meta_critic_tokens: {
|
|
10796
|
+
type: 'integer',
|
|
10797
|
+
set: '{{json._criticTokens}}',
|
|
10798
|
+
},
|
|
10799
|
+
meta_error_step: {
|
|
10800
|
+
type: 'string',
|
|
10801
|
+
set: '{{json._error.step}}',
|
|
10802
|
+
},
|
|
10803
|
+
generated_at: {
|
|
10804
|
+
type: 'string',
|
|
10805
|
+
set: '{{json._generatedAt}}',
|
|
10806
|
+
},
|
|
10807
|
+
generated_at_unix: {
|
|
10808
|
+
type: 'integer',
|
|
10809
|
+
set: '{{toUnix json._generatedAt}}',
|
|
10810
|
+
},
|
|
10811
|
+
has_error: {
|
|
10812
|
+
type: 'boolean',
|
|
10813
|
+
set: '{{#if json._error}}true{{else}}false{{/if}}',
|
|
10814
|
+
},
|
|
10815
|
+
},
|
|
10816
|
+
},
|
|
10817
|
+
],
|
|
10818
|
+
render: {
|
|
10819
|
+
frontmatter: [
|
|
10820
|
+
'meta_id',
|
|
10821
|
+
'generated_at',
|
|
10822
|
+
'*',
|
|
10823
|
+
'!_*',
|
|
10824
|
+
'!json',
|
|
10825
|
+
'!file',
|
|
10826
|
+
'!has_error',
|
|
10827
|
+
],
|
|
10828
|
+
body: [{ path: 'json._content', heading: 1, label: 'Synthesis' }],
|
|
10829
|
+
},
|
|
10830
|
+
renderAs: 'md',
|
|
10831
|
+
},
|
|
10832
|
+
{
|
|
10833
|
+
name: 'meta-archive',
|
|
10834
|
+
description: 'Archived jeeves-meta .meta/archive snapshots',
|
|
10835
|
+
match: {
|
|
10836
|
+
properties: {
|
|
10837
|
+
file: {
|
|
10838
|
+
properties: {
|
|
10839
|
+
path: { type: 'string', glob: '**/.meta/archive/*.json' },
|
|
10840
|
+
},
|
|
10841
|
+
},
|
|
10842
|
+
},
|
|
10843
|
+
},
|
|
10844
|
+
schema: [
|
|
10845
|
+
'base',
|
|
10846
|
+
{
|
|
10847
|
+
properties: {
|
|
10848
|
+
...toSchemaSetDirectives(config.metaArchiveProperty),
|
|
10849
|
+
meta_id: { type: 'string', set: '{{json._id}}' },
|
|
10850
|
+
archived: { type: 'boolean', set: 'true' },
|
|
10851
|
+
archived_at: { type: 'string', set: '{{json._archivedAt}}' },
|
|
10852
|
+
},
|
|
10853
|
+
},
|
|
10854
|
+
],
|
|
10855
|
+
render: {
|
|
10856
|
+
frontmatter: ['meta_id', 'archived', 'archived_at'],
|
|
10857
|
+
body: [
|
|
10858
|
+
{
|
|
10859
|
+
path: 'json._content',
|
|
10860
|
+
heading: 1,
|
|
10861
|
+
label: 'Synthesis (archived)',
|
|
10862
|
+
},
|
|
10863
|
+
],
|
|
10864
|
+
},
|
|
10865
|
+
renderAs: 'md',
|
|
10866
|
+
},
|
|
10867
|
+
{
|
|
10868
|
+
name: 'meta-config',
|
|
10869
|
+
description: 'jeeves-meta configuration file',
|
|
10870
|
+
match: {
|
|
10871
|
+
properties: {
|
|
10872
|
+
file: {
|
|
10873
|
+
properties: {
|
|
10874
|
+
path: { type: 'string', glob: '**/jeeves-meta.config.json' },
|
|
10875
|
+
},
|
|
10876
|
+
},
|
|
10877
|
+
},
|
|
10878
|
+
},
|
|
10879
|
+
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
10880
|
+
render: {
|
|
10881
|
+
frontmatter: [
|
|
10882
|
+
'watcherUrl',
|
|
10883
|
+
'gatewayUrl',
|
|
10884
|
+
'architectEvery',
|
|
10885
|
+
'depthWeight',
|
|
10886
|
+
'maxArchive',
|
|
10887
|
+
'maxLines',
|
|
10888
|
+
],
|
|
10889
|
+
body: [
|
|
10890
|
+
{
|
|
10891
|
+
path: 'json.defaultArchitect',
|
|
10892
|
+
heading: 2,
|
|
10893
|
+
label: 'Default Architect Prompt',
|
|
10894
|
+
},
|
|
10895
|
+
{
|
|
10896
|
+
path: 'json.defaultCritic',
|
|
10897
|
+
heading: 2,
|
|
10898
|
+
label: 'Default Critic Prompt',
|
|
10899
|
+
},
|
|
10900
|
+
],
|
|
10901
|
+
},
|
|
10902
|
+
renderAs: 'md',
|
|
10903
|
+
},
|
|
10904
|
+
];
|
|
10905
|
+
}
|
|
10906
|
+
/**
|
|
10907
|
+
* Manages virtual rule registration with watcher.
|
|
10908
|
+
*
|
|
10909
|
+
* - Registers at startup with exponential retry
|
|
10910
|
+
* - Tracks watcher uptime for restart detection
|
|
10911
|
+
* - Re-registers opportunistically when uptime decreases
|
|
10912
|
+
*/
|
|
10913
|
+
class RuleRegistrar {
|
|
10914
|
+
config;
|
|
10915
|
+
logger;
|
|
10916
|
+
watcherClient;
|
|
10917
|
+
lastWatcherUptime = null;
|
|
10918
|
+
registered = false;
|
|
10919
|
+
constructor(config, logger, watcher) {
|
|
10920
|
+
this.config = config;
|
|
10921
|
+
this.logger = logger;
|
|
10922
|
+
this.watcherClient = watcher;
|
|
10923
|
+
}
|
|
10924
|
+
/** Whether rules have been successfully registered. */
|
|
10925
|
+
get isRegistered() {
|
|
10926
|
+
return this.registered;
|
|
10927
|
+
}
|
|
10928
|
+
/**
|
|
10929
|
+
* Register rules with watcher. Retries with exponential backoff.
|
|
10930
|
+
* Non-blocking — logs errors but never throws.
|
|
10931
|
+
*/
|
|
10932
|
+
async register() {
|
|
10933
|
+
const rules = buildMetaRules(this.config);
|
|
10934
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
10935
|
+
try {
|
|
10936
|
+
await this.watcherClient.registerRules(SOURCE, rules);
|
|
10937
|
+
this.registered = true;
|
|
10938
|
+
this.logger.info('Virtual rules registered with watcher');
|
|
10939
|
+
return;
|
|
10940
|
+
}
|
|
10941
|
+
catch (err) {
|
|
10942
|
+
const delayMs = RETRY_BASE_MS * Math.pow(2, attempt);
|
|
10943
|
+
this.logger.warn({ attempt: attempt + 1, delayMs, err }, 'Rule registration failed, retrying');
|
|
10944
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
this.logger.error('Rule registration failed after max retries — service degraded');
|
|
10948
|
+
}
|
|
10949
|
+
/**
|
|
10950
|
+
* Check watcher uptime and re-register if it decreased (restart detected).
|
|
10951
|
+
*
|
|
10952
|
+
* @param currentUptime - Current watcher uptime in seconds.
|
|
10953
|
+
*/
|
|
10954
|
+
async checkAndReregister(currentUptime) {
|
|
10955
|
+
if (this.lastWatcherUptime !== null &&
|
|
10956
|
+
currentUptime < this.lastWatcherUptime) {
|
|
10957
|
+
this.logger.info({ previous: this.lastWatcherUptime, current: currentUptime }, 'Watcher restart detected — re-registering rules');
|
|
10958
|
+
this.registered = false;
|
|
10959
|
+
await this.register();
|
|
10960
|
+
}
|
|
10961
|
+
this.lastWatcherUptime = currentUptime;
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
|
|
10965
|
+
/**
|
|
10966
|
+
* Post-registration verification of virtual rule application.
|
|
10967
|
+
*
|
|
10968
|
+
* After rules are registered with the watcher, verifies that .meta/meta.json
|
|
10969
|
+
* files are discoverable via watcher walk (which depends on virtual rules
|
|
10970
|
+
* being applied). Logs a warning if expected metas are not found.
|
|
10971
|
+
*
|
|
10972
|
+
* @module rules/verify
|
|
10973
|
+
*/
|
|
10974
|
+
/**
|
|
10975
|
+
* Verify that virtual rules are applied to indexed .meta/meta.json files.
|
|
10976
|
+
*
|
|
10977
|
+
* Runs a discovery pass and logs the result. If no metas are found but
|
|
10978
|
+
* the filesystem likely has some, logs a warning suggesting reindex.
|
|
10979
|
+
*
|
|
10980
|
+
* @param watcher - WatcherClient for discovery.
|
|
10981
|
+
* @param logger - Logger for reporting results.
|
|
10982
|
+
* @returns Number of metas discovered.
|
|
10983
|
+
*/
|
|
10984
|
+
async function verifyRuleApplication(watcher, logger) {
|
|
10985
|
+
try {
|
|
10986
|
+
const metaPaths = await discoverMetas(watcher);
|
|
10987
|
+
if (metaPaths.length === 0) {
|
|
10988
|
+
logger.warn({ count: 0 }, 'Post-registration verification: no .meta/meta.json files found via watcher walk. ' +
|
|
10989
|
+
'Virtual rules may not be applied to indexed files. ' +
|
|
10990
|
+
'If metas exist, a path-scoped reindex may be needed.');
|
|
10991
|
+
}
|
|
10992
|
+
else {
|
|
10993
|
+
logger.info({ count: metaPaths.length }, 'Post-registration verification: metas discoverable');
|
|
10994
|
+
}
|
|
10995
|
+
return metaPaths.length;
|
|
10996
|
+
}
|
|
10997
|
+
catch (err) {
|
|
10998
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, 'Post-registration verification failed (watcher may be unavailable)');
|
|
10999
|
+
return 0;
|
|
11000
|
+
}
|
|
11001
|
+
}
|
|
11002
|
+
|
|
11003
|
+
/**
|
|
11004
|
+
* Core seed logic — create a .meta/ directory with initial meta.json.
|
|
11005
|
+
*
|
|
11006
|
+
* Shared between the POST /seed route handler and the auto-seed pass.
|
|
11007
|
+
*
|
|
11008
|
+
* @module seed/createMeta
|
|
11009
|
+
*/
|
|
11010
|
+
/**
|
|
11011
|
+
* Create a .meta/ directory with an initial meta.json.
|
|
11012
|
+
*
|
|
11013
|
+
* Does NOT check for existing .meta/ — caller is responsible for that guard.
|
|
11014
|
+
*
|
|
11015
|
+
* @param ownerPath - The owner directory path.
|
|
11016
|
+
* @param options - Optional cross-refs and steering prompt.
|
|
11017
|
+
* @returns The meta directory path and generated ID.
|
|
11018
|
+
*/
|
|
11019
|
+
async function createMeta(ownerPath, options) {
|
|
11020
|
+
const metaDir = resolveMetaDir(ownerPath);
|
|
11021
|
+
await mkdir(metaDir, { recursive: true });
|
|
11022
|
+
const _id = randomUUID();
|
|
11023
|
+
const metaJson = { _id };
|
|
11024
|
+
if (options?.crossRefs !== undefined)
|
|
11025
|
+
metaJson._crossRefs = options.crossRefs;
|
|
11026
|
+
if (options?.steer !== undefined)
|
|
11027
|
+
metaJson._steer = options.steer;
|
|
11028
|
+
const metaJsonPath = join(metaDir, 'meta.json');
|
|
11029
|
+
await writeFile(metaJsonPath, JSON.stringify(metaJson, null, 2) + '\n');
|
|
11030
|
+
return { metaDir, _id };
|
|
11031
|
+
}
|
|
11032
|
+
/**
|
|
11033
|
+
* Check if a .meta/ directory already exists for an owner path.
|
|
11034
|
+
*
|
|
11035
|
+
* @param ownerPath - The owner directory path.
|
|
11036
|
+
* @returns True if .meta/ already exists.
|
|
11037
|
+
*/
|
|
11038
|
+
function metaExists(ownerPath) {
|
|
11039
|
+
return existsSync(resolveMetaDir(ownerPath));
|
|
11040
|
+
}
|
|
11041
|
+
|
|
11042
|
+
/**
|
|
11043
|
+
* Auto-seed pass — scan for directories matching policy rules and seed them.
|
|
11044
|
+
*
|
|
11045
|
+
* Runs before discovery in each scheduler tick. For each auto-seed rule,
|
|
11046
|
+
* walks matching directories via the watcher and creates .meta/ directories
|
|
11047
|
+
* for those that don't already have one.
|
|
11048
|
+
*
|
|
11049
|
+
* Rules are processed in array order; last match wins for steer/crossRefs.
|
|
11050
|
+
*
|
|
11051
|
+
* @module seed/autoSeed
|
|
11052
|
+
*/
|
|
11053
|
+
/**
|
|
11054
|
+
* Extract parent directory paths from watcher walk results.
|
|
11055
|
+
*
|
|
11056
|
+
* Walk returns file paths; we need the unique set of immediate parent
|
|
11057
|
+
* directories that could be owners.
|
|
11058
|
+
*/
|
|
11059
|
+
function extractDirectories(filePaths) {
|
|
11060
|
+
const dirs = new Set();
|
|
11061
|
+
for (const fp of filePaths) {
|
|
11062
|
+
const dir = posix.dirname(fp);
|
|
11063
|
+
if (dir !== '.' && dir !== '/') {
|
|
11064
|
+
dirs.add(dir);
|
|
11065
|
+
}
|
|
11066
|
+
}
|
|
11067
|
+
return [...dirs];
|
|
11068
|
+
}
|
|
11069
|
+
/**
|
|
11070
|
+
* Run the auto-seed pass: apply policy rules and create missing metas.
|
|
11071
|
+
*
|
|
11072
|
+
* @param rules - Auto-seed policy rules from config.
|
|
11073
|
+
* @param watcher - Watcher client for filesystem enumeration.
|
|
11074
|
+
* @param logger - Logger for reporting seed actions.
|
|
11075
|
+
* @returns Summary of what was seeded.
|
|
11076
|
+
*/
|
|
11077
|
+
async function autoSeedPass(rules, watcher, logger) {
|
|
11078
|
+
if (rules.length === 0)
|
|
11079
|
+
return { seeded: 0, paths: [] };
|
|
11080
|
+
// Build a map of ownerPath → effective options (last match wins)
|
|
11081
|
+
const candidates = new Map();
|
|
11082
|
+
for (const rule of rules) {
|
|
11083
|
+
const files = await watcher.walk([rule.match]);
|
|
11084
|
+
const dirs = extractDirectories(files);
|
|
11085
|
+
for (const dir of dirs) {
|
|
11086
|
+
candidates.set(dir, {
|
|
11087
|
+
steer: rule.steer,
|
|
11088
|
+
crossRefs: rule.crossRefs,
|
|
11089
|
+
});
|
|
11090
|
+
}
|
|
11091
|
+
}
|
|
11092
|
+
// Filter out paths that already have .meta/meta.json
|
|
11093
|
+
const toSeed = [];
|
|
11094
|
+
for (const [path, opts] of candidates) {
|
|
11095
|
+
if (!metaExists(path)) {
|
|
11096
|
+
toSeed.push({ path, ...opts });
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
// Seed remaining
|
|
11100
|
+
const seededPaths = [];
|
|
11101
|
+
for (const candidate of toSeed) {
|
|
11102
|
+
try {
|
|
11103
|
+
await createMeta(candidate.path, {
|
|
11104
|
+
steer: candidate.steer,
|
|
11105
|
+
crossRefs: candidate.crossRefs,
|
|
11106
|
+
});
|
|
11107
|
+
seededPaths.push(candidate.path);
|
|
11108
|
+
logger?.info({ path: candidate.path }, 'auto-seeded meta');
|
|
11109
|
+
}
|
|
11110
|
+
catch (err) {
|
|
11111
|
+
logger?.warn({
|
|
11112
|
+
path: candidate.path,
|
|
11113
|
+
err: err instanceof Error ? err.message : String(err),
|
|
11114
|
+
}, 'auto-seed failed for path');
|
|
11115
|
+
}
|
|
11116
|
+
}
|
|
11117
|
+
return { seeded: seededPaths.length, paths: seededPaths };
|
|
11118
|
+
}
|
|
11119
|
+
|
|
11120
|
+
/**
|
|
11121
|
+
* Croner-based scheduler that discovers the stalest meta candidate each tick
|
|
11122
|
+
* and enqueues it for synthesis.
|
|
11123
|
+
*
|
|
11124
|
+
* @module scheduler
|
|
11125
|
+
*/
|
|
11126
|
+
const MAX_BACKOFF_MULTIPLIER = 4;
|
|
11127
|
+
/**
|
|
11128
|
+
* Periodic scheduler that discovers stale meta candidates and enqueues them.
|
|
11129
|
+
*
|
|
11130
|
+
* Supports adaptive backoff when no candidates are found and hot-reloadable
|
|
11131
|
+
* cron expressions via {@link Scheduler.updateSchedule}.
|
|
11132
|
+
*/
|
|
11133
|
+
class Scheduler {
|
|
11134
|
+
job = null;
|
|
11135
|
+
backoffMultiplier = 1;
|
|
11136
|
+
tickCount = 0;
|
|
11137
|
+
config;
|
|
11138
|
+
queue;
|
|
11139
|
+
logger;
|
|
11140
|
+
watcher;
|
|
11141
|
+
registrar = null;
|
|
11142
|
+
currentExpression;
|
|
11143
|
+
constructor(config, queue, logger, watcher) {
|
|
11144
|
+
this.config = config;
|
|
11145
|
+
this.queue = queue;
|
|
11146
|
+
this.logger = logger;
|
|
11147
|
+
this.watcher = watcher;
|
|
11148
|
+
this.currentExpression = config.schedule;
|
|
11149
|
+
}
|
|
11150
|
+
/** Set the rule registrar for watcher restart detection. */
|
|
11151
|
+
setRegistrar(registrar) {
|
|
11152
|
+
this.registrar = registrar;
|
|
11153
|
+
}
|
|
11154
|
+
/** Start the cron job. */
|
|
11155
|
+
start() {
|
|
11156
|
+
if (this.job)
|
|
11157
|
+
return;
|
|
11158
|
+
this.job = new Cron(this.currentExpression, () => {
|
|
11159
|
+
void this.tick();
|
|
11160
|
+
});
|
|
11161
|
+
this.logger.info({ schedule: this.currentExpression }, 'Scheduler started');
|
|
11162
|
+
}
|
|
11163
|
+
/** Stop the cron job. */
|
|
11164
|
+
stop() {
|
|
11165
|
+
if (!this.job)
|
|
11166
|
+
return;
|
|
11167
|
+
this.job.stop();
|
|
11168
|
+
this.job = null;
|
|
11169
|
+
this.backoffMultiplier = 1;
|
|
11170
|
+
this.logger.info('Scheduler stopped');
|
|
11171
|
+
}
|
|
11172
|
+
/** Hot-reload the cron schedule expression. */
|
|
11173
|
+
updateSchedule(expression) {
|
|
11174
|
+
this.currentExpression = expression;
|
|
11175
|
+
if (this.job) {
|
|
11176
|
+
this.job.stop();
|
|
11177
|
+
this.job = new Cron(expression, () => {
|
|
11178
|
+
void this.tick();
|
|
11179
|
+
});
|
|
11180
|
+
this.logger.info({ schedule: expression }, 'Schedule updated');
|
|
11181
|
+
}
|
|
11182
|
+
}
|
|
11183
|
+
/** Reset backoff multiplier (call after successful synthesis). */
|
|
11184
|
+
resetBackoff() {
|
|
11185
|
+
if (this.backoffMultiplier > 1) {
|
|
11186
|
+
this.logger.debug('Backoff reset after successful synthesis');
|
|
11187
|
+
}
|
|
11188
|
+
this.backoffMultiplier = 1;
|
|
11189
|
+
}
|
|
11190
|
+
/** Whether the scheduler is currently running. */
|
|
11191
|
+
get isRunning() {
|
|
11192
|
+
return this.job !== null;
|
|
11193
|
+
}
|
|
11194
|
+
/** Next scheduled tick time, or null if not running. */
|
|
11195
|
+
get nextRunAt() {
|
|
11196
|
+
if (!this.job)
|
|
11197
|
+
return null;
|
|
11198
|
+
return this.job.nextRun() ?? null;
|
|
11199
|
+
}
|
|
11200
|
+
/**
|
|
11201
|
+
* Single tick: discover stalest candidate and enqueue it.
|
|
11202
|
+
*
|
|
11203
|
+
* Skips if the queue is currently processing. Applies adaptive backoff
|
|
11204
|
+
* when no candidates are found.
|
|
11205
|
+
*/
|
|
11206
|
+
async tick() {
|
|
11207
|
+
this.tickCount++;
|
|
11208
|
+
// Apply backoff: skip ticks when backing off
|
|
11209
|
+
if (this.backoffMultiplier > 1 &&
|
|
11210
|
+
this.tickCount % this.backoffMultiplier !== 0) {
|
|
11211
|
+
this.logger.trace({
|
|
11212
|
+
backoffMultiplier: this.backoffMultiplier,
|
|
11213
|
+
tickCount: this.tickCount,
|
|
11214
|
+
}, 'Skipping tick (backoff)');
|
|
11215
|
+
return;
|
|
11216
|
+
}
|
|
11217
|
+
// Auto-seed pass: create .meta/ for matching directories
|
|
11218
|
+
if (this.config.autoSeed.length > 0) {
|
|
11219
|
+
try {
|
|
11220
|
+
const result = await autoSeedPass(this.config.autoSeed, this.watcher, this.logger);
|
|
11221
|
+
if (result.seeded > 0) {
|
|
11222
|
+
this.logger.info({ seeded: result.seeded }, 'Auto-seed pass completed');
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
catch (err) {
|
|
11226
|
+
this.logger.warn({ err }, 'Auto-seed pass failed');
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
11229
|
+
const candidate = await this.discoverStalest();
|
|
11230
|
+
if (!candidate) {
|
|
11231
|
+
this.backoffMultiplier = Math.min(this.backoffMultiplier * 2, MAX_BACKOFF_MULTIPLIER);
|
|
11232
|
+
this.logger.debug({ backoffMultiplier: this.backoffMultiplier }, 'No stale candidates found, increasing backoff');
|
|
11233
|
+
return;
|
|
11234
|
+
}
|
|
11235
|
+
this.queue.enqueue(candidate);
|
|
11236
|
+
this.logger.info({ path: candidate }, 'Enqueued stale candidate');
|
|
11237
|
+
// Opportunistic watcher restart detection
|
|
11238
|
+
if (this.registrar) {
|
|
11239
|
+
try {
|
|
11240
|
+
const statusRes = await fetch(new URL('/status', this.config.watcherUrl), {
|
|
11241
|
+
signal: AbortSignal.timeout(3000),
|
|
11242
|
+
});
|
|
11243
|
+
if (statusRes.ok) {
|
|
11244
|
+
const status = (await statusRes.json());
|
|
11245
|
+
if (typeof status.uptime === 'number') {
|
|
11246
|
+
await this.registrar.checkAndReregister(status.uptime);
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11249
|
+
}
|
|
11250
|
+
catch {
|
|
11251
|
+
// Watcher unreachable — skip uptime check
|
|
11252
|
+
}
|
|
11253
|
+
}
|
|
11254
|
+
}
|
|
11255
|
+
/**
|
|
11256
|
+
* Discover the stalest meta candidate via watcher.
|
|
11257
|
+
*/
|
|
11258
|
+
async discoverStalest() {
|
|
11259
|
+
try {
|
|
11260
|
+
const result = await listMetas(this.config, this.watcher);
|
|
11261
|
+
const stale = result.entries
|
|
11262
|
+
.filter((e) => e.stalenessSeconds > 0)
|
|
11263
|
+
.map((e) => ({
|
|
11264
|
+
node: e.node,
|
|
11265
|
+
meta: e.meta,
|
|
11266
|
+
actualStaleness: e.stalenessSeconds,
|
|
11267
|
+
}));
|
|
11268
|
+
return discoverStalestPath(stale, this.config.depthWeight);
|
|
11269
|
+
}
|
|
11270
|
+
catch (err) {
|
|
11271
|
+
this.logger.warn({ err }, 'Failed to discover stalest candidate');
|
|
11272
|
+
return null;
|
|
11273
|
+
}
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
|
|
11277
|
+
/**
|
|
11278
|
+
* GET /config — query service configuration with optional JSONPath.
|
|
11279
|
+
*
|
|
11280
|
+
* Replaces the old GET /config/validate endpoint with the core SDK's
|
|
11281
|
+
* `createConfigQueryHandler()` for JSONPath support.
|
|
11282
|
+
*
|
|
11283
|
+
* @module routes/config
|
|
11284
|
+
*/
|
|
11285
|
+
/** Return a sanitized copy of the config (redact gatewayApiKey). */
|
|
11286
|
+
function sanitizeConfig(config) {
|
|
11287
|
+
return {
|
|
11288
|
+
...config,
|
|
11289
|
+
gatewayApiKey: config.gatewayApiKey ? '[REDACTED]' : undefined,
|
|
11290
|
+
};
|
|
11291
|
+
}
|
|
11292
|
+
function registerConfigRoute(app, deps) {
|
|
11293
|
+
const configHandler = createConfigQueryHandler(() => sanitizeConfig(deps.config));
|
|
11294
|
+
app.get('/config', async (request, reply) => {
|
|
11295
|
+
const { path } = request.query;
|
|
11296
|
+
const result = await configHandler({ path });
|
|
11297
|
+
return reply.status(result.status).send(result.body);
|
|
11298
|
+
});
|
|
11299
|
+
}
|
|
11300
|
+
|
|
11301
|
+
/**
|
|
11302
|
+
* POST /config/apply — apply a config patch via the core SDK handler.
|
|
11303
|
+
*
|
|
11304
|
+
* @module routes/configApply
|
|
11305
|
+
*/
|
|
11306
|
+
/** Register the POST /config/apply route. */
|
|
11307
|
+
function registerConfigApplyRoute(app) {
|
|
11308
|
+
const handler = createConfigApplyHandler(metaDescriptor);
|
|
11309
|
+
app.post('/config/apply', async (request, reply) => {
|
|
11310
|
+
const result = await handler(request.body);
|
|
11311
|
+
return reply.status(result.status).send(result.body);
|
|
11312
|
+
});
|
|
11313
|
+
}
|
|
11314
|
+
|
|
11315
|
+
/**
|
|
11316
|
+
* GET /metas — list metas with optional filters.
|
|
11317
|
+
* GET /metas/:path — single meta detail.
|
|
11318
|
+
*
|
|
11319
|
+
* @module routes/metas
|
|
11320
|
+
*/
|
|
11321
|
+
const metasQuerySchema = z.object({
|
|
11322
|
+
pathPrefix: z.string().optional(),
|
|
11323
|
+
hasError: z
|
|
11324
|
+
.enum(['true', 'false'])
|
|
11325
|
+
.transform((v) => v === 'true')
|
|
11326
|
+
.optional(),
|
|
11327
|
+
staleHours: z
|
|
11328
|
+
.string()
|
|
11329
|
+
.transform(Number)
|
|
11330
|
+
.pipe(z.number().positive())
|
|
11331
|
+
.optional(),
|
|
11332
|
+
neverSynthesized: z
|
|
11333
|
+
.enum(['true', 'false'])
|
|
11334
|
+
.transform((v) => v === 'true')
|
|
11335
|
+
.optional(),
|
|
11336
|
+
locked: z
|
|
11337
|
+
.enum(['true', 'false'])
|
|
11338
|
+
.transform((v) => v === 'true')
|
|
11339
|
+
.optional(),
|
|
11340
|
+
fields: z.string().optional(),
|
|
11341
|
+
});
|
|
11342
|
+
const metaDetailQuerySchema = z.object({
|
|
11343
|
+
fields: z.string().optional(),
|
|
11344
|
+
includeArchive: z
|
|
11345
|
+
.union([
|
|
11346
|
+
z.enum(['true', 'false']).transform((v) => v === 'true'),
|
|
11347
|
+
z.string().transform(Number).pipe(z.number().int().nonnegative()),
|
|
11348
|
+
])
|
|
11349
|
+
.optional(),
|
|
11350
|
+
});
|
|
11351
|
+
function registerMetasRoutes(app, deps) {
|
|
11352
|
+
app.get('/metas', async (request) => {
|
|
11353
|
+
const query = metasQuerySchema.parse(request.query);
|
|
11354
|
+
const { config, watcher } = deps;
|
|
11355
|
+
const result = await listMetas(config, watcher);
|
|
11356
|
+
let entries = result.entries;
|
|
11357
|
+
// Apply filters
|
|
11358
|
+
if (query.pathPrefix) {
|
|
11359
|
+
entries = entries.filter((e) => e.path.includes(query.pathPrefix));
|
|
11360
|
+
}
|
|
11361
|
+
if (query.hasError !== undefined) {
|
|
11362
|
+
entries = entries.filter((e) => e.hasError === query.hasError);
|
|
11363
|
+
}
|
|
11364
|
+
if (query.neverSynthesized !== undefined) {
|
|
11365
|
+
entries = entries.filter((e) => (e.lastSynthesized === null) === query.neverSynthesized);
|
|
11366
|
+
}
|
|
11367
|
+
if (query.locked !== undefined) {
|
|
11368
|
+
entries = entries.filter((e) => e.locked === query.locked);
|
|
11369
|
+
}
|
|
11370
|
+
if (typeof query.staleHours === 'number') {
|
|
11371
|
+
entries = entries.filter((e) => e.stalenessSeconds >= query.staleHours * 3600);
|
|
11372
|
+
}
|
|
11373
|
+
// Summary (computed from filtered entries)
|
|
11374
|
+
const summary = computeSummary(entries, config.depthWeight);
|
|
11375
|
+
// Field projection
|
|
11376
|
+
const fieldList = query.fields?.split(',');
|
|
11377
|
+
const defaultFields = [
|
|
11378
|
+
'path',
|
|
11379
|
+
'depth',
|
|
11380
|
+
'emphasis',
|
|
11381
|
+
'stalenessSeconds',
|
|
11382
|
+
'lastSynthesized',
|
|
11383
|
+
'hasError',
|
|
11384
|
+
'locked',
|
|
11385
|
+
'architectTokens',
|
|
11386
|
+
'builderTokens',
|
|
11387
|
+
'criticTokens',
|
|
11388
|
+
];
|
|
11389
|
+
const projectedFields = fieldList ?? defaultFields;
|
|
11390
|
+
const metas = entries.map((e) => {
|
|
11391
|
+
const full = {
|
|
11392
|
+
path: e.path,
|
|
11393
|
+
depth: e.depth,
|
|
11394
|
+
emphasis: e.emphasis,
|
|
11395
|
+
stalenessSeconds: e.stalenessSeconds === Infinity
|
|
11396
|
+
? null
|
|
11397
|
+
: Math.round(e.stalenessSeconds),
|
|
11398
|
+
lastSynthesized: e.lastSynthesized,
|
|
11399
|
+
hasError: e.hasError,
|
|
11400
|
+
locked: e.locked,
|
|
11401
|
+
architectTokens: e.architectTokens,
|
|
11402
|
+
builderTokens: e.builderTokens,
|
|
11403
|
+
criticTokens: e.criticTokens,
|
|
11404
|
+
};
|
|
11405
|
+
const projected = {};
|
|
11406
|
+
for (const f of projectedFields) {
|
|
11407
|
+
if (f in full)
|
|
11408
|
+
projected[f] = full[f];
|
|
11409
|
+
}
|
|
11410
|
+
return projected;
|
|
11411
|
+
});
|
|
11412
|
+
return { summary, metas };
|
|
11413
|
+
});
|
|
11414
|
+
app.get('/metas/:path', async (request, reply) => {
|
|
11415
|
+
const query = metaDetailQuerySchema.parse(request.query);
|
|
11416
|
+
const { config, watcher } = deps;
|
|
11417
|
+
const targetPath = normalizePath(decodeURIComponent(request.params.path));
|
|
11418
|
+
const result = await listMetas(config, watcher);
|
|
11419
|
+
const targetNode = findNode(result.tree, targetPath);
|
|
11420
|
+
if (!targetNode) {
|
|
11421
|
+
return reply.status(404).send({
|
|
11422
|
+
error: 'NOT_FOUND',
|
|
11423
|
+
message: 'Meta path not found: ' + targetPath,
|
|
11424
|
+
});
|
|
11425
|
+
}
|
|
11426
|
+
const meta = JSON.parse(await readFile(join(targetNode.metaPath, 'meta.json'), 'utf8'));
|
|
11427
|
+
// Field projection
|
|
11428
|
+
const defaultExclude = new Set([
|
|
11429
|
+
'_architect',
|
|
11430
|
+
'_builder',
|
|
11431
|
+
'_critic',
|
|
11432
|
+
'_content',
|
|
11433
|
+
'_feedback',
|
|
11434
|
+
]);
|
|
11435
|
+
const fieldList = query.fields?.split(',');
|
|
11436
|
+
const projectMeta = (m) => {
|
|
11437
|
+
if (fieldList) {
|
|
11438
|
+
const r = {};
|
|
11439
|
+
for (const f of fieldList)
|
|
11440
|
+
r[f] = m[f];
|
|
11441
|
+
return r;
|
|
11442
|
+
}
|
|
11443
|
+
const r = {};
|
|
11444
|
+
for (const [k, v] of Object.entries(m)) {
|
|
11445
|
+
if (!defaultExclude.has(k))
|
|
11446
|
+
r[k] = v;
|
|
11447
|
+
}
|
|
11448
|
+
return r;
|
|
11449
|
+
};
|
|
11450
|
+
// Compute scope
|
|
11451
|
+
const { scopeFiles, allFiles } = await getScopeFiles(targetNode, watcher);
|
|
11452
|
+
// Compute staleness
|
|
11453
|
+
const metaTyped = meta;
|
|
11454
|
+
const staleSeconds = metaTyped._generatedAt
|
|
11455
|
+
? Math.round((Date.now() - new Date(metaTyped._generatedAt).getTime()) / 1000)
|
|
11456
|
+
: null;
|
|
11457
|
+
const score = computeStalenessScore(staleSeconds, metaTyped._depth ?? 0, metaTyped._emphasis ?? 1, config.depthWeight);
|
|
11458
|
+
const response = {
|
|
11459
|
+
path: targetNode.metaPath,
|
|
11460
|
+
meta: projectMeta(meta),
|
|
11461
|
+
scope: {
|
|
11462
|
+
ownedFiles: scopeFiles.length,
|
|
11463
|
+
childMetas: targetNode.children.length,
|
|
11464
|
+
totalFiles: allFiles.length,
|
|
11465
|
+
},
|
|
11466
|
+
staleness: {
|
|
11467
|
+
seconds: staleSeconds,
|
|
11468
|
+
score: Math.round(score * 100) / 100,
|
|
11469
|
+
},
|
|
11470
|
+
};
|
|
11471
|
+
// Cross-refs status
|
|
11472
|
+
const crossRefsRaw = meta._crossRefs;
|
|
11473
|
+
if (Array.isArray(crossRefsRaw) && crossRefsRaw.length > 0) {
|
|
11474
|
+
response.crossRefs = await Promise.all(crossRefsRaw.map(async (refPath) => {
|
|
11475
|
+
const rp = String(refPath);
|
|
11476
|
+
const refMetaFile = join(rp, '.meta', 'meta.json');
|
|
11477
|
+
try {
|
|
11478
|
+
const refMeta = JSON.parse(await readFile(refMetaFile, 'utf8'));
|
|
11479
|
+
return {
|
|
11480
|
+
path: rp,
|
|
11481
|
+
status: 'resolved',
|
|
11482
|
+
hasContent: Boolean(refMeta._content),
|
|
11483
|
+
};
|
|
11484
|
+
}
|
|
11485
|
+
catch {
|
|
11486
|
+
return { path: rp, status: 'missing' };
|
|
11487
|
+
}
|
|
11488
|
+
}));
|
|
11489
|
+
}
|
|
11490
|
+
// Archive
|
|
11491
|
+
if (query.includeArchive) {
|
|
11492
|
+
const limit = typeof query.includeArchive === 'number'
|
|
11493
|
+
? query.includeArchive
|
|
11494
|
+
: undefined;
|
|
11495
|
+
try {
|
|
11496
|
+
response.archive = await readArchiveFromWatcher(watcher, targetNode.metaPath, config.metaArchiveProperty, limit, projectMeta);
|
|
11497
|
+
}
|
|
11498
|
+
catch {
|
|
11499
|
+
response.archive = await readArchiveFromDisk(targetNode.metaPath, limit, projectMeta);
|
|
11500
|
+
}
|
|
11501
|
+
}
|
|
11502
|
+
return response;
|
|
11503
|
+
});
|
|
11504
|
+
}
|
|
11505
|
+
|
|
11506
|
+
/**
|
|
11507
|
+
* GET /preview — dry-run synthesis preview.
|
|
11508
|
+
*
|
|
11509
|
+
* @module routes/preview
|
|
11510
|
+
*/
|
|
11511
|
+
function registerPreviewRoute(app, deps) {
|
|
11512
|
+
app.get('/preview', async (request, reply) => {
|
|
11513
|
+
const { config, watcher } = deps;
|
|
11212
11514
|
const query = request.query;
|
|
11213
11515
|
let result;
|
|
11214
11516
|
try {
|
|
@@ -11374,467 +11676,201 @@ function registerSeedRoute(app, deps) {
|
|
|
11374
11676
|
* GET /status — service health and status overview.
|
|
11375
11677
|
*
|
|
11376
11678
|
* Uses the core SDK's `createStatusHandler` factory with a custom
|
|
11377
|
-
* `getHealth` callback that preserves all existing health details.
|
|
11378
|
-
*
|
|
11379
|
-
* @module routes/status
|
|
11380
|
-
*/
|
|
11381
|
-
async function checkDependency(url, path) {
|
|
11382
|
-
const checkedAt = new Date().toISOString();
|
|
11383
|
-
try {
|
|
11384
|
-
const res = await fetch(new URL(path, url), {
|
|
11385
|
-
signal: AbortSignal.timeout(3000),
|
|
11386
|
-
});
|
|
11387
|
-
return { url, status: res.ok ? 'ok' : 'error', checkedAt };
|
|
11388
|
-
}
|
|
11389
|
-
catch {
|
|
11390
|
-
return { url, status: 'unreachable', checkedAt };
|
|
11391
|
-
}
|
|
11392
|
-
}
|
|
11393
|
-
/** Check watcher, surfacing initialScan.active as indexing state. */
|
|
11394
|
-
async function checkWatcher(url) {
|
|
11395
|
-
const checkedAt = new Date().toISOString();
|
|
11396
|
-
try {
|
|
11397
|
-
const res = await fetch(new URL('/status', url), {
|
|
11398
|
-
signal: AbortSignal.timeout(3000),
|
|
11399
|
-
});
|
|
11400
|
-
if (!res.ok)
|
|
11401
|
-
return { url, status: 'error', checkedAt };
|
|
11402
|
-
const data = (await res.json());
|
|
11403
|
-
const indexing = data.initialScan?.active === true;
|
|
11404
|
-
return {
|
|
11405
|
-
url,
|
|
11406
|
-
status: indexing ? 'indexing' : 'ok',
|
|
11407
|
-
checkedAt,
|
|
11408
|
-
indexing,
|
|
11409
|
-
};
|
|
11410
|
-
}
|
|
11411
|
-
catch {
|
|
11412
|
-
return { url, status: 'unreachable', checkedAt };
|
|
11413
|
-
}
|
|
11414
|
-
}
|
|
11415
|
-
function registerStatusRoute(app, deps) {
|
|
11416
|
-
const statusHandler = createStatusHandler({
|
|
11417
|
-
name: SERVICE_NAME,
|
|
11418
|
-
version: SERVICE_VERSION,
|
|
11419
|
-
getHealth: async () => {
|
|
11420
|
-
const { config, queue, scheduler, stats } = deps;
|
|
11421
|
-
// On-demand dependency checks
|
|
11422
|
-
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
11423
|
-
checkWatcher(config.watcherUrl),
|
|
11424
|
-
checkDependency(config.gatewayUrl, '/status'),
|
|
11425
|
-
]);
|
|
11426
|
-
return {
|
|
11427
|
-
currentTarget: queue.current?.path ?? null,
|
|
11428
|
-
queue: queue.getState(),
|
|
11429
|
-
stats: {
|
|
11430
|
-
totalSyntheses: stats.totalSyntheses,
|
|
11431
|
-
totalTokens: stats.totalTokens,
|
|
11432
|
-
totalErrors: stats.totalErrors,
|
|
11433
|
-
lastCycleDurationMs: stats.lastCycleDurationMs,
|
|
11434
|
-
lastCycleAt: stats.lastCycleAt,
|
|
11435
|
-
},
|
|
11436
|
-
schedule: {
|
|
11437
|
-
expression: config.schedule,
|
|
11438
|
-
nextAt: scheduler?.nextRunAt?.toISOString() ?? null,
|
|
11439
|
-
},
|
|
11440
|
-
dependencies: {
|
|
11441
|
-
watcher: {
|
|
11442
|
-
...watcherHealth,
|
|
11443
|
-
rulesRegistered: deps.registrar?.isRegistered ?? false,
|
|
11444
|
-
},
|
|
11445
|
-
gateway: gatewayHealth,
|
|
11446
|
-
},
|
|
11447
|
-
};
|
|
11448
|
-
},
|
|
11449
|
-
});
|
|
11450
|
-
app.get('/status', async (_request, reply) => {
|
|
11451
|
-
const result = await statusHandler();
|
|
11452
|
-
return reply.status(result.status).send(result.body);
|
|
11453
|
-
});
|
|
11454
|
-
}
|
|
11455
|
-
|
|
11456
|
-
/**
|
|
11457
|
-
* POST /synthesize route handler.
|
|
11458
|
-
*
|
|
11459
|
-
* @module routes/synthesize
|
|
11460
|
-
*/
|
|
11461
|
-
const synthesizeBodySchema = z.object({
|
|
11462
|
-
path: z.string().optional(),
|
|
11463
|
-
});
|
|
11464
|
-
/** Register the POST /synthesize route. */
|
|
11465
|
-
function registerSynthesizeRoute(app, deps) {
|
|
11466
|
-
app.post('/synthesize', async (request, reply) => {
|
|
11467
|
-
const body = synthesizeBodySchema.parse(request.body);
|
|
11468
|
-
const { config, watcher, queue } = deps;
|
|
11469
|
-
let targetPath;
|
|
11470
|
-
if (body.path) {
|
|
11471
|
-
targetPath = body.path;
|
|
11472
|
-
}
|
|
11473
|
-
else {
|
|
11474
|
-
// Discover stalest candidate
|
|
11475
|
-
let result;
|
|
11476
|
-
try {
|
|
11477
|
-
result = await listMetas(config, watcher);
|
|
11478
|
-
}
|
|
11479
|
-
catch {
|
|
11480
|
-
return reply.status(503).send({
|
|
11481
|
-
error: 'SERVICE_UNAVAILABLE',
|
|
11482
|
-
message: 'Watcher unreachable — cannot discover candidates',
|
|
11483
|
-
});
|
|
11484
|
-
}
|
|
11485
|
-
const stale = result.entries
|
|
11486
|
-
.filter((e) => e.stalenessSeconds > 0)
|
|
11487
|
-
.map((e) => ({
|
|
11488
|
-
node: e.node,
|
|
11489
|
-
meta: e.meta,
|
|
11490
|
-
actualStaleness: e.stalenessSeconds,
|
|
11491
|
-
}));
|
|
11492
|
-
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
11493
|
-
if (!stalest) {
|
|
11494
|
-
return reply.code(200).send({
|
|
11495
|
-
status: 'skipped',
|
|
11496
|
-
message: 'No stale metas found. Nothing to synthesize.',
|
|
11497
|
-
});
|
|
11498
|
-
}
|
|
11499
|
-
targetPath = stalest;
|
|
11500
|
-
}
|
|
11501
|
-
const result = queue.enqueue(targetPath, body.path !== undefined);
|
|
11502
|
-
return reply.code(202).send({
|
|
11503
|
-
status: 'accepted',
|
|
11504
|
-
path: targetPath,
|
|
11505
|
-
queuePosition: result.position,
|
|
11506
|
-
alreadyQueued: result.alreadyQueued,
|
|
11507
|
-
});
|
|
11508
|
-
});
|
|
11509
|
-
}
|
|
11510
|
-
|
|
11511
|
-
/**
|
|
11512
|
-
* POST /unlock — remove .lock from a .meta/ directory.
|
|
11513
|
-
*
|
|
11514
|
-
* @module routes/unlock
|
|
11515
|
-
*/
|
|
11516
|
-
const unlockBodySchema = z.object({
|
|
11517
|
-
path: z.string().min(1),
|
|
11518
|
-
});
|
|
11519
|
-
function registerUnlockRoute(app, deps) {
|
|
11520
|
-
app.post('/unlock', (request, reply) => {
|
|
11521
|
-
const body = unlockBodySchema.parse(request.body);
|
|
11522
|
-
const metaDir = resolveMetaDir(body.path);
|
|
11523
|
-
const lockPath = join(metaDir, '.lock');
|
|
11524
|
-
if (!existsSync(lockPath)) {
|
|
11525
|
-
return reply.status(409).send({
|
|
11526
|
-
error: 'ALREADY_UNLOCKED',
|
|
11527
|
-
message: `No lock file at ${body.path} (already unlocked)`,
|
|
11528
|
-
});
|
|
11529
|
-
}
|
|
11530
|
-
deps.logger.info({ lockPath }, 'removing lock file');
|
|
11531
|
-
unlinkSync(lockPath);
|
|
11532
|
-
return reply.status(200).send({
|
|
11533
|
-
status: 'unlocked',
|
|
11534
|
-
path: body.path,
|
|
11535
|
-
});
|
|
11536
|
-
});
|
|
11537
|
-
}
|
|
11538
|
-
|
|
11539
|
-
/**
|
|
11540
|
-
* Route registration for jeeves-meta service.
|
|
11541
|
-
*
|
|
11542
|
-
* @module routes
|
|
11543
|
-
*/
|
|
11544
|
-
/** Register all HTTP routes on the Fastify instance. */
|
|
11545
|
-
function registerRoutes(app, deps) {
|
|
11546
|
-
// Global error handler for validation + watcher errors
|
|
11547
|
-
app.setErrorHandler((error, _request, reply) => {
|
|
11548
|
-
if (error.validation) {
|
|
11549
|
-
return reply
|
|
11550
|
-
.status(400)
|
|
11551
|
-
.send({ error: 'BAD_REQUEST', message: error.message });
|
|
11552
|
-
}
|
|
11553
|
-
if (error.statusCode === 404) {
|
|
11554
|
-
return reply
|
|
11555
|
-
.status(404)
|
|
11556
|
-
.send({ error: 'NOT_FOUND', message: error.message });
|
|
11557
|
-
}
|
|
11558
|
-
deps.logger.error(error, 'Unhandled route error');
|
|
11559
|
-
return reply
|
|
11560
|
-
.status(500)
|
|
11561
|
-
.send({ error: 'INTERNAL_ERROR', message: error.message });
|
|
11562
|
-
});
|
|
11563
|
-
registerStatusRoute(app, deps);
|
|
11564
|
-
registerMetasRoutes(app, deps);
|
|
11565
|
-
registerSynthesizeRoute(app, deps);
|
|
11566
|
-
registerPreviewRoute(app, deps);
|
|
11567
|
-
registerSeedRoute(app, deps);
|
|
11568
|
-
registerUnlockRoute(app, deps);
|
|
11569
|
-
registerConfigRoute(app, deps);
|
|
11570
|
-
registerConfigApplyRoute(app);
|
|
11571
|
-
registerQueueRoutes(app, deps);
|
|
11572
|
-
}
|
|
11573
|
-
|
|
11574
|
-
/**
|
|
11575
|
-
* Virtual rule registration with jeeves-watcher.
|
|
11576
|
-
*
|
|
11577
|
-
* Service registers inference rules at startup (with retry) and
|
|
11578
|
-
* re-registers opportunistically when watcher restart is detected.
|
|
11579
|
-
*
|
|
11580
|
-
* @module rules
|
|
11581
|
-
*/
|
|
11582
|
-
const SOURCE = 'jeeves-meta';
|
|
11583
|
-
const MAX_RETRIES = 10;
|
|
11584
|
-
const RETRY_BASE_MS = 2000;
|
|
11585
|
-
/**
|
|
11586
|
-
* Convert a `Record<string, unknown>` config property into watcher
|
|
11587
|
-
* schema `set` directives: `{ key: { set: value } }` per entry.
|
|
11679
|
+
* `getHealth` callback that preserves all existing health details.
|
|
11680
|
+
*
|
|
11681
|
+
* @module routes/status
|
|
11588
11682
|
*/
|
|
11589
|
-
function
|
|
11590
|
-
|
|
11683
|
+
async function checkDependency(url, path) {
|
|
11684
|
+
const checkedAt = new Date().toISOString();
|
|
11685
|
+
try {
|
|
11686
|
+
const res = await fetch(new URL(path, url), {
|
|
11687
|
+
signal: AbortSignal.timeout(3000),
|
|
11688
|
+
});
|
|
11689
|
+
return { url, status: res.ok ? 'ok' : 'error', checkedAt };
|
|
11690
|
+
}
|
|
11691
|
+
catch {
|
|
11692
|
+
return { url, status: 'unreachable', checkedAt };
|
|
11693
|
+
}
|
|
11591
11694
|
}
|
|
11592
|
-
/**
|
|
11593
|
-
function
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
|
|
11605
|
-
|
|
11606
|
-
|
|
11607
|
-
|
|
11608
|
-
|
|
11609
|
-
|
|
11610
|
-
|
|
11611
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
11619
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
set: '{{json._criticTokens}}',
|
|
11635
|
-
},
|
|
11636
|
-
meta_error_step: {
|
|
11637
|
-
type: 'string',
|
|
11638
|
-
set: '{{json._error.step}}',
|
|
11639
|
-
},
|
|
11640
|
-
generated_at: {
|
|
11641
|
-
type: 'string',
|
|
11642
|
-
set: '{{json._generatedAt}}',
|
|
11643
|
-
},
|
|
11644
|
-
generated_at_unix: {
|
|
11645
|
-
type: 'integer',
|
|
11646
|
-
set: '{{toUnix json._generatedAt}}',
|
|
11647
|
-
},
|
|
11648
|
-
has_error: {
|
|
11649
|
-
type: 'boolean',
|
|
11650
|
-
set: '{{#if json._error}}true{{else}}false{{/if}}',
|
|
11651
|
-
},
|
|
11652
|
-
},
|
|
11653
|
-
},
|
|
11654
|
-
],
|
|
11655
|
-
render: {
|
|
11656
|
-
frontmatter: [
|
|
11657
|
-
'meta_id',
|
|
11658
|
-
'generated_at',
|
|
11659
|
-
'*',
|
|
11660
|
-
'!_*',
|
|
11661
|
-
'!json',
|
|
11662
|
-
'!file',
|
|
11663
|
-
'!has_error',
|
|
11664
|
-
],
|
|
11665
|
-
body: [{ path: 'json._content', heading: 1, label: 'Synthesis' }],
|
|
11666
|
-
},
|
|
11667
|
-
renderAs: 'md',
|
|
11668
|
-
},
|
|
11669
|
-
{
|
|
11670
|
-
name: 'meta-archive',
|
|
11671
|
-
description: 'Archived jeeves-meta .meta/archive snapshots',
|
|
11672
|
-
match: {
|
|
11673
|
-
properties: {
|
|
11674
|
-
file: {
|
|
11675
|
-
properties: {
|
|
11676
|
-
path: { type: 'string', glob: '**/.meta/archive/*.json' },
|
|
11677
|
-
},
|
|
11678
|
-
},
|
|
11695
|
+
/** Check watcher, surfacing initialScan.active as indexing state. */
|
|
11696
|
+
async function checkWatcher(url) {
|
|
11697
|
+
const checkedAt = new Date().toISOString();
|
|
11698
|
+
try {
|
|
11699
|
+
const res = await fetch(new URL('/status', url), {
|
|
11700
|
+
signal: AbortSignal.timeout(3000),
|
|
11701
|
+
});
|
|
11702
|
+
if (!res.ok)
|
|
11703
|
+
return { url, status: 'error', checkedAt };
|
|
11704
|
+
const data = (await res.json());
|
|
11705
|
+
const indexing = data.initialScan?.active === true;
|
|
11706
|
+
return {
|
|
11707
|
+
url,
|
|
11708
|
+
status: indexing ? 'indexing' : 'ok',
|
|
11709
|
+
checkedAt,
|
|
11710
|
+
indexing,
|
|
11711
|
+
};
|
|
11712
|
+
}
|
|
11713
|
+
catch {
|
|
11714
|
+
return { url, status: 'unreachable', checkedAt };
|
|
11715
|
+
}
|
|
11716
|
+
}
|
|
11717
|
+
function registerStatusRoute(app, deps) {
|
|
11718
|
+
const statusHandler = createStatusHandler({
|
|
11719
|
+
name: SERVICE_NAME,
|
|
11720
|
+
version: SERVICE_VERSION,
|
|
11721
|
+
getHealth: async () => {
|
|
11722
|
+
const { config, queue, scheduler, stats } = deps;
|
|
11723
|
+
// On-demand dependency checks
|
|
11724
|
+
const [watcherHealth, gatewayHealth] = await Promise.all([
|
|
11725
|
+
checkWatcher(config.watcherUrl),
|
|
11726
|
+
checkDependency(config.gatewayUrl, '/status'),
|
|
11727
|
+
]);
|
|
11728
|
+
return {
|
|
11729
|
+
currentTarget: queue.current?.path ?? null,
|
|
11730
|
+
queue: queue.getState(),
|
|
11731
|
+
stats: {
|
|
11732
|
+
totalSyntheses: stats.totalSyntheses,
|
|
11733
|
+
totalTokens: stats.totalTokens,
|
|
11734
|
+
totalErrors: stats.totalErrors,
|
|
11735
|
+
lastCycleDurationMs: stats.lastCycleDurationMs,
|
|
11736
|
+
lastCycleAt: stats.lastCycleAt,
|
|
11679
11737
|
},
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
{
|
|
11684
|
-
properties: {
|
|
11685
|
-
...toSchemaSetDirectives(config.metaArchiveProperty),
|
|
11686
|
-
meta_id: { type: 'string', set: '{{json._id}}' },
|
|
11687
|
-
archived: { type: 'boolean', set: 'true' },
|
|
11688
|
-
archived_at: { type: 'string', set: '{{json._archivedAt}}' },
|
|
11689
|
-
},
|
|
11738
|
+
schedule: {
|
|
11739
|
+
expression: config.schedule,
|
|
11740
|
+
nextAt: scheduler?.nextRunAt?.toISOString() ?? null,
|
|
11690
11741
|
},
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
{
|
|
11696
|
-
path: 'json._content',
|
|
11697
|
-
heading: 1,
|
|
11698
|
-
label: 'Synthesis (archived)',
|
|
11699
|
-
},
|
|
11700
|
-
],
|
|
11701
|
-
},
|
|
11702
|
-
renderAs: 'md',
|
|
11703
|
-
},
|
|
11704
|
-
{
|
|
11705
|
-
name: 'meta-config',
|
|
11706
|
-
description: 'jeeves-meta configuration file',
|
|
11707
|
-
match: {
|
|
11708
|
-
properties: {
|
|
11709
|
-
file: {
|
|
11710
|
-
properties: {
|
|
11711
|
-
path: { type: 'string', glob: '**/jeeves-meta.config.json' },
|
|
11712
|
-
},
|
|
11742
|
+
dependencies: {
|
|
11743
|
+
watcher: {
|
|
11744
|
+
...watcherHealth,
|
|
11745
|
+
rulesRegistered: deps.registrar?.isRegistered ?? false,
|
|
11713
11746
|
},
|
|
11747
|
+
gateway: gatewayHealth,
|
|
11714
11748
|
},
|
|
11715
|
-
}
|
|
11716
|
-
schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
|
|
11717
|
-
render: {
|
|
11718
|
-
frontmatter: [
|
|
11719
|
-
'watcherUrl',
|
|
11720
|
-
'gatewayUrl',
|
|
11721
|
-
'architectEvery',
|
|
11722
|
-
'depthWeight',
|
|
11723
|
-
'maxArchive',
|
|
11724
|
-
'maxLines',
|
|
11725
|
-
],
|
|
11726
|
-
body: [
|
|
11727
|
-
{
|
|
11728
|
-
path: 'json.defaultArchitect',
|
|
11729
|
-
heading: 2,
|
|
11730
|
-
label: 'Default Architect Prompt',
|
|
11731
|
-
},
|
|
11732
|
-
{
|
|
11733
|
-
path: 'json.defaultCritic',
|
|
11734
|
-
heading: 2,
|
|
11735
|
-
label: 'Default Critic Prompt',
|
|
11736
|
-
},
|
|
11737
|
-
],
|
|
11738
|
-
},
|
|
11739
|
-
renderAs: 'md',
|
|
11749
|
+
};
|
|
11740
11750
|
},
|
|
11741
|
-
|
|
11751
|
+
});
|
|
11752
|
+
app.get('/status', async (_request, reply) => {
|
|
11753
|
+
const result = await statusHandler();
|
|
11754
|
+
return reply.status(result.status).send(result.body);
|
|
11755
|
+
});
|
|
11742
11756
|
}
|
|
11757
|
+
|
|
11743
11758
|
/**
|
|
11744
|
-
*
|
|
11759
|
+
* POST /synthesize route handler.
|
|
11745
11760
|
*
|
|
11746
|
-
*
|
|
11747
|
-
* - Tracks watcher uptime for restart detection
|
|
11748
|
-
* - Re-registers opportunistically when uptime decreases
|
|
11761
|
+
* @module routes/synthesize
|
|
11749
11762
|
*/
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11755
|
-
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
/**
|
|
11766
|
-
* Register rules with watcher. Retries with exponential backoff.
|
|
11767
|
-
* Non-blocking — logs errors but never throws.
|
|
11768
|
-
*/
|
|
11769
|
-
async register() {
|
|
11770
|
-
const rules = buildMetaRules(this.config);
|
|
11771
|
-
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
11763
|
+
const synthesizeBodySchema = z.object({
|
|
11764
|
+
path: z.string().optional(),
|
|
11765
|
+
});
|
|
11766
|
+
/** Register the POST /synthesize route. */
|
|
11767
|
+
function registerSynthesizeRoute(app, deps) {
|
|
11768
|
+
app.post('/synthesize', async (request, reply) => {
|
|
11769
|
+
const body = synthesizeBodySchema.parse(request.body);
|
|
11770
|
+
const { config, watcher, queue } = deps;
|
|
11771
|
+
let targetPath;
|
|
11772
|
+
if (body.path) {
|
|
11773
|
+
targetPath = body.path;
|
|
11774
|
+
}
|
|
11775
|
+
else {
|
|
11776
|
+
// Discover stalest candidate
|
|
11777
|
+
let result;
|
|
11772
11778
|
try {
|
|
11773
|
-
await
|
|
11774
|
-
this.registered = true;
|
|
11775
|
-
this.logger.info('Virtual rules registered with watcher');
|
|
11776
|
-
return;
|
|
11779
|
+
result = await listMetas(config, watcher);
|
|
11777
11780
|
}
|
|
11778
|
-
catch
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11781
|
+
catch {
|
|
11782
|
+
return reply.status(503).send({
|
|
11783
|
+
error: 'SERVICE_UNAVAILABLE',
|
|
11784
|
+
message: 'Watcher unreachable — cannot discover candidates',
|
|
11785
|
+
});
|
|
11782
11786
|
}
|
|
11787
|
+
const stale = result.entries
|
|
11788
|
+
.filter((e) => e.stalenessSeconds > 0)
|
|
11789
|
+
.map((e) => ({
|
|
11790
|
+
node: e.node,
|
|
11791
|
+
meta: e.meta,
|
|
11792
|
+
actualStaleness: e.stalenessSeconds,
|
|
11793
|
+
}));
|
|
11794
|
+
const stalest = discoverStalestPath(stale, config.depthWeight);
|
|
11795
|
+
if (!stalest) {
|
|
11796
|
+
return reply.code(200).send({
|
|
11797
|
+
status: 'skipped',
|
|
11798
|
+
message: 'No stale metas found. Nothing to synthesize.',
|
|
11799
|
+
});
|
|
11800
|
+
}
|
|
11801
|
+
targetPath = stalest;
|
|
11783
11802
|
}
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
if (this.lastWatcherUptime !== null &&
|
|
11793
|
-
currentUptime < this.lastWatcherUptime) {
|
|
11794
|
-
this.logger.info({ previous: this.lastWatcherUptime, current: currentUptime }, 'Watcher restart detected — re-registering rules');
|
|
11795
|
-
this.registered = false;
|
|
11796
|
-
await this.register();
|
|
11797
|
-
}
|
|
11798
|
-
this.lastWatcherUptime = currentUptime;
|
|
11799
|
-
}
|
|
11803
|
+
const result = queue.enqueue(targetPath, body.path !== undefined);
|
|
11804
|
+
return reply.code(202).send({
|
|
11805
|
+
status: 'accepted',
|
|
11806
|
+
path: targetPath,
|
|
11807
|
+
queuePosition: result.position,
|
|
11808
|
+
alreadyQueued: result.alreadyQueued,
|
|
11809
|
+
});
|
|
11810
|
+
});
|
|
11800
11811
|
}
|
|
11801
11812
|
|
|
11802
11813
|
/**
|
|
11803
|
-
*
|
|
11804
|
-
*
|
|
11805
|
-
* After rules are registered with the watcher, verifies that .meta/meta.json
|
|
11806
|
-
* files are discoverable via watcher walk (which depends on virtual rules
|
|
11807
|
-
* being applied). Logs a warning if expected metas are not found.
|
|
11814
|
+
* POST /unlock — remove .lock from a .meta/ directory.
|
|
11808
11815
|
*
|
|
11809
|
-
* @module
|
|
11816
|
+
* @module routes/unlock
|
|
11810
11817
|
*/
|
|
11818
|
+
const unlockBodySchema = z.object({
|
|
11819
|
+
path: z.string().min(1),
|
|
11820
|
+
});
|
|
11821
|
+
function registerUnlockRoute(app, deps) {
|
|
11822
|
+
app.post('/unlock', (request, reply) => {
|
|
11823
|
+
const body = unlockBodySchema.parse(request.body);
|
|
11824
|
+
const metaDir = resolveMetaDir(body.path);
|
|
11825
|
+
const lockPath = join(metaDir, '.lock');
|
|
11826
|
+
if (!existsSync(lockPath)) {
|
|
11827
|
+
return reply.status(409).send({
|
|
11828
|
+
error: 'ALREADY_UNLOCKED',
|
|
11829
|
+
message: `No lock file at ${body.path} (already unlocked)`,
|
|
11830
|
+
});
|
|
11831
|
+
}
|
|
11832
|
+
deps.logger.info({ lockPath }, 'removing lock file');
|
|
11833
|
+
unlinkSync(lockPath);
|
|
11834
|
+
return reply.status(200).send({
|
|
11835
|
+
status: 'unlocked',
|
|
11836
|
+
path: body.path,
|
|
11837
|
+
});
|
|
11838
|
+
});
|
|
11839
|
+
}
|
|
11840
|
+
|
|
11811
11841
|
/**
|
|
11812
|
-
*
|
|
11813
|
-
*
|
|
11814
|
-
* Runs a discovery pass and logs the result. If no metas are found but
|
|
11815
|
-
* the filesystem likely has some, logs a warning suggesting reindex.
|
|
11842
|
+
* Route registration for jeeves-meta service.
|
|
11816
11843
|
*
|
|
11817
|
-
* @
|
|
11818
|
-
* @param logger - Logger for reporting results.
|
|
11819
|
-
* @returns Number of metas discovered.
|
|
11844
|
+
* @module routes
|
|
11820
11845
|
*/
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11846
|
+
/** Register all HTTP routes on the Fastify instance. */
|
|
11847
|
+
function registerRoutes(app, deps) {
|
|
11848
|
+
// Global error handler for validation + watcher errors
|
|
11849
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
11850
|
+
if (error.validation) {
|
|
11851
|
+
return reply
|
|
11852
|
+
.status(400)
|
|
11853
|
+
.send({ error: 'BAD_REQUEST', message: error.message });
|
|
11828
11854
|
}
|
|
11829
|
-
|
|
11830
|
-
|
|
11855
|
+
if (error.statusCode === 404) {
|
|
11856
|
+
return reply
|
|
11857
|
+
.status(404)
|
|
11858
|
+
.send({ error: 'NOT_FOUND', message: error.message });
|
|
11831
11859
|
}
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11860
|
+
deps.logger.error(error, 'Unhandled route error');
|
|
11861
|
+
return reply
|
|
11862
|
+
.status(500)
|
|
11863
|
+
.send({ error: 'INTERNAL_ERROR', message: error.message });
|
|
11864
|
+
});
|
|
11865
|
+
registerStatusRoute(app, deps);
|
|
11866
|
+
registerMetasRoutes(app, deps);
|
|
11867
|
+
registerSynthesizeRoute(app, deps);
|
|
11868
|
+
registerPreviewRoute(app, deps);
|
|
11869
|
+
registerSeedRoute(app, deps);
|
|
11870
|
+
registerUnlockRoute(app, deps);
|
|
11871
|
+
registerConfigRoute(app, deps);
|
|
11872
|
+
registerConfigApplyRoute(app);
|
|
11873
|
+
registerQueueRoutes(app, deps);
|
|
11838
11874
|
}
|
|
11839
11875
|
|
|
11840
11876
|
/**
|
|
@@ -11994,80 +12030,6 @@ class HttpWatcherClient {
|
|
|
11994
12030
|
}
|
|
11995
12031
|
}
|
|
11996
12032
|
|
|
11997
|
-
/**
|
|
11998
|
-
* Periodic watcher health check for rule registration resilience.
|
|
11999
|
-
*
|
|
12000
|
-
* Pings watcher `/status` on a configurable interval, detects restarts
|
|
12001
|
-
* (uptime decrease), and re-registers virtual rules automatically.
|
|
12002
|
-
* Independent of the synthesis scheduler.
|
|
12003
|
-
*
|
|
12004
|
-
* @module rules/healthCheck
|
|
12005
|
-
*/
|
|
12006
|
-
/**
|
|
12007
|
-
* Manages the periodic watcher health check loop.
|
|
12008
|
-
*
|
|
12009
|
-
* Starts a `setInterval` that pings the watcher and delegates
|
|
12010
|
-
* restart detection to `RuleRegistrar.checkAndReregister()`.
|
|
12011
|
-
*/
|
|
12012
|
-
class WatcherHealthCheck {
|
|
12013
|
-
watcherUrl;
|
|
12014
|
-
intervalMs;
|
|
12015
|
-
registrar;
|
|
12016
|
-
logger;
|
|
12017
|
-
handle = null;
|
|
12018
|
-
constructor(opts) {
|
|
12019
|
-
this.watcherUrl = opts.watcherUrl.replace(/\/+$/, '');
|
|
12020
|
-
this.intervalMs = opts.intervalMs;
|
|
12021
|
-
this.registrar = opts.registrar;
|
|
12022
|
-
this.logger = opts.logger;
|
|
12023
|
-
}
|
|
12024
|
-
/** Start the periodic health check. No-op if intervalMs is 0. */
|
|
12025
|
-
start() {
|
|
12026
|
-
if (this.intervalMs <= 0) {
|
|
12027
|
-
this.logger.info('Watcher health check disabled (interval = 0)');
|
|
12028
|
-
return;
|
|
12029
|
-
}
|
|
12030
|
-
this.handle = setInterval(() => {
|
|
12031
|
-
void this.check();
|
|
12032
|
-
}, this.intervalMs);
|
|
12033
|
-
// Don't prevent process exit
|
|
12034
|
-
if (typeof this.handle === 'object' && 'unref' in this.handle) {
|
|
12035
|
-
this.handle.unref();
|
|
12036
|
-
}
|
|
12037
|
-
this.logger.info({ intervalMs: this.intervalMs }, 'Watcher health check started');
|
|
12038
|
-
}
|
|
12039
|
-
/** Stop the periodic health check. */
|
|
12040
|
-
stop() {
|
|
12041
|
-
if (this.handle) {
|
|
12042
|
-
clearInterval(this.handle);
|
|
12043
|
-
this.handle = null;
|
|
12044
|
-
}
|
|
12045
|
-
}
|
|
12046
|
-
/** Single health check iteration. */
|
|
12047
|
-
async check() {
|
|
12048
|
-
try {
|
|
12049
|
-
const res = await fetch(this.watcherUrl + '/status', {
|
|
12050
|
-
signal: AbortSignal.timeout(5000),
|
|
12051
|
-
});
|
|
12052
|
-
if (!res.ok) {
|
|
12053
|
-
this.logger.warn({ status: res.status }, 'Watcher health check: non-OK response');
|
|
12054
|
-
return;
|
|
12055
|
-
}
|
|
12056
|
-
const data = (await res.json());
|
|
12057
|
-
// If rules were never successfully registered (startup failure),
|
|
12058
|
-
// attempt registration now that the watcher is reachable.
|
|
12059
|
-
if (!this.registrar.isRegistered) {
|
|
12060
|
-
this.logger.info('Rules not registered — attempting registration');
|
|
12061
|
-
await this.registrar.register();
|
|
12062
|
-
}
|
|
12063
|
-
await this.registrar.checkAndReregister(data.uptime);
|
|
12064
|
-
}
|
|
12065
|
-
catch (err) {
|
|
12066
|
-
this.logger.debug({ err }, 'Watcher health check: unreachable (expected during startup)');
|
|
12067
|
-
}
|
|
12068
|
-
}
|
|
12069
|
-
}
|
|
12070
|
-
|
|
12071
12033
|
/**
|
|
12072
12034
|
* Service bootstrap — wire up all components and start listening.
|
|
12073
12035
|
*
|
|
@@ -12251,4 +12213,51 @@ async function startService(config, configPath) {
|
|
|
12251
12213
|
logger.info('Service fully initialized');
|
|
12252
12214
|
}
|
|
12253
12215
|
|
|
12216
|
+
/**
|
|
12217
|
+
* Jeeves component descriptor for jeeves-meta.
|
|
12218
|
+
*
|
|
12219
|
+
* Single source of truth consumed by the service CLI, plugin writer, and
|
|
12220
|
+
* config-apply pipeline.
|
|
12221
|
+
*
|
|
12222
|
+
* @module descriptor
|
|
12223
|
+
*/
|
|
12224
|
+
/**
|
|
12225
|
+
* Parsed jeeves-meta component descriptor.
|
|
12226
|
+
*/
|
|
12227
|
+
const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
12228
|
+
name: 'meta',
|
|
12229
|
+
version: SERVICE_VERSION,
|
|
12230
|
+
servicePackage: '@karmaniverous/jeeves-meta',
|
|
12231
|
+
pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
|
|
12232
|
+
defaultPort: 1938,
|
|
12233
|
+
// The runtime Zod custom validator only checks for a .parse() method.
|
|
12234
|
+
// Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
|
|
12235
|
+
configSchema: serviceConfigSchema,
|
|
12236
|
+
configFileName: 'config.json',
|
|
12237
|
+
initTemplate: () => serviceConfigSchema.parse({
|
|
12238
|
+
watcherUrl: 'http://127.0.0.1:1936',
|
|
12239
|
+
}),
|
|
12240
|
+
onConfigApply: (merged) => {
|
|
12241
|
+
const parsed = serviceConfigSchema.parse(merged);
|
|
12242
|
+
applyHotReloadedConfig(parsed);
|
|
12243
|
+
return Promise.resolve();
|
|
12244
|
+
},
|
|
12245
|
+
run: async (configPath) => {
|
|
12246
|
+
const config = loadServiceConfig(configPath);
|
|
12247
|
+
await startService(config, configPath);
|
|
12248
|
+
},
|
|
12249
|
+
startCommand: (configPath) => [
|
|
12250
|
+
'node',
|
|
12251
|
+
'dist/cli/jeeves-meta/index.js',
|
|
12252
|
+
'start',
|
|
12253
|
+
'-c',
|
|
12254
|
+
configPath,
|
|
12255
|
+
],
|
|
12256
|
+
sectionId: 'Meta',
|
|
12257
|
+
refreshIntervalSeconds: 73,
|
|
12258
|
+
generateToolsContent: () => '',
|
|
12259
|
+
dependencies: { hard: ['watcher'], soft: [] },
|
|
12260
|
+
customCliCommands: registerCustomCliCommands,
|
|
12261
|
+
});
|
|
12262
|
+
|
|
12254
12263
|
export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
|