@neotx/core 0.1.0-alpha.14 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -39
- package/dist/index.d.ts +391 -96
- package/dist/index.js +1786 -729
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -406,11 +406,10 @@ var Semaphore = class {
|
|
|
406
406
|
};
|
|
407
407
|
|
|
408
408
|
// src/config.ts
|
|
409
|
-
import { existsSync } from "fs";
|
|
409
|
+
import { existsSync as existsSync2 } from "fs";
|
|
410
410
|
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
411
411
|
import path4 from "path";
|
|
412
|
-
import { parse as
|
|
413
|
-
import { z as z2 } from "zod";
|
|
412
|
+
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
414
413
|
|
|
415
414
|
// src/paths.ts
|
|
416
415
|
import { homedir } from "os";
|
|
@@ -458,8 +457,12 @@ function getSupervisorEventsPath(name) {
|
|
|
458
457
|
function getSupervisorLockPath(name) {
|
|
459
458
|
return path3.join(getSupervisorDir(name), "daemon.lock");
|
|
460
459
|
}
|
|
460
|
+
function getSupervisorDecisionsPath(name) {
|
|
461
|
+
return path3.join(getSupervisorDir(name), "decisions.jsonl");
|
|
462
|
+
}
|
|
461
463
|
|
|
462
|
-
// src/config.ts
|
|
464
|
+
// src/config/schema.ts
|
|
465
|
+
import { z as z2 } from "zod";
|
|
463
466
|
var httpMcpServerSchema = z2.object({
|
|
464
467
|
type: z2.literal("http"),
|
|
465
468
|
url: z2.string(),
|
|
@@ -484,26 +487,54 @@ var repoConfigSchema = z2.object({
|
|
|
484
487
|
pushRemote: z2.string().default("origin"),
|
|
485
488
|
gitStrategy: gitStrategySchema
|
|
486
489
|
});
|
|
490
|
+
var concurrencyConfigSchema = z2.object({
|
|
491
|
+
maxSessions: z2.number().default(5),
|
|
492
|
+
maxPerRepo: z2.number().default(4),
|
|
493
|
+
queueMax: z2.number().default(50)
|
|
494
|
+
}).default({ maxSessions: 5, maxPerRepo: 4, queueMax: 50 });
|
|
495
|
+
var budgetConfigSchema = z2.object({
|
|
496
|
+
dailyCapUsd: z2.number().default(500),
|
|
497
|
+
alertThresholdPct: z2.number().default(80)
|
|
498
|
+
}).default({ dailyCapUsd: 500, alertThresholdPct: 80 });
|
|
499
|
+
var recoveryConfigSchema = z2.object({
|
|
500
|
+
maxRetries: z2.number().default(3),
|
|
501
|
+
backoffBaseMs: z2.number().default(3e4)
|
|
502
|
+
}).default({ maxRetries: 3, backoffBaseMs: 3e4 });
|
|
503
|
+
var sessionsConfigSchema = z2.object({
|
|
504
|
+
initTimeoutMs: z2.number().default(12e4),
|
|
505
|
+
maxDurationMs: z2.number().default(36e5),
|
|
506
|
+
dir: z2.string().default("/tmp/neo-sessions")
|
|
507
|
+
}).default({ initTimeoutMs: 12e4, maxDurationMs: 36e5, dir: "/tmp/neo-sessions" });
|
|
508
|
+
var supervisorConfigSchema = z2.object({
|
|
509
|
+
port: z2.number().default(7777),
|
|
510
|
+
secret: z2.string().optional(),
|
|
511
|
+
heartbeatTimeoutMs: z2.number().default(3e5),
|
|
512
|
+
maxConsecutiveFailures: z2.number().default(3),
|
|
513
|
+
maxEventsPerSec: z2.number().default(10),
|
|
514
|
+
dailyCapUsd: z2.number().default(50),
|
|
515
|
+
/** How often consolidation runs (ms) */
|
|
516
|
+
consolidationIntervalMs: z2.number().default(3e5),
|
|
517
|
+
/** How often compaction runs (ms) */
|
|
518
|
+
compactionIntervalMs: z2.number().default(36e5),
|
|
519
|
+
/** Safety timeout for waitForWork (ms) */
|
|
520
|
+
eventTimeoutMs: z2.number().default(3e5),
|
|
521
|
+
instructions: z2.string().optional()
|
|
522
|
+
}).default({
|
|
523
|
+
port: 7777,
|
|
524
|
+
heartbeatTimeoutMs: 3e5,
|
|
525
|
+
maxConsecutiveFailures: 3,
|
|
526
|
+
maxEventsPerSec: 10,
|
|
527
|
+
dailyCapUsd: 50,
|
|
528
|
+
consolidationIntervalMs: 3e5,
|
|
529
|
+
compactionIntervalMs: 36e5,
|
|
530
|
+
eventTimeoutMs: 3e5
|
|
531
|
+
});
|
|
487
532
|
var globalConfigSchema = z2.object({
|
|
488
533
|
repos: z2.array(repoConfigSchema).default([]),
|
|
489
|
-
concurrency:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}).default({ maxSessions: 5, maxPerRepo: 4, queueMax: 50 }),
|
|
494
|
-
budget: z2.object({
|
|
495
|
-
dailyCapUsd: z2.number().default(500),
|
|
496
|
-
alertThresholdPct: z2.number().default(80)
|
|
497
|
-
}).default({ dailyCapUsd: 500, alertThresholdPct: 80 }),
|
|
498
|
-
recovery: z2.object({
|
|
499
|
-
maxRetries: z2.number().default(3),
|
|
500
|
-
backoffBaseMs: z2.number().default(3e4)
|
|
501
|
-
}).default({ maxRetries: 3, backoffBaseMs: 3e4 }),
|
|
502
|
-
sessions: z2.object({
|
|
503
|
-
initTimeoutMs: z2.number().default(12e4),
|
|
504
|
-
maxDurationMs: z2.number().default(36e5),
|
|
505
|
-
dir: z2.string().default("/tmp/neo-sessions")
|
|
506
|
-
}).default({ initTimeoutMs: 12e4, maxDurationMs: 36e5, dir: "/tmp/neo-sessions" }),
|
|
534
|
+
concurrency: concurrencyConfigSchema,
|
|
535
|
+
budget: budgetConfigSchema,
|
|
536
|
+
recovery: recoveryConfigSchema,
|
|
537
|
+
sessions: sessionsConfigSchema,
|
|
507
538
|
webhooks: z2.array(
|
|
508
539
|
z2.object({
|
|
509
540
|
url: z2.string().url(),
|
|
@@ -512,30 +543,7 @@ var globalConfigSchema = z2.object({
|
|
|
512
543
|
timeoutMs: z2.number().default(5e3)
|
|
513
544
|
})
|
|
514
545
|
).default([]),
|
|
515
|
-
supervisor:
|
|
516
|
-
port: z2.number().default(7777),
|
|
517
|
-
secret: z2.string().optional(),
|
|
518
|
-
heartbeatTimeoutMs: z2.number().default(3e5),
|
|
519
|
-
maxConsecutiveFailures: z2.number().default(3),
|
|
520
|
-
maxEventsPerSec: z2.number().default(10),
|
|
521
|
-
dailyCapUsd: z2.number().default(50),
|
|
522
|
-
/** How often consolidation runs (ms) */
|
|
523
|
-
consolidationIntervalMs: z2.number().default(3e5),
|
|
524
|
-
/** How often compaction runs (ms) */
|
|
525
|
-
compactionIntervalMs: z2.number().default(36e5),
|
|
526
|
-
/** Safety timeout for waitForWork (ms) */
|
|
527
|
-
eventTimeoutMs: z2.number().default(3e5),
|
|
528
|
-
instructions: z2.string().optional()
|
|
529
|
-
}).default({
|
|
530
|
-
port: 7777,
|
|
531
|
-
heartbeatTimeoutMs: 3e5,
|
|
532
|
-
maxConsecutiveFailures: 3,
|
|
533
|
-
maxEventsPerSec: 10,
|
|
534
|
-
dailyCapUsd: 50,
|
|
535
|
-
consolidationIntervalMs: 3e5,
|
|
536
|
-
compactionIntervalMs: 36e5,
|
|
537
|
-
eventTimeoutMs: 3e5
|
|
538
|
-
}),
|
|
546
|
+
supervisor: supervisorConfigSchema,
|
|
539
547
|
memory: z2.object({
|
|
540
548
|
embeddings: z2.boolean().default(true)
|
|
541
549
|
}).default({ embeddings: true }),
|
|
@@ -548,6 +556,296 @@ var globalConfigSchema = z2.object({
|
|
|
548
556
|
}).optional()
|
|
549
557
|
});
|
|
550
558
|
var neoConfigSchema = globalConfigSchema;
|
|
559
|
+
var repoOverrideConfigSchema = z2.object({
|
|
560
|
+
concurrency: concurrencyConfigSchema.unwrap().partial().optional(),
|
|
561
|
+
budget: budgetConfigSchema.unwrap().partial().optional(),
|
|
562
|
+
recovery: recoveryConfigSchema.unwrap().partial().optional(),
|
|
563
|
+
sessions: sessionsConfigSchema.unwrap().partial().optional()
|
|
564
|
+
}).partial();
|
|
565
|
+
|
|
566
|
+
// src/config/dotNotation.ts
|
|
567
|
+
function getConfigValue(config, path19) {
|
|
568
|
+
if (path19 === "") {
|
|
569
|
+
return config;
|
|
570
|
+
}
|
|
571
|
+
const keys = path19.split(".");
|
|
572
|
+
let current = config;
|
|
573
|
+
for (const key of keys) {
|
|
574
|
+
if (current === null || current === void 0) {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
if (typeof current !== "object") {
|
|
578
|
+
return void 0;
|
|
579
|
+
}
|
|
580
|
+
current = current[key];
|
|
581
|
+
}
|
|
582
|
+
return current;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/config/merge.ts
|
|
586
|
+
var defaultConfig = {
|
|
587
|
+
repos: [],
|
|
588
|
+
concurrency: concurrencyConfigSchema.parse(void 0),
|
|
589
|
+
budget: budgetConfigSchema.parse(void 0),
|
|
590
|
+
recovery: recoveryConfigSchema.parse(void 0),
|
|
591
|
+
sessions: sessionsConfigSchema.parse(void 0),
|
|
592
|
+
webhooks: [],
|
|
593
|
+
supervisor: {
|
|
594
|
+
port: 7777,
|
|
595
|
+
heartbeatTimeoutMs: 3e5,
|
|
596
|
+
maxConsecutiveFailures: 3,
|
|
597
|
+
maxEventsPerSec: 10,
|
|
598
|
+
dailyCapUsd: 50,
|
|
599
|
+
consolidationIntervalMs: 3e5,
|
|
600
|
+
compactionIntervalMs: 36e5,
|
|
601
|
+
eventTimeoutMs: 3e5
|
|
602
|
+
},
|
|
603
|
+
memory: { embeddings: true }
|
|
604
|
+
};
|
|
605
|
+
function deepMerge(target, source) {
|
|
606
|
+
const result = { ...target };
|
|
607
|
+
for (const key of Object.keys(source)) {
|
|
608
|
+
const sourceValue = source[key];
|
|
609
|
+
const targetValue = target[key];
|
|
610
|
+
if (sourceValue === void 0) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && targetValue !== null && typeof targetValue === "object" && !Array.isArray(targetValue)) {
|
|
614
|
+
result[key] = deepMerge(
|
|
615
|
+
targetValue,
|
|
616
|
+
sourceValue
|
|
617
|
+
);
|
|
618
|
+
} else {
|
|
619
|
+
result[key] = sourceValue;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function deepFreeze(obj) {
|
|
625
|
+
if (obj === null || typeof obj !== "object") {
|
|
626
|
+
return obj;
|
|
627
|
+
}
|
|
628
|
+
if (Array.isArray(obj)) {
|
|
629
|
+
for (const item of obj) {
|
|
630
|
+
deepFreeze(item);
|
|
631
|
+
}
|
|
632
|
+
return Object.freeze(obj);
|
|
633
|
+
}
|
|
634
|
+
for (const value of Object.values(obj)) {
|
|
635
|
+
deepFreeze(value);
|
|
636
|
+
}
|
|
637
|
+
return Object.freeze(obj);
|
|
638
|
+
}
|
|
639
|
+
function mergeConfigs(defaults, globalConfig, repoConfig) {
|
|
640
|
+
let result = { ...defaults };
|
|
641
|
+
if (globalConfig) {
|
|
642
|
+
result = deepMerge(result, globalConfig);
|
|
643
|
+
}
|
|
644
|
+
if (repoConfig) {
|
|
645
|
+
result = deepMerge(result, repoConfig);
|
|
646
|
+
}
|
|
647
|
+
return deepFreeze(result);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/config/ConfigStore.ts
|
|
651
|
+
import { existsSync, readFileSync } from "fs";
|
|
652
|
+
import { homedir as homedir2 } from "os";
|
|
653
|
+
import { join } from "path";
|
|
654
|
+
import { parse as parseYaml2 } from "yaml";
|
|
655
|
+
var ConfigStore = class {
|
|
656
|
+
repoPath;
|
|
657
|
+
config = null;
|
|
658
|
+
constructor(repoPath) {
|
|
659
|
+
this.repoPath = repoPath;
|
|
660
|
+
}
|
|
661
|
+
// ─── Public API ────────────────────────────────────────
|
|
662
|
+
/**
|
|
663
|
+
* Loads and merges config files from all locations.
|
|
664
|
+
* Must be called before get() or getAll().
|
|
665
|
+
*/
|
|
666
|
+
async load() {
|
|
667
|
+
const globalConfig = await this.loadGlobalConfig();
|
|
668
|
+
const repoConfig = await this.loadRepoConfig();
|
|
669
|
+
this.config = mergeConfigs(defaultConfig, globalConfig, repoConfig);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Gets a config value using dot notation.
|
|
673
|
+
*
|
|
674
|
+
* @param key - Dot-separated path (e.g., "budget.dailyCapUsd")
|
|
675
|
+
* @returns The value at the path
|
|
676
|
+
* @throws Error if load() has not been called
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* store.get<number>('budget.dailyCapUsd') // 500
|
|
680
|
+
* store.get<string>('sessions.dir') // '/tmp/neo-sessions'
|
|
681
|
+
*/
|
|
682
|
+
get(key) {
|
|
683
|
+
if (this.config === null) {
|
|
684
|
+
throw new Error("ConfigStore not loaded. Call load() first.");
|
|
685
|
+
}
|
|
686
|
+
return getConfigValue(this.config, key);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Returns the full merged configuration.
|
|
690
|
+
*
|
|
691
|
+
* @throws Error if load() has not been called
|
|
692
|
+
*/
|
|
693
|
+
getAll() {
|
|
694
|
+
if (this.config === null) {
|
|
695
|
+
throw new Error("ConfigStore not loaded. Call load() first.");
|
|
696
|
+
}
|
|
697
|
+
return this.config;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Returns the repository path, if one was provided.
|
|
701
|
+
* Used by ConfigWatcher to determine which files to watch.
|
|
702
|
+
*/
|
|
703
|
+
getRepoPath() {
|
|
704
|
+
return this.repoPath;
|
|
705
|
+
}
|
|
706
|
+
// ─── Private loaders ───────────────────────────────────
|
|
707
|
+
/**
|
|
708
|
+
* Loads global config from ~/.neo/config.yml
|
|
709
|
+
*/
|
|
710
|
+
async loadGlobalConfig() {
|
|
711
|
+
const globalPath = join(homedir2(), ".neo", "config.yml");
|
|
712
|
+
const raw = await this.loadFile(globalPath);
|
|
713
|
+
if (raw === null) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
const parsed = neoConfigSchema.safeParse(raw);
|
|
717
|
+
if (!parsed.success) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
return parsed.data;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Loads repo-level overrides from <repoPath>/.neo/config.yml
|
|
724
|
+
*/
|
|
725
|
+
async loadRepoConfig() {
|
|
726
|
+
if (!this.repoPath) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const repoConfigPath = join(this.repoPath, ".neo", "config.yml");
|
|
730
|
+
const raw = await this.loadFile(repoConfigPath);
|
|
731
|
+
if (raw === null) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
const parsed = repoOverrideConfigSchema.safeParse(raw);
|
|
735
|
+
if (!parsed.success) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
return parsed.data;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Loads and parses a YAML config file.
|
|
742
|
+
*
|
|
743
|
+
* @param filePath - Absolute path to the config file
|
|
744
|
+
* @returns Parsed YAML content or null if file doesn't exist
|
|
745
|
+
*/
|
|
746
|
+
async loadFile(filePath) {
|
|
747
|
+
if (!existsSync(filePath)) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
const content = readFileSync(filePath, "utf-8");
|
|
752
|
+
const parsed = parseYaml2(content);
|
|
753
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
return parsed;
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
// src/config/ConfigWatcher.ts
|
|
764
|
+
import { EventEmitter } from "events";
|
|
765
|
+
import { homedir as homedir3 } from "os";
|
|
766
|
+
import { join as join2 } from "path";
|
|
767
|
+
import { watch } from "chokidar";
|
|
768
|
+
var ConfigWatcher = class extends EventEmitter {
|
|
769
|
+
store;
|
|
770
|
+
debounceMs;
|
|
771
|
+
repoPath;
|
|
772
|
+
watcher = null;
|
|
773
|
+
debounceTimer = null;
|
|
774
|
+
constructor(store, options) {
|
|
775
|
+
super();
|
|
776
|
+
this.store = store;
|
|
777
|
+
this.debounceMs = options?.debounceMs ?? 500;
|
|
778
|
+
this.repoPath = store.getRepoPath();
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Starts watching config files for changes.
|
|
782
|
+
* Watches both global (~/.neo/config.yml) and repo config files.
|
|
783
|
+
*/
|
|
784
|
+
start() {
|
|
785
|
+
if (this.watcher) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const paths = this.getConfigPaths();
|
|
789
|
+
this.watcher = watch(paths, {
|
|
790
|
+
ignoreInitial: true,
|
|
791
|
+
// Don't error if files don't exist — they may be created later
|
|
792
|
+
ignorePermissionErrors: true
|
|
793
|
+
});
|
|
794
|
+
this.watcher.on("change", () => this.handleChange());
|
|
795
|
+
this.watcher.on("add", () => this.handleChange());
|
|
796
|
+
this.watcher.on("unlink", () => this.handleChange());
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Stops watching config files.
|
|
800
|
+
*/
|
|
801
|
+
stop() {
|
|
802
|
+
if (this.debounceTimer) {
|
|
803
|
+
clearTimeout(this.debounceTimer);
|
|
804
|
+
this.debounceTimer = null;
|
|
805
|
+
}
|
|
806
|
+
if (this.watcher) {
|
|
807
|
+
this.watcher.close();
|
|
808
|
+
this.watcher = null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// ─── Private ─────────────────────────────────────────────
|
|
812
|
+
/**
|
|
813
|
+
* Returns the list of config file paths to watch.
|
|
814
|
+
*/
|
|
815
|
+
getConfigPaths() {
|
|
816
|
+
const globalPath = join2(homedir3(), ".neo", "config.yml");
|
|
817
|
+
const paths = [globalPath];
|
|
818
|
+
if (this.repoPath) {
|
|
819
|
+
const repoConfigPath = join2(this.repoPath, ".neo", "config.yml");
|
|
820
|
+
paths.push(repoConfigPath);
|
|
821
|
+
}
|
|
822
|
+
return paths;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Handles file change events with debouncing.
|
|
826
|
+
*/
|
|
827
|
+
handleChange() {
|
|
828
|
+
if (this.debounceTimer) {
|
|
829
|
+
clearTimeout(this.debounceTimer);
|
|
830
|
+
}
|
|
831
|
+
this.debounceTimer = setTimeout(() => {
|
|
832
|
+
this.debounceTimer = null;
|
|
833
|
+
this.reloadConfig();
|
|
834
|
+
}, this.debounceMs);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Reloads the config and emits 'change' event.
|
|
838
|
+
*/
|
|
839
|
+
async reloadConfig() {
|
|
840
|
+
try {
|
|
841
|
+
await this.store.load();
|
|
842
|
+
this.emit("change");
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/config.ts
|
|
551
849
|
var DEFAULT_GLOBAL_CONFIG = {
|
|
552
850
|
repos: [],
|
|
553
851
|
concurrency: {
|
|
@@ -562,7 +860,7 @@ var DEFAULT_GLOBAL_CONFIG = {
|
|
|
562
860
|
};
|
|
563
861
|
function parseYamlFile(raw, filePath) {
|
|
564
862
|
try {
|
|
565
|
-
return
|
|
863
|
+
return parseYaml3(raw);
|
|
566
864
|
} catch (err) {
|
|
567
865
|
throw new Error(
|
|
568
866
|
`Invalid YAML in ${filePath}: ${err instanceof Error ? err.message : String(err)}. Check YAML syntax at the indicated line.`
|
|
@@ -590,7 +888,7 @@ async function loadConfig(configPath) {
|
|
|
590
888
|
}
|
|
591
889
|
async function loadGlobalConfig() {
|
|
592
890
|
const configPath = path4.join(getDataDir(), "config.yml");
|
|
593
|
-
if (!
|
|
891
|
+
if (!existsSync2(configPath)) {
|
|
594
892
|
await mkdir(getDataDir(), { recursive: true });
|
|
595
893
|
await writeFile(configPath, stringifyYaml(DEFAULT_GLOBAL_CONFIG), "utf-8");
|
|
596
894
|
return globalConfigSchema.parse(DEFAULT_GLOBAL_CONFIG);
|
|
@@ -698,9 +996,9 @@ var CostJournal = class {
|
|
|
698
996
|
};
|
|
699
997
|
|
|
700
998
|
// src/events/emitter.ts
|
|
701
|
-
import { EventEmitter } from "events";
|
|
999
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
702
1000
|
var NeoEventEmitter = class {
|
|
703
|
-
emitter = new
|
|
1001
|
+
emitter = new EventEmitter2();
|
|
704
1002
|
emit(event) {
|
|
705
1003
|
this.safeEmit(event.type, event);
|
|
706
1004
|
this.safeEmit("*", event);
|
|
@@ -842,7 +1140,7 @@ function toSerializable(event) {
|
|
|
842
1140
|
|
|
843
1141
|
// src/isolation/clone.ts
|
|
844
1142
|
import { execFile } from "child_process";
|
|
845
|
-
import { existsSync as
|
|
1143
|
+
import { existsSync as existsSync3 } from "fs";
|
|
846
1144
|
import { mkdir as mkdir3, readdir as readdir2, rm } from "fs/promises";
|
|
847
1145
|
import { dirname, resolve } from "path";
|
|
848
1146
|
import { promisify } from "util";
|
|
@@ -886,14 +1184,14 @@ async function createSessionClone(options) {
|
|
|
886
1184
|
}
|
|
887
1185
|
async function removeSessionClone(sessionPath) {
|
|
888
1186
|
const absPath = resolve(sessionPath);
|
|
889
|
-
if (!
|
|
1187
|
+
if (!existsSync3(absPath)) {
|
|
890
1188
|
return;
|
|
891
1189
|
}
|
|
892
1190
|
await rm(absPath, { recursive: true, force: true });
|
|
893
1191
|
}
|
|
894
1192
|
async function listSessionClones(sessionsBaseDir) {
|
|
895
1193
|
const absBase = resolve(sessionsBaseDir);
|
|
896
|
-
if (!
|
|
1194
|
+
if (!existsSync3(absBase)) {
|
|
897
1195
|
return [];
|
|
898
1196
|
}
|
|
899
1197
|
const entries = await readdir2(absBase, { withFileTypes: true });
|
|
@@ -1191,11 +1489,11 @@ function loopDetection(options) {
|
|
|
1191
1489
|
// src/orchestrator.ts
|
|
1192
1490
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1193
1491
|
import { existsSync as existsSync6 } from "fs";
|
|
1194
|
-
import { mkdir as mkdir6, readFile as
|
|
1195
|
-
import
|
|
1492
|
+
import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
|
|
1493
|
+
import path10 from "path";
|
|
1196
1494
|
|
|
1197
1495
|
// src/orchestrator/run-store.ts
|
|
1198
|
-
import { existsSync as
|
|
1496
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1199
1497
|
import { mkdir as mkdir5, readdir as readdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
1200
1498
|
import path7 from "path";
|
|
1201
1499
|
|
|
@@ -1245,7 +1543,7 @@ var RunStore = class {
|
|
|
1245
1543
|
* Returns them so the caller can emit failure events and update status.
|
|
1246
1544
|
*/
|
|
1247
1545
|
async recoverOrphanedRuns() {
|
|
1248
|
-
if (!
|
|
1546
|
+
if (!existsSync4(this.runsDir)) return [];
|
|
1249
1547
|
const orphaned = [];
|
|
1250
1548
|
try {
|
|
1251
1549
|
const jsonFiles = await this.collectRunFiles();
|
|
@@ -1296,9 +1594,137 @@ var RunStore = class {
|
|
|
1296
1594
|
}
|
|
1297
1595
|
};
|
|
1298
1596
|
|
|
1299
|
-
// src/
|
|
1597
|
+
// src/orchestrator/prompt-builder.ts
|
|
1300
1598
|
import { readFile as readFile5 } from "fs/promises";
|
|
1301
1599
|
import path8 from "path";
|
|
1600
|
+
var INSTRUCTIONS_PATH = ".neo/INSTRUCTIONS.md";
|
|
1601
|
+
async function loadRepoInstructions(repoPath) {
|
|
1602
|
+
const filePath = path8.join(repoPath, INSTRUCTIONS_PATH);
|
|
1603
|
+
try {
|
|
1604
|
+
return await readFile5(filePath, "utf-8");
|
|
1605
|
+
} catch {
|
|
1606
|
+
return void 0;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function buildGitStrategyInstructions(strategy, agent, branch, baseBranch, remote, metadata) {
|
|
1610
|
+
const prNumber = metadata?.prNumber;
|
|
1611
|
+
if (agent.sandbox !== "writable") {
|
|
1612
|
+
if (prNumber) {
|
|
1613
|
+
return `## Pull Request
|
|
1614
|
+
|
|
1615
|
+
PR #${String(prNumber)} is open for this task. After your review, leave your findings as a comment: \`gh pr comment ${String(prNumber)} --body "..."\`.`;
|
|
1616
|
+
}
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
if (strategy === "pr") {
|
|
1620
|
+
if (prNumber) {
|
|
1621
|
+
return `## Git workflow
|
|
1622
|
+
|
|
1623
|
+
You are on branch \`${branch}\`.
|
|
1624
|
+
An open PR exists: #${String(prNumber)}.
|
|
1625
|
+
After committing, push your changes to the branch. The PR will be updated automatically.
|
|
1626
|
+
Leave a review comment on the PR summarizing what you did: \`gh pr comment ${String(prNumber)} --body "..."\`.`;
|
|
1627
|
+
}
|
|
1628
|
+
return `## Git workflow
|
|
1629
|
+
|
|
1630
|
+
You are on branch \`${branch}\` (base: \`${baseBranch}\`).
|
|
1631
|
+
After committing:
|
|
1632
|
+
1. Push: \`git push -u ${remote} ${branch}\`
|
|
1633
|
+
2. Create a PR against \`${baseBranch}\` \u2014 choose a title and description that reflect the work you completed. End the PR body with: \`\u{1F916} Generated with [neo](https://neotx.dev)\`
|
|
1634
|
+
3. Output the PR URL on a dedicated line: \`PR_URL: <url>\``;
|
|
1635
|
+
}
|
|
1636
|
+
return `## Git workflow
|
|
1637
|
+
|
|
1638
|
+
You are on branch \`${branch}\` (base: \`${baseBranch}\`).
|
|
1639
|
+
Commit your changes. The branch will be pushed automatically.`;
|
|
1640
|
+
}
|
|
1641
|
+
function buildReportingInstructions(_runId) {
|
|
1642
|
+
return `## Reporting & Memory
|
|
1643
|
+
|
|
1644
|
+
You have two tools to communicate what you learn and do: \`neo log\` (real-time visibility) and \`neo memory\` (persistent knowledge for future agents). Use both deliberately throughout your work.
|
|
1645
|
+
|
|
1646
|
+
### Progress reporting \u2014 \`neo log\`
|
|
1647
|
+
|
|
1648
|
+
Chain \`neo log\` with the command that triggered the event \u2014 never call it standalone.
|
|
1649
|
+
|
|
1650
|
+
<log-types>
|
|
1651
|
+
| Type | When to use | Example |
|
|
1652
|
+
|------|------------|---------|
|
|
1653
|
+
| \`milestone\` | A meaningful goal is achieved (tests pass, build succeeds, feature complete) | \`neo log milestone "auth middleware passing all tests"\` |
|
|
1654
|
+
| \`action\` | You performed a significant action (push, PR, deploy) | \`neo log action "pushed 3 commits to feat/auth"\` |
|
|
1655
|
+
| \`decision\` | You made a non-obvious choice \u2014 record WHY | \`neo log decision "chose JWT over sessions \u2014 stateless, simpler for MVP"\` |
|
|
1656
|
+
| \`blocker\` | Something is preventing progress | \`neo log blocker "CI fails: missing DATABASE_URL in test env"\` |
|
|
1657
|
+
| \`discovery\` | You found something surprising or important about the codebase | \`neo log discovery "API rate limiter is disabled in dev \u2014 tests hit real endpoints"\` |
|
|
1658
|
+
| \`progress\` | General progress update | \`neo log progress "3/5 endpoints migrated"\` |
|
|
1659
|
+
</log-types>
|
|
1660
|
+
|
|
1661
|
+
<log-rules>
|
|
1662
|
+
- **Chain with commands**: \`pnpm test && neo log milestone "tests passing" || neo log blocker "tests failing"\`
|
|
1663
|
+
- **Log decisions with reasoning**: the "why" is more valuable than the "what"
|
|
1664
|
+
- **Log blockers immediately**: do not continue silently \u2014 surface problems so the supervisor can act
|
|
1665
|
+
- **Log at natural boundaries**: after completing a subtask, before switching context, when hitting an obstacle
|
|
1666
|
+
</log-rules>
|
|
1667
|
+
|
|
1668
|
+
### Memory \u2014 \`neo memory\`
|
|
1669
|
+
|
|
1670
|
+
Memory is persistent knowledge injected into future agent prompts. Write a memory when you learn something that would change HOW the next agent approaches work on this repo.
|
|
1671
|
+
|
|
1672
|
+
<memory-types>
|
|
1673
|
+
| Type | When to write | Example |
|
|
1674
|
+
|------|--------------|---------|
|
|
1675
|
+
| \`fact\` | Stable truth that affects workflow decisions | Build quirks, CI config, auth patterns, deployment constraints |
|
|
1676
|
+
| \`procedure\` | Non-obvious multi-step workflow learned from failure | "Run X before Y otherwise Z breaks" |
|
|
1677
|
+
</memory-types>
|
|
1678
|
+
|
|
1679
|
+
<memory-decision-tree>
|
|
1680
|
+
Before writing a memory, apply this test:
|
|
1681
|
+
1. Can \`cat package.json\`, \`ls\`, or reading the README answer this? \u2192 Do NOT memorize.
|
|
1682
|
+
2. Would knowing this change how you approach the task? \u2192 Write a \`fact\`.
|
|
1683
|
+
3. Did you fail before discovering this workflow? \u2192 Write a \`procedure\`.
|
|
1684
|
+
4. Is this just a detail about what you did (file counts, line numbers, component names)? \u2192 Do NOT memorize.
|
|
1685
|
+
</memory-decision-tree>
|
|
1686
|
+
|
|
1687
|
+
<memory-examples type="good">
|
|
1688
|
+
# Affects workflow \u2014 non-obvious build/CI constraints
|
|
1689
|
+
neo memory write --type fact --scope $NEO_REPOSITORY "CI requires pnpm build before push \u2014 no auto-rebuild in pipeline"
|
|
1690
|
+
neo memory write --type fact --scope $NEO_REPOSITORY "Biome enforces complexity max 20 \u2014 extract helpers for large functions"
|
|
1691
|
+
|
|
1692
|
+
# Learned from failure \u2014 save the next agent from the same mistake
|
|
1693
|
+
neo memory write --type procedure --scope $NEO_REPOSITORY "Run pnpm db:generate after any schema.prisma change \u2014 TypeScript types won't update otherwise"
|
|
1694
|
+
neo memory write --type procedure --scope $NEO_REPOSITORY "E2E tests need STRIPE_TEST_KEY in .env.test \u2014 tests hang silently without it"
|
|
1695
|
+
</memory-examples>
|
|
1696
|
+
|
|
1697
|
+
<memory-examples type="bad">
|
|
1698
|
+
# NEVER write these \u2014 trivial or derivable
|
|
1699
|
+
# "packages/core has 71 files" \u2192 derivable from ls
|
|
1700
|
+
# "Uses React 19" \u2192 visible in package.json
|
|
1701
|
+
# "Main entry is src/index.ts" \u2192 visible in package.json
|
|
1702
|
+
# "Tests use vitest" \u2192 visible in config files
|
|
1703
|
+
</memory-examples>
|
|
1704
|
+
|
|
1705
|
+
<when-to-write>
|
|
1706
|
+
Write memories at these key moments:
|
|
1707
|
+
- **After resolving a non-obvious issue**: the fix revealed a constraint future agents should know
|
|
1708
|
+
- **After discovering a build/CI/deploy quirk**: the next agent will hit the same wall without this
|
|
1709
|
+
- **Before finishing your task**: review what you learned \u2014 anything that would save the next agent 10+ minutes?
|
|
1710
|
+
- **After a failed attempt**: if you tried something that seemed right but failed, document why
|
|
1711
|
+
</when-to-write>`;
|
|
1712
|
+
}
|
|
1713
|
+
function buildFullPrompt(agentPrompt, repoInstructions, gitInstructions, taskPrompt, memoryContext, cwdInstructions, reportingInstructions) {
|
|
1714
|
+
const sections = [];
|
|
1715
|
+
if (agentPrompt) sections.push(agentPrompt);
|
|
1716
|
+
if (cwdInstructions) sections.push(cwdInstructions);
|
|
1717
|
+
if (memoryContext) sections.push(memoryContext);
|
|
1718
|
+
if (repoInstructions) sections.push(`## Repository instructions
|
|
1719
|
+
|
|
1720
|
+
${repoInstructions}`);
|
|
1721
|
+
if (gitInstructions) sections.push(gitInstructions);
|
|
1722
|
+
if (reportingInstructions) sections.push(reportingInstructions);
|
|
1723
|
+
sections.push(`## Task
|
|
1724
|
+
|
|
1725
|
+
${taskPrompt}`);
|
|
1726
|
+
return sections.join("\n\n---\n\n");
|
|
1727
|
+
}
|
|
1302
1728
|
|
|
1303
1729
|
// src/runner/output-parser.ts
|
|
1304
1730
|
function extractJson(raw) {
|
|
@@ -1542,115 +1968,27 @@ async function runWithRecovery(options) {
|
|
|
1542
1968
|
}
|
|
1543
1969
|
|
|
1544
1970
|
// src/runner/session-executor.ts
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1971
|
+
function buildMiddlewareContext(runId, step, agent, repo, getContextValue) {
|
|
1972
|
+
const store = /* @__PURE__ */ new Map();
|
|
1973
|
+
return {
|
|
1974
|
+
runId,
|
|
1975
|
+
step,
|
|
1976
|
+
agent,
|
|
1977
|
+
repo,
|
|
1978
|
+
get: ((key) => {
|
|
1979
|
+
const value = getContextValue(key);
|
|
1980
|
+
if (value !== void 0) return value;
|
|
1981
|
+
return store.get(key);
|
|
1982
|
+
}),
|
|
1983
|
+
set: ((key, value) => {
|
|
1984
|
+
store.set(key, value);
|
|
1985
|
+
})
|
|
1986
|
+
};
|
|
1553
1987
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
return `## Pull Request
|
|
1559
|
-
|
|
1560
|
-
PR #${String(prNumber)} is open for this task. After your review, leave your findings as a comment: \`gh pr comment ${String(prNumber)} --body "..."\`.`;
|
|
1561
|
-
}
|
|
1562
|
-
return null;
|
|
1563
|
-
}
|
|
1564
|
-
if (strategy === "pr") {
|
|
1565
|
-
if (prNumber) {
|
|
1566
|
-
return `## Git workflow
|
|
1567
|
-
|
|
1568
|
-
You are on branch \`${branch}\`.
|
|
1569
|
-
An open PR exists: #${String(prNumber)}.
|
|
1570
|
-
After committing, push your changes to the branch. The PR will be updated automatically.
|
|
1571
|
-
Leave a review comment on the PR summarizing what you did: \`gh pr comment ${String(prNumber)} --body "..."\`.`;
|
|
1572
|
-
}
|
|
1573
|
-
return `## Git workflow
|
|
1574
|
-
|
|
1575
|
-
You are on branch \`${branch}\` (base: \`${baseBranch}\`).
|
|
1576
|
-
After committing:
|
|
1577
|
-
1. Push: \`git push -u ${remote} ${branch}\`
|
|
1578
|
-
2. Create a PR against \`${baseBranch}\` \u2014 choose a title and description that reflect the work you completed. End the PR body with: \`\u{1F916} Generated with [neo](https://neotx.dev)\`
|
|
1579
|
-
3. Output the PR URL on a dedicated line: \`PR_URL: <url>\``;
|
|
1580
|
-
}
|
|
1581
|
-
return `## Git workflow
|
|
1582
|
-
|
|
1583
|
-
You are on branch \`${branch}\` (base: \`${baseBranch}\`).
|
|
1584
|
-
Commit your changes. The branch will be pushed automatically.`;
|
|
1585
|
-
}
|
|
1586
|
-
function buildReportingInstructions(_runId) {
|
|
1587
|
-
return `## Reporting & Memory
|
|
1588
|
-
|
|
1589
|
-
### Progress reporting (real-time, visible in TUI)
|
|
1590
|
-
Chain \`neo log\` with the command that triggered it \u2014 never standalone:
|
|
1591
|
-
\`\`\`bash
|
|
1592
|
-
pnpm test && neo log milestone "all tests passing" || neo log blocker "tests failing"
|
|
1593
|
-
git push origin HEAD && neo log action "pushed to branch"
|
|
1594
|
-
neo log decision "chose JWT over sessions \u2014 simpler for MVP"
|
|
1595
|
-
\`\`\`
|
|
1596
|
-
|
|
1597
|
-
### Memory (persistent, injected into future agent prompts)
|
|
1598
|
-
Write discoveries so the next agent on this repo starts smarter.
|
|
1599
|
-
|
|
1600
|
-
**Be selective** \u2014 only write a memory if it would change HOW you or future agents approach work:
|
|
1601
|
-
\`\`\`bash
|
|
1602
|
-
# GOOD: affects workflow decisions
|
|
1603
|
-
neo memory write --type fact --scope $NEO_REPOSITORY "CI requires pnpm build before push \u2014 no auto-rebuild in pipeline"
|
|
1604
|
-
neo memory write --type fact --scope $NEO_REPOSITORY "Biome enforces complexity max 20 \u2014 extract helpers for large functions"
|
|
1605
|
-
neo memory write --type procedure --scope $NEO_REPOSITORY "Integration tests require DATABASE_URL env var \u2014 set before running"
|
|
1606
|
-
|
|
1607
|
-
# BAD: trivial or derivable \u2014 do NOT write these
|
|
1608
|
-
# "packages/core has 71 files" \u2014 derivable from ls
|
|
1609
|
-
# "Uses React 19" \u2014 visible in package.json
|
|
1610
|
-
# "apps/web has no test framework" \u2014 derivable from ls/cat
|
|
1611
|
-
\`\`\`
|
|
1612
|
-
|
|
1613
|
-
**The test**: if \`cat package.json\`, \`ls\`, or reading the README can answer it, do NOT memorize it. Only memorize truths that affect decisions or non-obvious workflows learned from failure.
|
|
1614
|
-
|
|
1615
|
-
Write at key moments: after resolving a non-obvious issue, after discovering a build/CI quirk, before finishing.`;
|
|
1616
|
-
}
|
|
1617
|
-
function buildFullPrompt(agentPrompt, repoInstructions, gitInstructions, taskPrompt, memoryContext, cwdInstructions, reportingInstructions) {
|
|
1618
|
-
const sections = [];
|
|
1619
|
-
if (agentPrompt) sections.push(agentPrompt);
|
|
1620
|
-
if (cwdInstructions) sections.push(cwdInstructions);
|
|
1621
|
-
if (memoryContext) sections.push(memoryContext);
|
|
1622
|
-
if (repoInstructions) sections.push(`## Repository instructions
|
|
1623
|
-
|
|
1624
|
-
${repoInstructions}`);
|
|
1625
|
-
if (gitInstructions) sections.push(gitInstructions);
|
|
1626
|
-
if (reportingInstructions) sections.push(reportingInstructions);
|
|
1627
|
-
sections.push(`## Task
|
|
1628
|
-
|
|
1629
|
-
${taskPrompt}`);
|
|
1630
|
-
return sections.join("\n\n---\n\n");
|
|
1631
|
-
}
|
|
1632
|
-
function buildMiddlewareContext(runId, workflow, step, agent, repo, getContextValue) {
|
|
1633
|
-
const store = /* @__PURE__ */ new Map();
|
|
1634
|
-
return {
|
|
1635
|
-
runId,
|
|
1636
|
-
workflow,
|
|
1637
|
-
step,
|
|
1638
|
-
agent,
|
|
1639
|
-
repo,
|
|
1640
|
-
get: ((key) => {
|
|
1641
|
-
const value = getContextValue(key);
|
|
1642
|
-
if (value !== void 0) return value;
|
|
1643
|
-
return store.get(key);
|
|
1644
|
-
}),
|
|
1645
|
-
set: ((key, value) => {
|
|
1646
|
-
store.set(key, value);
|
|
1647
|
-
})
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
var SessionExecutor = class {
|
|
1651
|
-
constructor(config, getContextValue) {
|
|
1652
|
-
this.config = config;
|
|
1653
|
-
this.getContextValue = getContextValue;
|
|
1988
|
+
var SessionExecutor = class {
|
|
1989
|
+
constructor(config, getContextValue) {
|
|
1990
|
+
this.config = config;
|
|
1991
|
+
this.getContextValue = getContextValue;
|
|
1654
1992
|
}
|
|
1655
1993
|
/**
|
|
1656
1994
|
* Execute an agent session with the given input and dependencies.
|
|
@@ -1660,7 +1998,6 @@ var SessionExecutor = class {
|
|
|
1660
1998
|
const {
|
|
1661
1999
|
runId,
|
|
1662
2000
|
agent,
|
|
1663
|
-
stepDef,
|
|
1664
2001
|
repoConfig,
|
|
1665
2002
|
repoPath,
|
|
1666
2003
|
prompt: taskPrompt,
|
|
@@ -1681,7 +2018,6 @@ var SessionExecutor = class {
|
|
|
1681
2018
|
const chain = buildMiddlewareChain(middleware);
|
|
1682
2019
|
const middlewareContext = buildMiddlewareContext(
|
|
1683
2020
|
runId,
|
|
1684
|
-
stepDef.prompt ? "workflow" : "direct",
|
|
1685
2021
|
"execute",
|
|
1686
2022
|
agent.name,
|
|
1687
2023
|
repoPath,
|
|
@@ -1706,12 +2042,11 @@ ALWAYS run commands from this directory. NEVER cd to or operate on any other rep
|
|
|
1706
2042
|
agent.definition.prompt,
|
|
1707
2043
|
repoInstructions,
|
|
1708
2044
|
gitInstructions,
|
|
1709
|
-
|
|
2045
|
+
taskPrompt,
|
|
1710
2046
|
memoryContext,
|
|
1711
2047
|
cwdInstructions,
|
|
1712
2048
|
reportingInstructions
|
|
1713
2049
|
);
|
|
1714
|
-
const recoveryOpts = stepDef.recovery;
|
|
1715
2050
|
const agentEnv = {
|
|
1716
2051
|
NEO_RUN_ID: runId,
|
|
1717
2052
|
NEO_AGENT_NAME: agent.name,
|
|
@@ -1726,11 +2061,10 @@ ALWAYS run commands from this directory. NEVER cd to or operate on any other rep
|
|
|
1726
2061
|
env: agentEnv,
|
|
1727
2062
|
initTimeoutMs: this.config.initTimeoutMs,
|
|
1728
2063
|
maxDurationMs: this.config.maxDurationMs,
|
|
1729
|
-
maxRetries:
|
|
2064
|
+
maxRetries: this.config.maxRetries,
|
|
1730
2065
|
backoffBaseMs: this.config.backoffBaseMs,
|
|
1731
2066
|
...sessionPath ? { sessionPath } : {},
|
|
1732
2067
|
...mcpServers ? { mcpServers } : {},
|
|
1733
|
-
...recoveryOpts?.nonRetryable ? { nonRetryable: recoveryOpts.nonRetryable } : {},
|
|
1734
2068
|
...onAttempt ? { onAttempt } : {}
|
|
1735
2069
|
});
|
|
1736
2070
|
const parsed = parseOutput(sessionResult.output);
|
|
@@ -1869,7 +2203,7 @@ ${sections.join("\n\n")}`;
|
|
|
1869
2203
|
|
|
1870
2204
|
// src/supervisor/memory/store.ts
|
|
1871
2205
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1872
|
-
import { existsSync as
|
|
2206
|
+
import { existsSync as existsSync5, mkdirSync } from "fs";
|
|
1873
2207
|
import { createRequire } from "module";
|
|
1874
2208
|
import path9 from "path";
|
|
1875
2209
|
var esmRequire = createRequire(import.meta.url);
|
|
@@ -1879,7 +2213,7 @@ var MemoryStore = class {
|
|
|
1879
2213
|
hasVec;
|
|
1880
2214
|
constructor(dbPath, embedder) {
|
|
1881
2215
|
const dir = path9.dirname(dbPath);
|
|
1882
|
-
if (!
|
|
2216
|
+
if (!existsSync5(dir)) {
|
|
1883
2217
|
mkdirSync(dir, { recursive: true });
|
|
1884
2218
|
}
|
|
1885
2219
|
const Database = esmRequire("better-sqlite3");
|
|
@@ -2082,7 +2416,6 @@ var MemoryStore = class {
|
|
|
2082
2416
|
case "createdAt":
|
|
2083
2417
|
orderBy = "ORDER BY created_at DESC";
|
|
2084
2418
|
break;
|
|
2085
|
-
case "relevance":
|
|
2086
2419
|
default:
|
|
2087
2420
|
orderBy = "ORDER BY (access_count * MAX(0, 1.0 - (julianday('now') - julianday(last_accessed_at)) / 60.0)) DESC";
|
|
2088
2421
|
break;
|
|
@@ -2222,128 +2555,6 @@ function rowToEntry(row) {
|
|
|
2222
2555
|
};
|
|
2223
2556
|
}
|
|
2224
2557
|
|
|
2225
|
-
// src/workflows/registry.ts
|
|
2226
|
-
import { existsSync as existsSync5 } from "fs";
|
|
2227
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
2228
|
-
import path10 from "path";
|
|
2229
|
-
|
|
2230
|
-
// src/workflows/loader.ts
|
|
2231
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
2232
|
-
import { parse } from "yaml";
|
|
2233
|
-
import { z as z4 } from "zod";
|
|
2234
|
-
var workflowStepDefSchema = z4.object({
|
|
2235
|
-
type: z4.literal("step").optional().default("step"),
|
|
2236
|
-
agent: z4.string(),
|
|
2237
|
-
dependsOn: z4.array(z4.string()).optional(),
|
|
2238
|
-
prompt: z4.string().optional(),
|
|
2239
|
-
sandbox: z4.enum(["writable", "readonly"]).optional(),
|
|
2240
|
-
maxTurns: z4.number().int().positive().optional(),
|
|
2241
|
-
mcpServers: z4.array(z4.string()).optional(),
|
|
2242
|
-
recovery: z4.object({
|
|
2243
|
-
maxRetries: z4.number().int().nonnegative().optional(),
|
|
2244
|
-
nonRetryable: z4.array(z4.string()).optional()
|
|
2245
|
-
}).optional(),
|
|
2246
|
-
condition: z4.string().optional()
|
|
2247
|
-
});
|
|
2248
|
-
var workflowGateDefSchema = z4.object({
|
|
2249
|
-
type: z4.literal("gate"),
|
|
2250
|
-
dependsOn: z4.array(z4.string()).optional(),
|
|
2251
|
-
description: z4.string(),
|
|
2252
|
-
timeout: z4.string().optional(),
|
|
2253
|
-
autoApprove: z4.boolean().optional()
|
|
2254
|
-
});
|
|
2255
|
-
var workflowHeaderSchema = z4.object({
|
|
2256
|
-
name: z4.string().min(1),
|
|
2257
|
-
description: z4.string().optional(),
|
|
2258
|
-
steps: z4.record(z4.string(), z4.unknown())
|
|
2259
|
-
});
|
|
2260
|
-
function parseStepEntry(stepName, stepValue) {
|
|
2261
|
-
const obj = stepValue;
|
|
2262
|
-
const schema = obj.type === "gate" ? workflowGateDefSchema : workflowStepDefSchema;
|
|
2263
|
-
const result = schema.safeParse(stepValue);
|
|
2264
|
-
if (result.success) {
|
|
2265
|
-
return { step: result.data, errors: [] };
|
|
2266
|
-
}
|
|
2267
|
-
return {
|
|
2268
|
-
step: stepValue,
|
|
2269
|
-
errors: result.error.issues.map(
|
|
2270
|
-
(i) => ` - steps.${stepName}.${i.path.join(".")}: ${i.message}`
|
|
2271
|
-
)
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
function parseSteps(rawSteps, filePath) {
|
|
2275
|
-
if (Object.keys(rawSteps).length === 0) {
|
|
2276
|
-
throw new Error(
|
|
2277
|
-
`Invalid workflow definition in ${filePath}:
|
|
2278
|
-
- steps: Workflow must have at least one step`
|
|
2279
|
-
);
|
|
2280
|
-
}
|
|
2281
|
-
const steps = {};
|
|
2282
|
-
const errors = [];
|
|
2283
|
-
for (const [name, value] of Object.entries(rawSteps)) {
|
|
2284
|
-
const { step, errors: stepErrors } = parseStepEntry(name, value);
|
|
2285
|
-
if (stepErrors.length > 0) {
|
|
2286
|
-
errors.push(...stepErrors);
|
|
2287
|
-
} else {
|
|
2288
|
-
steps[name] = step;
|
|
2289
|
-
}
|
|
2290
|
-
}
|
|
2291
|
-
if (errors.length > 0) {
|
|
2292
|
-
throw new Error(`Invalid workflow definition in ${filePath}:
|
|
2293
|
-
${errors.join("\n")}`);
|
|
2294
|
-
}
|
|
2295
|
-
return steps;
|
|
2296
|
-
}
|
|
2297
|
-
async function loadWorkflow(filePath) {
|
|
2298
|
-
const content = await readFile6(filePath, "utf-8");
|
|
2299
|
-
const raw = parse(content);
|
|
2300
|
-
const headerResult = workflowHeaderSchema.safeParse(raw);
|
|
2301
|
-
if (!headerResult.success) {
|
|
2302
|
-
const issues = headerResult.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
2303
|
-
throw new Error(`Invalid workflow definition in ${filePath}:
|
|
2304
|
-
${issues}`);
|
|
2305
|
-
}
|
|
2306
|
-
const { name, description, steps: rawSteps } = headerResult.data;
|
|
2307
|
-
const steps = parseSteps(rawSteps, filePath);
|
|
2308
|
-
return { name, description, steps };
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// src/workflows/registry.ts
|
|
2312
|
-
var WorkflowRegistry = class {
|
|
2313
|
-
builtInDir;
|
|
2314
|
-
customDir;
|
|
2315
|
-
workflows = /* @__PURE__ */ new Map();
|
|
2316
|
-
constructor(builtInDir, customDir) {
|
|
2317
|
-
this.builtInDir = builtInDir;
|
|
2318
|
-
this.customDir = customDir;
|
|
2319
|
-
}
|
|
2320
|
-
async load() {
|
|
2321
|
-
await this.loadFromDir(this.builtInDir);
|
|
2322
|
-
if (this.customDir) {
|
|
2323
|
-
await this.loadFromDir(this.customDir);
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
get(name) {
|
|
2327
|
-
return this.workflows.get(name);
|
|
2328
|
-
}
|
|
2329
|
-
list() {
|
|
2330
|
-
return [...this.workflows.values()];
|
|
2331
|
-
}
|
|
2332
|
-
has(name) {
|
|
2333
|
-
return this.workflows.has(name);
|
|
2334
|
-
}
|
|
2335
|
-
async loadFromDir(dir) {
|
|
2336
|
-
if (!existsSync5(dir)) return;
|
|
2337
|
-
const files = await readdir4(dir);
|
|
2338
|
-
for (const file of files) {
|
|
2339
|
-
if (!file.endsWith(".yml") && !file.endsWith(".yaml")) continue;
|
|
2340
|
-
const filePath = path10.join(dir, file);
|
|
2341
|
-
const workflow = await loadWorkflow(filePath);
|
|
2342
|
-
this.workflows.set(workflow.name, workflow);
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
};
|
|
2346
|
-
|
|
2347
2558
|
// src/orchestrator.ts
|
|
2348
2559
|
var MAX_PROMPT_SIZE = 100 * 1024;
|
|
2349
2560
|
var MAX_METADATA_DEPTH = 5;
|
|
@@ -2353,7 +2564,6 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2353
2564
|
config;
|
|
2354
2565
|
semaphore;
|
|
2355
2566
|
userMiddleware;
|
|
2356
|
-
workflows = /* @__PURE__ */ new Map();
|
|
2357
2567
|
registeredAgents = /* @__PURE__ */ new Map();
|
|
2358
2568
|
_activeSessions = /* @__PURE__ */ new Map();
|
|
2359
2569
|
idempotencyCache = /* @__PURE__ */ new Map();
|
|
@@ -2361,8 +2571,6 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2361
2571
|
repoIndex = /* @__PURE__ */ new Map();
|
|
2362
2572
|
runStore = new RunStore();
|
|
2363
2573
|
journalDir;
|
|
2364
|
-
builtInWorkflowDir;
|
|
2365
|
-
customWorkflowDir;
|
|
2366
2574
|
costJournal = null;
|
|
2367
2575
|
eventJournal = null;
|
|
2368
2576
|
webhookDispatcher = null;
|
|
@@ -2377,11 +2585,9 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2377
2585
|
this.config = config;
|
|
2378
2586
|
this.userMiddleware = options.middleware ?? [];
|
|
2379
2587
|
this.journalDir = options.journalDir ?? getJournalsDir();
|
|
2380
|
-
this.builtInWorkflowDir = options.builtInWorkflowDir;
|
|
2381
|
-
this.customWorkflowDir = options.customWorkflowDir;
|
|
2382
2588
|
this.skipOrphanRecovery = options.skipOrphanRecovery ?? false;
|
|
2383
2589
|
for (const repo of config.repos) {
|
|
2384
|
-
const resolvedPath =
|
|
2590
|
+
const resolvedPath = path10.resolve(repo.path);
|
|
2385
2591
|
const normalizedRepo = { ...repo, path: resolvedPath };
|
|
2386
2592
|
this.repoIndex.set(resolvedPath, normalizedRepo);
|
|
2387
2593
|
}
|
|
@@ -2414,9 +2620,6 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2414
2620
|
);
|
|
2415
2621
|
}
|
|
2416
2622
|
// ─── Registration ──────────────────────────────────────
|
|
2417
|
-
registerWorkflow(definition) {
|
|
2418
|
-
this.workflows.set(definition.name, definition);
|
|
2419
|
-
}
|
|
2420
2623
|
registerAgent(agent) {
|
|
2421
2624
|
this.registeredAgents.set(agent.name, agent);
|
|
2422
2625
|
}
|
|
@@ -2490,13 +2693,6 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2490
2693
|
);
|
|
2491
2694
|
}
|
|
2492
2695
|
this._costToday = await this.costJournal.getDayTotal();
|
|
2493
|
-
if (this.builtInWorkflowDir) {
|
|
2494
|
-
const registry = new WorkflowRegistry(this.builtInWorkflowDir, this.customWorkflowDir);
|
|
2495
|
-
await registry.load();
|
|
2496
|
-
for (const workflow of registry.list()) {
|
|
2497
|
-
this.registerWorkflow(workflow);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
2696
|
if (!this.skipOrphanRecovery) {
|
|
2501
2697
|
await this.recoverOrphanedRuns();
|
|
2502
2698
|
}
|
|
@@ -2568,21 +2764,18 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2568
2764
|
buildDispatchContext(input) {
|
|
2569
2765
|
const runId = input.runId ?? randomUUID3();
|
|
2570
2766
|
const sessionId = randomUUID3();
|
|
2571
|
-
const
|
|
2572
|
-
if (!
|
|
2573
|
-
const available = [...this.
|
|
2767
|
+
const agent = this.registeredAgents.get(input.agent);
|
|
2768
|
+
if (!agent) {
|
|
2769
|
+
const available = [...this.registeredAgents.keys()].join(", ") || "none";
|
|
2574
2770
|
throw new Error(
|
|
2575
|
-
`
|
|
2771
|
+
`Agent "${input.agent}" not found. Available agents: ${available}. Register the agent first.`
|
|
2576
2772
|
);
|
|
2577
2773
|
}
|
|
2578
|
-
const [stepName, stepDef] = this.getFirstStep(workflow, input);
|
|
2579
|
-
const agent = this.resolveStepAgent(stepDef, workflow.name);
|
|
2580
2774
|
const repoConfig = this.resolveRepo(input.repo);
|
|
2581
2775
|
const activeSession = {
|
|
2582
2776
|
sessionId,
|
|
2583
2777
|
runId,
|
|
2584
|
-
|
|
2585
|
-
step: stepName,
|
|
2778
|
+
step: "execute",
|
|
2586
2779
|
agent: agent.name,
|
|
2587
2780
|
repo: input.repo,
|
|
2588
2781
|
status: "queued",
|
|
@@ -2594,8 +2787,6 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2594
2787
|
runId,
|
|
2595
2788
|
sessionId,
|
|
2596
2789
|
startedAt: Date.now(),
|
|
2597
|
-
stepName,
|
|
2598
|
-
stepDef,
|
|
2599
2790
|
agent,
|
|
2600
2791
|
repoConfig,
|
|
2601
2792
|
activeSession
|
|
@@ -2607,7 +2798,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2607
2798
|
await this.persistRun({
|
|
2608
2799
|
version: 1,
|
|
2609
2800
|
runId,
|
|
2610
|
-
|
|
2801
|
+
agent: agent.name,
|
|
2611
2802
|
repo: input.repo,
|
|
2612
2803
|
prompt: input.prompt,
|
|
2613
2804
|
pid: process.pid,
|
|
@@ -2619,7 +2810,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2619
2810
|
});
|
|
2620
2811
|
try {
|
|
2621
2812
|
const branchName = input.branch || repoConfig.defaultBranch;
|
|
2622
|
-
const sessionDir =
|
|
2813
|
+
const sessionDir = path10.join(this.config.sessions.dir, runId);
|
|
2623
2814
|
const info = await createSessionClone({
|
|
2624
2815
|
repoPath: input.repo,
|
|
2625
2816
|
branch: branchName,
|
|
@@ -2692,13 +2883,12 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2692
2883
|
}
|
|
2693
2884
|
}
|
|
2694
2885
|
async runAgentSession(ctx, sessionPath) {
|
|
2695
|
-
const { input, runId, sessionId,
|
|
2886
|
+
const { input, runId, sessionId, agent, repoConfig, activeSession } = ctx;
|
|
2696
2887
|
this.emit({
|
|
2697
2888
|
type: "session:start",
|
|
2698
2889
|
sessionId,
|
|
2699
2890
|
runId,
|
|
2700
|
-
|
|
2701
|
-
step: stepName,
|
|
2891
|
+
step: "execute",
|
|
2702
2892
|
agent: agent.name,
|
|
2703
2893
|
repo: input.repo,
|
|
2704
2894
|
metadata: input.metadata,
|
|
@@ -2718,15 +2908,13 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2718
2908
|
}
|
|
2719
2909
|
);
|
|
2720
2910
|
const strategy = input.gitStrategy ?? repoConfig.gitStrategy ?? "branch";
|
|
2721
|
-
const mcpServers = this.resolveMcpServers(
|
|
2911
|
+
const mcpServers = this.resolveMcpServers(agent);
|
|
2722
2912
|
const memoryContext = this.loadMemoryContext(input.repo);
|
|
2723
|
-
const recoveryOpts = stepDef.recovery;
|
|
2724
2913
|
const result = await executor.execute(
|
|
2725
2914
|
{
|
|
2726
2915
|
runId,
|
|
2727
2916
|
sessionId,
|
|
2728
2917
|
agent,
|
|
2729
|
-
stepDef,
|
|
2730
2918
|
repoConfig,
|
|
2731
2919
|
repoPath: input.repo,
|
|
2732
2920
|
prompt: input.prompt,
|
|
@@ -2748,7 +2936,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2748
2936
|
runId,
|
|
2749
2937
|
error: `Retrying with strategy: ${strategy2}`,
|
|
2750
2938
|
attempt: attempt - 1,
|
|
2751
|
-
maxRetries:
|
|
2939
|
+
maxRetries: this.config.recovery.maxRetries,
|
|
2752
2940
|
willRetry: true,
|
|
2753
2941
|
metadata: input.metadata,
|
|
2754
2942
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -2773,13 +2961,13 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2773
2961
|
return result;
|
|
2774
2962
|
}
|
|
2775
2963
|
async finalizeDispatch(ctx, stepResult, idempotencyKey) {
|
|
2776
|
-
const { input, runId,
|
|
2964
|
+
const { input, runId, agent, activeSession } = ctx;
|
|
2777
2965
|
const taskResult = {
|
|
2778
2966
|
runId,
|
|
2779
|
-
|
|
2967
|
+
agent: agent.name,
|
|
2780
2968
|
repo: input.repo,
|
|
2781
2969
|
status: stepResult.status === "success" ? "success" : "failure",
|
|
2782
|
-
steps: {
|
|
2970
|
+
steps: { execute: stepResult },
|
|
2783
2971
|
branch: stepResult.status === "success" && activeSession.sessionPath ? input.branch : void 0,
|
|
2784
2972
|
costUsd: stepResult.costUsd,
|
|
2785
2973
|
durationMs: Date.now() - ctx.startedAt,
|
|
@@ -2795,7 +2983,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2795
2983
|
await this.persistRun({
|
|
2796
2984
|
version: 1,
|
|
2797
2985
|
runId,
|
|
2798
|
-
|
|
2986
|
+
agent: agent.name,
|
|
2799
2987
|
repo: input.repo,
|
|
2800
2988
|
prompt: input.prompt,
|
|
2801
2989
|
pid: process.pid,
|
|
@@ -2818,8 +3006,8 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2818
3006
|
// ─── Private: Memory injection ──────────────────────────
|
|
2819
3007
|
getMemoryStore() {
|
|
2820
3008
|
if (!this.memoryStore) {
|
|
2821
|
-
const supervisorDir =
|
|
2822
|
-
this.memoryStore = new MemoryStore(
|
|
3009
|
+
const supervisorDir = path10.join(getSupervisorsDir(), "supervisor");
|
|
3010
|
+
this.memoryStore = new MemoryStore(path10.join(supervisorDir, "memory.sqlite"));
|
|
2823
3011
|
}
|
|
2824
3012
|
return this.memoryStore;
|
|
2825
3013
|
}
|
|
@@ -2846,8 +3034,7 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2846
3034
|
const costEntry = {
|
|
2847
3035
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2848
3036
|
runId: ctx.runId,
|
|
2849
|
-
|
|
2850
|
-
step: ctx.stepName,
|
|
3037
|
+
step: "execute",
|
|
2851
3038
|
sessionId,
|
|
2852
3039
|
agent: ctx.agent.name,
|
|
2853
3040
|
costUsd: sessionCost,
|
|
@@ -2916,8 +3103,8 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2916
3103
|
if (!existsSync6(input.repo)) {
|
|
2917
3104
|
throw new Error(`Validation error: repo path does not exist: ${input.repo}`);
|
|
2918
3105
|
}
|
|
2919
|
-
if (!this.
|
|
2920
|
-
throw new Error(`Validation error:
|
|
3106
|
+
if (!this.registeredAgents.has(input.agent)) {
|
|
3107
|
+
throw new Error(`Validation error: agent "${input.agent}" not found in registry`);
|
|
2921
3108
|
}
|
|
2922
3109
|
if (input.metadata !== void 0) {
|
|
2923
3110
|
if (!isPlainObject(input.metadata)) {
|
|
@@ -2948,45 +3135,12 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
2948
3135
|
if (!idempotency?.enabled) return null;
|
|
2949
3136
|
const key = idempotency.key ?? "metadata";
|
|
2950
3137
|
if (key === "prompt") {
|
|
2951
|
-
return `${input.
|
|
2952
|
-
}
|
|
2953
|
-
return `${input.workflow}:${input.repo}:${JSON.stringify(input.metadata ?? {})}`;
|
|
2954
|
-
}
|
|
2955
|
-
getFirstStep(workflow, input) {
|
|
2956
|
-
if (input.step) {
|
|
2957
|
-
const step = workflow.steps[input.step];
|
|
2958
|
-
if (!step || step.type === "gate") {
|
|
2959
|
-
throw new Error(
|
|
2960
|
-
`Step "${input.step}" not found in workflow "${workflow.name}" or is a gate step. Check the step name in the workflow definition.`
|
|
2961
|
-
);
|
|
2962
|
-
}
|
|
2963
|
-
return [input.step, step];
|
|
2964
|
-
}
|
|
2965
|
-
for (const [name, step] of Object.entries(workflow.steps)) {
|
|
2966
|
-
if (step.type === "gate") continue;
|
|
2967
|
-
const stepDef = step;
|
|
2968
|
-
if (!stepDef.dependsOn || stepDef.dependsOn.length === 0) {
|
|
2969
|
-
return [name, stepDef];
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
const entries = Object.entries(workflow.steps);
|
|
2973
|
-
const first = entries[0];
|
|
2974
|
-
if (!first) {
|
|
2975
|
-
throw new Error(`Workflow "${workflow.name}" has no steps`);
|
|
2976
|
-
}
|
|
2977
|
-
return [first[0], first[1]];
|
|
2978
|
-
}
|
|
2979
|
-
resolveStepAgent(step, workflowName) {
|
|
2980
|
-
const agent = this.registeredAgents.get(step.agent);
|
|
2981
|
-
if (!agent) {
|
|
2982
|
-
throw new Error(
|
|
2983
|
-
`Agent "${step.agent}" required by workflow "${workflowName}" not found in registry. Register the agent or check the workflow definition.`
|
|
2984
|
-
);
|
|
3138
|
+
return `${input.agent}:${input.repo}:${input.prompt}`;
|
|
2985
3139
|
}
|
|
2986
|
-
return agent
|
|
3140
|
+
return `${input.agent}:${input.repo}:${JSON.stringify(input.metadata ?? {})}`;
|
|
2987
3141
|
}
|
|
2988
3142
|
resolveRepo(repoPath) {
|
|
2989
|
-
const repo = this.repoIndex.get(
|
|
3143
|
+
const repo = this.repoIndex.get(path10.resolve(repoPath));
|
|
2990
3144
|
if (repo) return repo;
|
|
2991
3145
|
return {
|
|
2992
3146
|
path: repoPath,
|
|
@@ -3002,17 +3156,11 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
3002
3156
|
return Math.max(0, (cap - this._costToday) / cap * 100);
|
|
3003
3157
|
}
|
|
3004
3158
|
// ─── Private: MCP server resolution ────────────────────
|
|
3005
|
-
resolveMcpServers(
|
|
3159
|
+
resolveMcpServers(agent) {
|
|
3006
3160
|
const configServers = this.config.mcpServers;
|
|
3007
3161
|
if (!configServers) return void 0;
|
|
3008
|
-
const names =
|
|
3009
|
-
if (
|
|
3010
|
-
for (const name of stepDef.mcpServers) names.add(name);
|
|
3011
|
-
}
|
|
3012
|
-
if (agent.definition.mcpServers) {
|
|
3013
|
-
for (const name of agent.definition.mcpServers) names.add(name);
|
|
3014
|
-
}
|
|
3015
|
-
if (names.size === 0) return void 0;
|
|
3162
|
+
const names = agent.definition.mcpServers;
|
|
3163
|
+
if (!names || names.length === 0) return void 0;
|
|
3016
3164
|
const resolved = {};
|
|
3017
3165
|
for (const name of names) {
|
|
3018
3166
|
const serverConfig = configServers[name];
|
|
@@ -3034,8 +3182,8 @@ var Orchestrator = class extends NeoEventEmitter {
|
|
|
3034
3182
|
for (const entry of entries) {
|
|
3035
3183
|
if (!entry.isDirectory()) continue;
|
|
3036
3184
|
try {
|
|
3037
|
-
const statePath =
|
|
3038
|
-
const raw = await
|
|
3185
|
+
const statePath = path10.join(supervisorsDir, entry.name, "state.json");
|
|
3186
|
+
const raw = await readFile6(statePath, "utf-8");
|
|
3039
3187
|
const state = JSON.parse(raw);
|
|
3040
3188
|
if (state.status !== "running" || !state.port) continue;
|
|
3041
3189
|
if (state.pid && !isProcessAlive(state.pid)) continue;
|
|
@@ -3088,13 +3236,176 @@ function objectDepth(obj, current = 0) {
|
|
|
3088
3236
|
|
|
3089
3237
|
// src/supervisor/schemas.ts
|
|
3090
3238
|
import { z as z5 } from "zod";
|
|
3239
|
+
|
|
3240
|
+
// src/supervisor/decisions.ts
|
|
3241
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
3242
|
+
import { appendFile as appendFile4, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
3243
|
+
import path11 from "path";
|
|
3244
|
+
import { z as z4 } from "zod";
|
|
3245
|
+
var decisionOptionSchema = z4.object({
|
|
3246
|
+
key: z4.string(),
|
|
3247
|
+
label: z4.string(),
|
|
3248
|
+
description: z4.string().optional()
|
|
3249
|
+
});
|
|
3250
|
+
var decisionSchema = z4.object({
|
|
3251
|
+
id: z4.string(),
|
|
3252
|
+
question: z4.string(),
|
|
3253
|
+
context: z4.string().optional(),
|
|
3254
|
+
options: z4.array(decisionOptionSchema).optional(),
|
|
3255
|
+
type: z4.string().default("generic"),
|
|
3256
|
+
source: z4.string(),
|
|
3257
|
+
metadata: z4.record(z4.string(), z4.unknown()).optional(),
|
|
3258
|
+
createdAt: z4.string(),
|
|
3259
|
+
expiresAt: z4.string().optional(),
|
|
3260
|
+
defaultAnswer: z4.string().optional(),
|
|
3261
|
+
answeredAt: z4.string().optional(),
|
|
3262
|
+
answer: z4.string().optional(),
|
|
3263
|
+
expiredAt: z4.string().optional()
|
|
3264
|
+
});
|
|
3265
|
+
var DecisionStore = class {
|
|
3266
|
+
filePath;
|
|
3267
|
+
dir;
|
|
3268
|
+
dirCache = /* @__PURE__ */ new Set();
|
|
3269
|
+
constructor(filePath) {
|
|
3270
|
+
this.filePath = filePath;
|
|
3271
|
+
this.dir = path11.dirname(filePath);
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Create a new decision and persist it.
|
|
3275
|
+
* @returns The generated decision ID
|
|
3276
|
+
*/
|
|
3277
|
+
async create(input) {
|
|
3278
|
+
await ensureDir(this.dir, this.dirCache);
|
|
3279
|
+
const id = `dec_${randomUUID4().replace(/-/g, "").slice(0, 20)}`;
|
|
3280
|
+
const decision = {
|
|
3281
|
+
...input,
|
|
3282
|
+
id,
|
|
3283
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3284
|
+
};
|
|
3285
|
+
await appendFile4(this.filePath, `${JSON.stringify(decision)}
|
|
3286
|
+
`, "utf-8");
|
|
3287
|
+
return id;
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Answer a decision by ID.
|
|
3291
|
+
* Reads all entries, updates the matching one, and rewrites the file.
|
|
3292
|
+
*/
|
|
3293
|
+
async answer(id, answer) {
|
|
3294
|
+
const decisions = await this.readAll();
|
|
3295
|
+
const decision = decisions.find((d) => d.id === id);
|
|
3296
|
+
if (!decision) {
|
|
3297
|
+
throw new Error(`Decision not found: ${id}`);
|
|
3298
|
+
}
|
|
3299
|
+
if (decision.answer !== void 0) {
|
|
3300
|
+
throw new Error(`Decision already answered: ${id}`);
|
|
3301
|
+
}
|
|
3302
|
+
decision.answer = answer;
|
|
3303
|
+
decision.answeredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3304
|
+
await this.writeAll(decisions);
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Get all pending decisions (unanswered, not expired, not timed out).
|
|
3308
|
+
*/
|
|
3309
|
+
async pending() {
|
|
3310
|
+
const decisions = await this.readAll();
|
|
3311
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3312
|
+
return decisions.filter((d) => {
|
|
3313
|
+
if (d.answer !== void 0) return false;
|
|
3314
|
+
if (d.expiredAt !== void 0) return false;
|
|
3315
|
+
if (d.expiresAt && d.expiresAt < now) return false;
|
|
3316
|
+
return true;
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Get answered decisions, optionally filtered by timestamp.
|
|
3321
|
+
* @param since - ISO timestamp to filter decisions answered after this time
|
|
3322
|
+
*/
|
|
3323
|
+
async answered(since) {
|
|
3324
|
+
const decisions = await this.readAll();
|
|
3325
|
+
return decisions.filter((d) => {
|
|
3326
|
+
if (d.answer === void 0) return false;
|
|
3327
|
+
if (since && d.answeredAt && d.answeredAt < since) return false;
|
|
3328
|
+
return true;
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* Get a specific decision by ID.
|
|
3333
|
+
*/
|
|
3334
|
+
async get(id) {
|
|
3335
|
+
const decisions = await this.readAll();
|
|
3336
|
+
return decisions.find((d) => d.id === id) ?? null;
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Auto-answer expired decisions with their defaultAnswer.
|
|
3340
|
+
* Decisions without defaultAnswer are marked as expired (expiredAt).
|
|
3341
|
+
* @returns The decisions that were auto-answered or marked expired
|
|
3342
|
+
*/
|
|
3343
|
+
async expire() {
|
|
3344
|
+
const decisions = await this.readAll();
|
|
3345
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3346
|
+
const expired = [];
|
|
3347
|
+
for (const decision of decisions) {
|
|
3348
|
+
if (decision.answer === void 0 && decision.expiredAt === void 0 && decision.expiresAt && decision.expiresAt < now) {
|
|
3349
|
+
if (decision.defaultAnswer !== void 0) {
|
|
3350
|
+
decision.answer = decision.defaultAnswer;
|
|
3351
|
+
decision.answeredAt = now;
|
|
3352
|
+
} else {
|
|
3353
|
+
decision.expiredAt = now;
|
|
3354
|
+
}
|
|
3355
|
+
expired.push(decision);
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
if (expired.length > 0) {
|
|
3359
|
+
await this.writeAll(decisions);
|
|
3360
|
+
}
|
|
3361
|
+
return expired;
|
|
3362
|
+
}
|
|
3363
|
+
// ─── Private helpers ─────────────────────────────────────
|
|
3364
|
+
async readAll() {
|
|
3365
|
+
let content;
|
|
3366
|
+
try {
|
|
3367
|
+
content = await readFile7(this.filePath, "utf-8");
|
|
3368
|
+
} catch (error) {
|
|
3369
|
+
if (error.code === "ENOENT") {
|
|
3370
|
+
return [];
|
|
3371
|
+
}
|
|
3372
|
+
throw error;
|
|
3373
|
+
}
|
|
3374
|
+
const decisions = [];
|
|
3375
|
+
for (const line of content.split("\n")) {
|
|
3376
|
+
if (!line.trim()) continue;
|
|
3377
|
+
try {
|
|
3378
|
+
const parsed = decisionSchema.parse(JSON.parse(line));
|
|
3379
|
+
decisions.push(parsed);
|
|
3380
|
+
} catch (error) {
|
|
3381
|
+
console.warn(
|
|
3382
|
+
`[DecisionStore] Skipping malformed JSONL line: ${error instanceof Error ? error.message : "unknown error"}`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return decisions;
|
|
3387
|
+
}
|
|
3388
|
+
async writeAll(decisions) {
|
|
3389
|
+
await ensureDir(this.dir, this.dirCache);
|
|
3390
|
+
const content = `${decisions.map((d) => JSON.stringify(d)).join("\n")}
|
|
3391
|
+
`;
|
|
3392
|
+
await writeFile3(this.filePath, content, "utf-8");
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
|
|
3396
|
+
// src/supervisor/schemas.ts
|
|
3091
3397
|
var wakeReasonSchema = z5.enum(["events", "timer", "active_runs", "forced"]);
|
|
3092
|
-
var
|
|
3398
|
+
var supervisorBaseFieldsSchema = z5.object({
|
|
3093
3399
|
pid: z5.number(),
|
|
3094
3400
|
sessionId: z5.string(),
|
|
3401
|
+
startedAt: z5.string(),
|
|
3402
|
+
heartbeatCount: z5.number(),
|
|
3403
|
+
totalCostUsd: z5.number(),
|
|
3404
|
+
todayCostUsd: z5.number()
|
|
3405
|
+
});
|
|
3406
|
+
var supervisorDaemonStateSchema = supervisorBaseFieldsSchema.extend({
|
|
3095
3407
|
port: z5.number(),
|
|
3096
3408
|
cwd: z5.string(),
|
|
3097
|
-
startedAt: z5.string(),
|
|
3098
3409
|
lastHeartbeat: z5.string().optional(),
|
|
3099
3410
|
heartbeatCount: z5.number().default(0),
|
|
3100
3411
|
totalCostUsd: z5.number().default(0),
|
|
@@ -3130,6 +3441,7 @@ var activityEntrySchema = z5.object({
|
|
|
3130
3441
|
"decision",
|
|
3131
3442
|
"action",
|
|
3132
3443
|
"error",
|
|
3444
|
+
"warning",
|
|
3133
3445
|
"event",
|
|
3134
3446
|
"message",
|
|
3135
3447
|
"thinking",
|
|
@@ -3153,10 +3465,32 @@ var logBufferEntrySchema = z5.object({
|
|
|
3153
3465
|
consolidatedAt: z5.string().optional()
|
|
3154
3466
|
});
|
|
3155
3467
|
var internalEventKindSchema = z5.enum(["consolidation_timer", "active_run_check"]);
|
|
3468
|
+
var supervisorStatusSchema = supervisorBaseFieldsSchema.extend({
|
|
3469
|
+
status: z5.enum(["running", "idle", "stopping"]),
|
|
3470
|
+
lastHeartbeat: z5.string(),
|
|
3471
|
+
activeRunCount: z5.number(),
|
|
3472
|
+
recentActivitySummary: z5.array(z5.string())
|
|
3473
|
+
});
|
|
3474
|
+
var activityTypeFilterSchema = z5.enum([
|
|
3475
|
+
"decision",
|
|
3476
|
+
"action",
|
|
3477
|
+
"error",
|
|
3478
|
+
"event",
|
|
3479
|
+
"message",
|
|
3480
|
+
"plan",
|
|
3481
|
+
"dispatch"
|
|
3482
|
+
]);
|
|
3483
|
+
var activityQueryOptionsSchema = z5.object({
|
|
3484
|
+
limit: z5.number().int().min(1).max(500).default(50).optional(),
|
|
3485
|
+
offset: z5.number().int().min(0).default(0).optional(),
|
|
3486
|
+
type: activityTypeFilterSchema.optional(),
|
|
3487
|
+
since: z5.string().datetime().optional(),
|
|
3488
|
+
until: z5.string().datetime().optional()
|
|
3489
|
+
});
|
|
3156
3490
|
|
|
3157
3491
|
// src/supervisor/activity-log.ts
|
|
3158
|
-
import { randomUUID as
|
|
3159
|
-
import { appendFile as
|
|
3492
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3493
|
+
import { appendFile as appendFile5, readFile as readFile8, rename, stat } from "fs/promises";
|
|
3160
3494
|
import path12 from "path";
|
|
3161
3495
|
var ACTIVITY_FILE = "activity.jsonl";
|
|
3162
3496
|
var MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
@@ -3175,14 +3509,14 @@ var ActivityLog = class {
|
|
|
3175
3509
|
await this.checkRotation();
|
|
3176
3510
|
const line = `${JSON.stringify(entry)}
|
|
3177
3511
|
`;
|
|
3178
|
-
await
|
|
3512
|
+
await appendFile5(this.filePath, line, "utf-8");
|
|
3179
3513
|
}
|
|
3180
3514
|
/**
|
|
3181
3515
|
* Create and append a new entry with auto-generated id and timestamp.
|
|
3182
3516
|
*/
|
|
3183
3517
|
async log(type, summary, detail) {
|
|
3184
3518
|
await this.append({
|
|
3185
|
-
id:
|
|
3519
|
+
id: randomUUID5(),
|
|
3186
3520
|
type,
|
|
3187
3521
|
summary,
|
|
3188
3522
|
detail,
|
|
@@ -3224,15 +3558,15 @@ var ActivityLog = class {
|
|
|
3224
3558
|
};
|
|
3225
3559
|
|
|
3226
3560
|
// src/supervisor/daemon.ts
|
|
3227
|
-
import { randomUUID as
|
|
3561
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3228
3562
|
import { existsSync as existsSync8 } from "fs";
|
|
3229
|
-
import { mkdir as mkdir7, readFile as readFile12, rm as rm2, writeFile as
|
|
3230
|
-
import { homedir as
|
|
3563
|
+
import { mkdir as mkdir7, readFile as readFile12, rm as rm2, writeFile as writeFile7 } from "fs/promises";
|
|
3564
|
+
import { homedir as homedir5 } from "os";
|
|
3231
3565
|
import path15 from "path";
|
|
3232
3566
|
|
|
3233
3567
|
// src/supervisor/event-queue.ts
|
|
3234
|
-
import { watch } from "fs";
|
|
3235
|
-
import { readFile as readFile9, writeFile as
|
|
3568
|
+
import { watch as watch2 } from "fs";
|
|
3569
|
+
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3236
3570
|
var EventQueue = class {
|
|
3237
3571
|
queue = [];
|
|
3238
3572
|
seenIds = /* @__PURE__ */ new Set();
|
|
@@ -3282,13 +3616,14 @@ var EventQueue = class {
|
|
|
3282
3616
|
/**
|
|
3283
3617
|
* Drain and group events: deduplicates messages by content,
|
|
3284
3618
|
* keeps webhooks and run completions separate.
|
|
3619
|
+
* Returns both grouped events AND original raw events for later marking as processed.
|
|
3285
3620
|
*/
|
|
3286
3621
|
drainAndGroup() {
|
|
3287
|
-
const
|
|
3622
|
+
const rawEvents = this.drain();
|
|
3288
3623
|
const messageMap = /* @__PURE__ */ new Map();
|
|
3289
3624
|
const webhooks = [];
|
|
3290
3625
|
const runCompletions = [];
|
|
3291
|
-
for (const event of
|
|
3626
|
+
for (const event of rawEvents) {
|
|
3292
3627
|
if (event.kind === "message") {
|
|
3293
3628
|
const key = event.data.text.trim().toLowerCase();
|
|
3294
3629
|
const existing = messageMap.get(key);
|
|
@@ -3304,9 +3639,12 @@ var EventQueue = class {
|
|
|
3304
3639
|
}
|
|
3305
3640
|
}
|
|
3306
3641
|
return {
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3642
|
+
grouped: {
|
|
3643
|
+
messages: [...messageMap.values()],
|
|
3644
|
+
webhooks,
|
|
3645
|
+
runCompletions
|
|
3646
|
+
},
|
|
3647
|
+
rawEvents
|
|
3310
3648
|
};
|
|
3311
3649
|
}
|
|
3312
3650
|
size() {
|
|
@@ -3319,9 +3657,8 @@ var EventQueue = class {
|
|
|
3319
3657
|
async startWatching(inboxPath, eventsPath) {
|
|
3320
3658
|
for (const p of [inboxPath, eventsPath]) {
|
|
3321
3659
|
try {
|
|
3322
|
-
await
|
|
3323
|
-
} catch
|
|
3324
|
-
console.error(`[EventQueue] Failed to ensure file exists: ${p}`, err);
|
|
3660
|
+
await writeFile4(p, "", { flag: "a" });
|
|
3661
|
+
} catch {
|
|
3325
3662
|
}
|
|
3326
3663
|
}
|
|
3327
3664
|
this.watchJsonlFile(inboxPath, "message");
|
|
@@ -3370,14 +3707,12 @@ var EventQueue = class {
|
|
|
3370
3707
|
}
|
|
3371
3708
|
watchJsonlFile(filePath, kind) {
|
|
3372
3709
|
try {
|
|
3373
|
-
const watcher =
|
|
3374
|
-
this.readNewLines(filePath, kind).catch((
|
|
3375
|
-
console.error(`[EventQueue] Failed to read new lines from ${filePath}:`, err);
|
|
3710
|
+
const watcher = watch2(filePath, () => {
|
|
3711
|
+
this.readNewLines(filePath, kind).catch(() => {
|
|
3376
3712
|
});
|
|
3377
3713
|
});
|
|
3378
3714
|
this.watchers.push(watcher);
|
|
3379
|
-
} catch
|
|
3380
|
-
console.error(`[EventQueue] Cannot watch file (may not exist yet): ${filePath}`, err);
|
|
3715
|
+
} catch {
|
|
3381
3716
|
}
|
|
3382
3717
|
}
|
|
3383
3718
|
async readNewLines(filePath, kind) {
|
|
@@ -3461,24 +3796,23 @@ var EventQueue = class {
|
|
|
3461
3796
|
return line;
|
|
3462
3797
|
});
|
|
3463
3798
|
if (changed) {
|
|
3464
|
-
await
|
|
3799
|
+
await writeFile4(filePath, updated.join("\n"), "utf-8");
|
|
3465
3800
|
this.fileOffsets.set(filePath, updated.join("\n").length);
|
|
3466
3801
|
}
|
|
3467
|
-
} catch
|
|
3468
|
-
console.error(`[EventQueue] Failed to mark events as processed in ${filePath}:`, err);
|
|
3802
|
+
} catch {
|
|
3469
3803
|
}
|
|
3470
3804
|
}
|
|
3471
3805
|
};
|
|
3472
3806
|
|
|
3473
3807
|
// src/supervisor/heartbeat.ts
|
|
3474
|
-
import { randomUUID as
|
|
3808
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3475
3809
|
import { existsSync as existsSync7 } from "fs";
|
|
3476
|
-
import { readdir as
|
|
3477
|
-
import { homedir as
|
|
3810
|
+
import { readdir as readdir4, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3811
|
+
import { homedir as homedir4 } from "os";
|
|
3478
3812
|
import path14 from "path";
|
|
3479
3813
|
|
|
3480
3814
|
// src/supervisor/log-buffer.ts
|
|
3481
|
-
import { appendFile as
|
|
3815
|
+
import { appendFile as appendFile6, readFile as readFile10, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
3482
3816
|
import path13 from "path";
|
|
3483
3817
|
var LOG_BUFFER_FILE = "log-buffer.jsonl";
|
|
3484
3818
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
@@ -3532,7 +3866,7 @@ async function markConsolidated(dir, ids) {
|
|
|
3532
3866
|
updated.push(line);
|
|
3533
3867
|
}
|
|
3534
3868
|
}
|
|
3535
|
-
await
|
|
3869
|
+
await writeFile5(filePath, `${updated.join("\n")}
|
|
3536
3870
|
`, "utf-8");
|
|
3537
3871
|
}
|
|
3538
3872
|
async function compactLogBuffer(dir) {
|
|
@@ -3566,37 +3900,31 @@ async function compactLogBuffer(dir) {
|
|
|
3566
3900
|
result = `${kept.join("\n")}
|
|
3567
3901
|
`;
|
|
3568
3902
|
}
|
|
3569
|
-
await
|
|
3903
|
+
await writeFile5(filePath, result, "utf-8");
|
|
3570
3904
|
}
|
|
3571
3905
|
async function appendLogBuffer(dir, entry) {
|
|
3572
3906
|
try {
|
|
3573
|
-
await
|
|
3907
|
+
await appendFile6(bufferPath(dir), `${JSON.stringify(entry)}
|
|
3574
3908
|
`, "utf-8");
|
|
3575
3909
|
} catch {
|
|
3576
3910
|
}
|
|
3577
3911
|
}
|
|
3578
3912
|
|
|
3579
3913
|
// src/supervisor/prompt-builder.ts
|
|
3580
|
-
var ROLE = `You are the neo autonomous supervisor \u2014
|
|
3581
|
-
|
|
3582
|
-
You
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
-
|
|
3586
|
-
-
|
|
3587
|
-
-
|
|
3588
|
-
-
|
|
3589
|
-
-
|
|
3590
|
-
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
- Your ONLY visible output is \`neo log\` commands. The TUI shows these and nothing else.
|
|
3595
|
-
- Your text output is NEVER shown to anyone \u2014 every token of text is wasted cost.
|
|
3596
|
-
- Produce tool calls, not explanations. Do not narrate your reasoning.
|
|
3597
|
-
- You NEVER modify code \u2014 that is the agents' job.
|
|
3598
|
-
- You can read code in the available repos (path in \`neo repos\` command)
|
|
3599
|
-
</behavioral-contract>`;
|
|
3914
|
+
var ROLE = `You are the neo autonomous supervisor \u2014 accountable for delivery across parallel initiatives.
|
|
3915
|
+
|
|
3916
|
+
You do not write code directly; you ensure the right work is assigned, executed, reviewed, and completed by the right agent.`;
|
|
3917
|
+
var OPERATING_PRINCIPLES = `### Operating principles
|
|
3918
|
+
|
|
3919
|
+
- Own delivery end-to-end: any queued task without an active owner is your responsibility.
|
|
3920
|
+
- Operate like a strong engineering lead: provide clear context, dispatch deliberately, validate outcomes, and remove blockers quickly.
|
|
3921
|
+
- On run completion: read \`neo runs <runId>\`, verify acceptance criteria, then decide next action (done, follow-up, redispatch, escalate).
|
|
3922
|
+
- On run failure: diagnose root cause before retrying (prompt quality, branch conflict, known issue, environment/tooling), then fix the cause.
|
|
3923
|
+
- Prevent silent stalls: monitor long-running jobs, detect blocked work early, and actively unblock.
|
|
3924
|
+
- Keep initiative boundaries strict: decisions for initiative A must not be influenced by unrelated state from B.
|
|
3925
|
+
- Your user-visible channel is \`neo log\` only; produce concise tool calls (not reasoning/explanations) and avoid wasted tokens.
|
|
3926
|
+
- You may inspect repositories available via \`neo repos\`, read-only to launch agents.
|
|
3927
|
+
- Task hygiene is non-negotiable: update task outcomes EVERY heartbeat. A task without a current outcome is a blind spot.`;
|
|
3600
3928
|
var COMMANDS = `### Dispatching agents
|
|
3601
3929
|
\`\`\`bash
|
|
3602
3930
|
neo run <agent> --prompt "..." --repo <path> --branch <name> [--priority critical|high|medium|low] [--meta '<json>']
|
|
@@ -3607,7 +3935,7 @@ neo run <agent> --prompt "..." --repo <path> --branch <name> [--priority critica
|
|
|
3607
3935
|
| \`--prompt\` | always | Task description for the agent |
|
|
3608
3936
|
| \`--repo\` | always | Target repository path |
|
|
3609
3937
|
| \`--branch\` | always | Branch name for the isolated clone |
|
|
3610
|
-
| \`--priority\` |
|
|
3938
|
+
| \`--priority\` | optional | \`critical\`, \`high\`, \`medium\`, \`low\` |
|
|
3611
3939
|
| \`--meta\` | **always** | JSON with \`"label"\` for identification + \`"ticketId"\`, \`"stage"\`, etc. |
|
|
3612
3940
|
|
|
3613
3941
|
All agents require \`--branch\`. Each agent session runs in an isolated clone on that branch.
|
|
@@ -3635,6 +3963,27 @@ neo memory search "keyword"
|
|
|
3635
3963
|
neo memory list --type fact
|
|
3636
3964
|
\`\`\`
|
|
3637
3965
|
|
|
3966
|
+
### Configuration
|
|
3967
|
+
\`\`\`bash
|
|
3968
|
+
neo config get <key> # read a value (dot notation)
|
|
3969
|
+
neo config set <key> <value> --global # update global config (~/.neo/config.yml)
|
|
3970
|
+
neo config list # show full merged config
|
|
3971
|
+
\`\`\`
|
|
3972
|
+
|
|
3973
|
+
Keys use dot notation (e.g., \`budget.dailyCapUsd\`, \`supervisor.dailyCapUsd\`, \`concurrency.maxSessions\`).
|
|
3974
|
+
Changes are hot-reloaded \u2014 the new values take effect at the next heartbeat.
|
|
3975
|
+
|
|
3976
|
+
Use cases: raise budget cap mid-run, adjust concurrency, change heartbeat timeout.
|
|
3977
|
+
|
|
3978
|
+
### Decisions
|
|
3979
|
+
When you need human input on something that cannot be decided autonomously:
|
|
3980
|
+
\`\`\`bash
|
|
3981
|
+
neo decision create "<question>" --options "key1:label1,key2:label2:description" [--default <key>] [--expires-in 24h] [--context "..."]
|
|
3982
|
+
neo decision list # show pending decisions
|
|
3983
|
+
neo decision answer <id> <answer> # answer a decision (usually done by human via TUI)
|
|
3984
|
+
\`\`\`
|
|
3985
|
+
The decision ID is returned by \`create\`. If no answer arrives before expiration, the \`--default\` answer is applied automatically (or the decision expires without resolution).
|
|
3986
|
+
|
|
3638
3987
|
### Reporting
|
|
3639
3988
|
\`\`\`bash
|
|
3640
3989
|
neo log <type> "<message>" # visible in TUI only
|
|
@@ -3643,7 +3992,8 @@ var COMMANDS_COMPACT = `### Commands (reference)
|
|
|
3643
3992
|
\`neo run <agent> --prompt "..." --repo <path> --branch <name> --meta '{"label":"T1-auth",...}'\`
|
|
3644
3993
|
\`neo runs [--short | <runId>]\` \xB7 \`neo runs --short --status running\` \xB7 \`neo cost --short\`
|
|
3645
3994
|
\`neo memory write|update|forget|search|list\` \xB7 \`neo log <type> "<msg>"\`
|
|
3646
|
-
|
|
3995
|
+
\`neo config get <key>\` \xB7 \`neo config set <key> <value> --global\` \xB7 \`neo config list\`
|
|
3996
|
+
\`neo decision create "<question>" --options "..." [--default <key>]\` \xB7 \`neo decision list\``;
|
|
3647
3997
|
var HEARTBEAT_RULES = `### Heartbeat lifecycle
|
|
3648
3998
|
|
|
3649
3999
|
<decision-tree>
|
|
@@ -3653,7 +4003,8 @@ var HEARTBEAT_RULES = `### Heartbeat lifecycle
|
|
|
3653
4003
|
4. EVENTS? \u2014 process run completions, messages, webhooks. Parse agent JSON output.
|
|
3654
4004
|
5. FOLLOW-UPS? \u2014 check CI (\`gh pr checks\`), deferred dispatches.
|
|
3655
4005
|
6. DISPATCH \u2014 route work to agents. Mark tasks \`in_progress\`, add ACTIVE to focus.
|
|
3656
|
-
7.
|
|
4006
|
+
7. UPDATE TASKS \u2014 review ALL in_progress/blocked tasks. For each: confirm status matches reality (run still active? PR merged? blocked resolved?). Update outcomes immediately \u2014 do not defer to next heartbeat.
|
|
4007
|
+
8. SERIALIZE & YIELD \u2014 rewrite focus (see <focus>), log your decisions, and yield. Do not poll.
|
|
3657
4008
|
</decision-tree>
|
|
3658
4009
|
|
|
3659
4010
|
<run-monitoring>
|
|
@@ -3664,43 +4015,21 @@ Runs are your agents in the field. You MUST actively track them:
|
|
|
3664
4015
|
- **Active runs**: check \`neo runs --short --status running\` to verify your runs are still alive. If a run disappeared, investigate.
|
|
3665
4016
|
</run-monitoring>
|
|
3666
4017
|
|
|
3667
|
-
<
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
**
|
|
3671
|
-
|
|
3672
|
-
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
-
|
|
3676
|
-
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
4. Always pass the same \`--branch\` as previous tasks in the initiative
|
|
3683
|
-
|
|
3684
|
-
**After a run completes:**
|
|
3685
|
-
1. \`neo runs <runId>\` \u2014 read the FULL output, not just status
|
|
3686
|
-
2. Extract: PR URL/number, files changed, test results, any issues
|
|
3687
|
-
3. Verify the output matches the task's acceptance criteria
|
|
3688
|
-
4. If the agent opened a PR: dispatch \`reviewer\` in parallel with CI (do not wait for CI)
|
|
3689
|
-
5. Update the task outcome and log the result with concrete details (PR#, branch, what was done)
|
|
3690
|
-
6. Update the initiative note with the completed milestone
|
|
3691
|
-
|
|
3692
|
-
**Cross-task context:**
|
|
3693
|
-
- Each task builds on previous ones. When dispatching T6, tell the agent what T1-T5 produced (commits, APIs added, files changed)
|
|
3694
|
-
- Store key outputs as facts if they affect future tasks: "T5 added dateRange param to fetchAllFstRecords"
|
|
3695
|
-
- Use notes for initiative-level plans: \`cat notes/plan-<initiative>.md\` \u2014 update as tasks complete
|
|
3696
|
-
</orchestration>
|
|
3697
|
-
|
|
3698
|
-
<rules>
|
|
3699
|
-
- Work queue IS your plan. Never re-plan existing tasks.
|
|
3700
|
-
- Maximize parallelism: dispatch independent tasks in the same heartbeat.
|
|
3701
|
-
- After dispatch: update focus, yield immediately. Do NOT wait for results.
|
|
3702
|
-
- Deferred work (CI pending): MUST check at next heartbeat.
|
|
3703
|
-
</rules>`;
|
|
4018
|
+
<multi-task-initiatives>
|
|
4019
|
+
**Branch strategy:** one branch per initiative \u2014 all tasks push to the same branch sequentially (never in parallel). First task creates the branch; open PR after it completes. Later tasks add commits to the same PR. Independent initiatives CAN run in parallel on different branches.
|
|
4020
|
+
|
|
4021
|
+
**Dispatch quality:** write a detailed \`--prompt\` with acceptance criteria, files to modify, and context from completed sibling tasks (commits, APIs added, files changed). When dispatching task N, summarize what tasks 1..N-1 produced.
|
|
4022
|
+
|
|
4023
|
+
**Post-completion:** if agent opened a PR, dispatch \`reviewer\` in parallel with CI (do not wait). Update task outcome with concrete details (PR#, what was done) and update the initiative note.
|
|
4024
|
+
|
|
4025
|
+
**Task tracking discipline:**
|
|
4026
|
+
- On dispatch: \`neo memory update <id> --outcome in_progress\` immediately \u2014 never dispatch without updating the task.
|
|
4027
|
+
- On run completion: update to \`done\` with details OR \`blocked\` with reason. Do this in the SAME heartbeat you read the run output.
|
|
4028
|
+
- On run failure: update to \`blocked\` with root cause. Never leave a failed run's task as \`in_progress\`.
|
|
4029
|
+
- Every heartbeat: cross-check active tasks against \`neo runs --short\`. If a run finished but the task is still \`in_progress\`, something was missed \u2014 fix it now.
|
|
4030
|
+
|
|
4031
|
+
**Memory:** store key outputs as facts if they affect future tasks (e.g. "T5 added dateRange param to fetchAllFstRecords").
|
|
4032
|
+
</multi-task-initiatives>`;
|
|
3704
4033
|
var REPORTING_RULES = `### Reporting
|
|
3705
4034
|
|
|
3706
4035
|
\`neo log\` is your ONLY visible output. Use telegraphic format.
|
|
@@ -3711,19 +4040,14 @@ neo log action "<agent> <repo>:<branch> run:<runId> | <context>"
|
|
|
3711
4040
|
neo log discovery "<what> in <where>"
|
|
3712
4041
|
</log-format>
|
|
3713
4042
|
|
|
3714
|
-
<examples>
|
|
3715
|
-
<example type="good">
|
|
4043
|
+
<examples type="good">
|
|
3716
4044
|
neo log decision "YC-42 \u2192 developer | clear spec, complexity 3"
|
|
3717
4045
|
neo log action "developer standards:feat/YC-42-auth run:5900a64a | task T1"
|
|
3718
4046
|
neo log discovery "CI requires node 20 in api-service"
|
|
3719
|
-
</example>
|
|
3720
|
-
<example type="bad">
|
|
3721
|
-
neo log plan "Good! Now let me check the status and update things accordingly."
|
|
3722
|
-
neo log decision "Heartbeat #309: Idle cycle - no action required. All 4 repositories stable."
|
|
3723
|
-
neo log action "I've dispatched a developer agent to work on the authentication feature."
|
|
3724
|
-
</example>
|
|
3725
4047
|
</examples>`;
|
|
3726
|
-
|
|
4048
|
+
function buildMemoryRulesCore(supervisorDir) {
|
|
4049
|
+
const notesDir = `${supervisorDir}/notes`;
|
|
4050
|
+
return `### Memory
|
|
3727
4051
|
|
|
3728
4052
|
<memory-types>
|
|
3729
4053
|
| Type | Store when | TTL |
|
|
@@ -3736,7 +4060,7 @@ var MEMORY_RULES_CORE = `### Memory
|
|
|
3736
4060
|
</memory-types>
|
|
3737
4061
|
|
|
3738
4062
|
<memory-rules>
|
|
3739
|
-
- Focus
|
|
4063
|
+
- Focus is free-form working memory \u2014 rewrite at end of EVERY heartbeat (see <focus>).
|
|
3740
4064
|
- NEVER store: file counts, line numbers, completed work details, data available via \`neo runs <id>\`.
|
|
3741
4065
|
- After PR merge: forget related facts unless they are reusable architectural truths.
|
|
3742
4066
|
- Pattern escalation: same failure 3+ times \u2192 write a \`procedure\`.
|
|
@@ -3744,53 +4068,43 @@ var MEMORY_RULES_CORE = `### Memory
|
|
|
3744
4068
|
</memory-rules>
|
|
3745
4069
|
|
|
3746
4070
|
<task-workflow>
|
|
3747
|
-
|
|
4071
|
+
Queue markers: \u25CB pending \xB7 [ACTIVE] in_progress \xB7 [BLOCKED] blocked.
|
|
4072
|
+
Create tasks for: incoming tickets, architect decompositions, sub-tickets, follow-ups, CI fixes.
|
|
4073
|
+
- \`--tags "initiative:<name>"\` \u2014 groups related tasks
|
|
4074
|
+
- \`--tags "depends:mem_<id>"\` \u2014 blocks until dependency is done
|
|
4075
|
+
- \`--category\` \u2014 retrieval command (MANDATORY). Examples: \`"neo runs <runId>"\` \xB7 \`"cat ${notesDir}/plan-feature.md"\` \xB7 \`"API-retrieve-a-page <notionPageId>"\`
|
|
4076
|
+
Lifecycle: create \u2192 in_progress (on dispatch) \u2192 done | blocked | abandoned
|
|
3748
4077
|
|
|
3749
|
-
|
|
3750
|
-
- \`--severity critical|high|medium|low\` \u2014 dispatch highest severity first
|
|
3751
|
-
- \`--tags "initiative:<name>"\` \u2014 groups related tasks (shown as [initiative] headers in queue)
|
|
3752
|
-
- \`--tags "depends:mem_<id>"\` \u2014 task cannot start until dependency is done
|
|
3753
|
-
- \`--category\` \u2014 **MANDATORY** \u2014 the command to retrieve context for this task (shown as \`\u2192 <command>\` in queue)
|
|
4078
|
+
**Update frequency:** task outcomes MUST be updated in the same heartbeat as the triggering event. Never defer a task update to "next heartbeat" \u2014 by then you will have forgotten. Stale task states cause duplicate dispatches and wasted budget.
|
|
3754
4079
|
|
|
3755
|
-
**
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
- Architect decomposition: \`--category "neo runs <architectRunId>"\` (contains milestones + tasks)
|
|
3760
|
-
|
|
3761
|
-
Lifecycle: create \u2192 \`neo memory update <id> --outcome in_progress\` (on dispatch) \u2192 \`done\` (on success) / \`blocked\` (on failure, will retry) / \`abandoned\` (terminal, won't retry)
|
|
3762
|
-
|
|
3763
|
-
Dispatch rule: pick the highest-severity task with no unmet dependencies. Dispatch independent tasks in parallel. Before dispatching, run the \`--category\` command to retrieve task context.
|
|
4080
|
+
**Mandatory cross-check:** before yielding, verify that:
|
|
4081
|
+
1. Every dispatched run has a corresponding \`in_progress\` task
|
|
4082
|
+
2. Every completed run has a corresponding \`done\` or \`blocked\` task
|
|
4083
|
+
3. No task is \`in_progress\` without an active run (unless manually worked)
|
|
3764
4084
|
</task-workflow>
|
|
3765
4085
|
|
|
3766
|
-
<focus
|
|
3767
|
-
|
|
3768
|
-
PENDING: <taskId> "<description>" depends:<taskId>
|
|
3769
|
-
WAITING: <what> since:HB<N>
|
|
3770
|
-
PROCESSED: <runId> \u2192 <outcome> PR#<N>
|
|
3771
|
-
</focus-format>
|
|
3772
|
-
|
|
3773
|
-
<notes>
|
|
3774
|
-
|
|
3775
|
-
You have a notes/ directory for rich markdown documents that persist across heartbeats.
|
|
4086
|
+
<focus>
|
|
4087
|
+
You are stateless between heartbeats. Focus is your scratchpad \u2014 the only thing future-you will read before acting.
|
|
3776
4088
|
|
|
3777
|
-
|
|
3778
|
-
- Architect decompositions: save the full plan with milestones, tasks, acceptance criteria, dependency graph
|
|
3779
|
-
- Initiative tracking: progress log with completed/pending tasks, PRs merged, blockers
|
|
3780
|
-
- Complex debugging: accumulate findings across multiple heartbeats
|
|
3781
|
-
- Review checklists: aggregate reviewer feedback across fix/review cycles
|
|
4089
|
+
Write it like a handoff note to yourself: what's happening, what you decided, what to do next, what to watch for. Free-form. No format imposed. The only rule: if you don't write it down, you lose it.
|
|
3782
4090
|
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
- Read: \`cat notes/plan-YC-2670-kanban.md\` \u2014 retrieve full context at any heartbeat
|
|
3786
|
-
- Link to tasks: \`neo memory write --type task --category "cat notes/plan-YC-2670-kanban.md" "M3: UI"\`
|
|
3787
|
-
- Update: check off completed milestones, add PR numbers, note blockers after each task completes
|
|
3788
|
-
- Cleanup: \`rm notes/plan-*.md\` when the initiative is done
|
|
4091
|
+
Rewrite focus at the END of every heartbeat. Never leave it empty after a heartbeat with activity.
|
|
4092
|
+
</focus>
|
|
3789
4093
|
|
|
3790
|
-
|
|
4094
|
+
<notes>
|
|
4095
|
+
Notes directory: \`${notesDir}/\`
|
|
4096
|
+
Use notes for any initiative with 3+ tasks (persists across heartbeats).
|
|
4097
|
+
- Write: \`cat > ${notesDir}/plan-<initiative>.md << 'EOF' ... EOF\`
|
|
4098
|
+
- Link to tasks: \`--category "cat ${notesDir}/plan-<initiative>.md"\`
|
|
4099
|
+
- Update after each task: check off milestones, add PR numbers, note blockers
|
|
4100
|
+
- Delete when initiative is done
|
|
4101
|
+
Use cases: architect decompositions, initiative tracking, debugging across heartbeats, review checklists.
|
|
3791
4102
|
</notes>`;
|
|
3792
|
-
|
|
3793
|
-
|
|
4103
|
+
}
|
|
4104
|
+
function buildMemoryRulesExamples(supervisorDir) {
|
|
4105
|
+
const notesDir = `${supervisorDir}/notes`;
|
|
4106
|
+
return `<memory-examples>
|
|
4107
|
+
neo memory write --type focus --expires 2h "ACTIVE: 5900a64a developer 'T1' branch:feat/x (cat ${notesDir}/plan-YC-2670-kanban.md)"
|
|
3794
4108
|
neo memory write --type fact --scope /repo "main branch uses protected merges \u2014 agents must create PRs, never push directly"
|
|
3795
4109
|
neo memory write --type fact --scope /repo "pnpm build must pass before push \u2014 CI does not rebuild, run 2g589f34a5a failed without it"
|
|
3796
4110
|
neo memory write --type procedure --scope /repo "After architect run: parse milestones from JSON output, create one task per milestone with --tags initiative:<name>"
|
|
@@ -3800,42 +4114,156 @@ neo memory write --type task --scope /repo --severity high --category "neo runs
|
|
|
3800
4114
|
neo memory update <id> --outcome in_progress|done|blocked|abandoned
|
|
3801
4115
|
neo memory forget <id>
|
|
3802
4116
|
</memory-examples>`;
|
|
4117
|
+
}
|
|
4118
|
+
function buildRoleSection(heartbeatCount, label) {
|
|
4119
|
+
const suffix = label ? ` (${label})` : "";
|
|
4120
|
+
return `<role>
|
|
4121
|
+
${ROLE}
|
|
4122
|
+
Heartbeat #${heartbeatCount}${suffix}
|
|
4123
|
+
</role>`;
|
|
4124
|
+
}
|
|
3803
4125
|
function getCommandsSection(heartbeatCount) {
|
|
3804
4126
|
return heartbeatCount <= 3 ? COMMANDS : COMMANDS_COMPACT;
|
|
3805
4127
|
}
|
|
3806
|
-
function
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
parts.push(`Repositories:
|
|
3811
|
-
${repoList}`);
|
|
3812
|
-
}
|
|
3813
|
-
if (opts.mcpServerNames.length > 0) {
|
|
3814
|
-
const mcpList = opts.mcpServerNames.map((n) => `- ${n}`).join("\n");
|
|
3815
|
-
parts.push(`Integrations (MCP):
|
|
3816
|
-
${mcpList}`);
|
|
3817
|
-
}
|
|
3818
|
-
parts.push(
|
|
3819
|
-
`Budget: $${opts.budgetStatus.todayUsd.toFixed(2)} / $${opts.budgetStatus.capUsd.toFixed(2)} (${opts.budgetStatus.remainingPct.toFixed(0)}% remaining)`
|
|
3820
|
-
);
|
|
3821
|
-
return parts;
|
|
4128
|
+
function buildReferenceSection(heartbeatCount) {
|
|
4129
|
+
return `<reference>
|
|
4130
|
+
${getCommandsSection(heartbeatCount)}
|
|
4131
|
+
</reference>`;
|
|
3822
4132
|
}
|
|
3823
|
-
function
|
|
4133
|
+
function buildFocusSection(memories) {
|
|
3824
4134
|
const focusEntries = memories.filter((m) => m.type === "focus");
|
|
3825
|
-
const factEntries = memories.filter((m) => m.type === "fact");
|
|
3826
|
-
const procedureEntries = memories.filter((m) => m.type === "procedure");
|
|
3827
|
-
const feedbackEntries = memories.filter((m) => m.type === "feedback");
|
|
3828
|
-
const parts = [];
|
|
3829
4135
|
if (focusEntries.length > 0) {
|
|
3830
4136
|
const lines = focusEntries.map((m) => `- ${m.content}`).join("\n");
|
|
3831
|
-
|
|
4137
|
+
return `<focus>
|
|
3832
4138
|
${lines}
|
|
3833
|
-
</focus
|
|
3834
|
-
} else {
|
|
3835
|
-
parts.push(
|
|
3836
|
-
"<focus>\n(empty \u2014 use neo memory write --type focus to set working context)\n</focus>"
|
|
3837
|
-
);
|
|
4139
|
+
</focus>`;
|
|
3838
4140
|
}
|
|
4141
|
+
return "<focus>\n(empty \u2014 use neo memory write --type focus to set working context)\n</focus>";
|
|
4142
|
+
}
|
|
4143
|
+
function buildPendingDecisionsSection(decisions) {
|
|
4144
|
+
if (!decisions || decisions.length === 0) {
|
|
4145
|
+
return "";
|
|
4146
|
+
}
|
|
4147
|
+
const lines = [];
|
|
4148
|
+
for (const d of decisions) {
|
|
4149
|
+
const expiry = d.expiresAt ? ` (expires: ${d.expiresAt})` : "";
|
|
4150
|
+
const defaultHint = d.defaultAnswer ? ` [default: ${d.defaultAnswer}]` : "";
|
|
4151
|
+
lines.push(`- **${d.id}**: ${d.question}${expiry}${defaultHint}`);
|
|
4152
|
+
if (d.options && d.options.length > 0) {
|
|
4153
|
+
for (const opt of d.options) {
|
|
4154
|
+
const desc = opt.description ? ` \u2014 ${opt.description}` : "";
|
|
4155
|
+
lines.push(` \u2022 \`${opt.key}\`: ${opt.label}${desc}`);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
if (d.context) {
|
|
4159
|
+
lines.push(` Context: ${d.context}`);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
return `Pending decisions (${decisions.length}):
|
|
4163
|
+
${lines.join("\n")}
|
|
4164
|
+
|
|
4165
|
+
To answer a decision, emit a \`decision:answer\` event:
|
|
4166
|
+
\`\`\`bash
|
|
4167
|
+
neo event emit decision:answer --data '{"id":"<decision_id>","answer":"<option_key>"}'
|
|
4168
|
+
\`\`\``;
|
|
4169
|
+
}
|
|
4170
|
+
function buildAnsweredDecisionsSection(decisions) {
|
|
4171
|
+
if (!decisions || decisions.length === 0) {
|
|
4172
|
+
return "";
|
|
4173
|
+
}
|
|
4174
|
+
const lines = decisions.map((d) => {
|
|
4175
|
+
const answeredBy = d.source ? ` (by ${d.source})` : "";
|
|
4176
|
+
return `- ${d.id}: "${d.question}" \u2192 **${d.answer}**${answeredBy}`;
|
|
4177
|
+
});
|
|
4178
|
+
return `Recent decisions (${decisions.length}):
|
|
4179
|
+
${lines.join("\n")}`;
|
|
4180
|
+
}
|
|
4181
|
+
function buildFullContext(opts) {
|
|
4182
|
+
const parts = [];
|
|
4183
|
+
parts.push(buildFocusSection(opts.memories));
|
|
4184
|
+
const workQueue = buildWorkQueueSection(opts.memories);
|
|
4185
|
+
if (workQueue) {
|
|
4186
|
+
parts.push(workQueue);
|
|
4187
|
+
}
|
|
4188
|
+
if (opts.activeRuns.length > 0) {
|
|
4189
|
+
parts.push(`Active runs:
|
|
4190
|
+
${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
|
|
4191
|
+
}
|
|
4192
|
+
const recentActions = buildRecentActionsSection(opts.recentActions);
|
|
4193
|
+
if (recentActions) {
|
|
4194
|
+
parts.push(recentActions);
|
|
4195
|
+
}
|
|
4196
|
+
const pendingDecisions = buildPendingDecisionsSection(opts.pendingDecisions);
|
|
4197
|
+
if (pendingDecisions) {
|
|
4198
|
+
parts.push(pendingDecisions);
|
|
4199
|
+
}
|
|
4200
|
+
const answeredDecisions = buildAnsweredDecisionsSection(opts.answeredDecisions);
|
|
4201
|
+
if (answeredDecisions) {
|
|
4202
|
+
parts.push(answeredDecisions);
|
|
4203
|
+
}
|
|
4204
|
+
parts.push(buildKnowledgeSection(opts.memories));
|
|
4205
|
+
parts.push(...buildEnvironmentSections(opts));
|
|
4206
|
+
parts.push(`Events:
|
|
4207
|
+
${buildEventsSection(opts.grouped)}`);
|
|
4208
|
+
return `<context>
|
|
4209
|
+
${parts.join("\n\n")}
|
|
4210
|
+
</context>`;
|
|
4211
|
+
}
|
|
4212
|
+
function buildCompactionContext(opts) {
|
|
4213
|
+
const parts = [];
|
|
4214
|
+
parts.push(buildFocusSection(opts.memories));
|
|
4215
|
+
parts.push(buildKnowledgeSection(opts.memories));
|
|
4216
|
+
const workQueue = buildWorkQueueSection(opts.memories);
|
|
4217
|
+
if (workQueue) {
|
|
4218
|
+
parts.push(workQueue);
|
|
4219
|
+
}
|
|
4220
|
+
parts.push(...buildEnvironmentSections(opts));
|
|
4221
|
+
return `<context>
|
|
4222
|
+
${parts.join("\n\n")}
|
|
4223
|
+
</context>`;
|
|
4224
|
+
}
|
|
4225
|
+
function buildBaseInstructions(opts, options) {
|
|
4226
|
+
const parts = [];
|
|
4227
|
+
parts.push(OPERATING_PRINCIPLES);
|
|
4228
|
+
parts.push(HEARTBEAT_RULES);
|
|
4229
|
+
parts.push(REPORTING_RULES);
|
|
4230
|
+
parts.push(buildMemoryRulesCore(opts.supervisorDir));
|
|
4231
|
+
if (options.includeExamples) {
|
|
4232
|
+
parts.push(buildMemoryRulesExamples(opts.supervisorDir));
|
|
4233
|
+
}
|
|
4234
|
+
if (opts.customInstructions) {
|
|
4235
|
+
parts.push(`### Custom instructions
|
|
4236
|
+
${opts.customInstructions}`);
|
|
4237
|
+
}
|
|
4238
|
+
return parts;
|
|
4239
|
+
}
|
|
4240
|
+
function wrapInstructions(parts) {
|
|
4241
|
+
return `<instructions>
|
|
4242
|
+
${parts.join("\n\n")}
|
|
4243
|
+
</instructions>`;
|
|
4244
|
+
}
|
|
4245
|
+
function buildEnvironmentSections(opts) {
|
|
4246
|
+
const parts = [];
|
|
4247
|
+
if (opts.repos.length > 0) {
|
|
4248
|
+
const repoList = opts.repos.map((r) => `- ${r.path} (branch: ${r.defaultBranch})`).join("\n");
|
|
4249
|
+
parts.push(`Repositories:
|
|
4250
|
+
${repoList}`);
|
|
4251
|
+
}
|
|
4252
|
+
if (opts.mcpServerNames.length > 0) {
|
|
4253
|
+
const mcpList = opts.mcpServerNames.map((n) => `- ${n}`).join("\n");
|
|
4254
|
+
parts.push(`Integrations (MCP):
|
|
4255
|
+
${mcpList}`);
|
|
4256
|
+
}
|
|
4257
|
+
parts.push(
|
|
4258
|
+
`Budget: $${opts.budgetStatus.todayUsd.toFixed(2)} / $${opts.budgetStatus.capUsd.toFixed(2)} (${opts.budgetStatus.remainingPct.toFixed(0)}% remaining)`
|
|
4259
|
+
);
|
|
4260
|
+
return parts;
|
|
4261
|
+
}
|
|
4262
|
+
function buildKnowledgeSection(memories) {
|
|
4263
|
+
const factEntries = memories.filter((m) => m.type === "fact");
|
|
4264
|
+
const procedureEntries = memories.filter((m) => m.type === "procedure");
|
|
4265
|
+
const feedbackEntries = memories.filter((m) => m.type === "feedback");
|
|
4266
|
+
const parts = [];
|
|
3839
4267
|
if (factEntries.length > 0) {
|
|
3840
4268
|
const byScope = /* @__PURE__ */ new Map();
|
|
3841
4269
|
for (const m of factEntries) {
|
|
@@ -3871,12 +4299,6 @@ ${lines}`);
|
|
|
3871
4299
|
parts.push(`Recurring review issues:
|
|
3872
4300
|
${lines}`);
|
|
3873
4301
|
}
|
|
3874
|
-
parts.push(`For detailed plans and checklists, use notes:
|
|
3875
|
-
\`\`\`bash
|
|
3876
|
-
cat > ${supervisorDir}/notes/plan-feature.md << 'EOF'
|
|
3877
|
-
<your detailed plan here>
|
|
3878
|
-
EOF
|
|
3879
|
-
\`\`\``);
|
|
3880
4302
|
return parts.join("\n\n");
|
|
3881
4303
|
}
|
|
3882
4304
|
var DONE_OUTCOMES = /* @__PURE__ */ new Set(["done", "abandoned"]);
|
|
@@ -3925,18 +4347,72 @@ function groupTasksByInitiative(tasks) {
|
|
|
3925
4347
|
}
|
|
3926
4348
|
return groups;
|
|
3927
4349
|
}
|
|
4350
|
+
var SEVERITY_ORDER = {
|
|
4351
|
+
critical: 0,
|
|
4352
|
+
high: 1,
|
|
4353
|
+
medium: 2,
|
|
4354
|
+
low: 3
|
|
4355
|
+
};
|
|
4356
|
+
function bySeverity(a, b) {
|
|
4357
|
+
const aOrder = SEVERITY_ORDER[a.severity ?? "medium"] ?? 2;
|
|
4358
|
+
const bOrder = SEVERITY_ORDER[b.severity ?? "medium"] ?? 2;
|
|
4359
|
+
return aOrder - bOrder;
|
|
4360
|
+
}
|
|
4361
|
+
function partitionTasks(tasks) {
|
|
4362
|
+
const active = [];
|
|
4363
|
+
const blocked = [];
|
|
4364
|
+
const pending = [];
|
|
4365
|
+
for (const t of tasks) {
|
|
4366
|
+
if (t.outcome === "in_progress") active.push(t);
|
|
4367
|
+
else if (t.outcome === "blocked") blocked.push(t);
|
|
4368
|
+
else pending.push(t);
|
|
4369
|
+
}
|
|
4370
|
+
return { active, blocked, pending };
|
|
4371
|
+
}
|
|
4372
|
+
function renderInitiativeSummary(group) {
|
|
4373
|
+
const { active, pending } = partitionTasks(group.tasks);
|
|
4374
|
+
const nextEligible = [...pending].sort(bySeverity)[0];
|
|
4375
|
+
const cat = nextEligible?.category ? ` -> ${nextEligible.category}` : "";
|
|
4376
|
+
const nextLabel = nextEligible ? ` (next: ${nextEligible.content.slice(0, 30)}${nextEligible.content.length > 30 ? "..." : ""} [${nextEligible.severity ?? "medium"}])` : "";
|
|
4377
|
+
return `[${group.initiative}] ${active.length} active, ${pending.length} pending${nextLabel}${cat}`;
|
|
4378
|
+
}
|
|
4379
|
+
function renderCompactInitiative(group, lines, rendered) {
|
|
4380
|
+
lines.push(` ${renderInitiativeSummary(group)}`);
|
|
4381
|
+
const { active, blocked, pending } = partitionTasks(group.tasks);
|
|
4382
|
+
const nextEligible = [...pending].sort(bySeverity)[0];
|
|
4383
|
+
for (const task of [...active, ...blocked]) {
|
|
4384
|
+
if (rendered >= MAX_TASKS) break;
|
|
4385
|
+
lines.push(` ${formatTaskLine(task)}`);
|
|
4386
|
+
rendered++;
|
|
4387
|
+
}
|
|
4388
|
+
if (nextEligible && active.length === 0 && blocked.length === 0 && rendered < MAX_TASKS) {
|
|
4389
|
+
lines.push(` ${formatTaskLine(nextEligible)}`);
|
|
4390
|
+
rendered++;
|
|
4391
|
+
}
|
|
4392
|
+
return rendered;
|
|
4393
|
+
}
|
|
4394
|
+
function renderFlatGroup(group, showHeader, lines, rendered) {
|
|
4395
|
+
if (showHeader && group.initiative) {
|
|
4396
|
+
lines.push(` [${group.initiative}]`);
|
|
4397
|
+
}
|
|
4398
|
+
for (const task of group.tasks) {
|
|
4399
|
+
if (rendered >= MAX_TASKS) break;
|
|
4400
|
+
lines.push(` ${formatTaskLine(task)}`);
|
|
4401
|
+
rendered++;
|
|
4402
|
+
}
|
|
4403
|
+
return rendered;
|
|
4404
|
+
}
|
|
3928
4405
|
function renderTaskGroups(groups) {
|
|
3929
4406
|
const lines = [];
|
|
3930
4407
|
let rendered = 0;
|
|
3931
4408
|
for (const group of groups) {
|
|
3932
4409
|
if (rendered >= MAX_TASKS) break;
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
lines
|
|
3939
|
-
rendered++;
|
|
4410
|
+
const useCompactMode = group.initiative && group.tasks.length >= 3;
|
|
4411
|
+
if (useCompactMode) {
|
|
4412
|
+
rendered = renderCompactInitiative(group, lines, rendered);
|
|
4413
|
+
} else {
|
|
4414
|
+
const showHeader = group.initiative !== null && groups.length > 1;
|
|
4415
|
+
rendered = renderFlatGroup(group, showHeader, lines, rendered);
|
|
3940
4416
|
}
|
|
3941
4417
|
}
|
|
3942
4418
|
return lines;
|
|
@@ -4017,112 +4493,100 @@ ${JSON.stringify(event.data.payload ?? {}, null, 2)}
|
|
|
4017
4493
|
return `Internal event: ${event.eventKind}`;
|
|
4018
4494
|
}
|
|
4019
4495
|
}
|
|
4496
|
+
function countEvents(grouped) {
|
|
4497
|
+
return grouped.messages.length + grouped.webhooks.length + grouped.runCompletions.length;
|
|
4498
|
+
}
|
|
4020
4499
|
function isIdleHeartbeat(opts) {
|
|
4021
|
-
const { messages, webhooks, runCompletions } = opts.grouped;
|
|
4022
|
-
const totalEvents = messages.length + webhooks.length + runCompletions.length;
|
|
4023
4500
|
const hasWork = buildWorkQueueSection(opts.memories) !== "";
|
|
4024
|
-
return
|
|
4501
|
+
return countEvents(opts.grouped) === 0 && opts.activeRuns.length === 0 && !hasWork;
|
|
4025
4502
|
}
|
|
4026
4503
|
function buildIdlePrompt(opts) {
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4504
|
+
const budgetLine = `Budget: $${opts.budgetStatus.todayUsd.toFixed(2)} / $${opts.budgetStatus.capUsd.toFixed(2)} (${opts.budgetStatus.remainingPct.toFixed(0)}% remaining)`;
|
|
4505
|
+
const hasRepos = opts.repos.length > 0;
|
|
4506
|
+
const hasBudget = opts.budgetStatus.remainingPct > 10;
|
|
4507
|
+
const hasPendingDecisions = (opts.pendingDecisions?.length ?? 0) > 0;
|
|
4508
|
+
if (!hasRepos || !hasBudget) {
|
|
4509
|
+
return `${buildRoleSection(opts.heartbeatCount)}
|
|
4031
4510
|
|
|
4032
4511
|
<context>
|
|
4033
4512
|
No events. No active runs. No pending tasks.
|
|
4034
|
-
|
|
4513
|
+
${budgetLine}
|
|
4035
4514
|
</context>
|
|
4036
4515
|
|
|
4037
4516
|
<directive>
|
|
4038
4517
|
Nothing to do. Run \`neo log discovery "idle"\` and yield. Do not produce any other output.
|
|
4039
4518
|
</directive>`;
|
|
4040
|
-
}
|
|
4041
|
-
function buildStandardPrompt(opts) {
|
|
4042
|
-
const sections = [];
|
|
4043
|
-
sections.push(`<role>
|
|
4044
|
-
${ROLE}
|
|
4045
|
-
Heartbeat #${opts.heartbeatCount}
|
|
4046
|
-
</role>`);
|
|
4047
|
-
const contextParts = [];
|
|
4048
|
-
const workQueue = buildWorkQueueSection(opts.memories);
|
|
4049
|
-
if (workQueue) {
|
|
4050
|
-
contextParts.push(workQueue);
|
|
4051
4519
|
}
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4520
|
+
const repoList = opts.repos.map((r) => `- ${r.path} (branch: ${r.defaultBranch})`).join("\n");
|
|
4521
|
+
if (hasPendingDecisions) {
|
|
4522
|
+
const pendingSection = buildPendingDecisionsSection(opts.pendingDecisions);
|
|
4523
|
+
return `${buildRoleSection(opts.heartbeatCount)}
|
|
4524
|
+
|
|
4525
|
+
<context>
|
|
4526
|
+
No events. No active runs. No pending tasks.
|
|
4527
|
+
${budgetLine}
|
|
4528
|
+
|
|
4529
|
+
${pendingSection}
|
|
4530
|
+
|
|
4531
|
+
Repositories:
|
|
4532
|
+
${repoList}
|
|
4533
|
+
</context>
|
|
4534
|
+
|
|
4535
|
+
<directive>
|
|
4536
|
+
Idle \u2014 but there are pending decisions awaiting user response.
|
|
4537
|
+
Run \`neo log discovery "idle \u2014 waiting on ${String(opts.pendingDecisions?.length ?? 0)} pending decision(s)"\` and yield.
|
|
4538
|
+
</directive>`;
|
|
4061
4539
|
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4540
|
+
return `${buildRoleSection(opts.heartbeatCount)}
|
|
4541
|
+
|
|
4542
|
+
<context>
|
|
4543
|
+
No events. No active runs. No pending tasks.
|
|
4544
|
+
${budgetLine}
|
|
4545
|
+
|
|
4546
|
+
Repositories:
|
|
4547
|
+
${repoList}
|
|
4548
|
+
</context>
|
|
4549
|
+
|
|
4550
|
+
<reference>
|
|
4068
4551
|
${getCommandsSection(opts.heartbeatCount)}
|
|
4069
|
-
</reference
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4552
|
+
</reference>
|
|
4553
|
+
|
|
4554
|
+
<directive>
|
|
4555
|
+
Idle \u2014 no work in progress. Use this downtime to dispatch a \`scout\` agent on one of your repositories.
|
|
4556
|
+
|
|
4557
|
+
The scout explores the codebase and surfaces bugs, improvements, security issues, and tech debt. It creates decisions (via \`neo decision create\`) for each critical or high-impact finding, so the user can choose what to act on.
|
|
4558
|
+
|
|
4559
|
+
**Rules:**
|
|
4560
|
+
- Pick the repo that was least recently scouted (check your memory for previous scout runs).
|
|
4561
|
+
- Only ONE scout at a time \u2014 never dispatch multiple scouts in parallel.
|
|
4562
|
+
- Use \`--branch main\` (or the repo's default branch) \u2014 scouts are read-only.
|
|
4563
|
+
- Log your decision before dispatching.
|
|
4564
|
+
|
|
4565
|
+
**Example:**
|
|
4566
|
+
\`\`\`bash
|
|
4567
|
+
neo log decision "Idle \u2014 dispatching scout on <repo>"
|
|
4568
|
+
neo run scout --prompt "Explore this repository. Surface bugs, improvements, security issues, and tech debt. Create decisions for critical and high-impact findings." \\
|
|
4569
|
+
--repo <path> \\
|
|
4570
|
+
--branch <default-branch> \\
|
|
4571
|
+
--meta '{"stage":"scout","label":"scout-<repo-name>"}'
|
|
4572
|
+
\`\`\`
|
|
4573
|
+
</directive>`;
|
|
4574
|
+
}
|
|
4575
|
+
function buildStandardPrompt(opts) {
|
|
4576
|
+
const instructionParts = buildBaseInstructions(opts, { includeExamples: false });
|
|
4577
|
+
const hasEvents = countEvents(opts.grouped) > 0;
|
|
4080
4578
|
instructionParts.push(
|
|
4081
4579
|
hasEvents ? "Process events, dispatch eligible work, yield. Each heartbeat costs ~$0.10 \u2014 be efficient." : "No events. If pending work exists, dispatch it. Otherwise yield immediately."
|
|
4082
4580
|
);
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4581
|
+
return [
|
|
4582
|
+
buildRoleSection(opts.heartbeatCount),
|
|
4583
|
+
buildFullContext(opts),
|
|
4584
|
+
buildReferenceSection(opts.heartbeatCount),
|
|
4585
|
+
wrapInstructions(instructionParts)
|
|
4586
|
+
].join("\n\n");
|
|
4087
4587
|
}
|
|
4088
4588
|
function buildConsolidationPrompt(opts) {
|
|
4089
|
-
const
|
|
4090
|
-
sections.push(`<role>
|
|
4091
|
-
${ROLE}
|
|
4092
|
-
Heartbeat #${opts.heartbeatCount} (CONSOLIDATION)
|
|
4093
|
-
</role>`);
|
|
4094
|
-
const contextParts = [];
|
|
4095
|
-
const workQueueConsolidation = buildWorkQueueSection(opts.memories);
|
|
4096
|
-
if (workQueueConsolidation) {
|
|
4097
|
-
contextParts.push(workQueueConsolidation);
|
|
4098
|
-
}
|
|
4099
|
-
if (opts.activeRuns.length > 0) {
|
|
4100
|
-
contextParts.push(`Active runs:
|
|
4101
|
-
${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
|
|
4102
|
-
}
|
|
4103
|
-
contextParts.push(...buildContextSections(opts));
|
|
4104
|
-
contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
|
|
4105
|
-
const recentActions = buildRecentActionsSection(opts.recentActions);
|
|
4106
|
-
if (recentActions) {
|
|
4107
|
-
contextParts.push(recentActions);
|
|
4108
|
-
}
|
|
4109
|
-
contextParts.push(`Events:
|
|
4110
|
-
${buildEventsSection(opts.grouped)}`);
|
|
4111
|
-
sections.push(`<context>
|
|
4112
|
-
${contextParts.join("\n\n")}
|
|
4113
|
-
</context>`);
|
|
4114
|
-
sections.push(`<reference>
|
|
4115
|
-
${getCommandsSection(opts.heartbeatCount)}
|
|
4116
|
-
</reference>`);
|
|
4117
|
-
const instructionParts = [];
|
|
4118
|
-
instructionParts.push(HEARTBEAT_RULES);
|
|
4119
|
-
instructionParts.push(REPORTING_RULES);
|
|
4120
|
-
instructionParts.push(MEMORY_RULES_CORE);
|
|
4121
|
-
instructionParts.push(MEMORY_RULES_EXAMPLES);
|
|
4122
|
-
if (opts.customInstructions) {
|
|
4123
|
-
instructionParts.push(`### Custom instructions
|
|
4124
|
-
${opts.customInstructions}`);
|
|
4125
|
-
}
|
|
4589
|
+
const instructionParts = buildBaseInstructions(opts, { includeExamples: true });
|
|
4126
4590
|
instructionParts.push(
|
|
4127
4591
|
`### Consolidation
|
|
4128
4592
|
This is a CONSOLIDATION heartbeat.
|
|
@@ -4137,39 +4601,16 @@ If there IS active work, your job:
|
|
|
4137
4601
|
4. **Prune completed work** \u2014 if a PR is merged or an initiative is done, forget related facts that are no longer actionable. Keep only reusable architectural truths.
|
|
4138
4602
|
5. **Prune done tasks** \u2014 forget tasks with outcome \`done\` or \`abandoned\` older than 7 days.`
|
|
4139
4603
|
);
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4604
|
+
return [
|
|
4605
|
+
buildRoleSection(opts.heartbeatCount, "CONSOLIDATION"),
|
|
4606
|
+
buildFullContext(opts),
|
|
4607
|
+
buildReferenceSection(opts.heartbeatCount),
|
|
4608
|
+
wrapInstructions(instructionParts)
|
|
4609
|
+
].join("\n\n");
|
|
4144
4610
|
}
|
|
4145
4611
|
function buildCompactionPrompt(opts) {
|
|
4146
|
-
const
|
|
4147
|
-
|
|
4148
|
-
${ROLE}
|
|
4149
|
-
Heartbeat #${opts.heartbeatCount} (COMPACTION)
|
|
4150
|
-
</role>`);
|
|
4151
|
-
const contextParts = [];
|
|
4152
|
-
contextParts.push(...buildContextSections(opts));
|
|
4153
|
-
contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
|
|
4154
|
-
const workQueueCompaction = buildWorkQueueSection(opts.memories);
|
|
4155
|
-
if (workQueueCompaction) {
|
|
4156
|
-
contextParts.push(workQueueCompaction);
|
|
4157
|
-
}
|
|
4158
|
-
sections.push(`<context>
|
|
4159
|
-
${contextParts.join("\n\n")}
|
|
4160
|
-
</context>`);
|
|
4161
|
-
sections.push(`<reference>
|
|
4162
|
-
${getCommandsSection(opts.heartbeatCount)}
|
|
4163
|
-
</reference>`);
|
|
4164
|
-
const instructionParts = [];
|
|
4165
|
-
instructionParts.push(HEARTBEAT_RULES);
|
|
4166
|
-
instructionParts.push(REPORTING_RULES);
|
|
4167
|
-
instructionParts.push(MEMORY_RULES_CORE);
|
|
4168
|
-
instructionParts.push(MEMORY_RULES_EXAMPLES);
|
|
4169
|
-
if (opts.customInstructions) {
|
|
4170
|
-
instructionParts.push(`### Custom instructions
|
|
4171
|
-
${opts.customInstructions}`);
|
|
4172
|
-
}
|
|
4612
|
+
const notesDir = `${opts.supervisorDir}/notes`;
|
|
4613
|
+
const instructionParts = buildBaseInstructions(opts, { includeExamples: true });
|
|
4173
4614
|
instructionParts.push(`### Compaction
|
|
4174
4615
|
This is a COMPACTION heartbeat. Deep-clean your ENTIRE memory.
|
|
4175
4616
|
|
|
@@ -4179,7 +4620,7 @@ This is a COMPACTION heartbeat. Deep-clean your ENTIRE memory.
|
|
|
4179
4620
|
4. **Merge duplicates** \u2014 combine similar facts within the same scope into one.
|
|
4180
4621
|
5. **Clean up focus** \u2014 forget resolved items, rewrite remaining in structured format.
|
|
4181
4622
|
6. **Prune done tasks** \u2014 forget tasks with outcome \`done\` or \`abandoned\` older than 7 days.
|
|
4182
|
-
7. **Delete completed notes** from
|
|
4623
|
+
7. **Delete completed notes** from \`${notesDir}/\` directory.
|
|
4183
4624
|
8. **Stay under 15 facts per scope** \u2014 prioritize facts that affect dispatch decisions.
|
|
4184
4625
|
|
|
4185
4626
|
Flag contradictions: if two facts contradict, keep the newer one.
|
|
@@ -4188,12 +4629,66 @@ Flag contradictions: if two facts contradict, keep the newer one.
|
|
|
4188
4629
|
neo memory list --type fact
|
|
4189
4630
|
neo memory forget <stale-id>
|
|
4190
4631
|
\`\`\``);
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4632
|
+
return [
|
|
4633
|
+
buildRoleSection(opts.heartbeatCount, "COMPACTION"),
|
|
4634
|
+
buildCompactionContext(opts),
|
|
4635
|
+
buildReferenceSection(opts.heartbeatCount),
|
|
4636
|
+
wrapInstructions(instructionParts)
|
|
4637
|
+
].join("\n\n");
|
|
4195
4638
|
}
|
|
4196
4639
|
|
|
4640
|
+
// src/supervisor/webhookEvents.ts
|
|
4641
|
+
import { z as z6 } from "zod";
|
|
4642
|
+
var supervisorStartedEventSchema = z6.object({
|
|
4643
|
+
type: z6.literal("supervisor_started"),
|
|
4644
|
+
supervisorId: z6.string(),
|
|
4645
|
+
startedAt: z6.string().datetime()
|
|
4646
|
+
});
|
|
4647
|
+
var heartbeatEventSchema = z6.object({
|
|
4648
|
+
type: z6.literal("heartbeat"),
|
|
4649
|
+
supervisorId: z6.string(),
|
|
4650
|
+
heartbeatNumber: z6.number().int().min(0),
|
|
4651
|
+
timestamp: z6.string().datetime(),
|
|
4652
|
+
runsActive: z6.number().int().min(0),
|
|
4653
|
+
budget: z6.object({
|
|
4654
|
+
todayUsd: z6.number().min(0),
|
|
4655
|
+
limitUsd: z6.number().min(0)
|
|
4656
|
+
})
|
|
4657
|
+
});
|
|
4658
|
+
var runDispatchedEventSchema = z6.object({
|
|
4659
|
+
type: z6.literal("run_dispatched"),
|
|
4660
|
+
supervisorId: z6.string(),
|
|
4661
|
+
runId: z6.string(),
|
|
4662
|
+
agent: z6.string(),
|
|
4663
|
+
repo: z6.string(),
|
|
4664
|
+
branch: z6.string(),
|
|
4665
|
+
prompt: z6.string().max(500)
|
|
4666
|
+
// truncated
|
|
4667
|
+
});
|
|
4668
|
+
var runCompletedEventSchema = z6.object({
|
|
4669
|
+
type: z6.literal("run_completed"),
|
|
4670
|
+
supervisorId: z6.string(),
|
|
4671
|
+
runId: z6.string(),
|
|
4672
|
+
status: z6.enum(["completed", "failed", "cancelled"]),
|
|
4673
|
+
output: z6.string().max(1e3).optional(),
|
|
4674
|
+
// truncated
|
|
4675
|
+
costUsd: z6.number().min(0),
|
|
4676
|
+
durationMs: z6.number().int().min(0)
|
|
4677
|
+
});
|
|
4678
|
+
var supervisorStoppedEventSchema = z6.object({
|
|
4679
|
+
type: z6.literal("supervisor_stopped"),
|
|
4680
|
+
supervisorId: z6.string(),
|
|
4681
|
+
stoppedAt: z6.string().datetime(),
|
|
4682
|
+
reason: z6.enum(["shutdown", "budget_exceeded", "error", "manual"])
|
|
4683
|
+
});
|
|
4684
|
+
var supervisorWebhookEventSchema = z6.discriminatedUnion("type", [
|
|
4685
|
+
supervisorStartedEventSchema,
|
|
4686
|
+
heartbeatEventSchema,
|
|
4687
|
+
runDispatchedEventSchema,
|
|
4688
|
+
runCompletedEventSchema,
|
|
4689
|
+
supervisorStoppedEventSchema
|
|
4690
|
+
]);
|
|
4691
|
+
|
|
4197
4692
|
// src/supervisor/heartbeat.ts
|
|
4198
4693
|
var DEFAULT_IDLE_SKIP_MAX = 20;
|
|
4199
4694
|
var DEFAULT_ACTIVE_WORK_SKIP_MAX = 3;
|
|
@@ -4208,6 +4703,23 @@ function shouldCompact(heartbeatCount, lastCompactionHeartbeat, compactionInterv
|
|
|
4208
4703
|
const since = heartbeatCount - lastCompactionHeartbeat;
|
|
4209
4704
|
return since >= compactionInterval;
|
|
4210
4705
|
}
|
|
4706
|
+
var STALE_GRACE_PERIOD_MS = 3e4;
|
|
4707
|
+
function isRunActive(run, isAlive = isProcessAlive, now = Date.now()) {
|
|
4708
|
+
if (run.status !== "running" && run.status !== "paused") {
|
|
4709
|
+
return false;
|
|
4710
|
+
}
|
|
4711
|
+
if (run.status === "paused") {
|
|
4712
|
+
return true;
|
|
4713
|
+
}
|
|
4714
|
+
if (run.pid && isAlive(run.pid)) {
|
|
4715
|
+
return true;
|
|
4716
|
+
}
|
|
4717
|
+
if (run.pid) {
|
|
4718
|
+
return false;
|
|
4719
|
+
}
|
|
4720
|
+
const ageMs = now - new Date(run.createdAt).getTime();
|
|
4721
|
+
return ageMs < STALE_GRACE_PERIOD_MS;
|
|
4722
|
+
}
|
|
4211
4723
|
var HeartbeatLoop = class {
|
|
4212
4724
|
stopping = false;
|
|
4213
4725
|
consecutiveFailures = 0;
|
|
@@ -4218,10 +4730,18 @@ var HeartbeatLoop = class {
|
|
|
4218
4730
|
sessionId;
|
|
4219
4731
|
eventQueue;
|
|
4220
4732
|
activityLog;
|
|
4733
|
+
_eventsPath;
|
|
4221
4734
|
customInstructions;
|
|
4222
4735
|
defaultInstructionsPath;
|
|
4223
4736
|
memoryStore = null;
|
|
4224
4737
|
memoryDbPath;
|
|
4738
|
+
onWebhookEvent;
|
|
4739
|
+
decisionStore = null;
|
|
4740
|
+
/** ConfigWatcher for hot-reload support */
|
|
4741
|
+
configWatcher = null;
|
|
4742
|
+
configStore = null;
|
|
4743
|
+
repoPath;
|
|
4744
|
+
configWatcherDebounceMs;
|
|
4225
4745
|
constructor(options) {
|
|
4226
4746
|
this.config = options.config;
|
|
4227
4747
|
this.supervisorDir = options.supervisorDir;
|
|
@@ -4229,8 +4749,16 @@ var HeartbeatLoop = class {
|
|
|
4229
4749
|
this.sessionId = options.sessionId;
|
|
4230
4750
|
this.eventQueue = options.eventQueue;
|
|
4231
4751
|
this.activityLog = options.activityLog;
|
|
4752
|
+
this._eventsPath = options.eventsPath;
|
|
4232
4753
|
this.defaultInstructionsPath = options.defaultInstructionsPath;
|
|
4233
4754
|
this.memoryDbPath = options.memoryDbPath;
|
|
4755
|
+
this.onWebhookEvent = options.onWebhookEvent;
|
|
4756
|
+
this.repoPath = options.repoPath;
|
|
4757
|
+
this.configWatcherDebounceMs = options.configWatcherDebounceMs;
|
|
4758
|
+
}
|
|
4759
|
+
/** Path to the inbox/events directory for markProcessed() calls */
|
|
4760
|
+
get eventsPath() {
|
|
4761
|
+
return this._eventsPath;
|
|
4234
4762
|
}
|
|
4235
4763
|
getMemoryStore() {
|
|
4236
4764
|
if (!this.memoryStore && this.memoryDbPath) {
|
|
@@ -4241,9 +4769,17 @@ var HeartbeatLoop = class {
|
|
|
4241
4769
|
}
|
|
4242
4770
|
return this.memoryStore;
|
|
4243
4771
|
}
|
|
4772
|
+
getDecisionStore() {
|
|
4773
|
+
if (!this.decisionStore) {
|
|
4774
|
+
this.decisionStore = new DecisionStore(path14.join(this.supervisorDir, "decisions.jsonl"));
|
|
4775
|
+
}
|
|
4776
|
+
return this.decisionStore;
|
|
4777
|
+
}
|
|
4244
4778
|
async start() {
|
|
4245
4779
|
this.customInstructions = await this.loadInstructions();
|
|
4780
|
+
await this.initConfigWatcher();
|
|
4246
4781
|
await this.activityLog.log("heartbeat", "Supervisor heartbeat loop started");
|
|
4782
|
+
await this.emitSupervisorStarted();
|
|
4247
4783
|
while (!this.stopping) {
|
|
4248
4784
|
try {
|
|
4249
4785
|
await this.runHeartbeat();
|
|
@@ -4269,23 +4805,60 @@ var HeartbeatLoop = class {
|
|
|
4269
4805
|
if (this.stopping) break;
|
|
4270
4806
|
await this.eventQueue.waitForEvent(this.config.supervisor.eventTimeoutMs);
|
|
4271
4807
|
}
|
|
4808
|
+
await this.emitSupervisorStopped("shutdown");
|
|
4272
4809
|
await this.activityLog.log("heartbeat", "Supervisor heartbeat loop stopped");
|
|
4273
4810
|
}
|
|
4274
4811
|
stop() {
|
|
4275
4812
|
this.stopping = true;
|
|
4276
4813
|
this.activeAbort?.abort(new Error("Supervisor shutting down"));
|
|
4277
4814
|
this.eventQueue.interrupt();
|
|
4815
|
+
if (this.configWatcher) {
|
|
4816
|
+
this.configWatcher.stop();
|
|
4817
|
+
this.configWatcher = null;
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
4821
|
+
* Initialize and start the ConfigWatcher for hot-reload support.
|
|
4822
|
+
* Subscribes to config file changes and logs reload events.
|
|
4823
|
+
*/
|
|
4824
|
+
async initConfigWatcher() {
|
|
4825
|
+
this.configStore = new ConfigStore(this.repoPath);
|
|
4826
|
+
await this.configStore.load();
|
|
4827
|
+
const watcherOptions = this.configWatcherDebounceMs !== void 0 ? { debounceMs: this.configWatcherDebounceMs } : void 0;
|
|
4828
|
+
this.configWatcher = new ConfigWatcher(this.configStore, watcherOptions);
|
|
4829
|
+
this.configWatcher.on("change", () => {
|
|
4830
|
+
this.handleConfigChange();
|
|
4831
|
+
});
|
|
4832
|
+
this.configWatcher.start();
|
|
4833
|
+
await this.activityLog.log("event", "ConfigWatcher started for hot-reload");
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Handle config file changes. Propagates reloaded config to the running
|
|
4837
|
+
* loop and triggers an immediate heartbeat.
|
|
4838
|
+
*/
|
|
4839
|
+
handleConfigChange() {
|
|
4840
|
+
if (this.configStore) {
|
|
4841
|
+
this.config = this.configStore.getAll();
|
|
4842
|
+
}
|
|
4843
|
+
this.activityLog.log("event", "Configuration reloaded (hot-reload)").catch(() => {
|
|
4844
|
+
});
|
|
4845
|
+
this.eventQueue.interrupt();
|
|
4278
4846
|
}
|
|
4279
4847
|
async runHeartbeat() {
|
|
4280
4848
|
const startTime = Date.now();
|
|
4281
|
-
const heartbeatId =
|
|
4849
|
+
const heartbeatId = randomUUID6();
|
|
4282
4850
|
const state = await this.readState();
|
|
4283
4851
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4284
4852
|
const budgetCheck = await this.checkBudgetExceeded(state, today);
|
|
4285
4853
|
if (budgetCheck.exceeded) return;
|
|
4286
|
-
const grouped = this.eventQueue.drainAndGroup();
|
|
4854
|
+
const { grouped, rawEvents } = this.eventQueue.drainAndGroup();
|
|
4287
4855
|
const totalEventCount = grouped.messages.length + grouped.webhooks.length + grouped.runCompletions.length;
|
|
4288
4856
|
const activeRuns = await this.getActiveRuns();
|
|
4857
|
+
const decisionStore = this.getDecisionStore();
|
|
4858
|
+
await this.processDecisionAnswers(rawEvents, decisionStore);
|
|
4859
|
+
await decisionStore.expire();
|
|
4860
|
+
const _pendingDecisions = await decisionStore.pending();
|
|
4861
|
+
void _pendingDecisions;
|
|
4289
4862
|
const skipResult = await this.handleSkipLogic({
|
|
4290
4863
|
state,
|
|
4291
4864
|
totalEventCount,
|
|
@@ -4320,6 +4893,17 @@ var HeartbeatLoop = class {
|
|
|
4320
4893
|
}
|
|
4321
4894
|
);
|
|
4322
4895
|
const { costUsd, turnCount } = await this.callSdk(prompt, heartbeatId);
|
|
4896
|
+
if (turnCount === 0) {
|
|
4897
|
+
await this.activityLog.log(
|
|
4898
|
+
"warning",
|
|
4899
|
+
`Heartbeat #${modeResult.heartbeatCount} completed with turnCount=0. SDK stream may have timed out before any turns completed.`,
|
|
4900
|
+
{ heartbeatId }
|
|
4901
|
+
);
|
|
4902
|
+
}
|
|
4903
|
+
if (rawEvents.length > 0) {
|
|
4904
|
+
const inboxPath = path14.join(this.supervisorDir, "inbox.jsonl");
|
|
4905
|
+
await this.eventQueue.markProcessed(inboxPath, this.eventsPath, rawEvents);
|
|
4906
|
+
}
|
|
4323
4907
|
if (modeResult.isConsolidation) {
|
|
4324
4908
|
const allIds = modeResult.unconsolidated.map((e) => e.id);
|
|
4325
4909
|
if (allIds.length > 0) {
|
|
@@ -4349,6 +4933,27 @@ var HeartbeatLoop = class {
|
|
|
4349
4933
|
isConsolidation: modeResult.isConsolidation
|
|
4350
4934
|
}
|
|
4351
4935
|
);
|
|
4936
|
+
await this.emitHeartbeatCompleted({
|
|
4937
|
+
heartbeatNumber: modeResult.heartbeatCount + 1,
|
|
4938
|
+
runsActive: activeRuns.length,
|
|
4939
|
+
todayUsd: budgetCheck.todayCost + costUsd,
|
|
4940
|
+
limitUsd: this.config.supervisor.dailyCapUsd
|
|
4941
|
+
});
|
|
4942
|
+
for (const event of rawEvents) {
|
|
4943
|
+
if (event.kind === "run_complete") {
|
|
4944
|
+
const runData = await this.readPersistedRun(event.runId);
|
|
4945
|
+
const emitOpts = {
|
|
4946
|
+
runId: event.runId,
|
|
4947
|
+
status: runData?.status === "failed" ? "failed" : "completed",
|
|
4948
|
+
costUsd: runData?.totalCostUsd ?? 0,
|
|
4949
|
+
durationMs: runData?.durationMs ?? 0
|
|
4950
|
+
};
|
|
4951
|
+
if (runData?.output) {
|
|
4952
|
+
emitOpts.output = runData.output;
|
|
4953
|
+
}
|
|
4954
|
+
await this.emitRunCompleted(emitOpts);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4352
4957
|
}
|
|
4353
4958
|
/**
|
|
4354
4959
|
* Check if supervisor daily budget is exceeded.
|
|
@@ -4424,7 +5029,6 @@ var HeartbeatLoop = class {
|
|
|
4424
5029
|
isCompaction,
|
|
4425
5030
|
unconsolidated,
|
|
4426
5031
|
heartbeatCount,
|
|
4427
|
-
lastConsolidation,
|
|
4428
5032
|
lastConsolidationTs
|
|
4429
5033
|
};
|
|
4430
5034
|
}
|
|
@@ -4504,6 +5108,12 @@ var HeartbeatLoop = class {
|
|
|
4504
5108
|
}
|
|
4505
5109
|
/**
|
|
4506
5110
|
* Call the Claude SDK and stream results.
|
|
5111
|
+
*
|
|
5112
|
+
* Uses Promise.race to enable non-blocking abort detection. The standard
|
|
5113
|
+
* `for await (const message of stream)` pattern only checks the abort signal
|
|
5114
|
+
* AFTER each yield — if the SDK hangs (no messages), the abort never executes.
|
|
5115
|
+
* This implementation races each iterator.next() against an abort promise,
|
|
5116
|
+
* allowing immediate response to shutdown/timeout signals.
|
|
4507
5117
|
*/
|
|
4508
5118
|
async callSdk(prompt, heartbeatId) {
|
|
4509
5119
|
const abortController = new AbortController();
|
|
@@ -4523,25 +5133,48 @@ var HeartbeatLoop = class {
|
|
|
4523
5133
|
}
|
|
4524
5134
|
}
|
|
4525
5135
|
const queryOptions = {
|
|
4526
|
-
cwd:
|
|
5136
|
+
cwd: homedir4(),
|
|
4527
5137
|
allowedTools,
|
|
4528
5138
|
permissionMode: "bypassPermissions",
|
|
4529
5139
|
allowDangerouslySkipPermissions: true,
|
|
4530
|
-
mcpServers: this.config.mcpServers ?? {}
|
|
5140
|
+
mcpServers: this.config.mcpServers ?? {},
|
|
5141
|
+
// Don't persist session history — each heartbeat is a fresh conversation.
|
|
5142
|
+
// Without this, supervisor restarts could replay old messages.
|
|
5143
|
+
persistSession: false
|
|
4531
5144
|
};
|
|
4532
5145
|
const stream = sdk.query({ prompt, options: queryOptions });
|
|
4533
|
-
|
|
4534
|
-
if (abortController.signal.aborted)
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
this.sessionId = msg.session_id;
|
|
5146
|
+
const abortPromise = new Promise((resolve4) => {
|
|
5147
|
+
if (abortController.signal.aborted) {
|
|
5148
|
+
resolve4({ aborted: true });
|
|
5149
|
+
return;
|
|
4538
5150
|
}
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
5151
|
+
abortController.signal.addEventListener("abort", () => resolve4({ aborted: true }), {
|
|
5152
|
+
once: true
|
|
5153
|
+
});
|
|
5154
|
+
});
|
|
5155
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
5156
|
+
try {
|
|
5157
|
+
while (true) {
|
|
5158
|
+
const raceResult = await Promise.race([iterator.next(), abortPromise]);
|
|
5159
|
+
if ("aborted" in raceResult) {
|
|
5160
|
+
await this.activityLog.log("heartbeat", "Heartbeat aborted", { heartbeatId });
|
|
5161
|
+
break;
|
|
5162
|
+
}
|
|
5163
|
+
const iterResult = raceResult;
|
|
5164
|
+
if (iterResult.done) break;
|
|
5165
|
+
const msg = iterResult.value;
|
|
5166
|
+
if (isInitMessage(msg)) {
|
|
5167
|
+
this.sessionId = msg.session_id;
|
|
5168
|
+
}
|
|
5169
|
+
if (isResultMessage(msg)) {
|
|
5170
|
+
output = msg.result ?? "";
|
|
5171
|
+
costUsd = msg.total_cost_usd ?? 0;
|
|
5172
|
+
turnCount = msg.num_turns ?? 0;
|
|
5173
|
+
}
|
|
5174
|
+
await this.logStreamMessage(msg, heartbeatId);
|
|
4543
5175
|
}
|
|
4544
|
-
|
|
5176
|
+
} finally {
|
|
5177
|
+
await iterator.return?.();
|
|
4545
5178
|
}
|
|
4546
5179
|
} finally {
|
|
4547
5180
|
clearTimeout(timeout);
|
|
@@ -4562,29 +5195,33 @@ var HeartbeatLoop = class {
|
|
|
4562
5195
|
const raw = await readFile11(this.statePath, "utf-8");
|
|
4563
5196
|
const state = JSON.parse(raw);
|
|
4564
5197
|
Object.assign(state, updates);
|
|
4565
|
-
await
|
|
5198
|
+
await writeFile6(this.statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
4566
5199
|
} catch {
|
|
4567
5200
|
}
|
|
4568
5201
|
}
|
|
4569
|
-
/**
|
|
5202
|
+
/**
|
|
5203
|
+
* Read persisted run files and return summaries of active (running/paused) runs.
|
|
5204
|
+
* Validates that "running" runs are actually alive by checking their PID.
|
|
5205
|
+
* Stale runs (dead PID past grace period) are filtered out to prevent ghost runs.
|
|
5206
|
+
*/
|
|
4570
5207
|
async getActiveRuns() {
|
|
4571
5208
|
const runsDir = getRunsDir();
|
|
4572
5209
|
if (!existsSync7(runsDir)) return [];
|
|
4573
5210
|
try {
|
|
4574
|
-
const entries = await
|
|
5211
|
+
const entries = await readdir4(runsDir, { withFileTypes: true });
|
|
4575
5212
|
const active = [];
|
|
4576
5213
|
for (const entry of entries) {
|
|
4577
5214
|
if (!entry.isDirectory()) continue;
|
|
4578
5215
|
const subDir = path14.join(runsDir, entry.name);
|
|
4579
|
-
const files = await
|
|
5216
|
+
const files = await readdir4(subDir);
|
|
4580
5217
|
for (const f of files) {
|
|
4581
5218
|
if (!f.endsWith(".json")) continue;
|
|
4582
5219
|
try {
|
|
4583
5220
|
const raw = await readFile11(path14.join(subDir, f), "utf-8");
|
|
4584
5221
|
const run = JSON.parse(raw);
|
|
4585
|
-
if (run
|
|
5222
|
+
if (isRunActive(run)) {
|
|
4586
5223
|
active.push(
|
|
4587
|
-
`${run.runId} [${run.status}] ${run.
|
|
5224
|
+
`${run.runId} [${run.status}] ${run.agent} on ${path14.basename(run.repo)}`
|
|
4588
5225
|
);
|
|
4589
5226
|
}
|
|
4590
5227
|
} catch {
|
|
@@ -4663,21 +5300,191 @@ var HeartbeatLoop = class {
|
|
|
4663
5300
|
if (!isToolResultMessage(msg)) return;
|
|
4664
5301
|
const result = msg.result ?? "";
|
|
4665
5302
|
const runMatch = /Run\s+(\S+)\s+dispatched/i.exec(result);
|
|
4666
|
-
|
|
4667
|
-
|
|
5303
|
+
const runId = runMatch?.[1];
|
|
5304
|
+
if (runId) {
|
|
5305
|
+
await this.activityLog.log("dispatch", `Agent dispatched: ${runId}`, {
|
|
4668
5306
|
heartbeatId,
|
|
4669
|
-
runId
|
|
5307
|
+
runId
|
|
5308
|
+
});
|
|
5309
|
+
const agentMatch = /agent[:\s]+(\S+)/i.exec(result);
|
|
5310
|
+
const repoMatch = /repo[:\s]+(\S+)/i.exec(result);
|
|
5311
|
+
const branchMatch = /branch[:\s]+(\S+)/i.exec(result);
|
|
5312
|
+
const agent = agentMatch?.[1] ?? "unknown";
|
|
5313
|
+
const repo = repoMatch?.[1] ?? "unknown";
|
|
5314
|
+
const branch = branchMatch?.[1] ?? "unknown";
|
|
5315
|
+
await this.emitRunDispatched({
|
|
5316
|
+
runId,
|
|
5317
|
+
agent,
|
|
5318
|
+
repo,
|
|
5319
|
+
branch,
|
|
5320
|
+
prompt: result.slice(0, 500)
|
|
4670
5321
|
});
|
|
4671
5322
|
}
|
|
4672
5323
|
}
|
|
4673
5324
|
sleep(ms) {
|
|
4674
5325
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
4675
5326
|
}
|
|
5327
|
+
/**
|
|
5328
|
+
* Process decision:answer events from inbox messages.
|
|
5329
|
+
* Expected format: "decision:answer <decisionId> <answer>"
|
|
5330
|
+
*/
|
|
5331
|
+
async processDecisionAnswers(rawEvents, store) {
|
|
5332
|
+
for (const event of rawEvents) {
|
|
5333
|
+
if (event.kind !== "message") continue;
|
|
5334
|
+
const text = event.data.text.trim();
|
|
5335
|
+
const match = /^decision:answer\s+(\S+)\s+(.+)$/i.exec(text);
|
|
5336
|
+
if (!match) continue;
|
|
5337
|
+
const decisionId = match[1];
|
|
5338
|
+
const answer = match[2];
|
|
5339
|
+
if (!decisionId || !answer) continue;
|
|
5340
|
+
try {
|
|
5341
|
+
await store.answer(decisionId, answer);
|
|
5342
|
+
await this.activityLog.log("event", `Decision answered: ${decisionId}`, {
|
|
5343
|
+
decisionId,
|
|
5344
|
+
answer
|
|
5345
|
+
});
|
|
5346
|
+
} catch (error) {
|
|
5347
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5348
|
+
await this.activityLog.log("error", `Failed to answer decision ${decisionId}: ${msg}`, {
|
|
5349
|
+
decisionId,
|
|
5350
|
+
answer
|
|
5351
|
+
});
|
|
5352
|
+
}
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
/**
|
|
5356
|
+
* Read persisted run data to extract actual status, cost, and duration.
|
|
5357
|
+
* Returns null if the run file cannot be found or parsed.
|
|
5358
|
+
*/
|
|
5359
|
+
async readPersistedRun(runId) {
|
|
5360
|
+
const runsDir = getRunsDir();
|
|
5361
|
+
if (!existsSync7(runsDir)) return null;
|
|
5362
|
+
try {
|
|
5363
|
+
const entries = await readdir4(runsDir, { withFileTypes: true });
|
|
5364
|
+
for (const entry of entries) {
|
|
5365
|
+
if (!entry.isDirectory()) continue;
|
|
5366
|
+
const subDir = path14.join(runsDir, entry.name);
|
|
5367
|
+
const runPath = path14.join(subDir, `${runId}.json`);
|
|
5368
|
+
if (existsSync7(runPath)) {
|
|
5369
|
+
const raw = await readFile11(runPath, "utf-8");
|
|
5370
|
+
const run = JSON.parse(raw);
|
|
5371
|
+
const totalCostUsd = Object.values(run.steps).reduce(
|
|
5372
|
+
(sum, step) => sum + (step.costUsd ?? 0),
|
|
5373
|
+
0
|
|
5374
|
+
);
|
|
5375
|
+
const durationMs = new Date(run.updatedAt).getTime() - new Date(run.createdAt).getTime();
|
|
5376
|
+
const completedSteps = Object.values(run.steps).filter(
|
|
5377
|
+
(s) => s.status === "success" || s.status === "failure"
|
|
5378
|
+
);
|
|
5379
|
+
const lastStep = completedSteps[completedSteps.length - 1];
|
|
5380
|
+
const output = typeof lastStep?.rawOutput === "string" ? lastStep.rawOutput.slice(0, 1e3) : void 0;
|
|
5381
|
+
return { status: run.status, totalCostUsd, durationMs, output };
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
} catch {
|
|
5385
|
+
}
|
|
5386
|
+
return null;
|
|
5387
|
+
}
|
|
5388
|
+
// ─── Webhook event emission ───────────────────────────────
|
|
5389
|
+
/**
|
|
5390
|
+
* Emit a webhook event if a callback is configured.
|
|
5391
|
+
* Validates the event against its schema before emission.
|
|
5392
|
+
*/
|
|
5393
|
+
async emitWebhookEvent(event) {
|
|
5394
|
+
if (!this.onWebhookEvent) return;
|
|
5395
|
+
try {
|
|
5396
|
+
switch (event.type) {
|
|
5397
|
+
case "supervisor_started":
|
|
5398
|
+
supervisorStartedEventSchema.parse(event);
|
|
5399
|
+
break;
|
|
5400
|
+
case "heartbeat":
|
|
5401
|
+
heartbeatEventSchema.parse(event);
|
|
5402
|
+
break;
|
|
5403
|
+
case "run_dispatched":
|
|
5404
|
+
runDispatchedEventSchema.parse(event);
|
|
5405
|
+
break;
|
|
5406
|
+
case "run_completed":
|
|
5407
|
+
runCompletedEventSchema.parse(event);
|
|
5408
|
+
break;
|
|
5409
|
+
case "supervisor_stopped":
|
|
5410
|
+
supervisorStoppedEventSchema.parse(event);
|
|
5411
|
+
break;
|
|
5412
|
+
}
|
|
5413
|
+
await this.onWebhookEvent(event);
|
|
5414
|
+
} catch (error) {
|
|
5415
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5416
|
+
await this.activityLog.log("error", `Webhook event emission failed: ${msg}`, {
|
|
5417
|
+
eventType: event.type
|
|
5418
|
+
});
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
/** Emit SupervisorStartedEvent */
|
|
5422
|
+
async emitSupervisorStarted() {
|
|
5423
|
+
const event = {
|
|
5424
|
+
type: "supervisor_started",
|
|
5425
|
+
supervisorId: this.sessionId,
|
|
5426
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5427
|
+
};
|
|
5428
|
+
await this.emitWebhookEvent(event);
|
|
5429
|
+
}
|
|
5430
|
+
/** Emit SupervisorStoppedEvent */
|
|
5431
|
+
async emitSupervisorStopped(reason) {
|
|
5432
|
+
const event = {
|
|
5433
|
+
type: "supervisor_stopped",
|
|
5434
|
+
supervisorId: this.sessionId,
|
|
5435
|
+
stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5436
|
+
reason
|
|
5437
|
+
};
|
|
5438
|
+
await this.emitWebhookEvent(event);
|
|
5439
|
+
}
|
|
5440
|
+
/** Emit HeartbeatEvent */
|
|
5441
|
+
async emitHeartbeatCompleted(opts) {
|
|
5442
|
+
const event = {
|
|
5443
|
+
type: "heartbeat",
|
|
5444
|
+
supervisorId: this.sessionId,
|
|
5445
|
+
heartbeatNumber: opts.heartbeatNumber,
|
|
5446
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5447
|
+
runsActive: opts.runsActive,
|
|
5448
|
+
budget: {
|
|
5449
|
+
todayUsd: opts.todayUsd,
|
|
5450
|
+
limitUsd: opts.limitUsd
|
|
5451
|
+
}
|
|
5452
|
+
};
|
|
5453
|
+
await this.emitWebhookEvent(event);
|
|
5454
|
+
}
|
|
5455
|
+
/** Emit RunDispatchedEvent from tool result detection */
|
|
5456
|
+
async emitRunDispatched(opts) {
|
|
5457
|
+
const event = {
|
|
5458
|
+
type: "run_dispatched",
|
|
5459
|
+
supervisorId: this.sessionId,
|
|
5460
|
+
runId: opts.runId,
|
|
5461
|
+
agent: opts.agent,
|
|
5462
|
+
repo: opts.repo,
|
|
5463
|
+
branch: opts.branch,
|
|
5464
|
+
prompt: opts.prompt.slice(0, 500)
|
|
5465
|
+
// Truncate to schema max
|
|
5466
|
+
};
|
|
5467
|
+
await this.emitWebhookEvent(event);
|
|
5468
|
+
}
|
|
5469
|
+
/** Emit RunCompletedEvent when processing run_complete events */
|
|
5470
|
+
async emitRunCompleted(opts) {
|
|
5471
|
+
const event = {
|
|
5472
|
+
type: "run_completed",
|
|
5473
|
+
supervisorId: this.sessionId,
|
|
5474
|
+
runId: opts.runId,
|
|
5475
|
+
status: opts.status,
|
|
5476
|
+
output: opts.output?.slice(0, 1e3),
|
|
5477
|
+
// Truncate to schema max
|
|
5478
|
+
costUsd: opts.costUsd,
|
|
5479
|
+
durationMs: opts.durationMs
|
|
5480
|
+
};
|
|
5481
|
+
await this.emitWebhookEvent(event);
|
|
5482
|
+
}
|
|
4676
5483
|
};
|
|
4677
5484
|
|
|
4678
5485
|
// src/supervisor/webhook-server.ts
|
|
4679
5486
|
import { createHmac as createHmac2, timingSafeEqual } from "crypto";
|
|
4680
|
-
import { appendFile as
|
|
5487
|
+
import { appendFile as appendFile7 } from "fs/promises";
|
|
4681
5488
|
import { createServer } from "http";
|
|
4682
5489
|
var MAX_BODY_SIZE = 1024 * 1024;
|
|
4683
5490
|
var WebhookServer = class {
|
|
@@ -4762,7 +5569,7 @@ var WebhookServer = class {
|
|
|
4762
5569
|
payload: parsed.payload ?? parsed,
|
|
4763
5570
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4764
5571
|
};
|
|
4765
|
-
await
|
|
5572
|
+
await appendFile7(this.eventsPath, `${JSON.stringify(event)}
|
|
4766
5573
|
`, "utf-8");
|
|
4767
5574
|
this.onEvent(event);
|
|
4768
5575
|
this.sendJson(res, 200, { ok: true, id: event.id });
|
|
@@ -4802,6 +5609,7 @@ var SupervisorDaemon = class {
|
|
|
4802
5609
|
eventQueue = null;
|
|
4803
5610
|
heartbeatLoop = null;
|
|
4804
5611
|
activityLog = null;
|
|
5612
|
+
decisionStore = null;
|
|
4805
5613
|
sessionId = "";
|
|
4806
5614
|
constructor(options) {
|
|
4807
5615
|
this.name = options.name;
|
|
@@ -4822,16 +5630,17 @@ var SupervisorDaemon = class {
|
|
|
4822
5630
|
await rm2(lockPath, { force: true });
|
|
4823
5631
|
}
|
|
4824
5632
|
const tempLock = `${lockPath}.${process.pid}`;
|
|
4825
|
-
await
|
|
5633
|
+
await writeFile7(tempLock, String(process.pid), "utf-8");
|
|
4826
5634
|
const { rename: rename2 } = await import("fs/promises");
|
|
4827
5635
|
await rename2(tempLock, lockPath);
|
|
4828
5636
|
const existingState = await this.readState();
|
|
4829
5637
|
if (existingState?.sessionId && existingState.status !== "stopped") {
|
|
4830
5638
|
this.sessionId = existingState.sessionId;
|
|
4831
5639
|
} else {
|
|
4832
|
-
this.sessionId =
|
|
5640
|
+
this.sessionId = randomUUID7();
|
|
4833
5641
|
}
|
|
4834
5642
|
this.activityLog = new ActivityLog(this.dir);
|
|
5643
|
+
this.decisionStore = new DecisionStore(getSupervisorDecisionsPath(this.name));
|
|
4835
5644
|
this.eventQueue = new EventQueue({
|
|
4836
5645
|
maxEventsPerSec: this.config.supervisor.maxEventsPerSec
|
|
4837
5646
|
});
|
|
@@ -4845,6 +5654,12 @@ var SupervisorDaemon = class {
|
|
|
4845
5654
|
eventsPath,
|
|
4846
5655
|
onEvent: (event) => {
|
|
4847
5656
|
this.eventQueue?.push({ kind: "webhook", data: event });
|
|
5657
|
+
this.handleDecisionAnswer(event).catch((err) => {
|
|
5658
|
+
this.activityLog?.log(
|
|
5659
|
+
"error",
|
|
5660
|
+
`Failed to handle decision:answer: ${err instanceof Error ? err.message : String(err)}`
|
|
5661
|
+
);
|
|
5662
|
+
});
|
|
4848
5663
|
if ((event.event === "session:complete" || event.event === "session:fail") && event.payload) {
|
|
4849
5664
|
const runId = typeof event.payload.runId === "string" ? event.payload.runId : void 0;
|
|
4850
5665
|
if (runId) {
|
|
@@ -4863,7 +5678,7 @@ var SupervisorDaemon = class {
|
|
|
4863
5678
|
pid: process.pid,
|
|
4864
5679
|
sessionId: this.sessionId,
|
|
4865
5680
|
port: this.config.supervisor.port,
|
|
4866
|
-
cwd:
|
|
5681
|
+
cwd: homedir5(),
|
|
4867
5682
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4868
5683
|
lastHeartbeat: existingState?.lastHeartbeat,
|
|
4869
5684
|
heartbeatCount: existingState?.heartbeatCount ?? 0,
|
|
@@ -4894,6 +5709,7 @@ var SupervisorDaemon = class {
|
|
|
4894
5709
|
sessionId: this.sessionId,
|
|
4895
5710
|
eventQueue: this.eventQueue,
|
|
4896
5711
|
activityLog: this.activityLog,
|
|
5712
|
+
eventsPath,
|
|
4897
5713
|
defaultInstructionsPath: this.defaultInstructionsPath
|
|
4898
5714
|
});
|
|
4899
5715
|
await this.heartbeatLoop.start();
|
|
@@ -4936,7 +5752,7 @@ var SupervisorDaemon = class {
|
|
|
4936
5752
|
}
|
|
4937
5753
|
async writeState(state) {
|
|
4938
5754
|
const statePath = path15.join(this.dir, "state.json");
|
|
4939
|
-
await
|
|
5755
|
+
await writeFile7(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
4940
5756
|
}
|
|
4941
5757
|
async readLockPid(lockPath) {
|
|
4942
5758
|
try {
|
|
@@ -4947,14 +5763,248 @@ var SupervisorDaemon = class {
|
|
|
4947
5763
|
return null;
|
|
4948
5764
|
}
|
|
4949
5765
|
}
|
|
5766
|
+
/**
|
|
5767
|
+
* Handle decision:answer webhook events.
|
|
5768
|
+
* Extracts decisionId and answer from the payload and records the answer.
|
|
5769
|
+
*/
|
|
5770
|
+
async handleDecisionAnswer(event) {
|
|
5771
|
+
if (event.event !== "decision:answer") return;
|
|
5772
|
+
if (!this.decisionStore || !event.payload) return;
|
|
5773
|
+
const decisionId = typeof event.payload.decisionId === "string" ? event.payload.decisionId : void 0;
|
|
5774
|
+
const answer = typeof event.payload.answer === "string" ? event.payload.answer : void 0;
|
|
5775
|
+
if (!decisionId || !answer) {
|
|
5776
|
+
await this.activityLog?.log(
|
|
5777
|
+
"error",
|
|
5778
|
+
`decision:answer webhook missing required fields (decisionId: ${decisionId}, answer: ${answer})`
|
|
5779
|
+
);
|
|
5780
|
+
return;
|
|
5781
|
+
}
|
|
5782
|
+
try {
|
|
5783
|
+
await this.decisionStore.answer(decisionId, answer);
|
|
5784
|
+
await this.activityLog?.log(
|
|
5785
|
+
"decision",
|
|
5786
|
+
`Decision ${decisionId} answered via webhook: "${answer}"`
|
|
5787
|
+
);
|
|
5788
|
+
} catch (err) {
|
|
5789
|
+
await this.activityLog?.log(
|
|
5790
|
+
"error",
|
|
5791
|
+
`Failed to answer decision ${decisionId}: ${err instanceof Error ? err.message : String(err)}`
|
|
5792
|
+
);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
};
|
|
5796
|
+
|
|
5797
|
+
// src/supervisor/StatusReader.ts
|
|
5798
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5799
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
5800
|
+
import path16 from "path";
|
|
5801
|
+
var STATE_FILE = "state.json";
|
|
5802
|
+
var ACTIVITY_FILE2 = "activity.jsonl";
|
|
5803
|
+
var StatusReader = class {
|
|
5804
|
+
dataDir;
|
|
5805
|
+
statePath;
|
|
5806
|
+
activityPath;
|
|
5807
|
+
constructor(dataDir) {
|
|
5808
|
+
this.dataDir = dataDir;
|
|
5809
|
+
this.statePath = path16.join(dataDir, STATE_FILE);
|
|
5810
|
+
this.activityPath = path16.join(dataDir, ACTIVITY_FILE2);
|
|
5811
|
+
}
|
|
5812
|
+
/**
|
|
5813
|
+
* Read and parse supervisor status from disk.
|
|
5814
|
+
* Returns null if the state file doesn't exist (supervisor not running).
|
|
5815
|
+
*/
|
|
5816
|
+
async getStatus() {
|
|
5817
|
+
let raw;
|
|
5818
|
+
try {
|
|
5819
|
+
raw = await readFile13(this.statePath, "utf-8");
|
|
5820
|
+
} catch {
|
|
5821
|
+
return null;
|
|
5822
|
+
}
|
|
5823
|
+
let parsed;
|
|
5824
|
+
try {
|
|
5825
|
+
parsed = JSON.parse(raw);
|
|
5826
|
+
} catch {
|
|
5827
|
+
return null;
|
|
5828
|
+
}
|
|
5829
|
+
const result = supervisorDaemonStateSchema.safeParse(parsed);
|
|
5830
|
+
if (!result.success) {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
const daemon = result.data;
|
|
5834
|
+
const statusMap = {
|
|
5835
|
+
running: "running",
|
|
5836
|
+
draining: "stopping",
|
|
5837
|
+
stopped: "idle"
|
|
5838
|
+
};
|
|
5839
|
+
const recentActivity = this.queryActivity({ limit: 5 });
|
|
5840
|
+
return {
|
|
5841
|
+
pid: daemon.pid,
|
|
5842
|
+
sessionId: daemon.sessionId,
|
|
5843
|
+
startedAt: daemon.startedAt,
|
|
5844
|
+
heartbeatCount: daemon.heartbeatCount,
|
|
5845
|
+
totalCostUsd: daemon.totalCostUsd,
|
|
5846
|
+
todayCostUsd: daemon.todayCostUsd,
|
|
5847
|
+
status: statusMap[daemon.status],
|
|
5848
|
+
lastHeartbeat: daemon.lastHeartbeat ?? daemon.startedAt,
|
|
5849
|
+
activeRunCount: 0,
|
|
5850
|
+
// TODO: count active runs from .neo/runs/
|
|
5851
|
+
recentActivitySummary: recentActivity.map((e) => `[${e.type}] ${e.summary}`)
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
5854
|
+
/**
|
|
5855
|
+
* Query activity entries with optional filtering.
|
|
5856
|
+
* Returns empty array if the activity file doesn't exist or is empty.
|
|
5857
|
+
*/
|
|
5858
|
+
queryActivity(options = {}) {
|
|
5859
|
+
const { limit = 50, offset = 0, type, since, until } = options;
|
|
5860
|
+
let content;
|
|
5861
|
+
try {
|
|
5862
|
+
content = readFileSync2(this.activityPath, "utf-8");
|
|
5863
|
+
} catch {
|
|
5864
|
+
return [];
|
|
5865
|
+
}
|
|
5866
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
5867
|
+
let entries = [];
|
|
5868
|
+
for (const line of lines) {
|
|
5869
|
+
try {
|
|
5870
|
+
const parsed = JSON.parse(line);
|
|
5871
|
+
const result = activityEntrySchema.safeParse(parsed);
|
|
5872
|
+
if (result.success) {
|
|
5873
|
+
entries.push(result.data);
|
|
5874
|
+
}
|
|
5875
|
+
} catch {
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
if (type) {
|
|
5879
|
+
entries = entries.filter((e) => e.type === type);
|
|
5880
|
+
}
|
|
5881
|
+
if (since) {
|
|
5882
|
+
const sinceDate = new Date(since);
|
|
5883
|
+
entries = entries.filter((e) => new Date(e.timestamp) >= sinceDate);
|
|
5884
|
+
}
|
|
5885
|
+
if (until) {
|
|
5886
|
+
const untilDate = new Date(until);
|
|
5887
|
+
entries = entries.filter((e) => new Date(e.timestamp) <= untilDate);
|
|
5888
|
+
}
|
|
5889
|
+
return entries.slice(offset, offset + limit);
|
|
5890
|
+
}
|
|
4950
5891
|
};
|
|
4951
5892
|
|
|
5893
|
+
// src/supervisor/shutdown.ts
|
|
5894
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5895
|
+
import { readdir as readdir5, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
5896
|
+
import path17 from "path";
|
|
5897
|
+
|
|
5898
|
+
// src/webhook-config.ts
|
|
5899
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5900
|
+
import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
|
|
5901
|
+
import path18 from "path";
|
|
5902
|
+
import { z as z7 } from "zod";
|
|
5903
|
+
var webhookEntrySchema = z7.object({
|
|
5904
|
+
url: z7.string().url(),
|
|
5905
|
+
events: z7.array(z7.string()).optional(),
|
|
5906
|
+
secret: z7.string().optional(),
|
|
5907
|
+
timeoutMs: z7.number().default(5e3),
|
|
5908
|
+
createdAt: z7.string().default(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
5909
|
+
});
|
|
5910
|
+
var webhooksConfigSchema = z7.object({
|
|
5911
|
+
webhooks: z7.array(webhookEntrySchema).default([])
|
|
5912
|
+
});
|
|
5913
|
+
function getWebhooksConfigPath() {
|
|
5914
|
+
return path18.join(getDataDir(), "webhooks.json");
|
|
5915
|
+
}
|
|
5916
|
+
async function loadWebhooksConfig() {
|
|
5917
|
+
const configPath = getWebhooksConfigPath();
|
|
5918
|
+
if (!existsSync10(configPath)) {
|
|
5919
|
+
return { webhooks: [] };
|
|
5920
|
+
}
|
|
5921
|
+
const raw = await readFile15(configPath, "utf-8");
|
|
5922
|
+
const parsed = JSON.parse(raw);
|
|
5923
|
+
return webhooksConfigSchema.parse(parsed);
|
|
5924
|
+
}
|
|
5925
|
+
async function saveWebhooksConfig(config) {
|
|
5926
|
+
const configPath = getWebhooksConfigPath();
|
|
5927
|
+
await mkdir8(getDataDir(), { recursive: true });
|
|
5928
|
+
await writeFile9(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
5929
|
+
}
|
|
5930
|
+
async function addWebhook(input) {
|
|
5931
|
+
const config = await loadWebhooksConfig();
|
|
5932
|
+
const entry = webhookEntrySchema.parse(input);
|
|
5933
|
+
const existing = config.webhooks.findIndex((w) => w.url === entry.url);
|
|
5934
|
+
if (existing >= 0) {
|
|
5935
|
+
config.webhooks[existing] = entry;
|
|
5936
|
+
} else {
|
|
5937
|
+
config.webhooks.push(entry);
|
|
5938
|
+
}
|
|
5939
|
+
await saveWebhooksConfig(config);
|
|
5940
|
+
return entry;
|
|
5941
|
+
}
|
|
5942
|
+
async function removeWebhook(url) {
|
|
5943
|
+
const config = await loadWebhooksConfig();
|
|
5944
|
+
const initialLength = config.webhooks.length;
|
|
5945
|
+
config.webhooks = config.webhooks.filter((w) => w.url !== url);
|
|
5946
|
+
if (config.webhooks.length === initialLength) {
|
|
5947
|
+
return false;
|
|
5948
|
+
}
|
|
5949
|
+
await saveWebhooksConfig(config);
|
|
5950
|
+
return true;
|
|
5951
|
+
}
|
|
5952
|
+
async function listWebhooks() {
|
|
5953
|
+
const config = await loadWebhooksConfig();
|
|
5954
|
+
return config.webhooks;
|
|
5955
|
+
}
|
|
5956
|
+
async function testWebhooks() {
|
|
5957
|
+
const webhooks = await listWebhooks();
|
|
5958
|
+
if (webhooks.length === 0) {
|
|
5959
|
+
return [];
|
|
5960
|
+
}
|
|
5961
|
+
const payload = {
|
|
5962
|
+
type: "test",
|
|
5963
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5964
|
+
runId: `test-${Date.now()}`,
|
|
5965
|
+
status: "test",
|
|
5966
|
+
summary: "Test webhook from neo CLI"
|
|
5967
|
+
};
|
|
5968
|
+
const results = await Promise.all(
|
|
5969
|
+
webhooks.map(async (webhook) => {
|
|
5970
|
+
const start = Date.now();
|
|
5971
|
+
const body = JSON.stringify(payload);
|
|
5972
|
+
try {
|
|
5973
|
+
const response = await fetch(webhook.url, {
|
|
5974
|
+
method: "POST",
|
|
5975
|
+
headers: {
|
|
5976
|
+
"Content-Type": "application/json"
|
|
5977
|
+
},
|
|
5978
|
+
body,
|
|
5979
|
+
signal: AbortSignal.timeout(webhook.timeoutMs)
|
|
5980
|
+
});
|
|
5981
|
+
return {
|
|
5982
|
+
url: webhook.url,
|
|
5983
|
+
success: response.ok,
|
|
5984
|
+
statusCode: response.status,
|
|
5985
|
+
durationMs: Date.now() - start
|
|
5986
|
+
};
|
|
5987
|
+
} catch (err) {
|
|
5988
|
+
return {
|
|
5989
|
+
url: webhook.url,
|
|
5990
|
+
success: false,
|
|
5991
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5992
|
+
durationMs: Date.now() - start
|
|
5993
|
+
};
|
|
5994
|
+
}
|
|
5995
|
+
})
|
|
5996
|
+
);
|
|
5997
|
+
return results;
|
|
5998
|
+
}
|
|
5999
|
+
|
|
4952
6000
|
// src/index.ts
|
|
4953
6001
|
var VERSION = "0.1.0";
|
|
4954
6002
|
export {
|
|
4955
6003
|
ActivityLog,
|
|
4956
6004
|
AgentRegistry,
|
|
6005
|
+
ConfigStore,
|
|
4957
6006
|
CostJournal,
|
|
6007
|
+
DecisionStore,
|
|
4958
6008
|
EventJournal,
|
|
4959
6009
|
EventQueue,
|
|
4960
6010
|
HeartbeatLoop,
|
|
@@ -4965,13 +6015,14 @@ export {
|
|
|
4965
6015
|
Semaphore,
|
|
4966
6016
|
SessionError,
|
|
4967
6017
|
SessionExecutor,
|
|
6018
|
+
StatusReader,
|
|
4968
6019
|
SupervisorDaemon,
|
|
4969
6020
|
VERSION,
|
|
4970
6021
|
WebhookDispatcher,
|
|
4971
6022
|
WebhookServer,
|
|
4972
|
-
WorkflowRegistry,
|
|
4973
6023
|
activityEntrySchema,
|
|
4974
6024
|
addRepoToGlobalConfig,
|
|
6025
|
+
addWebhook,
|
|
4975
6026
|
agentConfigSchema,
|
|
4976
6027
|
agentModelSchema,
|
|
4977
6028
|
agentSandboxSchema,
|
|
@@ -4988,6 +6039,8 @@ export {
|
|
|
4988
6039
|
buildSandboxConfig,
|
|
4989
6040
|
createBranch,
|
|
4990
6041
|
createSessionClone,
|
|
6042
|
+
decisionOptionSchema,
|
|
6043
|
+
decisionSchema,
|
|
4991
6044
|
deleteBranch,
|
|
4992
6045
|
fetchRemote,
|
|
4993
6046
|
getBranchName,
|
|
@@ -4999,6 +6052,7 @@ export {
|
|
|
4999
6052
|
getRunLogPath,
|
|
5000
6053
|
getRunsDir,
|
|
5001
6054
|
getSupervisorActivityPath,
|
|
6055
|
+
getSupervisorDecisionsPath,
|
|
5002
6056
|
getSupervisorDir,
|
|
5003
6057
|
getSupervisorEventsPath,
|
|
5004
6058
|
getSupervisorInboxPath,
|
|
@@ -5010,11 +6064,11 @@ export {
|
|
|
5010
6064
|
isProcessAlive,
|
|
5011
6065
|
listReposFromGlobalConfig,
|
|
5012
6066
|
listSessionClones,
|
|
6067
|
+
listWebhooks,
|
|
5013
6068
|
loadAgentFile,
|
|
5014
6069
|
loadConfig,
|
|
5015
6070
|
loadGlobalConfig,
|
|
5016
6071
|
loadRepoInstructions,
|
|
5017
|
-
loadWorkflow,
|
|
5018
6072
|
loopDetection,
|
|
5019
6073
|
matchesFilter,
|
|
5020
6074
|
mcpServerConfigSchema,
|
|
@@ -5024,15 +6078,18 @@ export {
|
|
|
5024
6078
|
pushSessionBranch,
|
|
5025
6079
|
removeRepoFromGlobalConfig,
|
|
5026
6080
|
removeSessionClone,
|
|
6081
|
+
removeWebhook,
|
|
5027
6082
|
repoConfigSchema,
|
|
6083
|
+
repoOverrideConfigSchema,
|
|
5028
6084
|
resolveAgent,
|
|
5029
6085
|
runSession,
|
|
5030
6086
|
runWithRecovery,
|
|
5031
6087
|
supervisorDaemonStateSchema,
|
|
5032
6088
|
supervisorDaemonStateSchema as supervisorStateSchema,
|
|
6089
|
+
supervisorStatusSchema,
|
|
6090
|
+
testWebhooks,
|
|
5033
6091
|
toRepoSlug,
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
workflowStepDefSchema
|
|
6092
|
+
webhookEntrySchema,
|
|
6093
|
+
webhookIncomingEventSchema
|
|
5037
6094
|
};
|
|
5038
6095
|
//# sourceMappingURL=index.js.map
|