@karmaniverous/jeeves-meta 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js DELETED
@@ -1,2159 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
- import { dirname, join, relative, sep, resolve } from 'node:path';
4
- import { z } from 'zod';
5
- import { createHash } from 'node:crypto';
6
-
7
- /**
8
- * Zod schema for jeeves-meta configuration.
9
- *
10
- * Consumers load config however they want (file, env, constructor).
11
- * The library validates via this schema.
12
- *
13
- * @module schema/config
14
- */
15
- /** Zod schema for jeeves-meta configuration. */
16
- const synthConfigSchema = z.object({
17
- /** Filesystem paths to watch for .meta/ directories. */
18
- /** Watcher service base URL. */
19
- watcherUrl: z.url(),
20
- /** OpenClaw gateway base URL for subprocess spawning. */
21
- gatewayUrl: z.url().default('http://127.0.0.1:3000'),
22
- /** Optional API key for gateway authentication. */
23
- gatewayApiKey: z.string().optional(),
24
- /** Run architect every N cycles (per meta). */
25
- architectEvery: z.number().int().min(1).default(10),
26
- /** Exponent for depth weighting in staleness formula. */
27
- depthWeight: z.number().min(0).default(0.5),
28
- /** Maximum archive snapshots to retain per meta. */
29
- maxArchive: z.number().int().min(1).default(20),
30
- /** Maximum lines of context to include in subprocess prompts. */
31
- maxLines: z.number().int().min(50).default(500),
32
- /** Architect subprocess timeout in seconds. */
33
- architectTimeout: z.number().int().min(30).default(120),
34
- /** Builder subprocess timeout in seconds. */
35
- builderTimeout: z.number().int().min(60).default(600),
36
- /** Critic subprocess timeout in seconds. */
37
- criticTimeout: z.number().int().min(30).default(300),
38
- /** Resolved architect system prompt text. */
39
- defaultArchitect: z.string(),
40
- /** Resolved critic system prompt text. */
41
- defaultCritic: z.string(),
42
- /**
43
- * When true, skip unchanged candidates and iterate to the next-stalest
44
- * until finding one with actual changes. Skipped candidates get their
45
- * _generatedAt bumped to prevent re-selection next cycle.
46
- */
47
- skipUnchanged: z.boolean().default(true),
48
- /** Number of metas to synthesize per invocation. */
49
- batchSize: z.number().int().min(1).default(1),
50
- /**
51
- * Watcher metadata properties for live .meta/meta.json files.
52
- * Virtual rules use these to tag live metas; scan queries derive
53
- * their filter from the first domain value.
54
- */
55
- metaProperty: z
56
- .object({ domains: z.array(z.string()).min(1) })
57
- .default({ domains: ['meta'] }),
58
- /**
59
- * Watcher metadata properties for .meta/archive/** snapshots.
60
- * Virtual rules use these to tag archive files.
61
- */
62
- metaArchiveProperty: z
63
- .object({ domains: z.array(z.string()).min(1) })
64
- .default({ domains: ['meta-archive'] }),
65
- });
66
-
67
- /**
68
- * Structured error from a synthesis step failure.
69
- *
70
- * @module schema/error
71
- */
72
- /** Zod schema for synthesis step errors. */
73
- const synthErrorSchema = z.object({
74
- /** Which step failed: 'architect', 'builder', or 'critic'. */
75
- step: z.enum(['architect', 'builder', 'critic']),
76
- /** Error classification code. */
77
- code: z.string(),
78
- /** Human-readable error message. */
79
- message: z.string(),
80
- });
81
-
82
- /**
83
- * Zod schema for .meta/meta.json files.
84
- *
85
- * Reserved properties are underscore-prefixed and engine-managed.
86
- * All other keys are open schema (builder output).
87
- *
88
- * @module schema/meta
89
- */
90
- /** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
91
- const metaJsonSchema = z
92
- .object({
93
- /** Stable identity. Generated on first synthesis, never changes. */
94
- _id: z.uuid(),
95
- /** Human-provided steering prompt. Optional. */
96
- _steer: z.string().optional(),
97
- /** Architect system prompt used this turn. Defaults from config. */
98
- _architect: z.string().optional(),
99
- /**
100
- * Task brief generated by the architect. Cached and reused across cycles;
101
- * regenerated only when triggered.
102
- */
103
- _builder: z.string().optional(),
104
- /** Critic system prompt used this turn. Defaults from config. */
105
- _critic: z.string().optional(),
106
- /** Timestamp of last synthesis. ISO 8601. */
107
- _generatedAt: z.iso.datetime().optional(),
108
- /** Narrative synthesis output. Rendered by watcher for embedding. */
109
- _content: z.string().optional(),
110
- /**
111
- * Hash of sorted file listing in scope. Detects directory structure
112
- * changes that trigger an architect re-run.
113
- */
114
- _structureHash: z.string().optional(),
115
- /**
116
- * Cycles since last architect run. Reset to 0 when architect runs.
117
- * Used with architectEvery to trigger periodic re-prompting.
118
- */
119
- _synthesisCount: z.number().int().min(0).optional(),
120
- /** Critic evaluation of the last synthesis. */
121
- _feedback: z.string().optional(),
122
- /**
123
- * Present and true on archive snapshots. Distinguishes live vs. archived
124
- * metas.
125
- */
126
- _archived: z.boolean().optional(),
127
- /** Timestamp when this snapshot was archived. ISO 8601. */
128
- _archivedAt: z.iso.datetime().optional(),
129
- /**
130
- * Scheduling priority. Higher = updates more often. Negative allowed;
131
- * normalized to min 0 at scheduling time.
132
- */
133
- _depth: z.number().optional(),
134
- /**
135
- * Emphasis multiplier for depth weighting in scheduling.
136
- * Default 1. Higher values increase this meta's scheduling priority
137
- * relative to its depth. Set to 0.5 to halve the depth effect,
138
- * 2 to double it, 0 to ignore depth entirely for this meta.
139
- */
140
- _emphasis: z.number().min(0).optional(),
141
- /** Token count from last architect subprocess call. */
142
- _architectTokens: z.number().int().optional(),
143
- /** Token count from last builder subprocess call. */
144
- _builderTokens: z.number().int().optional(),
145
- /** Token count from last critic subprocess call. */
146
- _criticTokens: z.number().int().optional(),
147
- /** Exponential moving average of architect token usage (decay 0.3). */
148
- _architectTokensAvg: z.number().optional(),
149
- /** Exponential moving average of builder token usage (decay 0.3). */
150
- _builderTokensAvg: z.number().optional(),
151
- /** Exponential moving average of critic token usage (decay 0.3). */
152
- _criticTokensAvg: z.number().optional(),
153
- /**
154
- * Structured error from last cycle. Present when a step failed.
155
- * Cleared on successful cycle.
156
- */
157
- _error: synthErrorSchema.optional(),
158
- })
159
- .loose();
160
-
161
- /**
162
- * Load and resolve jeeves-meta config with \@file: indirection.
163
- *
164
- * @module configLoader
165
- */
166
- /**
167
- * Resolve \@file: references in a config value.
168
- *
169
- * @param value - String value that may start with "\@file:".
170
- * @param baseDir - Base directory for resolving relative paths.
171
- * @returns The resolved string (file contents or original value).
172
- */
173
- function resolveFileRef(value, baseDir) {
174
- if (!value.startsWith('@file:'))
175
- return value;
176
- const filePath = join(baseDir, value.slice(6));
177
- return readFileSync(filePath, 'utf8');
178
- }
179
- /**
180
- * Load synth config from a JSON file, resolving \@file: references.
181
- *
182
- * @param configPath - Path to jeeves-meta.config.json.
183
- * @returns Validated SynthConfig with resolved prompt strings.
184
- */
185
- function loadSynthConfig(configPath) {
186
- const raw = JSON.parse(readFileSync(configPath, 'utf8'));
187
- const baseDir = dirname(configPath);
188
- if (typeof raw.defaultArchitect === 'string') {
189
- raw.defaultArchitect = resolveFileRef(raw.defaultArchitect, baseDir);
190
- }
191
- if (typeof raw.defaultCritic === 'string') {
192
- raw.defaultCritic = resolveFileRef(raw.defaultCritic, baseDir);
193
- }
194
- return synthConfigSchema.parse(raw);
195
- }
196
- /**
197
- * Resolve config path from --config flag or JEEVES_META_CONFIG env var.
198
- *
199
- * @param args - CLI arguments (process.argv.slice(2)).
200
- * @returns Resolved config path.
201
- * @throws If no config path found.
202
- */
203
- function resolveConfigPath(args) {
204
- // Check --config flag
205
- const configIdx = args.indexOf('--config');
206
- if (configIdx !== -1 && args[configIdx + 1]) {
207
- return args[configIdx + 1];
208
- }
209
- // Check env var
210
- const envPath = process.env['JEEVES_META_CONFIG'];
211
- if (envPath)
212
- return envPath;
213
- throw new Error('Config path required. Use --config <path> or set JEEVES_META_CONFIG env var.');
214
- }
215
-
216
- /**
217
- * List archive snapshot files in chronological order.
218
- *
219
- * @module archive/listArchive
220
- */
221
- /**
222
- * List archive .json files sorted chronologically (oldest first).
223
- *
224
- * @param metaPath - Absolute path to the .meta directory.
225
- * @returns Array of absolute paths to archive files, or empty if none.
226
- */
227
- function listArchiveFiles(metaPath) {
228
- const archiveDir = join(metaPath, 'archive');
229
- try {
230
- return readdirSync(archiveDir)
231
- .filter((f) => f.endsWith('.json'))
232
- .sort()
233
- .map((f) => join(archiveDir, f));
234
- }
235
- catch {
236
- return [];
237
- }
238
- }
239
-
240
- /**
241
- * Prune old archive snapshots beyond maxArchive.
242
- *
243
- * @module archive/prune
244
- */
245
- /**
246
- * Prune archive directory to keep at most maxArchive snapshots.
247
- * Removes the oldest files.
248
- *
249
- * @param metaPath - Absolute path to the .meta directory.
250
- * @param maxArchive - Maximum snapshots to retain.
251
- * @returns Number of files pruned.
252
- */
253
- function pruneArchive(metaPath, maxArchive) {
254
- const files = listArchiveFiles(metaPath);
255
- const toRemove = files.length - maxArchive;
256
- if (toRemove <= 0)
257
- return 0;
258
- for (let i = 0; i < toRemove; i++) {
259
- unlinkSync(files[i]);
260
- }
261
- return toRemove;
262
- }
263
-
264
- /**
265
- * Read the latest archive snapshot for steer change detection.
266
- *
267
- * @module archive/readLatest
268
- */
269
- /**
270
- * Read the most recent archive snapshot.
271
- *
272
- * @param metaPath - Absolute path to the .meta directory.
273
- * @returns The latest archived meta, or null if no archives exist.
274
- */
275
- function readLatestArchive(metaPath) {
276
- const files = listArchiveFiles(metaPath);
277
- if (files.length === 0)
278
- return null;
279
- const raw = readFileSync(files[files.length - 1], 'utf8');
280
- return JSON.parse(raw);
281
- }
282
-
283
- /**
284
- * Create archive snapshots of meta.json.
285
- *
286
- * Copies current meta.json to archive/\{ISO-timestamp\}.json with
287
- * _archived: true and _archivedAt added.
288
- *
289
- * @module archive/snapshot
290
- */
291
- /**
292
- * Create an archive snapshot of the current meta.json.
293
- *
294
- * @param metaPath - Absolute path to the .meta directory.
295
- * @param meta - Current meta.json content.
296
- * @returns The archive file path.
297
- */
298
- function createSnapshot(metaPath, meta) {
299
- const archiveDir = join(metaPath, 'archive');
300
- mkdirSync(archiveDir, { recursive: true });
301
- const now = new Date().toISOString().replace(/[:.]/g, '-');
302
- const archiveFile = join(archiveDir, now + '.json');
303
- const archived = {
304
- ...meta,
305
- _archived: true,
306
- _archivedAt: new Date().toISOString(),
307
- };
308
- writeFileSync(archiveFile, JSON.stringify(archived, null, 2) + '\n');
309
- return archiveFile;
310
- }
311
-
312
- /**
313
- * Archive module — snapshot creation, pruning, and reading.
314
- *
315
- * @module archive
316
- */
317
-
318
- var index$1 = /*#__PURE__*/Object.freeze({
319
- __proto__: null,
320
- createSnapshot: createSnapshot,
321
- listArchiveFiles: listArchiveFiles,
322
- pruneArchive: pruneArchive,
323
- readLatestArchive: readLatestArchive
324
- });
325
-
326
- /**
327
- * Normalize file paths to forward slashes for consistency with watcher-indexed paths.
328
- *
329
- * Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
330
- * ensures all paths in the library use the same convention, regardless of
331
- * the platform's native separator.
332
- *
333
- * @module normalizePath
334
- */
335
- /**
336
- * Normalize a file path to forward slashes.
337
- *
338
- * @param p - File path (may contain backslashes).
339
- * @returns Path with all backslashes replaced by forward slashes.
340
- */
341
- function normalizePath$1(p) {
342
- return p.replaceAll('\\', '/');
343
- }
344
-
345
- /**
346
- * Paginated scan helper for exhaustive scope enumeration.
347
- *
348
- * @module paginatedScan
349
- */
350
- /**
351
- * Perform a paginated scan that follows cursor tokens until exhausted.
352
- *
353
- * @param watcher - WatcherClient instance.
354
- * @param params - Base scan parameters (cursor is managed internally).
355
- * @returns All matching files across all pages.
356
- */
357
- async function paginatedScan(watcher, params) {
358
- const allFiles = [];
359
- let cursor;
360
- do {
361
- const result = await watcher.scan({ ...params, cursor });
362
- allFiles.push(...result.files);
363
- cursor = result.next;
364
- } while (cursor);
365
- return allFiles;
366
- }
367
-
368
- /**
369
- * Discover .meta/ directories via watcher scan.
370
- *
371
- * Replaces filesystem-based globMetas() with a watcher query
372
- * that returns indexed .meta/meta.json points, filtered by domain.
373
- *
374
- * @module discovery/discoverMetas
375
- */
376
- /**
377
- * Build a Qdrant filter from config metaProperty.
378
- *
379
- * @param config - Synth config with metaProperty.
380
- * @returns Qdrant filter object for scanning live metas.
381
- */
382
- function buildMetaFilter(config) {
383
- return {
384
- must: [
385
- {
386
- key: 'domains',
387
- match: { value: config.metaProperty.domains[0] },
388
- },
389
- {
390
- key: 'file_path',
391
- match: { text: 'meta.json' },
392
- },
393
- ],
394
- };
395
- }
396
- /**
397
- * Discover all .meta/ directories via watcher scan.
398
- *
399
- * Queries the watcher for indexed .meta/meta.json points using the
400
- * configured domain filter. Returns deduplicated meta directory paths.
401
- *
402
- * @param config - Synth config (for domain filter).
403
- * @param watcher - WatcherClient for scan queries.
404
- * @returns Array of normalized .meta/ directory paths.
405
- */
406
- async function discoverMetas(config, watcher) {
407
- const filter = buildMetaFilter(config);
408
- const scanFiles = await paginatedScan(watcher, {
409
- filter,
410
- fields: ['file_path'],
411
- });
412
- // Deduplicate by .meta/ directory path (handles multi-chunk files)
413
- const seen = new Set();
414
- const metaPaths = [];
415
- for (const sf of scanFiles) {
416
- const fp = normalizePath$1(sf.file_path);
417
- // Derive .meta/ directory from file_path (strip /meta.json)
418
- const metaPath = fp.replace(/\/meta\.json$/, '');
419
- if (seen.has(metaPath))
420
- continue;
421
- seen.add(metaPath);
422
- metaPaths.push(metaPath);
423
- }
424
- return metaPaths;
425
- }
426
-
427
- /**
428
- * File-system lock for preventing concurrent synthesis on the same meta.
429
- *
430
- * Lock file: .meta/.lock containing PID + timestamp.
431
- * Stale timeout: 30 minutes.
432
- *
433
- * @module lock
434
- */
435
- const LOCK_FILE = '.lock';
436
- const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
437
- /**
438
- * Attempt to acquire a lock on a .meta directory.
439
- *
440
- * @param metaPath - Absolute path to the .meta directory.
441
- * @returns True if lock was acquired, false if already locked (non-stale).
442
- */
443
- function acquireLock(metaPath) {
444
- const lockPath = join(metaPath, LOCK_FILE);
445
- if (existsSync(lockPath)) {
446
- try {
447
- const raw = readFileSync(lockPath, 'utf8');
448
- const data = JSON.parse(raw);
449
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
450
- if (lockAge < STALE_TIMEOUT_MS) {
451
- return false; // Lock is active
452
- }
453
- // Stale lock — fall through to overwrite
454
- }
455
- catch {
456
- // Corrupt lock file — overwrite
457
- }
458
- }
459
- const lock = {
460
- pid: process.pid,
461
- startedAt: new Date().toISOString(),
462
- };
463
- writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
464
- return true;
465
- }
466
- /**
467
- * Release a lock on a .meta directory.
468
- *
469
- * @param metaPath - Absolute path to the .meta directory.
470
- */
471
- function releaseLock(metaPath) {
472
- const lockPath = join(metaPath, LOCK_FILE);
473
- try {
474
- unlinkSync(lockPath);
475
- }
476
- catch {
477
- // Already removed or never existed
478
- }
479
- }
480
- /**
481
- * Check if a .meta directory is currently locked (non-stale).
482
- *
483
- * @param metaPath - Absolute path to the .meta directory.
484
- * @returns True if locked and not stale.
485
- */
486
- function isLocked(metaPath) {
487
- const lockPath = join(metaPath, LOCK_FILE);
488
- if (!existsSync(lockPath))
489
- return false;
490
- try {
491
- const raw = readFileSync(lockPath, 'utf8');
492
- const data = JSON.parse(raw);
493
- const lockAge = Date.now() - new Date(data.startedAt).getTime();
494
- return lockAge < STALE_TIMEOUT_MS;
495
- }
496
- catch {
497
- return false; // Corrupt lock = not locked
498
- }
499
- }
500
-
501
- /**
502
- * Build the ownership tree from discovered .meta/ paths.
503
- *
504
- * Each .meta/ directory owns its parent directory and all descendants,
505
- * except subtrees that contain their own .meta/. For those subtrees,
506
- * the parent meta consumes the child meta's synthesis output.
507
- *
508
- * @module discovery/ownershipTree
509
- */
510
- /** Normalize path separators to forward slashes for consistent comparison. */
511
- function normalizePath(p) {
512
- return p.split(sep).join('/');
513
- }
514
- /**
515
- * Build an ownership tree from an array of .meta/ directory paths.
516
- *
517
- * @param metaPaths - Absolute paths to .meta/ directories.
518
- * @returns The ownership tree with parent/child relationships.
519
- */
520
- function buildOwnershipTree(metaPaths) {
521
- const nodes = new Map();
522
- // Create nodes, sorted by ownerPath length (shortest first = shallowest)
523
- const sorted = [...metaPaths]
524
- .map((mp) => ({
525
- metaPath: normalizePath(mp),
526
- ownerPath: normalizePath(dirname(mp)),
527
- }))
528
- .sort((a, b) => a.ownerPath.length - b.ownerPath.length);
529
- for (const { metaPath, ownerPath } of sorted) {
530
- nodes.set(metaPath, {
531
- metaPath,
532
- ownerPath,
533
- treeDepth: 0,
534
- children: [],
535
- parent: null,
536
- });
537
- }
538
- const roots = [];
539
- // For each node, find its closest ancestor meta
540
- for (const node of nodes.values()) {
541
- let bestParent = null;
542
- let bestParentLen = -1;
543
- for (const candidate of nodes.values()) {
544
- if (candidate === node)
545
- continue;
546
- // Check if node's ownerPath is under candidate's ownerPath
547
- const rel = relative(candidate.ownerPath, node.ownerPath);
548
- if (rel.startsWith('..') || rel === '')
549
- continue;
550
- // candidate.ownerPath is an ancestor of node.ownerPath
551
- if (candidate.ownerPath.length > bestParentLen) {
552
- bestParent = candidate;
553
- bestParentLen = candidate.ownerPath.length;
554
- }
555
- }
556
- if (bestParent) {
557
- node.parent = bestParent;
558
- node.treeDepth = bestParent.treeDepth + 1;
559
- bestParent.children.push(node);
560
- }
561
- else {
562
- roots.push(node);
563
- }
564
- }
565
- return { nodes, roots };
566
- }
567
- /**
568
- * Find a node in the ownership tree by meta path or owner path.
569
- *
570
- * @param tree - The ownership tree to search.
571
- * @param targetPath - Path to search for (meta path or owner path).
572
- * @returns The matching node, or undefined if not found.
573
- */
574
- function findNode(tree, targetPath) {
575
- return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
576
- }
577
-
578
- /**
579
- * Unified meta listing: scan, dedup, enrich.
580
- *
581
- * Single source of truth for all consumers that need a list of metas
582
- * with enriched metadata. Replaces duplicated scan+dedup logic in
583
- * plugin tools, CLI, and prompt injection.
584
- *
585
- * @module discovery/listMetas
586
- */
587
- /**
588
- * Discover, deduplicate, and enrich all metas.
589
- *
590
- * This is the single consolidated function that replaces all duplicated
591
- * scan+dedup+enrich logic across the codebase. All enrichment comes from
592
- * reading meta.json on disk (the canonical source).
593
- *
594
- * @param config - Validated synthesis config.
595
- * @param watcher - Watcher HTTP client for discovery.
596
- * @returns Enriched meta list with summary statistics and ownership tree.
597
- */
598
- async function listMetas(config, watcher) {
599
- // Step 1: Discover deduplicated meta paths via watcher scan
600
- const metaPaths = await discoverMetas(config, watcher);
601
- // Step 2: Build ownership tree
602
- const tree = buildOwnershipTree(metaPaths);
603
- // Step 3: Read and enrich each meta from disk
604
- const entries = [];
605
- let staleCount = 0;
606
- let errorCount = 0;
607
- let lockedCount = 0;
608
- let neverSynthesizedCount = 0;
609
- let totalArchTokens = 0;
610
- let totalBuilderTokens = 0;
611
- let totalCriticTokens = 0;
612
- let lastSynthPath = null;
613
- let lastSynthAt = null;
614
- let stalestPath = null;
615
- let stalestEffective = -1;
616
- for (const node of tree.nodes.values()) {
617
- let meta;
618
- try {
619
- meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
620
- }
621
- catch {
622
- // Skip unreadable metas
623
- continue;
624
- }
625
- const depth = meta._depth ?? node.treeDepth;
626
- const emphasis = meta._emphasis ?? 1;
627
- const hasError = Boolean(meta._error);
628
- const locked = isLocked(normalizePath$1(node.metaPath));
629
- const neverSynth = !meta._generatedAt;
630
- // Compute staleness
631
- let stalenessSeconds;
632
- if (neverSynth) {
633
- stalenessSeconds = Infinity;
634
- }
635
- else {
636
- const genAt = new Date(meta._generatedAt).getTime();
637
- stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
638
- }
639
- // Tokens
640
- const archTokens = meta._architectTokens ?? 0;
641
- const buildTokens = meta._builderTokens ?? 0;
642
- const critTokens = meta._criticTokens ?? 0;
643
- // Accumulate summary stats
644
- if (stalenessSeconds > 0)
645
- staleCount++;
646
- if (hasError)
647
- errorCount++;
648
- if (locked)
649
- lockedCount++;
650
- if (neverSynth)
651
- neverSynthesizedCount++;
652
- totalArchTokens += archTokens;
653
- totalBuilderTokens += buildTokens;
654
- totalCriticTokens += critTokens;
655
- // Track last synthesized
656
- if (meta._generatedAt) {
657
- if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
658
- lastSynthAt = meta._generatedAt;
659
- lastSynthPath = node.metaPath;
660
- }
661
- }
662
- // Track stalest (effective staleness for scheduling)
663
- const depthFactor = Math.pow(1 + config.depthWeight, depth);
664
- const effectiveStaleness = (stalenessSeconds === Infinity
665
- ? Number.MAX_SAFE_INTEGER
666
- : stalenessSeconds) *
667
- depthFactor *
668
- emphasis;
669
- if (effectiveStaleness > stalestEffective) {
670
- stalestEffective = effectiveStaleness;
671
- stalestPath = node.metaPath;
672
- }
673
- entries.push({
674
- path: node.metaPath,
675
- depth,
676
- emphasis,
677
- stalenessSeconds,
678
- lastSynthesized: meta._generatedAt ?? null,
679
- hasError,
680
- locked,
681
- architectTokens: archTokens > 0 ? archTokens : null,
682
- builderTokens: buildTokens > 0 ? buildTokens : null,
683
- criticTokens: critTokens > 0 ? critTokens : null,
684
- children: node.children.length,
685
- node,
686
- meta,
687
- });
688
- }
689
- return {
690
- summary: {
691
- total: entries.length,
692
- stale: staleCount,
693
- errors: errorCount,
694
- locked: lockedCount,
695
- neverSynthesized: neverSynthesizedCount,
696
- tokens: {
697
- architect: totalArchTokens,
698
- builder: totalBuilderTokens,
699
- critic: totalCriticTokens,
700
- },
701
- stalestPath,
702
- lastSynthesizedPath: lastSynthPath,
703
- lastSynthesizedAt: lastSynthAt,
704
- },
705
- entries,
706
- tree,
707
- };
708
- }
709
-
710
- /**
711
- * Compute the file scope owned by a meta node.
712
- *
713
- * A meta owns: parent dir + all descendants, minus child .meta/ subtrees.
714
- * For child subtrees, it consumes the child's .meta/meta.json as a rollup input.
715
- *
716
- * @module discovery/scope
717
- */
718
- /**
719
- * Get the scope path prefix for a meta node.
720
- *
721
- * This is the ownerPath — all files under this path are in scope,
722
- * except subtrees owned by child metas.
723
- *
724
- * @param node - The meta node to compute scope for.
725
- * @returns The scope path prefix.
726
- */
727
- function getScopePrefix(node) {
728
- return node.ownerPath;
729
- }
730
- /**
731
- * Filter a list of file paths to only those in scope for a meta node.
732
- *
733
- * Includes files under ownerPath, excludes files under child meta ownerPaths,
734
- * but includes child .meta/meta.json files as rollup inputs.
735
- *
736
- * @param node - The meta node.
737
- * @param files - Array of file paths to filter.
738
- * @returns Filtered array of in-scope file paths.
739
- */
740
- function filterInScope(node, files) {
741
- const prefix = node.ownerPath + '/';
742
- const exclusions = node.children.map((c) => c.ownerPath + '/');
743
- const childMetaJsons = new Set(node.children.map((c) => c.metaPath + '/meta.json'));
744
- return files.filter((f) => {
745
- const normalized = f.split('\\').join('/');
746
- // Must be under ownerPath
747
- if (!normalized.startsWith(prefix) && normalized !== node.ownerPath)
748
- return false;
749
- // Check if under a child meta's subtree
750
- for (const excl of exclusions) {
751
- if (normalized.startsWith(excl)) {
752
- // Exception: child meta.json files are included as rollup inputs
753
- return childMetaJsons.has(normalized);
754
- }
755
- }
756
- return true;
757
- });
758
- }
759
-
760
- /**
761
- * Exponential moving average helper for token tracking.
762
- *
763
- * @module ema
764
- */
765
- const DEFAULT_DECAY = 0.3;
766
- /**
767
- * Compute exponential moving average.
768
- *
769
- * @param current - New observation.
770
- * @param previous - Previous EMA value, or undefined for first observation.
771
- * @param decay - Decay factor (0-1). Higher = more weight on new value. Default 0.3.
772
- * @returns Updated EMA.
773
- */
774
- function computeEma(current, previous, decay = DEFAULT_DECAY) {
775
- if (previous === undefined)
776
- return current;
777
- return decay * current + (1 - decay) * previous;
778
- }
779
-
780
- /**
781
- * Shared error utilities.
782
- *
783
- * @module errors
784
- */
785
- /**
786
- * Wrap an unknown caught value into a SynthError.
787
- *
788
- * @param step - Which synthesis step failed.
789
- * @param err - The caught error value.
790
- * @param code - Error classification code.
791
- * @returns A structured SynthError.
792
- */
793
- function toSynthError(step, err, code = 'FAILED') {
794
- return {
795
- step,
796
- code,
797
- message: err instanceof Error ? err.message : String(err),
798
- };
799
- }
800
-
801
- /**
802
- * SynthExecutor implementation using the OpenClaw gateway HTTP API.
803
- *
804
- * Lives in the library package so both plugin and runner can import it.
805
- * Spawns sub-agent sessions via the gateway, polls for completion,
806
- * and extracts output text.
807
- *
808
- * @module executor/GatewayExecutor
809
- */
810
- const DEFAULT_POLL_INTERVAL_MS = 5000;
811
- const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
812
- /** Sleep helper. */
813
- function sleep$1(ms) {
814
- return new Promise((resolve) => setTimeout(resolve, ms));
815
- }
816
- /**
817
- * SynthExecutor that spawns OpenClaw sessions via the gateway HTTP API.
818
- *
819
- * Used by both the OpenClaw plugin (in-process tool calls) and the
820
- * runner/CLI (external invocation). Constructs from `gatewayUrl` and
821
- * optional `apiKey` — typically sourced from `SynthConfig`.
822
- */
823
- class GatewayExecutor {
824
- gatewayUrl;
825
- apiKey;
826
- pollIntervalMs;
827
- constructor(options = {}) {
828
- this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:3000').replace(/\/+$/, '');
829
- this.apiKey = options.apiKey;
830
- this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
831
- }
832
- async spawn(task, options) {
833
- const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000;
834
- const deadline = Date.now() + timeoutMs;
835
- const headers = {
836
- 'Content-Type': 'application/json',
837
- };
838
- if (this.apiKey) {
839
- headers['Authorization'] = 'Bearer ' + this.apiKey;
840
- }
841
- const spawnRes = await fetch(this.gatewayUrl + '/api/sessions/spawn', {
842
- method: 'POST',
843
- headers,
844
- body: JSON.stringify({
845
- task,
846
- mode: 'run',
847
- model: options?.model,
848
- runTimeoutSeconds: options?.timeout,
849
- }),
850
- });
851
- if (!spawnRes.ok) {
852
- const text = await spawnRes.text();
853
- throw new Error('Gateway spawn failed: HTTP ' +
854
- spawnRes.status.toString() +
855
- ' - ' +
856
- text);
857
- }
858
- const spawnData = (await spawnRes.json());
859
- if (!spawnData.sessionKey) {
860
- throw new Error('Gateway spawn returned no sessionKey: ' + JSON.stringify(spawnData));
861
- }
862
- const { sessionKey } = spawnData;
863
- // Poll for completion
864
- while (Date.now() < deadline) {
865
- await sleep$1(this.pollIntervalMs);
866
- const historyRes = await fetch(this.gatewayUrl +
867
- '/api/sessions/' +
868
- encodeURIComponent(sessionKey) +
869
- '/history?limit=50', { headers });
870
- if (!historyRes.ok)
871
- continue;
872
- const history = (await historyRes.json());
873
- if (history.status === 'completed' || history.status === 'done') {
874
- // Extract token usage from session-level or message-level usage
875
- let tokens;
876
- if (history.usage?.totalTokens) {
877
- tokens = history.usage.totalTokens;
878
- }
879
- else {
880
- // Sum message-level usage as fallback
881
- let sum = 0;
882
- for (const msg of history.messages ?? []) {
883
- if (msg.usage?.totalTokens)
884
- sum += msg.usage.totalTokens;
885
- }
886
- if (sum > 0)
887
- tokens = sum;
888
- }
889
- // Extract the last assistant message as output
890
- const messages = history.messages ?? [];
891
- for (let i = messages.length - 1; i >= 0; i--) {
892
- if (messages[i].role === 'assistant' && messages[i].content) {
893
- return { output: messages[i].content, tokens };
894
- }
895
- }
896
- return { output: '', tokens };
897
- }
898
- }
899
- throw new Error('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms');
900
- }
901
- }
902
-
903
- /**
904
- * Build the SynthContext for a synthesis cycle.
905
- *
906
- * Computes shared inputs once: scope files, delta files, child meta outputs,
907
- * previous content/feedback, steer, and archive paths.
908
- *
909
- * @module orchestrator/contextPackage
910
- */
911
- /**
912
- * Condense a file list into glob-like summaries.
913
- * Groups by directory + extension pattern.
914
- *
915
- * @param files - Array of file paths.
916
- * @param maxIndividual - Show individual files up to this count.
917
- * @returns Condensed summary string.
918
- */
919
- function condenseScopeFiles(files, maxIndividual = 30) {
920
- if (files.length <= maxIndividual)
921
- return files.join('\n');
922
- // Group by dir + extension
923
- const groups = new Map();
924
- for (const f of files) {
925
- const dir = f.substring(0, f.lastIndexOf('/') + 1) || './';
926
- const ext = f.includes('.') ? f.substring(f.lastIndexOf('.')) : '(no ext)';
927
- const key = dir + '*' + ext;
928
- groups.set(key, (groups.get(key) ?? 0) + 1);
929
- }
930
- // Sort by count descending
931
- const sorted = [...groups.entries()].sort((a, b) => b[1] - a[1]);
932
- return sorted
933
- .map(([pattern, count]) => pattern + ' (' + count.toString() + ' files)')
934
- .join('\n');
935
- }
936
- /**
937
- * Build the context package for a synthesis cycle.
938
- *
939
- * @param node - The meta node being synthesized.
940
- * @param meta - Current meta.json content.
941
- * @param watcher - WatcherClient for scope enumeration.
942
- * @returns The computed context package.
943
- */
944
- async function buildContextPackage(node, meta, watcher) {
945
- // Scope files via watcher scan, excluding child subtrees
946
- const allScanFiles = await paginatedScan(watcher, {
947
- pathPrefix: node.ownerPath,
948
- });
949
- const scopeFiles = filterInScope(node, allScanFiles.map((f) => f.file_path));
950
- // Delta files: modified since _generatedAt
951
- let deltaFiles;
952
- if (meta._generatedAt) {
953
- const modifiedAfter = Math.floor(new Date(meta._generatedAt).getTime() / 1000);
954
- const deltaScanFiles = await paginatedScan(watcher, {
955
- pathPrefix: node.ownerPath,
956
- modifiedAfter,
957
- });
958
- deltaFiles = filterInScope(node, deltaScanFiles.map((f) => f.file_path));
959
- }
960
- else {
961
- deltaFiles = scopeFiles; // First run: all files are delta
962
- }
963
- // Child meta outputs
964
- const childMetas = {};
965
- for (const child of node.children) {
966
- const childMetaFile = join(child.metaPath, 'meta.json');
967
- try {
968
- const raw = readFileSync(childMetaFile, 'utf8');
969
- const childMeta = JSON.parse(raw);
970
- childMetas[child.ownerPath] = childMeta._content ?? null;
971
- }
972
- catch {
973
- childMetas[child.ownerPath] = null;
974
- }
975
- }
976
- // Archive paths
977
- const archives = listArchiveFiles(node.metaPath);
978
- return {
979
- path: node.metaPath,
980
- scopeFiles,
981
- deltaFiles,
982
- childMetas,
983
- previousContent: meta._content ?? null,
984
- previousFeedback: meta._feedback ?? null,
985
- steer: meta._steer ?? null,
986
- archives,
987
- };
988
- }
989
-
990
- /**
991
- * Build task prompts for each synthesis step.
992
- *
993
- * @module orchestrator/buildTask
994
- */
995
- /** Append optional context sections shared across all step prompts. */
996
- function appendSharedSections(sections, ctx, options) {
997
- const opts = {
998
- includeSteer: true,
999
- includePreviousContent: true,
1000
- includePreviousFeedback: true,
1001
- feedbackHeading: '## PREVIOUS FEEDBACK',
1002
- includeChildMetas: true,
1003
- ...options,
1004
- };
1005
- if (opts.includeSteer && ctx.steer) {
1006
- sections.push('', '## STEERING PROMPT', ctx.steer);
1007
- }
1008
- if (opts.includePreviousContent && ctx.previousContent) {
1009
- sections.push('', '## PREVIOUS SYNTHESIS', ctx.previousContent);
1010
- }
1011
- if (opts.includePreviousFeedback && ctx.previousFeedback) {
1012
- sections.push('', opts.feedbackHeading, ctx.previousFeedback);
1013
- }
1014
- if (opts.includeChildMetas && Object.keys(ctx.childMetas).length > 0) {
1015
- sections.push('', '## CHILD META OUTPUTS');
1016
- for (const [childPath, content] of Object.entries(ctx.childMetas)) {
1017
- sections.push(`### ${childPath}`, typeof content === 'string' ? content : '(not yet synthesized)');
1018
- }
1019
- }
1020
- }
1021
- /**
1022
- * Build the architect task prompt.
1023
- *
1024
- * @param ctx - Synthesis context.
1025
- * @param meta - Current meta.json.
1026
- * @param config - Synthesis config.
1027
- * @returns The architect task prompt string.
1028
- */
1029
- function buildArchitectTask(ctx, meta, config) {
1030
- const sections = [
1031
- meta._architect ?? config.defaultArchitect,
1032
- '',
1033
- '## SCOPE',
1034
- `Path: ${ctx.path}`,
1035
- `Total files in scope: ${ctx.scopeFiles.length.toString()}`,
1036
- `Files changed since last synthesis: ${ctx.deltaFiles.length.toString()}`,
1037
- '',
1038
- '### File listing (scope)',
1039
- condenseScopeFiles(ctx.scopeFiles),
1040
- ];
1041
- // Inject previous _builder so architect can see its own prior output
1042
- if (meta._builder) {
1043
- sections.push('', '## PREVIOUS TASK BRIEF', meta._builder);
1044
- }
1045
- appendSharedSections(sections, ctx);
1046
- if (ctx.archives.length > 0) {
1047
- sections.push('', '## ARCHIVE HISTORY', `${ctx.archives.length.toString()} previous synthesis snapshots available in .meta/archive/.`, 'Review these to understand how the synthesis has evolved over time.');
1048
- }
1049
- return sections.join('\n');
1050
- }
1051
- /**
1052
- * Build the builder task prompt.
1053
- *
1054
- * @param ctx - Synthesis context.
1055
- * @param meta - Current meta.json.
1056
- * @param config - Synthesis config.
1057
- * @returns The builder task prompt string.
1058
- */
1059
- function buildBuilderTask(ctx, meta, config) {
1060
- const sections = [
1061
- '## TASK BRIEF (from Architect)',
1062
- meta._builder ?? '(No architect brief available)',
1063
- '',
1064
- '## SCOPE',
1065
- `Path: ${ctx.path}`,
1066
- `Delta files (${ctx.deltaFiles.length.toString()} changed):`,
1067
- ...ctx.deltaFiles.slice(0, config.maxLines).map((f) => `- ${f}`),
1068
- ];
1069
- appendSharedSections(sections, ctx, {
1070
- includeSteer: false,
1071
- feedbackHeading: '## FEEDBACK FROM CRITIC',
1072
- });
1073
- sections.push('', '## OUTPUT FORMAT', 'Return a JSON object with:', '- "_content": Markdown narrative synthesis (required)', '- Any additional structured fields as non-underscore keys');
1074
- return sections.join('\n');
1075
- }
1076
- /**
1077
- * Build the critic task prompt.
1078
- *
1079
- * @param ctx - Synthesis context.
1080
- * @param meta - Current meta.json (with _content already set by builder).
1081
- * @param config - Synthesis config.
1082
- * @returns The critic task prompt string.
1083
- */
1084
- function buildCriticTask(ctx, meta, config) {
1085
- const sections = [
1086
- meta._critic ?? config.defaultCritic,
1087
- '',
1088
- '## SYNTHESIS TO EVALUATE',
1089
- meta._content ?? '(No content produced)',
1090
- '',
1091
- '## SCOPE',
1092
- `Path: ${ctx.path}`,
1093
- `Files in scope: ${ctx.scopeFiles.length.toString()}`,
1094
- ];
1095
- appendSharedSections(sections, ctx, {
1096
- includePreviousContent: false,
1097
- feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
1098
- includeChildMetas: false,
1099
- });
1100
- sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
1101
- return sections.join('\n');
1102
- }
1103
-
1104
- /**
1105
- * Merge synthesis results into meta.json.
1106
- *
1107
- * Preserves human-set fields (_id, _steer, _depth).
1108
- * Writes engine fields (_generatedAt, _structureHash, etc.).
1109
- * Validates against schema before writing.
1110
- *
1111
- * @module orchestrator/merge
1112
- */
1113
- /**
1114
- * Merge results into meta.json and write atomically.
1115
- *
1116
- * @param options - Merge options.
1117
- * @returns The updated MetaJson.
1118
- * @throws If validation fails (malformed output).
1119
- */
1120
- function mergeAndWrite(options) {
1121
- const merged = {
1122
- // Preserve human-set fields
1123
- _id: options.current._id,
1124
- _steer: options.current._steer,
1125
- _depth: options.current._depth,
1126
- _emphasis: options.current._emphasis,
1127
- // Engine fields
1128
- _architect: options.architect,
1129
- _builder: options.builder,
1130
- _critic: options.critic,
1131
- _generatedAt: new Date().toISOString(),
1132
- _structureHash: options.structureHash,
1133
- _synthesisCount: options.synthesisCount,
1134
- // Token tracking
1135
- _architectTokens: options.architectTokens,
1136
- _builderTokens: options.builderTokens,
1137
- _criticTokens: options.criticTokens,
1138
- _architectTokensAvg: options.architectTokens !== undefined
1139
- ? computeEma(options.architectTokens, options.current._architectTokensAvg)
1140
- : options.current._architectTokensAvg,
1141
- _builderTokensAvg: options.builderTokens !== undefined
1142
- ? computeEma(options.builderTokens, options.current._builderTokensAvg)
1143
- : options.current._builderTokensAvg,
1144
- _criticTokensAvg: options.criticTokens !== undefined
1145
- ? computeEma(options.criticTokens, options.current._criticTokensAvg)
1146
- : options.current._criticTokensAvg,
1147
- // Content from builder
1148
- _content: options.builderOutput?.content ?? options.current._content,
1149
- // Feedback from critic
1150
- _feedback: options.feedback ?? options.current._feedback,
1151
- // Error handling
1152
- _error: options.error ?? undefined,
1153
- // Spread structured fields from builder
1154
- ...options.builderOutput?.fields,
1155
- };
1156
- // Clean up undefined optional fields
1157
- if (merged._steer === undefined)
1158
- delete merged._steer;
1159
- if (merged._depth === undefined)
1160
- delete merged._depth;
1161
- if (merged._emphasis === undefined)
1162
- delete merged._emphasis;
1163
- if (merged._architectTokens === undefined)
1164
- delete merged._architectTokens;
1165
- if (merged._builderTokens === undefined)
1166
- delete merged._builderTokens;
1167
- if (merged._criticTokens === undefined)
1168
- delete merged._criticTokens;
1169
- if (merged._architectTokensAvg === undefined)
1170
- delete merged._architectTokensAvg;
1171
- if (merged._builderTokensAvg === undefined)
1172
- delete merged._builderTokensAvg;
1173
- if (merged._criticTokensAvg === undefined)
1174
- delete merged._criticTokensAvg;
1175
- if (merged._error === undefined)
1176
- delete merged._error;
1177
- if (merged._content === undefined)
1178
- delete merged._content;
1179
- if (merged._feedback === undefined)
1180
- delete merged._feedback;
1181
- // Validate
1182
- const result = metaJsonSchema.safeParse(merged);
1183
- if (!result.success) {
1184
- throw new Error(`Meta validation failed: ${result.error.message}`);
1185
- }
1186
- // Write atomically
1187
- const filePath = join(options.metaPath, 'meta.json');
1188
- writeFileSync(filePath, JSON.stringify(result.data, null, 2) + '\n');
1189
- return result.data;
1190
- }
1191
-
1192
- /**
1193
- * Select the best synthesis candidate from stale metas.
1194
- *
1195
- * Picks the meta with highest effective staleness.
1196
- *
1197
- * @module scheduling/selectCandidate
1198
- */
1199
- /**
1200
- * Select the candidate with the highest effective staleness.
1201
- *
1202
- * @param candidates - Array of candidates with computed effective staleness.
1203
- * @returns The winning candidate, or null if no candidates.
1204
- */
1205
- function selectCandidate(candidates) {
1206
- if (candidates.length === 0)
1207
- return null;
1208
- let best = candidates[0];
1209
- for (let i = 1; i < candidates.length; i++) {
1210
- if (candidates[i].effectiveStaleness > best.effectiveStaleness) {
1211
- best = candidates[i];
1212
- }
1213
- }
1214
- return best;
1215
- }
1216
-
1217
- /**
1218
- * Staleness detection via watcher scan.
1219
- *
1220
- * A meta is stale when any file in its scope was modified after _generatedAt.
1221
- *
1222
- * @module scheduling/staleness
1223
- */
1224
- /**
1225
- * Check if a meta is stale by querying the watcher for modified files.
1226
- *
1227
- * @param scopePrefix - Path prefix for this meta's scope.
1228
- * @param meta - Current meta.json content.
1229
- * @param watcher - WatcherClient instance.
1230
- * @returns True if any file in scope was modified after _generatedAt.
1231
- */
1232
- async function isStale(scopePrefix, meta, watcher) {
1233
- if (!meta._generatedAt)
1234
- return true; // Never synthesized = stale
1235
- const generatedAtUnix = Math.floor(new Date(meta._generatedAt).getTime() / 1000);
1236
- const result = await watcher.scan({
1237
- pathPrefix: scopePrefix,
1238
- modifiedAfter: generatedAtUnix,
1239
- limit: 1,
1240
- });
1241
- return result.files.length > 0;
1242
- }
1243
- /**
1244
- * Compute actual staleness in seconds (now minus _generatedAt).
1245
- *
1246
- * @param meta - Current meta.json content.
1247
- * @returns Staleness in seconds, or Infinity if never synthesized.
1248
- */
1249
- function actualStaleness(meta) {
1250
- if (!meta._generatedAt)
1251
- return Infinity;
1252
- const generatedMs = new Date(meta._generatedAt).getTime();
1253
- return (Date.now() - generatedMs) / 1000;
1254
- }
1255
- /**
1256
- * Check whether the architect step should be triggered.
1257
- *
1258
- * @param meta - Current meta.json.
1259
- * @param structureChanged - Whether the structure hash changed.
1260
- * @param steerChanged - Whether the steer directive changed.
1261
- * @param architectEvery - Config: run architect every N cycles.
1262
- * @returns True if the architect step should run.
1263
- */
1264
- function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
1265
- return (!meta._builder ||
1266
- structureChanged ||
1267
- steerChanged ||
1268
- (meta._synthesisCount ?? 0) >= architectEvery);
1269
- }
1270
- /**
1271
- * Detect whether the steer directive changed since the last archive.
1272
- *
1273
- * @param currentSteer - Current _steer value (or undefined).
1274
- * @param archiveSteer - Archive _steer value (or undefined).
1275
- * @param hasArchive - Whether an archive snapshot exists.
1276
- * @returns True if steer changed.
1277
- */
1278
- function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
1279
- if (!hasArchive)
1280
- return Boolean(currentSteer);
1281
- return currentSteer !== archiveSteer;
1282
- }
1283
-
1284
- /**
1285
- * Weighted staleness formula for candidate selection.
1286
- *
1287
- * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
1288
- *
1289
- * @module scheduling/weightedFormula
1290
- */
1291
- /**
1292
- * Compute effective staleness for a set of candidates.
1293
- *
1294
- * Normalizes depths so the minimum becomes 0, then applies the formula:
1295
- * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
1296
- *
1297
- * Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
1298
- * metas to tune how much their tree position affects scheduling.
1299
- *
1300
- * @param candidates - Array of \{ node, meta, actualStaleness \}.
1301
- * @param depthWeight - Exponent for depth weighting (0 = pure staleness).
1302
- * @returns Same array with effectiveStaleness computed.
1303
- */
1304
- function computeEffectiveStaleness(candidates, depthWeight) {
1305
- if (candidates.length === 0)
1306
- return [];
1307
- // Get depth for each candidate: use _depth override or tree depth
1308
- const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
1309
- // Normalize: shift so minimum becomes 0
1310
- const minDepth = Math.min(...depths);
1311
- const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
1312
- return candidates.map((c, i) => {
1313
- const emphasis = c.meta._emphasis ?? 1;
1314
- return {
1315
- ...c,
1316
- effectiveStaleness: c.actualStaleness *
1317
- Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
1318
- };
1319
- });
1320
- }
1321
-
1322
- /**
1323
- * Compute a structure hash from a sorted file listing.
1324
- *
1325
- * Used to detect when directory structure changes, triggering
1326
- * an architect re-run.
1327
- *
1328
- * @module structureHash
1329
- */
1330
- /**
1331
- * Compute a SHA-256 hash of a sorted file listing.
1332
- *
1333
- * @param filePaths - Array of file paths in scope.
1334
- * @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
1335
- */
1336
- function computeStructureHash(filePaths) {
1337
- const sorted = [...filePaths].sort();
1338
- const content = sorted.join('\n');
1339
- return createHash('sha256').update(content).digest('hex');
1340
- }
1341
-
1342
- /**
1343
- * Parse subprocess outputs for each synthesis step.
1344
- *
1345
- * - Architect: returns text \> _builder
1346
- * - Builder: returns JSON \> _content + structured fields
1347
- * - Critic: returns text \> _feedback
1348
- *
1349
- * @module orchestrator/parseOutput
1350
- */
1351
- /**
1352
- * Parse architect output. The architect returns a task brief as text.
1353
- *
1354
- * @param output - Raw subprocess output.
1355
- * @returns The task brief string.
1356
- */
1357
- function parseArchitectOutput(output) {
1358
- return output.trim();
1359
- }
1360
- /**
1361
- * Parse builder output. The builder returns JSON with _content and optional fields.
1362
- *
1363
- * Attempts JSON parse first. If that fails, treats the entire output as _content.
1364
- *
1365
- * @param output - Raw subprocess output.
1366
- * @returns Parsed builder output with content and structured fields.
1367
- */
1368
- function parseBuilderOutput(output) {
1369
- const trimmed = output.trim();
1370
- // Try to extract JSON from the output (may be wrapped in markdown code fences)
1371
- let jsonStr = trimmed;
1372
- const fenceMatch = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
1373
- if (fenceMatch) {
1374
- jsonStr = fenceMatch[1].trim();
1375
- }
1376
- try {
1377
- const parsed = JSON.parse(jsonStr);
1378
- // Extract _content
1379
- const content = typeof parsed._content === 'string'
1380
- ? parsed._content
1381
- : typeof parsed.content === 'string'
1382
- ? parsed.content
1383
- : trimmed;
1384
- // Extract non-underscore fields
1385
- const fields = {};
1386
- for (const [key, value] of Object.entries(parsed)) {
1387
- if (!key.startsWith('_') && key !== 'content') {
1388
- fields[key] = value;
1389
- }
1390
- }
1391
- return { content, fields };
1392
- }
1393
- catch {
1394
- // Not valid JSON — treat entire output as content
1395
- return { content: trimmed, fields: {} };
1396
- }
1397
- }
1398
- /**
1399
- * Parse critic output. The critic returns evaluation text.
1400
- *
1401
- * @param output - Raw subprocess output.
1402
- * @returns The feedback string.
1403
- */
1404
- function parseCriticOutput(output) {
1405
- return output.trim();
1406
- }
1407
-
1408
- /**
1409
- * Main orchestration function — the 13-step synthesis cycle.
1410
- *
1411
- * Wires together discovery, scheduling, archiving, executor calls,
1412
- * and merge/write-back.
1413
- *
1414
- * @module orchestrator/orchestrate
1415
- */
1416
- /** Finalize a cycle: merge, snapshot, prune. */
1417
- function finalizeCycle(metaPath, current, config, architect, builder, critic, builderOutput, feedback, structureHash, synthesisCount, error, architectTokens, builderTokens, criticTokens) {
1418
- const updated = mergeAndWrite({
1419
- metaPath,
1420
- current,
1421
- architect,
1422
- builder,
1423
- critic,
1424
- builderOutput,
1425
- feedback,
1426
- structureHash,
1427
- synthesisCount,
1428
- error,
1429
- architectTokens,
1430
- builderTokens,
1431
- criticTokens,
1432
- });
1433
- createSnapshot(metaPath, updated);
1434
- pruneArchive(metaPath, config.maxArchive);
1435
- return updated;
1436
- }
1437
- /**
1438
- * Run a single synthesis cycle.
1439
- *
1440
- * Discovers all metas, selects the stalest candidate, and runs the
1441
- * three-step synthesis (architect, builder, critic).
1442
- *
1443
- * @param config - Validated synthesis config.
1444
- * @param executor - Pluggable LLM executor.
1445
- * @param watcher - Watcher HTTP client.
1446
- * @returns Result indicating whether synthesis occurred.
1447
- */
1448
- async function orchestrateOnce(config, executor, watcher, targetPath) {
1449
- // Step 1: Discover via watcher scan
1450
- const metaPaths = await discoverMetas(config, watcher);
1451
- if (metaPaths.length === 0)
1452
- return { synthesized: false };
1453
- // Read meta.json for each discovered meta
1454
- const metas = new Map();
1455
- for (const mp of metaPaths) {
1456
- const metaFilePath = join(mp, 'meta.json');
1457
- try {
1458
- metas.set(normalizePath$1(mp), JSON.parse(readFileSync(metaFilePath, 'utf8')));
1459
- }
1460
- catch {
1461
- // Skip metas with unreadable meta.json
1462
- continue;
1463
- }
1464
- }
1465
- const tree = buildOwnershipTree(metaPaths);
1466
- // If targetPath specified, skip candidate selection — go directly to that meta
1467
- let targetNode;
1468
- if (targetPath) {
1469
- const normalized = normalizePath$1(targetPath);
1470
- targetNode = findNode(tree, normalized) ?? undefined;
1471
- if (!targetNode)
1472
- return { synthesized: false };
1473
- }
1474
- // Steps 3-4: Staleness check + candidate selection
1475
- const candidates = [];
1476
- for (const node of tree.nodes.values()) {
1477
- const meta = metas.get(node.metaPath);
1478
- const staleness = actualStaleness(meta);
1479
- if (staleness > 0) {
1480
- candidates.push({ node, meta, actualStaleness: staleness });
1481
- }
1482
- }
1483
- const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
1484
- // Sort by effective staleness descending
1485
- const ranked = [...weighted].sort((a, b) => b.effectiveStaleness - a.effectiveStaleness);
1486
- if (ranked.length === 0)
1487
- return { synthesized: false };
1488
- // Find the first candidate with actual changes (if skipUnchanged)
1489
- let winner = null;
1490
- for (const candidate of ranked) {
1491
- if (!acquireLock(candidate.node.metaPath))
1492
- continue;
1493
- const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
1494
- if (!verifiedStale && candidate.meta._generatedAt) {
1495
- // Bump _generatedAt so it doesn't win next cycle
1496
- const metaFilePath = join(candidate.node.metaPath, 'meta.json');
1497
- const freshMeta = JSON.parse(readFileSync(metaFilePath, 'utf8'));
1498
- freshMeta._generatedAt = new Date().toISOString();
1499
- writeFileSync(metaFilePath, JSON.stringify(freshMeta, null, 2));
1500
- releaseLock(candidate.node.metaPath);
1501
- if (config.skipUnchanged)
1502
- continue;
1503
- return { synthesized: false };
1504
- }
1505
- winner = candidate;
1506
- break;
1507
- }
1508
- if (!winner && !targetNode)
1509
- return { synthesized: false };
1510
- const node = targetNode ?? winner.node;
1511
- // For targeted path, acquire lock now (candidate selection already locked for stalest)
1512
- if (targetNode && !acquireLock(node.metaPath)) {
1513
- return { synthesized: false };
1514
- }
1515
- try {
1516
- // Re-read meta after lock (may have changed)
1517
- const currentMeta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
1518
- const architectPrompt = currentMeta._architect ?? config.defaultArchitect;
1519
- const criticPrompt = currentMeta._critic ?? config.defaultCritic;
1520
- // Step 5: Structure hash
1521
- const scopePrefix = getScopePrefix(node);
1522
- const allScanFiles = await paginatedScan(watcher, {
1523
- pathPrefix: scopePrefix,
1524
- });
1525
- const allFilePaths = allScanFiles.map((f) => f.file_path);
1526
- // Structure hash uses scope-filtered files (excluding child subtrees)
1527
- // so changes in child scopes don't trigger parent architect re-runs
1528
- const scopeFiles = filterInScope(node, allFilePaths);
1529
- const newStructureHash = computeStructureHash(scopeFiles);
1530
- const structureChanged = newStructureHash !== currentMeta._structureHash;
1531
- // Step 6: Steer change detection
1532
- const latestArchive = readLatestArchive(node.metaPath);
1533
- const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
1534
- // Step 7: Compute context
1535
- const ctx = await buildContextPackage(node, currentMeta, watcher);
1536
- // Step 8: Architect (conditional)
1537
- const architectTriggered = isArchitectTriggered(currentMeta, structureChanged, steerChanged, config.architectEvery);
1538
- let builderBrief = currentMeta._builder ?? '';
1539
- let synthesisCount = currentMeta._synthesisCount ?? 0;
1540
- let stepError = null;
1541
- let architectTokens;
1542
- let builderTokens;
1543
- let criticTokens;
1544
- if (architectTriggered) {
1545
- try {
1546
- const architectTask = buildArchitectTask(ctx, currentMeta, config);
1547
- const architectResult = await executor.spawn(architectTask, {
1548
- timeout: config.architectTimeout,
1549
- });
1550
- builderBrief = parseArchitectOutput(architectResult.output);
1551
- architectTokens = architectResult.tokens;
1552
- synthesisCount = 0;
1553
- }
1554
- catch (err) {
1555
- stepError = toSynthError('architect', err);
1556
- if (!currentMeta._builder) {
1557
- // No cached builder — cycle fails
1558
- finalizeCycle(node.metaPath, currentMeta, config, architectPrompt, '', criticPrompt, null, null, newStructureHash, synthesisCount, stepError, architectTokens);
1559
- return {
1560
- synthesized: true,
1561
- metaPath: node.metaPath,
1562
- error: stepError,
1563
- };
1564
- }
1565
- // Has cached builder — continue with existing
1566
- }
1567
- }
1568
- // Step 9: Builder
1569
- const metaForBuilder = { ...currentMeta, _builder: builderBrief };
1570
- let builderOutput = null;
1571
- try {
1572
- const builderTask = buildBuilderTask(ctx, metaForBuilder, config);
1573
- const builderResult = await executor.spawn(builderTask, {
1574
- timeout: config.builderTimeout,
1575
- });
1576
- builderOutput = parseBuilderOutput(builderResult.output);
1577
- builderTokens = builderResult.tokens;
1578
- synthesisCount++;
1579
- }
1580
- catch (err) {
1581
- stepError = toSynthError('builder', err);
1582
- return { synthesized: true, metaPath: node.metaPath, error: stepError };
1583
- }
1584
- // Step 10: Critic
1585
- const metaForCritic = {
1586
- ...currentMeta,
1587
- _content: builderOutput.content,
1588
- };
1589
- let feedback = null;
1590
- try {
1591
- const criticTask = buildCriticTask(ctx, metaForCritic, config);
1592
- const criticResult = await executor.spawn(criticTask, {
1593
- timeout: config.criticTimeout,
1594
- });
1595
- feedback = parseCriticOutput(criticResult.output);
1596
- criticTokens = criticResult.tokens;
1597
- stepError = null; // Clear any architect error on full success
1598
- }
1599
- catch (err) {
1600
- stepError = stepError ?? toSynthError('critic', err);
1601
- }
1602
- // Steps 11-12: Merge, archive, prune
1603
- finalizeCycle(node.metaPath, currentMeta, config, architectPrompt, builderBrief, criticPrompt, builderOutput, feedback, newStructureHash, synthesisCount, stepError, architectTokens, builderTokens, criticTokens);
1604
- return {
1605
- synthesized: true,
1606
- metaPath: node.metaPath,
1607
- error: stepError ?? undefined,
1608
- };
1609
- }
1610
- finally {
1611
- // Step 13: Release lock
1612
- releaseLock(node.metaPath);
1613
- }
1614
- }
1615
- /**
1616
- * Run synthesis cycles up to batchSize.
1617
- *
1618
- * Calls orchestrateOnce() in a loop, stopping when batchSize is reached
1619
- * or no more candidates are available.
1620
- *
1621
- * @param config - Validated synthesis config.
1622
- * @param executor - Pluggable LLM executor.
1623
- * @param watcher - Watcher HTTP client.
1624
- * @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
1625
- * @returns Array of results, one per cycle attempted.
1626
- */
1627
- async function orchestrate(config, executor, watcher, targetPath) {
1628
- const results = [];
1629
- for (let i = 0; i < config.batchSize; i++) {
1630
- const result = await orchestrateOnce(config, executor, watcher, targetPath);
1631
- results.push(result);
1632
- if (!result.synthesized)
1633
- break; // No more candidates
1634
- }
1635
- return results;
1636
- }
1637
-
1638
- /**
1639
- * HTTP implementation of the WatcherClient interface.
1640
- *
1641
- * Talks to jeeves-watcher's POST /scan and POST /rules endpoints
1642
- * with retry and exponential backoff.
1643
- *
1644
- * @module watcher-client/HttpWatcherClient
1645
- */
1646
- /** Default retry configuration. */
1647
- const DEFAULT_MAX_RETRIES = 3;
1648
- const DEFAULT_BACKOFF_BASE_MS = 1000;
1649
- const DEFAULT_BACKOFF_FACTOR = 4;
1650
- /** Sleep for a given number of milliseconds. */
1651
- function sleep(ms) {
1652
- return new Promise((resolve) => setTimeout(resolve, ms));
1653
- }
1654
- /** Check if an error is transient (worth retrying). */
1655
- function isTransient(status) {
1656
- return status >= 500 || status === 408 || status === 429;
1657
- }
1658
- /**
1659
- * HTTP-based WatcherClient implementation with retry.
1660
- */
1661
- class HttpWatcherClient {
1662
- baseUrl;
1663
- maxRetries;
1664
- backoffBaseMs;
1665
- backoffFactor;
1666
- constructor(options) {
1667
- this.baseUrl = options.baseUrl.replace(/\/+$/, '');
1668
- this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
1669
- this.backoffBaseMs = options.backoffBaseMs ?? DEFAULT_BACKOFF_BASE_MS;
1670
- this.backoffFactor = options.backoffFactor ?? DEFAULT_BACKOFF_FACTOR;
1671
- }
1672
- /** POST JSON with retry. */
1673
- async post(endpoint, body) {
1674
- const url = this.baseUrl + endpoint;
1675
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
1676
- const res = await fetch(url, {
1677
- method: 'POST',
1678
- headers: { 'Content-Type': 'application/json' },
1679
- body: JSON.stringify(body),
1680
- });
1681
- if (res.ok) {
1682
- return res.json();
1683
- }
1684
- if (!isTransient(res.status) || attempt === this.maxRetries) {
1685
- const text = await res.text();
1686
- throw new Error(`Watcher ${endpoint} failed: HTTP ${res.status.toString()} - ${text}`);
1687
- }
1688
- // Exponential backoff
1689
- const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
1690
- await sleep(delayMs);
1691
- }
1692
- // Unreachable, but TypeScript needs it
1693
- throw new Error('Retry exhausted');
1694
- }
1695
- async scan(params) {
1696
- // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
1697
- const mustClauses = [];
1698
- // Carry over any existing 'must' clauses from the provided filter
1699
- if (params.filter) {
1700
- const existing = params.filter.must;
1701
- if (Array.isArray(existing)) {
1702
- mustClauses.push(...existing);
1703
- }
1704
- }
1705
- // Translate pathPrefix into a Qdrant text match on file_path
1706
- if (params.pathPrefix !== undefined) {
1707
- mustClauses.push({
1708
- key: 'file_path',
1709
- match: { text: params.pathPrefix },
1710
- });
1711
- }
1712
- // Translate modifiedAfter into a Qdrant range filter on modified_at
1713
- if (params.modifiedAfter !== undefined) {
1714
- mustClauses.push({
1715
- key: 'modified_at',
1716
- range: { gt: params.modifiedAfter },
1717
- });
1718
- }
1719
- const filter = { must: mustClauses };
1720
- const body = { filter };
1721
- if (params.fields !== undefined) {
1722
- body.fields = params.fields;
1723
- }
1724
- if (params.limit !== undefined) {
1725
- body.limit = params.limit;
1726
- }
1727
- if (params.cursor !== undefined) {
1728
- body.cursor = params.cursor;
1729
- }
1730
- const raw = (await this.post('/scan', body));
1731
- // jeeves-watcher returns { points, cursor }; map to ScanResponse.
1732
- const points = (raw.points ?? raw.files ?? []);
1733
- const next = (raw.cursor ?? raw.next);
1734
- const files = points.map((p) => {
1735
- const payload = (p.payload ?? p);
1736
- return {
1737
- file_path: (payload.file_path ?? payload.path ?? ''),
1738
- modified_at: (payload.modified_at ?? payload.mtime ?? 0),
1739
- content_hash: (payload.content_hash ?? ''),
1740
- ...payload,
1741
- };
1742
- });
1743
- return { files, next: next ?? undefined };
1744
- }
1745
- async registerRules(source, rules) {
1746
- await this.post('/rules/register', { source, rules });
1747
- }
1748
- async unregisterRules(source) {
1749
- await this.post('/rules/unregister', { source });
1750
- }
1751
- }
1752
-
1753
- /**
1754
- * Knowledge synthesis engine for the Jeeves platform.
1755
- *
1756
- * @packageDocumentation
1757
- */
1758
-
1759
- var index = /*#__PURE__*/Object.freeze({
1760
- __proto__: null,
1761
- GatewayExecutor: GatewayExecutor,
1762
- HttpWatcherClient: HttpWatcherClient,
1763
- acquireLock: acquireLock,
1764
- actualStaleness: actualStaleness,
1765
- buildArchitectTask: buildArchitectTask,
1766
- buildBuilderTask: buildBuilderTask,
1767
- buildContextPackage: buildContextPackage,
1768
- buildCriticTask: buildCriticTask,
1769
- buildMetaFilter: buildMetaFilter,
1770
- buildOwnershipTree: buildOwnershipTree,
1771
- computeEffectiveStaleness: computeEffectiveStaleness,
1772
- computeEma: computeEma,
1773
- computeStructureHash: computeStructureHash,
1774
- createSnapshot: createSnapshot,
1775
- discoverMetas: discoverMetas,
1776
- filterInScope: filterInScope,
1777
- findNode: findNode,
1778
- getScopePrefix: getScopePrefix,
1779
- hasSteerChanged: hasSteerChanged,
1780
- isArchitectTriggered: isArchitectTriggered,
1781
- isLocked: isLocked,
1782
- isStale: isStale,
1783
- listArchiveFiles: listArchiveFiles,
1784
- listMetas: listMetas,
1785
- loadSynthConfig: loadSynthConfig,
1786
- mergeAndWrite: mergeAndWrite,
1787
- metaJsonSchema: metaJsonSchema,
1788
- normalizePath: normalizePath$1,
1789
- orchestrate: orchestrate,
1790
- paginatedScan: paginatedScan,
1791
- parseArchitectOutput: parseArchitectOutput,
1792
- parseBuilderOutput: parseBuilderOutput,
1793
- parseCriticOutput: parseCriticOutput,
1794
- pruneArchive: pruneArchive,
1795
- readLatestArchive: readLatestArchive,
1796
- releaseLock: releaseLock,
1797
- resolveConfigPath: resolveConfigPath,
1798
- selectCandidate: selectCandidate,
1799
- synthConfigSchema: synthConfigSchema,
1800
- synthErrorSchema: synthErrorSchema,
1801
- toSynthError: toSynthError
1802
- });
1803
-
1804
- /**
1805
- * jeeves-meta CLI — ad hoc invocation, debugging, and maintenance.
1806
- *
1807
- * Usage:
1808
- * npx \@karmaniverous/jeeves-meta <command> [options]
1809
- *
1810
- * Config resolution: --config <path> or JEEVES_META_CONFIG env var.
1811
- *
1812
- * @module cli
1813
- */
1814
- /** Read and parse a meta.json file with proper typing. */
1815
- function readMeta(metaPath) {
1816
- return JSON.parse(readFileSync(join(metaPath, 'meta.json'), 'utf8'));
1817
- }
1818
- const args = process.argv.slice(2);
1819
- const command = args.find((a) => !a.startsWith('-'));
1820
- const jsonOutput = args.includes('--json');
1821
- function usage() {
1822
- console.log(`jeeves-meta — Knowledge synthesis engine CLI
1823
-
1824
- Usage: npx @karmaniverous/jeeves-meta <command> [options]
1825
-
1826
- Commands:
1827
- status Summary: total, stale, errors, tokens
1828
- list [--prefix <p>] [--filter <f>] List metas with summary
1829
- detail <path> [--archive <n>] Full detail for a single meta
1830
- preview [--path <p>] Dry-run: what would the next cycle do
1831
- synthesize [--path <p>] [--batch <n>] Run synthesis cycle(s)
1832
- seed <path> Create .meta/ directory with fresh meta.json
1833
- unlock <path> Force-remove stale .lock file
1834
- validate Validate config + check service reachability
1835
- config show Dump resolved config
1836
-
1837
- Root options:
1838
- --config <path> Path to jeeves-meta.config.json (or JEEVES_META_CONFIG env)
1839
- --json Output as JSON
1840
- `);
1841
- }
1842
- function output(data) {
1843
- if (jsonOutput) {
1844
- console.log(JSON.stringify(data, null, 2));
1845
- }
1846
- else if (typeof data === 'string') {
1847
- console.log(data);
1848
- }
1849
- else {
1850
- console.log(JSON.stringify(data, null, 2));
1851
- }
1852
- }
1853
- async function runStatus(config) {
1854
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1855
- const result = await listMetas(config, watcher);
1856
- output(result.summary);
1857
- }
1858
- async function runList(config) {
1859
- const prefix = getArg('--prefix');
1860
- const filter = getArg('--filter');
1861
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1862
- const result = await listMetas(config, watcher);
1863
- let entries = result.entries;
1864
- if (prefix) {
1865
- entries = entries.filter((e) => e.path.includes(prefix));
1866
- }
1867
- if (filter === 'hasError')
1868
- entries = entries.filter((e) => e.hasError);
1869
- if (filter === 'stale')
1870
- entries = entries.filter((e) => e.stalenessSeconds > 0);
1871
- if (filter === 'locked')
1872
- entries = entries.filter((e) => e.locked);
1873
- if (filter === 'never')
1874
- entries = entries.filter((e) => e.stalenessSeconds === Infinity);
1875
- const rows = entries.map((e) => ({
1876
- path: e.path,
1877
- depth: e.depth,
1878
- staleness: e.stalenessSeconds === Infinity
1879
- ? 'never'
1880
- : String(Math.round(e.stalenessSeconds)) + 's',
1881
- hasError: e.hasError,
1882
- locked: e.locked,
1883
- children: e.children,
1884
- }));
1885
- output({ total: rows.length, items: rows });
1886
- }
1887
- async function runDetail(config) {
1888
- const targetPath = args.find((a) => !a.startsWith('-') && a !== 'detail');
1889
- if (!targetPath) {
1890
- console.error('Usage: jeeves-meta detail <path> [--archive <n>]');
1891
- process.exit(1);
1892
- }
1893
- const archiveArg = getArg('--archive');
1894
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1895
- const metaResult = await listMetas(config, watcher);
1896
- const normalized = normalizePath$1(targetPath);
1897
- const node = findNode(metaResult.tree, normalized);
1898
- if (!node) {
1899
- console.error('Meta not found: ' + targetPath);
1900
- process.exit(1);
1901
- }
1902
- const meta = readMeta(node.metaPath);
1903
- const result = { meta };
1904
- if (archiveArg) {
1905
- const { listArchiveFiles } = await Promise.resolve().then(function () { return index$1; });
1906
- const archiveFiles = listArchiveFiles(node.metaPath);
1907
- const limit = parseInt(archiveArg, 10) || archiveFiles.length;
1908
- const selected = archiveFiles.slice(-limit).reverse();
1909
- result.archive = selected.map((af) => {
1910
- const raw = readFileSync(join(node.metaPath, 'archive', af), 'utf8');
1911
- return JSON.parse(raw);
1912
- });
1913
- }
1914
- output(result);
1915
- }
1916
- async function runPreview(config) {
1917
- const targetPath = getArg('--path');
1918
- const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, } = await Promise.resolve().then(function () { return index; });
1919
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1920
- const metaResult = await listMetas(config, watcher);
1921
- let targetNode;
1922
- if (targetPath) {
1923
- const normalized = normalizePath$1(targetPath);
1924
- targetNode = findNode(metaResult.tree, normalized);
1925
- if (!targetNode) {
1926
- console.error('Meta not found: ' + targetPath);
1927
- process.exit(1);
1928
- }
1929
- }
1930
- else {
1931
- const candidates = metaResult.entries
1932
- .filter((e) => e.stalenessSeconds > 0)
1933
- .map((e) => ({
1934
- node: e.node,
1935
- meta: e.meta,
1936
- actualStaleness: e.stalenessSeconds,
1937
- }));
1938
- const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
1939
- const winner = selectCandidate(weighted);
1940
- if (!winner) {
1941
- output({ message: 'No stale metas found.' });
1942
- return;
1943
- }
1944
- targetNode = winner.node;
1945
- }
1946
- const meta = readMeta(targetNode.metaPath);
1947
- const allFiles = await paginatedScan(watcher, {
1948
- pathPrefix: targetNode.ownerPath,
1949
- });
1950
- const scopeFiles = filterInScope(targetNode, allFiles.map((f) => f.file_path));
1951
- const structureHash = computeStructureHash(scopeFiles);
1952
- const structureChanged = structureHash !== meta._structureHash;
1953
- const latestArchive = readLatestArchive(targetNode.metaPath);
1954
- const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
1955
- const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
1956
- output({
1957
- target: targetNode.metaPath,
1958
- ownerPath: targetNode.ownerPath,
1959
- depth: meta._depth ?? targetNode.treeDepth,
1960
- staleness: actualStaleness(meta) === Infinity
1961
- ? 'never'
1962
- : String(Math.round(actualStaleness(meta))) + 's',
1963
- scopeFiles: scopeFiles.length,
1964
- structureChanged,
1965
- steerChanged,
1966
- architectTriggered,
1967
- });
1968
- }
1969
- async function runSynthesize(config) {
1970
- const targetPath = getArg('--path');
1971
- const batchArg = getArg('--batch');
1972
- const effectiveConfig = {
1973
- ...config,
1974
- ...(batchArg ? { batchSize: parseInt(batchArg, 10) } : {}),
1975
- };
1976
- const executor = new GatewayExecutor({
1977
- gatewayUrl: config.gatewayUrl,
1978
- apiKey: config.gatewayApiKey,
1979
- });
1980
- const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
1981
- const results = await orchestrate(effectiveConfig, executor, watcher, targetPath ?? undefined);
1982
- const synthesized = results.filter((r) => r.synthesized);
1983
- output({
1984
- synthesizedCount: synthesized.length,
1985
- results: synthesized.map((r) => ({
1986
- metaPath: r.metaPath,
1987
- error: r.error ?? null,
1988
- })),
1989
- });
1990
- }
1991
- async function runSeed() {
1992
- const { mkdirSync, writeFileSync: writeFs } = await import('node:fs');
1993
- const { randomUUID } = await import('node:crypto');
1994
- const targetPath = args.find((a) => !a.startsWith('-') && a !== 'seed');
1995
- if (!targetPath) {
1996
- console.error('Usage: jeeves-meta seed <path>');
1997
- process.exit(1);
1998
- }
1999
- const metaDir = targetPath.endsWith('.meta')
2000
- ? targetPath
2001
- : join(targetPath, '.meta');
2002
- mkdirSync(metaDir, { recursive: true });
2003
- const metaFile = join(metaDir, 'meta.json');
2004
- writeFs(metaFile, JSON.stringify({ _id: randomUUID() }, null, 2) + '\n');
2005
- output({ created: metaFile });
2006
- }
2007
- async function runUnlock() {
2008
- const { unlinkSync } = await import('node:fs');
2009
- const targetPath = args.find((a) => !a.startsWith('-') && a !== 'unlock');
2010
- if (!targetPath) {
2011
- console.error('Usage: jeeves-meta unlock <path>');
2012
- process.exit(1);
2013
- }
2014
- const metaDir = targetPath.endsWith('.meta')
2015
- ? targetPath
2016
- : join(targetPath, '.meta');
2017
- const lockFile = join(metaDir, '.lock');
2018
- try {
2019
- unlinkSync(lockFile);
2020
- output({ unlocked: metaDir });
2021
- }
2022
- catch {
2023
- output({ message: 'No lock file found at ' + lockFile });
2024
- }
2025
- }
2026
- async function runValidate(config) {
2027
- const checks = {};
2028
- // Check watcher
2029
- try {
2030
- const res = await fetch(config.watcherUrl + '/status', {
2031
- signal: AbortSignal.timeout(5000),
2032
- });
2033
- checks.watcher = res.ok
2034
- ? 'OK (' + config.watcherUrl + ')'
2035
- : 'HTTP ' + res.status.toString();
2036
- }
2037
- catch {
2038
- checks.watcher = 'UNREACHABLE (' + config.watcherUrl + ')';
2039
- }
2040
- // Check gateway
2041
- try {
2042
- const res = await fetch(config.gatewayUrl + '/api/status', {
2043
- signal: AbortSignal.timeout(5000),
2044
- });
2045
- checks.gateway = res.ok
2046
- ? 'OK (' + config.gatewayUrl + ')'
2047
- : 'HTTP ' + res.status.toString();
2048
- }
2049
- catch {
2050
- checks.gateway = 'UNREACHABLE (' + config.gatewayUrl + ')';
2051
- }
2052
- // Check meta discovery via watcher
2053
- try {
2054
- const watcherClient = new HttpWatcherClient({ baseUrl: config.watcherUrl });
2055
- const metaPaths = await discoverMetas(config, watcherClient);
2056
- checks.metas =
2057
- String(metaPaths.length) + ' .meta/ entities discovered via watcher';
2058
- }
2059
- catch {
2060
- checks.metas = 'FAILED — could not discover metas (watcher may be down)';
2061
- }
2062
- output({ config: 'valid', checks });
2063
- }
2064
- function runConfigShow(config) {
2065
- // Show config with prompts truncated for readability
2066
- const display = {
2067
- ...config,
2068
- defaultArchitect: config.defaultArchitect.slice(0, 100) +
2069
- '... (' +
2070
- String(config.defaultArchitect.length) +
2071
- ' chars)',
2072
- defaultCritic: config.defaultCritic.slice(0, 100) +
2073
- '... (' +
2074
- String(config.defaultCritic.length) +
2075
- ' chars)',
2076
- };
2077
- output(display);
2078
- }
2079
- function getArg(flag) {
2080
- const idx = args.indexOf(flag);
2081
- if (idx !== -1 && args[idx + 1])
2082
- return args[idx + 1];
2083
- return undefined;
2084
- }
2085
- async function main() {
2086
- if (!command ||
2087
- command === 'help' ||
2088
- args.includes('--help') ||
2089
- args.includes('-h')) {
2090
- usage();
2091
- return;
2092
- }
2093
- // Commands that don't need config
2094
- if (command === 'seed') {
2095
- await runSeed();
2096
- return;
2097
- }
2098
- if (command === 'unlock') {
2099
- await runUnlock();
2100
- return;
2101
- }
2102
- // All other commands need config
2103
- let configPath;
2104
- try {
2105
- configPath = resolveConfigPath(args);
2106
- }
2107
- catch (err) {
2108
- console.error(err instanceof Error ? err.message : String(err));
2109
- process.exit(1);
2110
- }
2111
- let config;
2112
- try {
2113
- config = loadSynthConfig(resolve(configPath));
2114
- }
2115
- catch (err) {
2116
- console.error('Failed to load config:', err instanceof Error ? err.message : String(err));
2117
- process.exit(1);
2118
- }
2119
- switch (command) {
2120
- case 'status':
2121
- await runStatus(config);
2122
- break;
2123
- case 'list':
2124
- await runList(config);
2125
- break;
2126
- case 'detail':
2127
- await runDetail(config);
2128
- break;
2129
- case 'preview':
2130
- await runPreview(config);
2131
- break;
2132
- case 'synthesize':
2133
- await runSynthesize(config);
2134
- break;
2135
- case 'validate':
2136
- await runValidate(config);
2137
- break;
2138
- case 'config':
2139
- if (args.includes('show')) {
2140
- runConfigShow(config);
2141
- }
2142
- else if (args.includes('check')) {
2143
- await runValidate(config);
2144
- }
2145
- else {
2146
- console.error('Unknown config subcommand. Use: config show, config check');
2147
- process.exit(1);
2148
- }
2149
- break;
2150
- default:
2151
- console.error('Unknown command: ' + command);
2152
- usage();
2153
- process.exit(1);
2154
- }
2155
- }
2156
- main().catch((err) => {
2157
- console.error(err instanceof Error ? err.message : String(err));
2158
- process.exit(1);
2159
- });