@karmaniverous/jeeves-meta 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +628 -497
- package/dist/index.d.ts +306 -262
- package/dist/index.js +464 -341
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync, existsSync
|
|
2
|
+
import { readFileSync, readdirSync, unlinkSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, sep, resolve } from 'node:path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import {
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Zod schema for jeeves-meta configuration.
|
|
@@ -15,7 +15,6 @@ import { randomUUID, createHash } from 'node:crypto';
|
|
|
15
15
|
/** Zod schema for jeeves-meta configuration. */
|
|
16
16
|
const synthConfigSchema = z.object({
|
|
17
17
|
/** Filesystem paths to watch for .meta/ directories. */
|
|
18
|
-
watchPaths: z.array(z.string()).min(1),
|
|
19
18
|
/** Watcher service base URL. */
|
|
20
19
|
watcherUrl: z.url(),
|
|
21
20
|
/** OpenClaw gateway base URL for subprocess spawning. */
|
|
@@ -48,6 +47,21 @@ const synthConfigSchema = z.object({
|
|
|
48
47
|
skipUnchanged: z.boolean().default(true),
|
|
49
48
|
/** Number of metas to synthesize per invocation. */
|
|
50
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'] }),
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
/**
|
|
@@ -310,81 +324,174 @@ var index$1 = /*#__PURE__*/Object.freeze({
|
|
|
310
324
|
});
|
|
311
325
|
|
|
312
326
|
/**
|
|
313
|
-
*
|
|
327
|
+
* Normalize file paths to forward slashes for consistency with watcher-indexed paths.
|
|
314
328
|
*
|
|
315
|
-
*
|
|
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.
|
|
316
332
|
*
|
|
317
|
-
* @module
|
|
333
|
+
* @module normalizePath
|
|
318
334
|
*/
|
|
319
335
|
/**
|
|
320
|
-
*
|
|
336
|
+
* Normalize a file path to forward slashes.
|
|
321
337
|
*
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
338
|
+
* @param p - File path (may contain backslashes).
|
|
339
|
+
* @returns Path with all backslashes replaced by forward slashes.
|
|
324
340
|
*/
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Discover all .meta/ directories via watcher scan.
|
|
394
|
+
*
|
|
395
|
+
* Queries the watcher for indexed .meta/meta.json points using the
|
|
396
|
+
* configured domain filter. Returns deduplicated meta directory paths.
|
|
397
|
+
*
|
|
398
|
+
* @param config - Synth config (for domain filter).
|
|
399
|
+
* @param watcher - WatcherClient for scan queries.
|
|
400
|
+
* @returns Array of normalized .meta/ directory paths.
|
|
401
|
+
*/
|
|
402
|
+
async function discoverMetas(config, watcher) {
|
|
403
|
+
const filter = buildMetaFilter(config);
|
|
404
|
+
const scanFiles = await paginatedScan(watcher, {
|
|
405
|
+
filter,
|
|
406
|
+
fields: ['file_path'],
|
|
407
|
+
});
|
|
408
|
+
// Deduplicate by file_path (multi-chunk files)
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const metaPaths = [];
|
|
411
|
+
for (const sf of scanFiles) {
|
|
412
|
+
const fp = normalizePath$1(sf.file_path);
|
|
413
|
+
if (seen.has(fp))
|
|
414
|
+
continue;
|
|
415
|
+
seen.add(fp);
|
|
416
|
+
// Derive .meta/ directory from file_path (strip /meta.json)
|
|
417
|
+
const metaPath = fp.replace(/\/meta\.json$/, '');
|
|
418
|
+
metaPaths.push(metaPath);
|
|
335
419
|
}
|
|
336
|
-
|
|
337
|
-
writeFileSync(filePath, JSON.stringify(meta, null, 2) + '\n');
|
|
338
|
-
return meta;
|
|
420
|
+
return metaPaths;
|
|
339
421
|
}
|
|
340
422
|
|
|
341
423
|
/**
|
|
342
|
-
*
|
|
424
|
+
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
343
425
|
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
426
|
+
* Lock file: .meta/.lock containing PID + timestamp.
|
|
427
|
+
* Stale timeout: 30 minutes.
|
|
346
428
|
*
|
|
347
|
-
* @module
|
|
429
|
+
* @module lock
|
|
348
430
|
*/
|
|
431
|
+
const LOCK_FILE = '.lock';
|
|
432
|
+
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
349
433
|
/**
|
|
350
|
-
*
|
|
434
|
+
* Attempt to acquire a lock on a .meta directory.
|
|
351
435
|
*
|
|
352
|
-
* @param
|
|
353
|
-
* @returns
|
|
436
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
437
|
+
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
354
438
|
*/
|
|
355
|
-
function
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
let entries;
|
|
439
|
+
function acquireLock(metaPath) {
|
|
440
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
441
|
+
if (existsSync(lockPath)) {
|
|
359
442
|
try {
|
|
360
|
-
|
|
443
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
444
|
+
const data = JSON.parse(raw);
|
|
445
|
+
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
446
|
+
if (lockAge < STALE_TIMEOUT_MS) {
|
|
447
|
+
return false; // Lock is active
|
|
448
|
+
}
|
|
449
|
+
// Stale lock — fall through to overwrite
|
|
361
450
|
}
|
|
362
451
|
catch {
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
for (const entry of entries) {
|
|
366
|
-
const full = join(dir, entry);
|
|
367
|
-
let stat;
|
|
368
|
-
try {
|
|
369
|
-
stat = statSync(full);
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (!stat.isDirectory())
|
|
375
|
-
continue;
|
|
376
|
-
if (entry === '.meta') {
|
|
377
|
-
results.push(full);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
walk(full);
|
|
381
|
-
}
|
|
452
|
+
// Corrupt lock file — overwrite
|
|
382
453
|
}
|
|
383
454
|
}
|
|
384
|
-
|
|
385
|
-
|
|
455
|
+
const lock = {
|
|
456
|
+
pid: process.pid,
|
|
457
|
+
startedAt: new Date().toISOString(),
|
|
458
|
+
};
|
|
459
|
+
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Release a lock on a .meta directory.
|
|
464
|
+
*
|
|
465
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
466
|
+
*/
|
|
467
|
+
function releaseLock(metaPath) {
|
|
468
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
469
|
+
try {
|
|
470
|
+
unlinkSync(lockPath);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// Already removed or never existed
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check if a .meta directory is currently locked (non-stale).
|
|
478
|
+
*
|
|
479
|
+
* @param metaPath - Absolute path to the .meta directory.
|
|
480
|
+
* @returns True if locked and not stale.
|
|
481
|
+
*/
|
|
482
|
+
function isLocked(metaPath) {
|
|
483
|
+
const lockPath = join(metaPath, LOCK_FILE);
|
|
484
|
+
if (!existsSync(lockPath))
|
|
485
|
+
return false;
|
|
486
|
+
try {
|
|
487
|
+
const raw = readFileSync(lockPath, 'utf8');
|
|
488
|
+
const data = JSON.parse(raw);
|
|
489
|
+
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
490
|
+
return lockAge < STALE_TIMEOUT_MS;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return false; // Corrupt lock = not locked
|
|
386
494
|
}
|
|
387
|
-
return results;
|
|
388
495
|
}
|
|
389
496
|
|
|
390
497
|
/**
|
|
@@ -397,7 +504,7 @@ function globMetas(watchPaths) {
|
|
|
397
504
|
* @module discovery/ownershipTree
|
|
398
505
|
*/
|
|
399
506
|
/** Normalize path separators to forward slashes for consistent comparison. */
|
|
400
|
-
function normalizePath
|
|
507
|
+
function normalizePath(p) {
|
|
401
508
|
return p.split(sep).join('/');
|
|
402
509
|
}
|
|
403
510
|
/**
|
|
@@ -411,8 +518,8 @@ function buildOwnershipTree(metaPaths) {
|
|
|
411
518
|
// Create nodes, sorted by ownerPath length (shortest first = shallowest)
|
|
412
519
|
const sorted = [...metaPaths]
|
|
413
520
|
.map((mp) => ({
|
|
414
|
-
metaPath: normalizePath
|
|
415
|
-
ownerPath: normalizePath
|
|
521
|
+
metaPath: normalizePath(mp),
|
|
522
|
+
ownerPath: normalizePath(dirname(mp)),
|
|
416
523
|
}))
|
|
417
524
|
.sort((a, b) => a.ownerPath.length - b.ownerPath.length);
|
|
418
525
|
for (const { metaPath, ownerPath } of sorted) {
|
|
@@ -464,6 +571,138 @@ function findNode(tree, targetPath) {
|
|
|
464
571
|
return Array.from(tree.nodes.values()).find((n) => n.metaPath === targetPath || n.ownerPath === targetPath);
|
|
465
572
|
}
|
|
466
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Unified meta listing: scan, dedup, enrich.
|
|
576
|
+
*
|
|
577
|
+
* Single source of truth for all consumers that need a list of metas
|
|
578
|
+
* with enriched metadata. Replaces duplicated scan+dedup logic in
|
|
579
|
+
* plugin tools, CLI, and prompt injection.
|
|
580
|
+
*
|
|
581
|
+
* @module discovery/listMetas
|
|
582
|
+
*/
|
|
583
|
+
/**
|
|
584
|
+
* Discover, deduplicate, and enrich all metas.
|
|
585
|
+
*
|
|
586
|
+
* This is the single consolidated function that replaces all duplicated
|
|
587
|
+
* scan+dedup+enrich logic across the codebase. All enrichment comes from
|
|
588
|
+
* reading meta.json on disk (the canonical source).
|
|
589
|
+
*
|
|
590
|
+
* @param config - Validated synthesis config.
|
|
591
|
+
* @param watcher - Watcher HTTP client for discovery.
|
|
592
|
+
* @returns Enriched meta list with summary statistics and ownership tree.
|
|
593
|
+
*/
|
|
594
|
+
async function listMetas(config, watcher) {
|
|
595
|
+
// Step 1: Discover deduplicated meta paths via watcher scan
|
|
596
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
597
|
+
// Step 2: Build ownership tree
|
|
598
|
+
const tree = buildOwnershipTree(metaPaths);
|
|
599
|
+
// Step 3: Read and enrich each meta from disk
|
|
600
|
+
const entries = [];
|
|
601
|
+
let staleCount = 0;
|
|
602
|
+
let errorCount = 0;
|
|
603
|
+
let lockedCount = 0;
|
|
604
|
+
let neverSynthesizedCount = 0;
|
|
605
|
+
let totalArchTokens = 0;
|
|
606
|
+
let totalBuilderTokens = 0;
|
|
607
|
+
let totalCriticTokens = 0;
|
|
608
|
+
let lastSynthPath = null;
|
|
609
|
+
let lastSynthAt = null;
|
|
610
|
+
let stalestPath = null;
|
|
611
|
+
let stalestEffective = -1;
|
|
612
|
+
for (const node of tree.nodes.values()) {
|
|
613
|
+
let meta;
|
|
614
|
+
try {
|
|
615
|
+
meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
// Skip unreadable metas
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const depth = meta._depth ?? node.treeDepth;
|
|
622
|
+
const emphasis = meta._emphasis ?? 1;
|
|
623
|
+
const hasError = Boolean(meta._error);
|
|
624
|
+
const locked = isLocked(normalizePath$1(node.metaPath));
|
|
625
|
+
const neverSynth = !meta._generatedAt;
|
|
626
|
+
// Compute staleness
|
|
627
|
+
let stalenessSeconds;
|
|
628
|
+
if (neverSynth) {
|
|
629
|
+
stalenessSeconds = Infinity;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
const genAt = new Date(meta._generatedAt).getTime();
|
|
633
|
+
stalenessSeconds = Math.max(0, Math.floor((Date.now() - genAt) / 1000));
|
|
634
|
+
}
|
|
635
|
+
// Tokens
|
|
636
|
+
const archTokens = meta._architectTokens ?? 0;
|
|
637
|
+
const buildTokens = meta._builderTokens ?? 0;
|
|
638
|
+
const critTokens = meta._criticTokens ?? 0;
|
|
639
|
+
// Accumulate summary stats
|
|
640
|
+
if (stalenessSeconds > 0)
|
|
641
|
+
staleCount++;
|
|
642
|
+
if (hasError)
|
|
643
|
+
errorCount++;
|
|
644
|
+
if (locked)
|
|
645
|
+
lockedCount++;
|
|
646
|
+
if (neverSynth)
|
|
647
|
+
neverSynthesizedCount++;
|
|
648
|
+
totalArchTokens += archTokens;
|
|
649
|
+
totalBuilderTokens += buildTokens;
|
|
650
|
+
totalCriticTokens += critTokens;
|
|
651
|
+
// Track last synthesized
|
|
652
|
+
if (meta._generatedAt) {
|
|
653
|
+
if (!lastSynthAt || meta._generatedAt > lastSynthAt) {
|
|
654
|
+
lastSynthAt = meta._generatedAt;
|
|
655
|
+
lastSynthPath = node.metaPath;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Track stalest (effective staleness for scheduling)
|
|
659
|
+
const depthFactor = Math.pow(1 + config.depthWeight, depth);
|
|
660
|
+
const effectiveStaleness = (stalenessSeconds === Infinity
|
|
661
|
+
? Number.MAX_SAFE_INTEGER
|
|
662
|
+
: stalenessSeconds) *
|
|
663
|
+
depthFactor *
|
|
664
|
+
emphasis;
|
|
665
|
+
if (effectiveStaleness > stalestEffective) {
|
|
666
|
+
stalestEffective = effectiveStaleness;
|
|
667
|
+
stalestPath = node.metaPath;
|
|
668
|
+
}
|
|
669
|
+
entries.push({
|
|
670
|
+
path: node.metaPath,
|
|
671
|
+
depth,
|
|
672
|
+
emphasis,
|
|
673
|
+
stalenessSeconds,
|
|
674
|
+
lastSynthesized: meta._generatedAt ?? null,
|
|
675
|
+
hasError,
|
|
676
|
+
locked,
|
|
677
|
+
architectTokens: archTokens > 0 ? archTokens : null,
|
|
678
|
+
builderTokens: buildTokens > 0 ? buildTokens : null,
|
|
679
|
+
criticTokens: critTokens > 0 ? critTokens : null,
|
|
680
|
+
children: node.children.length,
|
|
681
|
+
node,
|
|
682
|
+
meta,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
summary: {
|
|
687
|
+
total: entries.length,
|
|
688
|
+
stale: staleCount,
|
|
689
|
+
errors: errorCount,
|
|
690
|
+
locked: lockedCount,
|
|
691
|
+
neverSynthesized: neverSynthesizedCount,
|
|
692
|
+
tokens: {
|
|
693
|
+
architect: totalArchTokens,
|
|
694
|
+
builder: totalBuilderTokens,
|
|
695
|
+
critic: totalCriticTokens,
|
|
696
|
+
},
|
|
697
|
+
stalestPath,
|
|
698
|
+
lastSynthesizedPath: lastSynthPath,
|
|
699
|
+
lastSynthesizedAt: lastSynthAt,
|
|
700
|
+
},
|
|
701
|
+
entries,
|
|
702
|
+
tree,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
467
706
|
/**
|
|
468
707
|
* Compute the file scope owned by a meta node.
|
|
469
708
|
*
|
|
@@ -535,26 +774,126 @@ function computeEma(current, previous, decay = DEFAULT_DECAY) {
|
|
|
535
774
|
}
|
|
536
775
|
|
|
537
776
|
/**
|
|
538
|
-
*
|
|
777
|
+
* Shared error utilities.
|
|
539
778
|
*
|
|
540
|
-
* @module
|
|
779
|
+
* @module errors
|
|
541
780
|
*/
|
|
542
781
|
/**
|
|
543
|
-
*
|
|
782
|
+
* Wrap an unknown caught value into a SynthError.
|
|
544
783
|
*
|
|
545
|
-
* @param
|
|
546
|
-
* @param
|
|
547
|
-
* @
|
|
784
|
+
* @param step - Which synthesis step failed.
|
|
785
|
+
* @param err - The caught error value.
|
|
786
|
+
* @param code - Error classification code.
|
|
787
|
+
* @returns A structured SynthError.
|
|
548
788
|
*/
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
789
|
+
function toSynthError(step, err, code = 'FAILED') {
|
|
790
|
+
return {
|
|
791
|
+
step,
|
|
792
|
+
code,
|
|
793
|
+
message: err instanceof Error ? err.message : String(err),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* SynthExecutor implementation using the OpenClaw gateway HTTP API.
|
|
799
|
+
*
|
|
800
|
+
* Lives in the library package so both plugin and runner can import it.
|
|
801
|
+
* Spawns sub-agent sessions via the gateway, polls for completion,
|
|
802
|
+
* and extracts output text.
|
|
803
|
+
*
|
|
804
|
+
* @module executor/GatewayExecutor
|
|
805
|
+
*/
|
|
806
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
807
|
+
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
808
|
+
/** Sleep helper. */
|
|
809
|
+
function sleep$1(ms) {
|
|
810
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* SynthExecutor that spawns OpenClaw sessions via the gateway HTTP API.
|
|
814
|
+
*
|
|
815
|
+
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
816
|
+
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
817
|
+
* optional `apiKey` — typically sourced from `SynthConfig`.
|
|
818
|
+
*/
|
|
819
|
+
class GatewayExecutor {
|
|
820
|
+
gatewayUrl;
|
|
821
|
+
apiKey;
|
|
822
|
+
pollIntervalMs;
|
|
823
|
+
constructor(options = {}) {
|
|
824
|
+
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:3000').replace(/\/+$/, '');
|
|
825
|
+
this.apiKey = options.apiKey;
|
|
826
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
827
|
+
}
|
|
828
|
+
async spawn(task, options) {
|
|
829
|
+
const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000;
|
|
830
|
+
const deadline = Date.now() + timeoutMs;
|
|
831
|
+
const headers = {
|
|
832
|
+
'Content-Type': 'application/json',
|
|
833
|
+
};
|
|
834
|
+
if (this.apiKey) {
|
|
835
|
+
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
836
|
+
}
|
|
837
|
+
const spawnRes = await fetch(this.gatewayUrl + '/api/sessions/spawn', {
|
|
838
|
+
method: 'POST',
|
|
839
|
+
headers,
|
|
840
|
+
body: JSON.stringify({
|
|
841
|
+
task,
|
|
842
|
+
mode: 'run',
|
|
843
|
+
model: options?.model,
|
|
844
|
+
runTimeoutSeconds: options?.timeout,
|
|
845
|
+
}),
|
|
846
|
+
});
|
|
847
|
+
if (!spawnRes.ok) {
|
|
848
|
+
const text = await spawnRes.text();
|
|
849
|
+
throw new Error('Gateway spawn failed: HTTP ' +
|
|
850
|
+
spawnRes.status.toString() +
|
|
851
|
+
' - ' +
|
|
852
|
+
text);
|
|
853
|
+
}
|
|
854
|
+
const spawnData = (await spawnRes.json());
|
|
855
|
+
if (!spawnData.sessionKey) {
|
|
856
|
+
throw new Error('Gateway spawn returned no sessionKey: ' + JSON.stringify(spawnData));
|
|
857
|
+
}
|
|
858
|
+
const { sessionKey } = spawnData;
|
|
859
|
+
// Poll for completion
|
|
860
|
+
while (Date.now() < deadline) {
|
|
861
|
+
await sleep$1(this.pollIntervalMs);
|
|
862
|
+
const historyRes = await fetch(this.gatewayUrl +
|
|
863
|
+
'/api/sessions/' +
|
|
864
|
+
encodeURIComponent(sessionKey) +
|
|
865
|
+
'/history?limit=50', { headers });
|
|
866
|
+
if (!historyRes.ok)
|
|
867
|
+
continue;
|
|
868
|
+
const history = (await historyRes.json());
|
|
869
|
+
if (history.status === 'completed' || history.status === 'done') {
|
|
870
|
+
// Extract token usage from session-level or message-level usage
|
|
871
|
+
let tokens;
|
|
872
|
+
if (history.usage?.totalTokens) {
|
|
873
|
+
tokens = history.usage.totalTokens;
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// Sum message-level usage as fallback
|
|
877
|
+
let sum = 0;
|
|
878
|
+
for (const msg of history.messages ?? []) {
|
|
879
|
+
if (msg.usage?.totalTokens)
|
|
880
|
+
sum += msg.usage.totalTokens;
|
|
881
|
+
}
|
|
882
|
+
if (sum > 0)
|
|
883
|
+
tokens = sum;
|
|
884
|
+
}
|
|
885
|
+
// Extract the last assistant message as output
|
|
886
|
+
const messages = history.messages ?? [];
|
|
887
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
888
|
+
if (messages[i].role === 'assistant' && messages[i].content) {
|
|
889
|
+
return { output: messages[i].content, tokens };
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return { output: '', tokens };
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
throw new Error('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms');
|
|
896
|
+
}
|
|
558
897
|
}
|
|
559
898
|
|
|
560
899
|
/**
|
|
@@ -737,227 +1076,113 @@ function buildBuilderTask(ctx, meta, config) {
|
|
|
737
1076
|
* @param meta - Current meta.json (with _content already set by builder).
|
|
738
1077
|
* @param config - Synthesis config.
|
|
739
1078
|
* @returns The critic task prompt string.
|
|
740
|
-
*/
|
|
741
|
-
function buildCriticTask(ctx, meta, config) {
|
|
742
|
-
const sections = [
|
|
743
|
-
meta._critic ?? config.defaultCritic,
|
|
744
|
-
'',
|
|
745
|
-
'## SYNTHESIS TO EVALUATE',
|
|
746
|
-
meta._content ?? '(No content produced)',
|
|
747
|
-
'',
|
|
748
|
-
'## SCOPE',
|
|
749
|
-
`Path: ${ctx.path}`,
|
|
750
|
-
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
751
|
-
];
|
|
752
|
-
appendSharedSections(sections, ctx, {
|
|
753
|
-
includePreviousContent: false,
|
|
754
|
-
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
755
|
-
includeChildMetas: false,
|
|
756
|
-
});
|
|
757
|
-
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
758
|
-
return sections.join('\n');
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Merge synthesis results into meta.json.
|
|
763
|
-
*
|
|
764
|
-
* Preserves human-set fields (_id, _steer, _depth).
|
|
765
|
-
* Writes engine fields (_generatedAt, _structureHash, etc.).
|
|
766
|
-
* Validates against schema before writing.
|
|
767
|
-
*
|
|
768
|
-
* @module orchestrator/merge
|
|
769
|
-
*/
|
|
770
|
-
/**
|
|
771
|
-
* Merge results into meta.json and write atomically.
|
|
772
|
-
*
|
|
773
|
-
* @param options - Merge options.
|
|
774
|
-
* @returns The updated MetaJson.
|
|
775
|
-
* @throws If validation fails (malformed output).
|
|
776
|
-
*/
|
|
777
|
-
function mergeAndWrite(options) {
|
|
778
|
-
const merged = {
|
|
779
|
-
// Preserve human-set fields
|
|
780
|
-
_id: options.current._id,
|
|
781
|
-
_steer: options.current._steer,
|
|
782
|
-
_depth: options.current._depth,
|
|
783
|
-
_emphasis: options.current._emphasis,
|
|
784
|
-
// Engine fields
|
|
785
|
-
_architect: options.architect,
|
|
786
|
-
_builder: options.builder,
|
|
787
|
-
_critic: options.critic,
|
|
788
|
-
_generatedAt: new Date().toISOString(),
|
|
789
|
-
_structureHash: options.structureHash,
|
|
790
|
-
_synthesisCount: options.synthesisCount,
|
|
791
|
-
// Token tracking
|
|
792
|
-
_architectTokens: options.architectTokens,
|
|
793
|
-
_builderTokens: options.builderTokens,
|
|
794
|
-
_criticTokens: options.criticTokens,
|
|
795
|
-
_architectTokensAvg: options.architectTokens !== undefined
|
|
796
|
-
? computeEma(options.architectTokens, options.current._architectTokensAvg)
|
|
797
|
-
: options.current._architectTokensAvg,
|
|
798
|
-
_builderTokensAvg: options.builderTokens !== undefined
|
|
799
|
-
? computeEma(options.builderTokens, options.current._builderTokensAvg)
|
|
800
|
-
: options.current._builderTokensAvg,
|
|
801
|
-
_criticTokensAvg: options.criticTokens !== undefined
|
|
802
|
-
? computeEma(options.criticTokens, options.current._criticTokensAvg)
|
|
803
|
-
: options.current._criticTokensAvg,
|
|
804
|
-
// Content from builder
|
|
805
|
-
_content: options.builderOutput?.content ?? options.current._content,
|
|
806
|
-
// Feedback from critic
|
|
807
|
-
_feedback: options.feedback ?? options.current._feedback,
|
|
808
|
-
// Error handling
|
|
809
|
-
_error: options.error ?? undefined,
|
|
810
|
-
// Spread structured fields from builder
|
|
811
|
-
...options.builderOutput?.fields,
|
|
812
|
-
};
|
|
813
|
-
// Clean up undefined optional fields
|
|
814
|
-
if (merged._steer === undefined)
|
|
815
|
-
delete merged._steer;
|
|
816
|
-
if (merged._depth === undefined)
|
|
817
|
-
delete merged._depth;
|
|
818
|
-
if (merged._emphasis === undefined)
|
|
819
|
-
delete merged._emphasis;
|
|
820
|
-
if (merged._architectTokens === undefined)
|
|
821
|
-
delete merged._architectTokens;
|
|
822
|
-
if (merged._builderTokens === undefined)
|
|
823
|
-
delete merged._builderTokens;
|
|
824
|
-
if (merged._criticTokens === undefined)
|
|
825
|
-
delete merged._criticTokens;
|
|
826
|
-
if (merged._architectTokensAvg === undefined)
|
|
827
|
-
delete merged._architectTokensAvg;
|
|
828
|
-
if (merged._builderTokensAvg === undefined)
|
|
829
|
-
delete merged._builderTokensAvg;
|
|
830
|
-
if (merged._criticTokensAvg === undefined)
|
|
831
|
-
delete merged._criticTokensAvg;
|
|
832
|
-
if (merged._error === undefined)
|
|
833
|
-
delete merged._error;
|
|
834
|
-
if (merged._content === undefined)
|
|
835
|
-
delete merged._content;
|
|
836
|
-
if (merged._feedback === undefined)
|
|
837
|
-
delete merged._feedback;
|
|
838
|
-
// Validate
|
|
839
|
-
const result = metaJsonSchema.safeParse(merged);
|
|
840
|
-
if (!result.success) {
|
|
841
|
-
throw new Error(`Meta validation failed: ${result.error.message}`);
|
|
842
|
-
}
|
|
843
|
-
// Write atomically
|
|
844
|
-
const filePath = join(options.metaPath, 'meta.json');
|
|
845
|
-
writeFileSync(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
846
|
-
return result.data;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Shared error utilities.
|
|
851
|
-
*
|
|
852
|
-
* @module errors
|
|
853
|
-
*/
|
|
854
|
-
/**
|
|
855
|
-
* Wrap an unknown caught value into a SynthError.
|
|
856
|
-
*
|
|
857
|
-
* @param step - Which synthesis step failed.
|
|
858
|
-
* @param err - The caught error value.
|
|
859
|
-
* @param code - Error classification code.
|
|
860
|
-
* @returns A structured SynthError.
|
|
861
|
-
*/
|
|
862
|
-
function toSynthError(step, err, code = 'FAILED') {
|
|
863
|
-
return {
|
|
864
|
-
step,
|
|
865
|
-
code,
|
|
866
|
-
message: err instanceof Error ? err.message : String(err),
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* File-system lock for preventing concurrent synthesis on the same meta.
|
|
872
|
-
*
|
|
873
|
-
* Lock file: .meta/.lock containing PID + timestamp.
|
|
874
|
-
* Stale timeout: 30 minutes.
|
|
875
|
-
*
|
|
876
|
-
* @module lock
|
|
877
|
-
*/
|
|
878
|
-
const LOCK_FILE = '.lock';
|
|
879
|
-
const STALE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
880
|
-
/**
|
|
881
|
-
* Attempt to acquire a lock on a .meta directory.
|
|
882
|
-
*
|
|
883
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
884
|
-
* @returns True if lock was acquired, false if already locked (non-stale).
|
|
885
|
-
*/
|
|
886
|
-
function acquireLock(metaPath) {
|
|
887
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
888
|
-
if (existsSync(lockPath)) {
|
|
889
|
-
try {
|
|
890
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
891
|
-
const data = JSON.parse(raw);
|
|
892
|
-
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
893
|
-
if (lockAge < STALE_TIMEOUT_MS) {
|
|
894
|
-
return false; // Lock is active
|
|
895
|
-
}
|
|
896
|
-
// Stale lock — fall through to overwrite
|
|
897
|
-
}
|
|
898
|
-
catch {
|
|
899
|
-
// Corrupt lock file — overwrite
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const lock = {
|
|
903
|
-
pid: process.pid,
|
|
904
|
-
startedAt: new Date().toISOString(),
|
|
905
|
-
};
|
|
906
|
-
writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
907
|
-
return true;
|
|
908
|
-
}
|
|
909
|
-
/**
|
|
910
|
-
* Release a lock on a .meta directory.
|
|
911
|
-
*
|
|
912
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
913
|
-
*/
|
|
914
|
-
function releaseLock(metaPath) {
|
|
915
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
916
|
-
try {
|
|
917
|
-
unlinkSync(lockPath);
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
// Already removed or never existed
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Check if a .meta directory is currently locked (non-stale).
|
|
925
|
-
*
|
|
926
|
-
* @param metaPath - Absolute path to the .meta directory.
|
|
927
|
-
* @returns True if locked and not stale.
|
|
928
|
-
*/
|
|
929
|
-
function isLocked(metaPath) {
|
|
930
|
-
const lockPath = join(metaPath, LOCK_FILE);
|
|
931
|
-
if (!existsSync(lockPath))
|
|
932
|
-
return false;
|
|
933
|
-
try {
|
|
934
|
-
const raw = readFileSync(lockPath, 'utf8');
|
|
935
|
-
const data = JSON.parse(raw);
|
|
936
|
-
const lockAge = Date.now() - new Date(data.startedAt).getTime();
|
|
937
|
-
return lockAge < STALE_TIMEOUT_MS;
|
|
938
|
-
}
|
|
939
|
-
catch {
|
|
940
|
-
return false; // Corrupt lock = not locked
|
|
941
|
-
}
|
|
1079
|
+
*/
|
|
1080
|
+
function buildCriticTask(ctx, meta, config) {
|
|
1081
|
+
const sections = [
|
|
1082
|
+
meta._critic ?? config.defaultCritic,
|
|
1083
|
+
'',
|
|
1084
|
+
'## SYNTHESIS TO EVALUATE',
|
|
1085
|
+
meta._content ?? '(No content produced)',
|
|
1086
|
+
'',
|
|
1087
|
+
'## SCOPE',
|
|
1088
|
+
`Path: ${ctx.path}`,
|
|
1089
|
+
`Files in scope: ${ctx.scopeFiles.length.toString()}`,
|
|
1090
|
+
];
|
|
1091
|
+
appendSharedSections(sections, ctx, {
|
|
1092
|
+
includePreviousContent: false,
|
|
1093
|
+
feedbackHeading: '## YOUR PREVIOUS FEEDBACK',
|
|
1094
|
+
includeChildMetas: false,
|
|
1095
|
+
});
|
|
1096
|
+
sections.push('', '## OUTPUT FORMAT', 'Return your evaluation as Markdown text. Be specific and actionable.');
|
|
1097
|
+
return sections.join('\n');
|
|
942
1098
|
}
|
|
943
1099
|
|
|
944
1100
|
/**
|
|
945
|
-
*
|
|
1101
|
+
* Merge synthesis results into meta.json.
|
|
946
1102
|
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
1103
|
+
* Preserves human-set fields (_id, _steer, _depth).
|
|
1104
|
+
* Writes engine fields (_generatedAt, _structureHash, etc.).
|
|
1105
|
+
* Validates against schema before writing.
|
|
950
1106
|
*
|
|
951
|
-
* @module
|
|
1107
|
+
* @module orchestrator/merge
|
|
952
1108
|
*/
|
|
953
1109
|
/**
|
|
954
|
-
*
|
|
1110
|
+
* Merge results into meta.json and write atomically.
|
|
955
1111
|
*
|
|
956
|
-
* @param
|
|
957
|
-
* @returns
|
|
1112
|
+
* @param options - Merge options.
|
|
1113
|
+
* @returns The updated MetaJson.
|
|
1114
|
+
* @throws If validation fails (malformed output).
|
|
958
1115
|
*/
|
|
959
|
-
function
|
|
960
|
-
|
|
1116
|
+
function mergeAndWrite(options) {
|
|
1117
|
+
const merged = {
|
|
1118
|
+
// Preserve human-set fields
|
|
1119
|
+
_id: options.current._id,
|
|
1120
|
+
_steer: options.current._steer,
|
|
1121
|
+
_depth: options.current._depth,
|
|
1122
|
+
_emphasis: options.current._emphasis,
|
|
1123
|
+
// Engine fields
|
|
1124
|
+
_architect: options.architect,
|
|
1125
|
+
_builder: options.builder,
|
|
1126
|
+
_critic: options.critic,
|
|
1127
|
+
_generatedAt: new Date().toISOString(),
|
|
1128
|
+
_structureHash: options.structureHash,
|
|
1129
|
+
_synthesisCount: options.synthesisCount,
|
|
1130
|
+
// Token tracking
|
|
1131
|
+
_architectTokens: options.architectTokens,
|
|
1132
|
+
_builderTokens: options.builderTokens,
|
|
1133
|
+
_criticTokens: options.criticTokens,
|
|
1134
|
+
_architectTokensAvg: options.architectTokens !== undefined
|
|
1135
|
+
? computeEma(options.architectTokens, options.current._architectTokensAvg)
|
|
1136
|
+
: options.current._architectTokensAvg,
|
|
1137
|
+
_builderTokensAvg: options.builderTokens !== undefined
|
|
1138
|
+
? computeEma(options.builderTokens, options.current._builderTokensAvg)
|
|
1139
|
+
: options.current._builderTokensAvg,
|
|
1140
|
+
_criticTokensAvg: options.criticTokens !== undefined
|
|
1141
|
+
? computeEma(options.criticTokens, options.current._criticTokensAvg)
|
|
1142
|
+
: options.current._criticTokensAvg,
|
|
1143
|
+
// Content from builder
|
|
1144
|
+
_content: options.builderOutput?.content ?? options.current._content,
|
|
1145
|
+
// Feedback from critic
|
|
1146
|
+
_feedback: options.feedback ?? options.current._feedback,
|
|
1147
|
+
// Error handling
|
|
1148
|
+
_error: options.error ?? undefined,
|
|
1149
|
+
// Spread structured fields from builder
|
|
1150
|
+
...options.builderOutput?.fields,
|
|
1151
|
+
};
|
|
1152
|
+
// Clean up undefined optional fields
|
|
1153
|
+
if (merged._steer === undefined)
|
|
1154
|
+
delete merged._steer;
|
|
1155
|
+
if (merged._depth === undefined)
|
|
1156
|
+
delete merged._depth;
|
|
1157
|
+
if (merged._emphasis === undefined)
|
|
1158
|
+
delete merged._emphasis;
|
|
1159
|
+
if (merged._architectTokens === undefined)
|
|
1160
|
+
delete merged._architectTokens;
|
|
1161
|
+
if (merged._builderTokens === undefined)
|
|
1162
|
+
delete merged._builderTokens;
|
|
1163
|
+
if (merged._criticTokens === undefined)
|
|
1164
|
+
delete merged._criticTokens;
|
|
1165
|
+
if (merged._architectTokensAvg === undefined)
|
|
1166
|
+
delete merged._architectTokensAvg;
|
|
1167
|
+
if (merged._builderTokensAvg === undefined)
|
|
1168
|
+
delete merged._builderTokensAvg;
|
|
1169
|
+
if (merged._criticTokensAvg === undefined)
|
|
1170
|
+
delete merged._criticTokensAvg;
|
|
1171
|
+
if (merged._error === undefined)
|
|
1172
|
+
delete merged._error;
|
|
1173
|
+
if (merged._content === undefined)
|
|
1174
|
+
delete merged._content;
|
|
1175
|
+
if (merged._feedback === undefined)
|
|
1176
|
+
delete merged._feedback;
|
|
1177
|
+
// Validate
|
|
1178
|
+
const result = metaJsonSchema.safeParse(merged);
|
|
1179
|
+
if (!result.success) {
|
|
1180
|
+
throw new Error(`Meta validation failed: ${result.error.message}`);
|
|
1181
|
+
}
|
|
1182
|
+
// Write atomically
|
|
1183
|
+
const filePath = join(options.metaPath, 'meta.json');
|
|
1184
|
+
writeFileSync(filePath, JSON.stringify(result.data, null, 2) + '\n');
|
|
1185
|
+
return result.data;
|
|
961
1186
|
}
|
|
962
1187
|
|
|
963
1188
|
/**
|
|
@@ -1216,17 +1441,32 @@ function finalizeCycle(metaPath, current, config, architect, builder, critic, bu
|
|
|
1216
1441
|
* @param watcher - Watcher HTTP client.
|
|
1217
1442
|
* @returns Result indicating whether synthesis occurred.
|
|
1218
1443
|
*/
|
|
1219
|
-
async function orchestrateOnce(config, executor, watcher) {
|
|
1220
|
-
// Step 1: Discover
|
|
1221
|
-
const metaPaths =
|
|
1444
|
+
async function orchestrateOnce(config, executor, watcher, targetPath) {
|
|
1445
|
+
// Step 1: Discover via watcher scan
|
|
1446
|
+
const metaPaths = await discoverMetas(config, watcher);
|
|
1222
1447
|
if (metaPaths.length === 0)
|
|
1223
1448
|
return { synthesized: false };
|
|
1224
|
-
//
|
|
1449
|
+
// Read meta.json for each discovered meta
|
|
1225
1450
|
const metas = new Map();
|
|
1226
1451
|
for (const mp of metaPaths) {
|
|
1227
|
-
|
|
1452
|
+
const metaFilePath = join(mp, 'meta.json');
|
|
1453
|
+
try {
|
|
1454
|
+
metas.set(normalizePath$1(mp), JSON.parse(readFileSync(metaFilePath, 'utf8')));
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
// Skip metas with unreadable meta.json
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1228
1460
|
}
|
|
1229
1461
|
const tree = buildOwnershipTree(metaPaths);
|
|
1462
|
+
// If targetPath specified, skip candidate selection — go directly to that meta
|
|
1463
|
+
let targetNode;
|
|
1464
|
+
if (targetPath) {
|
|
1465
|
+
const normalized = normalizePath$1(targetPath);
|
|
1466
|
+
targetNode = findNode(tree, normalized) ?? undefined;
|
|
1467
|
+
if (!targetNode)
|
|
1468
|
+
return { synthesized: false };
|
|
1469
|
+
}
|
|
1230
1470
|
// Steps 3-4: Staleness check + candidate selection
|
|
1231
1471
|
const candidates = [];
|
|
1232
1472
|
for (const node of tree.nodes.values()) {
|
|
@@ -1261,9 +1501,13 @@ async function orchestrateOnce(config, executor, watcher) {
|
|
|
1261
1501
|
winner = candidate;
|
|
1262
1502
|
break;
|
|
1263
1503
|
}
|
|
1264
|
-
if (!winner)
|
|
1504
|
+
if (!winner && !targetNode)
|
|
1505
|
+
return { synthesized: false };
|
|
1506
|
+
const node = targetNode ?? winner.node;
|
|
1507
|
+
// For targeted path, acquire lock now (candidate selection already locked for stalest)
|
|
1508
|
+
if (targetNode && !acquireLock(node.metaPath)) {
|
|
1265
1509
|
return { synthesized: false };
|
|
1266
|
-
|
|
1510
|
+
}
|
|
1267
1511
|
try {
|
|
1268
1512
|
// Re-read meta after lock (may have changed)
|
|
1269
1513
|
const currentMeta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
|
|
@@ -1373,12 +1617,13 @@ async function orchestrateOnce(config, executor, watcher) {
|
|
|
1373
1617
|
* @param config - Validated synthesis config.
|
|
1374
1618
|
* @param executor - Pluggable LLM executor.
|
|
1375
1619
|
* @param watcher - Watcher HTTP client.
|
|
1620
|
+
* @param targetPath - Optional: specific meta/owner path to synthesize instead of stalest candidate.
|
|
1376
1621
|
* @returns Array of results, one per cycle attempted.
|
|
1377
1622
|
*/
|
|
1378
|
-
async function orchestrate(config, executor, watcher) {
|
|
1623
|
+
async function orchestrate(config, executor, watcher, targetPath) {
|
|
1379
1624
|
const results = [];
|
|
1380
1625
|
for (let i = 0; i < config.batchSize; i++) {
|
|
1381
|
-
const result = await orchestrateOnce(config, executor, watcher);
|
|
1626
|
+
const result = await orchestrateOnce(config, executor, watcher, targetPath);
|
|
1382
1627
|
results.push(result);
|
|
1383
1628
|
if (!result.synthesized)
|
|
1384
1629
|
break; // No more candidates
|
|
@@ -1386,108 +1631,6 @@ async function orchestrate(config, executor, watcher) {
|
|
|
1386
1631
|
return results;
|
|
1387
1632
|
}
|
|
1388
1633
|
|
|
1389
|
-
/**
|
|
1390
|
-
* SynthExecutor implementation using the OpenClaw gateway HTTP API.
|
|
1391
|
-
*
|
|
1392
|
-
* Lives in the library package so both plugin and runner can import it.
|
|
1393
|
-
* Spawns sub-agent sessions via the gateway, polls for completion,
|
|
1394
|
-
* and extracts output text.
|
|
1395
|
-
*
|
|
1396
|
-
* @module executor/GatewayExecutor
|
|
1397
|
-
*/
|
|
1398
|
-
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
1399
|
-
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
1400
|
-
/** Sleep helper. */
|
|
1401
|
-
function sleep$1(ms) {
|
|
1402
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* SynthExecutor that spawns OpenClaw sessions via the gateway HTTP API.
|
|
1406
|
-
*
|
|
1407
|
-
* Used by both the OpenClaw plugin (in-process tool calls) and the
|
|
1408
|
-
* runner/CLI (external invocation). Constructs from `gatewayUrl` and
|
|
1409
|
-
* optional `apiKey` — typically sourced from `SynthConfig`.
|
|
1410
|
-
*/
|
|
1411
|
-
class GatewayExecutor {
|
|
1412
|
-
gatewayUrl;
|
|
1413
|
-
apiKey;
|
|
1414
|
-
pollIntervalMs;
|
|
1415
|
-
constructor(options = {}) {
|
|
1416
|
-
this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:3000').replace(/\/+$/, '');
|
|
1417
|
-
this.apiKey = options.apiKey;
|
|
1418
|
-
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1419
|
-
}
|
|
1420
|
-
async spawn(task, options) {
|
|
1421
|
-
const timeoutMs = (options?.timeout ?? DEFAULT_TIMEOUT_MS / 1000) * 1000;
|
|
1422
|
-
const deadline = Date.now() + timeoutMs;
|
|
1423
|
-
const headers = {
|
|
1424
|
-
'Content-Type': 'application/json',
|
|
1425
|
-
};
|
|
1426
|
-
if (this.apiKey) {
|
|
1427
|
-
headers['Authorization'] = 'Bearer ' + this.apiKey;
|
|
1428
|
-
}
|
|
1429
|
-
const spawnRes = await fetch(this.gatewayUrl + '/api/sessions/spawn', {
|
|
1430
|
-
method: 'POST',
|
|
1431
|
-
headers,
|
|
1432
|
-
body: JSON.stringify({
|
|
1433
|
-
task,
|
|
1434
|
-
mode: 'run',
|
|
1435
|
-
model: options?.model,
|
|
1436
|
-
runTimeoutSeconds: options?.timeout,
|
|
1437
|
-
}),
|
|
1438
|
-
});
|
|
1439
|
-
if (!spawnRes.ok) {
|
|
1440
|
-
const text = await spawnRes.text();
|
|
1441
|
-
throw new Error('Gateway spawn failed: HTTP ' +
|
|
1442
|
-
spawnRes.status.toString() +
|
|
1443
|
-
' - ' +
|
|
1444
|
-
text);
|
|
1445
|
-
}
|
|
1446
|
-
const spawnData = (await spawnRes.json());
|
|
1447
|
-
if (!spawnData.sessionKey) {
|
|
1448
|
-
throw new Error('Gateway spawn returned no sessionKey: ' + JSON.stringify(spawnData));
|
|
1449
|
-
}
|
|
1450
|
-
const { sessionKey } = spawnData;
|
|
1451
|
-
// Poll for completion
|
|
1452
|
-
while (Date.now() < deadline) {
|
|
1453
|
-
await sleep$1(this.pollIntervalMs);
|
|
1454
|
-
const historyRes = await fetch(this.gatewayUrl +
|
|
1455
|
-
'/api/sessions/' +
|
|
1456
|
-
encodeURIComponent(sessionKey) +
|
|
1457
|
-
'/history?limit=50', { headers });
|
|
1458
|
-
if (!historyRes.ok)
|
|
1459
|
-
continue;
|
|
1460
|
-
const history = (await historyRes.json());
|
|
1461
|
-
if (history.status === 'completed' || history.status === 'done') {
|
|
1462
|
-
// Extract token usage from session-level or message-level usage
|
|
1463
|
-
let tokens;
|
|
1464
|
-
if (history.usage?.totalTokens) {
|
|
1465
|
-
tokens = history.usage.totalTokens;
|
|
1466
|
-
}
|
|
1467
|
-
else {
|
|
1468
|
-
// Sum message-level usage as fallback
|
|
1469
|
-
let sum = 0;
|
|
1470
|
-
for (const msg of history.messages ?? []) {
|
|
1471
|
-
if (msg.usage?.totalTokens)
|
|
1472
|
-
sum += msg.usage.totalTokens;
|
|
1473
|
-
}
|
|
1474
|
-
if (sum > 0)
|
|
1475
|
-
tokens = sum;
|
|
1476
|
-
}
|
|
1477
|
-
// Extract the last assistant message as output
|
|
1478
|
-
const messages = history.messages ?? [];
|
|
1479
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1480
|
-
if (messages[i].role === 'assistant' && messages[i].content) {
|
|
1481
|
-
return { output: messages[i].content, tokens };
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return { output: '', tokens };
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
throw new Error('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms');
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
1634
|
/**
|
|
1492
1635
|
* HTTP implementation of the WatcherClient interface.
|
|
1493
1636
|
*
|
|
@@ -1546,16 +1689,31 @@ class HttpWatcherClient {
|
|
|
1546
1689
|
throw new Error('Retry exhausted');
|
|
1547
1690
|
}
|
|
1548
1691
|
async scan(params) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1692
|
+
// Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
|
|
1693
|
+
const mustClauses = [];
|
|
1694
|
+
// Carry over any existing 'must' clauses from the provided filter
|
|
1695
|
+
if (params.filter) {
|
|
1696
|
+
const existing = params.filter.must;
|
|
1697
|
+
if (Array.isArray(existing)) {
|
|
1698
|
+
mustClauses.push(...existing);
|
|
1699
|
+
}
|
|
1552
1700
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1701
|
+
// Translate pathPrefix into a Qdrant text match on file_path
|
|
1702
|
+
if (params.pathPrefix !== undefined) {
|
|
1703
|
+
mustClauses.push({
|
|
1704
|
+
key: 'file_path',
|
|
1705
|
+
match: { text: params.pathPrefix },
|
|
1706
|
+
});
|
|
1555
1707
|
}
|
|
1708
|
+
// Translate modifiedAfter into a Qdrant range filter on modified_at
|
|
1556
1709
|
if (params.modifiedAfter !== undefined) {
|
|
1557
|
-
|
|
1710
|
+
mustClauses.push({
|
|
1711
|
+
key: 'modified_at',
|
|
1712
|
+
range: { gt: params.modifiedAfter },
|
|
1713
|
+
});
|
|
1558
1714
|
}
|
|
1715
|
+
const filter = { must: mustClauses };
|
|
1716
|
+
const body = { filter };
|
|
1559
1717
|
if (params.fields !== undefined) {
|
|
1560
1718
|
body.fields = params.fields;
|
|
1561
1719
|
}
|
|
@@ -1604,25 +1762,26 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1604
1762
|
buildBuilderTask: buildBuilderTask,
|
|
1605
1763
|
buildContextPackage: buildContextPackage,
|
|
1606
1764
|
buildCriticTask: buildCriticTask,
|
|
1765
|
+
buildMetaFilter: buildMetaFilter,
|
|
1607
1766
|
buildOwnershipTree: buildOwnershipTree,
|
|
1608
1767
|
computeEffectiveStaleness: computeEffectiveStaleness,
|
|
1609
1768
|
computeEma: computeEma,
|
|
1610
1769
|
computeStructureHash: computeStructureHash,
|
|
1611
1770
|
createSnapshot: createSnapshot,
|
|
1612
|
-
|
|
1771
|
+
discoverMetas: discoverMetas,
|
|
1613
1772
|
filterInScope: filterInScope,
|
|
1614
1773
|
findNode: findNode,
|
|
1615
1774
|
getScopePrefix: getScopePrefix,
|
|
1616
|
-
globMetas: globMetas,
|
|
1617
1775
|
hasSteerChanged: hasSteerChanged,
|
|
1618
1776
|
isArchitectTriggered: isArchitectTriggered,
|
|
1619
1777
|
isLocked: isLocked,
|
|
1620
1778
|
isStale: isStale,
|
|
1621
1779
|
listArchiveFiles: listArchiveFiles,
|
|
1780
|
+
listMetas: listMetas,
|
|
1622
1781
|
loadSynthConfig: loadSynthConfig,
|
|
1623
1782
|
mergeAndWrite: mergeAndWrite,
|
|
1624
1783
|
metaJsonSchema: metaJsonSchema,
|
|
1625
|
-
normalizePath: normalizePath,
|
|
1784
|
+
normalizePath: normalizePath$1,
|
|
1626
1785
|
orchestrate: orchestrate,
|
|
1627
1786
|
paginatedScan: paginatedScan,
|
|
1628
1787
|
parseArchitectOutput: parseArchitectOutput,
|
|
@@ -1648,6 +1807,10 @@ var index = /*#__PURE__*/Object.freeze({
|
|
|
1648
1807
|
*
|
|
1649
1808
|
* @module cli
|
|
1650
1809
|
*/
|
|
1810
|
+
/** Read and parse a meta.json file with proper typing. */
|
|
1811
|
+
function readMeta(metaPath) {
|
|
1812
|
+
return JSON.parse(readFileSync(join(metaPath, 'meta.json'), 'utf8'));
|
|
1813
|
+
}
|
|
1651
1814
|
const args = process.argv.slice(2);
|
|
1652
1815
|
const command = args.find((a) => !a.startsWith('-'));
|
|
1653
1816
|
const jsonOutput = args.includes('--json');
|
|
@@ -1683,73 +1846,38 @@ function output(data) {
|
|
|
1683
1846
|
console.log(JSON.stringify(data, null, 2));
|
|
1684
1847
|
}
|
|
1685
1848
|
}
|
|
1686
|
-
function runStatus(config) {
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
let errors = 0;
|
|
1691
|
-
let locked = 0;
|
|
1692
|
-
let neverSynth = 0;
|
|
1693
|
-
let archTokens = 0;
|
|
1694
|
-
let buildTokens = 0;
|
|
1695
|
-
let critTokens = 0;
|
|
1696
|
-
for (const node of tree.nodes.values()) {
|
|
1697
|
-
const meta = ensureMetaJson(node.metaPath);
|
|
1698
|
-
const s = actualStaleness(meta);
|
|
1699
|
-
if (s > 0)
|
|
1700
|
-
stale++;
|
|
1701
|
-
if (meta._error)
|
|
1702
|
-
errors++;
|
|
1703
|
-
if (isLocked(normalizePath(node.metaPath)))
|
|
1704
|
-
locked++;
|
|
1705
|
-
if (!meta._generatedAt)
|
|
1706
|
-
neverSynth++;
|
|
1707
|
-
if (meta._architectTokens)
|
|
1708
|
-
archTokens += meta._architectTokens;
|
|
1709
|
-
if (meta._builderTokens)
|
|
1710
|
-
buildTokens += meta._builderTokens;
|
|
1711
|
-
if (meta._criticTokens)
|
|
1712
|
-
critTokens += meta._criticTokens;
|
|
1713
|
-
}
|
|
1714
|
-
output({
|
|
1715
|
-
total: tree.nodes.size,
|
|
1716
|
-
stale,
|
|
1717
|
-
errors,
|
|
1718
|
-
locked,
|
|
1719
|
-
neverSynthesized: neverSynth,
|
|
1720
|
-
tokens: { architect: archTokens, builder: buildTokens, critic: critTokens },
|
|
1721
|
-
});
|
|
1849
|
+
async function runStatus(config) {
|
|
1850
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1851
|
+
const result = await listMetas(config, watcher);
|
|
1852
|
+
output(result.summary);
|
|
1722
1853
|
}
|
|
1723
|
-
function runList(config) {
|
|
1854
|
+
async function runList(config) {
|
|
1724
1855
|
const prefix = getArg('--prefix');
|
|
1725
1856
|
const filter = getArg('--filter');
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
children: node.children.length,
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1857
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1858
|
+
const result = await listMetas(config, watcher);
|
|
1859
|
+
let entries = result.entries;
|
|
1860
|
+
if (prefix) {
|
|
1861
|
+
entries = entries.filter((e) => e.path.includes(prefix));
|
|
1862
|
+
}
|
|
1863
|
+
if (filter === 'hasError')
|
|
1864
|
+
entries = entries.filter((e) => e.hasError);
|
|
1865
|
+
if (filter === 'stale')
|
|
1866
|
+
entries = entries.filter((e) => e.stalenessSeconds > 0);
|
|
1867
|
+
if (filter === 'locked')
|
|
1868
|
+
entries = entries.filter((e) => e.locked);
|
|
1869
|
+
if (filter === 'never')
|
|
1870
|
+
entries = entries.filter((e) => e.stalenessSeconds === Infinity);
|
|
1871
|
+
const rows = entries.map((e) => ({
|
|
1872
|
+
path: e.path,
|
|
1873
|
+
depth: e.depth,
|
|
1874
|
+
staleness: e.stalenessSeconds === Infinity
|
|
1875
|
+
? 'never'
|
|
1876
|
+
: String(Math.round(e.stalenessSeconds)) + 's',
|
|
1877
|
+
hasError: e.hasError,
|
|
1878
|
+
locked: e.locked,
|
|
1879
|
+
children: e.children,
|
|
1880
|
+
}));
|
|
1753
1881
|
output({ total: rows.length, items: rows });
|
|
1754
1882
|
}
|
|
1755
1883
|
async function runDetail(config) {
|
|
@@ -1759,15 +1887,15 @@ async function runDetail(config) {
|
|
|
1759
1887
|
process.exit(1);
|
|
1760
1888
|
}
|
|
1761
1889
|
const archiveArg = getArg('--archive');
|
|
1762
|
-
const
|
|
1763
|
-
const
|
|
1764
|
-
const normalized = normalizePath(targetPath);
|
|
1765
|
-
const node = findNode(tree, normalized);
|
|
1890
|
+
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1891
|
+
const metaResult = await listMetas(config, watcher);
|
|
1892
|
+
const normalized = normalizePath$1(targetPath);
|
|
1893
|
+
const node = findNode(metaResult.tree, normalized);
|
|
1766
1894
|
if (!node) {
|
|
1767
1895
|
console.error('Meta not found: ' + targetPath);
|
|
1768
1896
|
process.exit(1);
|
|
1769
1897
|
}
|
|
1770
|
-
const meta =
|
|
1898
|
+
const meta = readMeta(node.metaPath);
|
|
1771
1899
|
const result = { meta };
|
|
1772
1900
|
if (archiveArg) {
|
|
1773
1901
|
const { listArchiveFiles } = await Promise.resolve().then(function () { return index$1; });
|
|
@@ -1783,27 +1911,26 @@ async function runDetail(config) {
|
|
|
1783
1911
|
}
|
|
1784
1912
|
async function runPreview(config) {
|
|
1785
1913
|
const targetPath = getArg('--path');
|
|
1786
|
-
const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash,
|
|
1787
|
-
const metaPaths = globMetas(config.watchPaths);
|
|
1788
|
-
const tree = buildOwnershipTree(metaPaths);
|
|
1914
|
+
const { filterInScope, paginatedScan, readLatestArchive, computeStructureHash, } = await Promise.resolve().then(function () { return index; });
|
|
1789
1915
|
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1916
|
+
const metaResult = await listMetas(config, watcher);
|
|
1790
1917
|
let targetNode;
|
|
1791
1918
|
if (targetPath) {
|
|
1792
|
-
const normalized = normalizePath(targetPath);
|
|
1793
|
-
targetNode = findNode(tree, normalized);
|
|
1919
|
+
const normalized = normalizePath$1(targetPath);
|
|
1920
|
+
targetNode = findNode(metaResult.tree, normalized);
|
|
1794
1921
|
if (!targetNode) {
|
|
1795
1922
|
console.error('Meta not found: ' + targetPath);
|
|
1796
1923
|
process.exit(1);
|
|
1797
1924
|
}
|
|
1798
1925
|
}
|
|
1799
1926
|
else {
|
|
1800
|
-
const candidates =
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
}
|
|
1927
|
+
const candidates = metaResult.entries
|
|
1928
|
+
.filter((e) => e.stalenessSeconds > 0)
|
|
1929
|
+
.map((e) => ({
|
|
1930
|
+
node: e.node,
|
|
1931
|
+
meta: e.meta,
|
|
1932
|
+
actualStaleness: e.stalenessSeconds,
|
|
1933
|
+
}));
|
|
1807
1934
|
const weighted = computeEffectiveStaleness(candidates, config.depthWeight);
|
|
1808
1935
|
const winner = selectCandidate(weighted);
|
|
1809
1936
|
if (!winner) {
|
|
@@ -1812,7 +1939,7 @@ async function runPreview(config) {
|
|
|
1812
1939
|
}
|
|
1813
1940
|
targetNode = winner.node;
|
|
1814
1941
|
}
|
|
1815
|
-
const meta =
|
|
1942
|
+
const meta = readMeta(targetNode.metaPath);
|
|
1816
1943
|
const allFiles = await paginatedScan(watcher, {
|
|
1817
1944
|
pathPrefix: targetNode.ownerPath,
|
|
1818
1945
|
});
|
|
@@ -1840,9 +1967,6 @@ async function runSynthesize(config) {
|
|
|
1840
1967
|
const batchArg = getArg('--batch');
|
|
1841
1968
|
const effectiveConfig = {
|
|
1842
1969
|
...config,
|
|
1843
|
-
...(targetPath
|
|
1844
|
-
? { watchPaths: [targetPath.replace(/[/\\]\.meta[/\\]?$/, '')] }
|
|
1845
|
-
: {}),
|
|
1846
1970
|
...(batchArg ? { batchSize: parseInt(batchArg, 10) } : {}),
|
|
1847
1971
|
};
|
|
1848
1972
|
const executor = new GatewayExecutor({
|
|
@@ -1850,7 +1974,7 @@ async function runSynthesize(config) {
|
|
|
1850
1974
|
apiKey: config.gatewayApiKey,
|
|
1851
1975
|
});
|
|
1852
1976
|
const watcher = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
1853
|
-
const results = await orchestrate(effectiveConfig, executor, watcher);
|
|
1977
|
+
const results = await orchestrate(effectiveConfig, executor, watcher, targetPath ?? undefined);
|
|
1854
1978
|
const synthesized = results.filter((r) => r.synthesized);
|
|
1855
1979
|
output({
|
|
1856
1980
|
synthesizedCount: synthesized.length,
|
|
@@ -1921,9 +2045,16 @@ async function runValidate(config) {
|
|
|
1921
2045
|
catch {
|
|
1922
2046
|
checks.gateway = 'UNREACHABLE (' + config.gatewayUrl + ')';
|
|
1923
2047
|
}
|
|
1924
|
-
// Check
|
|
1925
|
-
|
|
1926
|
-
|
|
2048
|
+
// Check meta discovery via watcher
|
|
2049
|
+
try {
|
|
2050
|
+
const watcherClient = new HttpWatcherClient({ baseUrl: config.watcherUrl });
|
|
2051
|
+
const metaPaths = await discoverMetas(config, watcherClient);
|
|
2052
|
+
checks.metas =
|
|
2053
|
+
String(metaPaths.length) + ' .meta/ entities discovered via watcher';
|
|
2054
|
+
}
|
|
2055
|
+
catch {
|
|
2056
|
+
checks.metas = 'FAILED — could not discover metas (watcher may be down)';
|
|
2057
|
+
}
|
|
1927
2058
|
output({ config: 'valid', checks });
|
|
1928
2059
|
}
|
|
1929
2060
|
function runConfigShow(config) {
|
|
@@ -1983,10 +2114,10 @@ async function main() {
|
|
|
1983
2114
|
}
|
|
1984
2115
|
switch (command) {
|
|
1985
2116
|
case 'status':
|
|
1986
|
-
runStatus(config);
|
|
2117
|
+
await runStatus(config);
|
|
1987
2118
|
break;
|
|
1988
2119
|
case 'list':
|
|
1989
|
-
runList(config);
|
|
2120
|
+
await runList(config);
|
|
1990
2121
|
break;
|
|
1991
2122
|
case 'detail':
|
|
1992
2123
|
await runDetail(config);
|