agentflow-core 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,10 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ auditProcesses: () => auditProcesses,
23
24
  checkGuards: () => checkGuards,
24
25
  createGraphBuilder: () => createGraphBuilder,
25
26
  createTraceStore: () => createTraceStore,
27
+ discoverProcessConfig: () => discoverProcessConfig,
26
28
  findWaitingOn: () => findWaitingOn,
29
+ formatAuditReport: () => formatAuditReport,
27
30
  getChildren: () => getChildren,
28
31
  getCriticalPath: () => getCriticalPath,
29
32
  getDepth: () => getDepth,
@@ -636,9 +639,8 @@ function withGuards(builder, config) {
636
639
  }
637
640
 
638
641
  // src/live.ts
639
- var import_node_fs = require("fs");
640
- var import_node_path = require("path");
641
- var import_node_child_process = require("child_process");
642
+ var import_node_fs2 = require("fs");
643
+ var import_node_path2 = require("path");
642
644
 
643
645
  // src/loader.ts
644
646
  function toNodesMap(raw) {
@@ -692,6 +694,262 @@ function graphToJson(graph) {
692
694
  };
693
695
  }
694
696
 
697
+ // src/process-audit.ts
698
+ var import_node_child_process = require("child_process");
699
+ var import_node_fs = require("fs");
700
+ var import_node_path = require("path");
701
+ function isPidAlive(pid) {
702
+ try {
703
+ process.kill(pid, 0);
704
+ return true;
705
+ } catch {
706
+ return false;
707
+ }
708
+ }
709
+ function pidMatchesName(pid, name) {
710
+ try {
711
+ const cmdline = (0, import_node_fs.readFileSync)(`/proc/${pid}/cmdline`, "utf8");
712
+ return cmdline.includes(name);
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+ function readPidFile(path) {
718
+ try {
719
+ const pid = parseInt((0, import_node_fs.readFileSync)(path, "utf8").trim(), 10);
720
+ return isNaN(pid) ? null : pid;
721
+ } catch {
722
+ return null;
723
+ }
724
+ }
725
+ function auditPidFile(config) {
726
+ if (!config.pidFile) return null;
727
+ const pid = readPidFile(config.pidFile);
728
+ if (pid === null) {
729
+ return {
730
+ path: config.pidFile,
731
+ pid: null,
732
+ alive: false,
733
+ matchesProcess: false,
734
+ stale: !(0, import_node_fs.existsSync)(config.pidFile),
735
+ reason: (0, import_node_fs.existsSync)(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
736
+ };
737
+ }
738
+ const alive = isPidAlive(pid);
739
+ const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
740
+ const stale = !alive || alive && !matchesProcess;
741
+ let reason;
742
+ if (alive && matchesProcess) {
743
+ reason = `PID ${pid} alive and matches ${config.processName}`;
744
+ } else if (alive && !matchesProcess) {
745
+ reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
746
+ } else {
747
+ reason = `PID ${pid} no longer exists`;
748
+ }
749
+ return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
750
+ }
751
+ function auditSystemd(config) {
752
+ if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
753
+ const unit = config.systemdUnit;
754
+ try {
755
+ const raw = (0, import_node_child_process.execSync)(
756
+ `systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
757
+ { encoding: "utf8", timeout: 5e3 }
758
+ );
759
+ const props = {};
760
+ for (const line of raw.trim().split("\n")) {
761
+ const [k, ...v] = line.split("=");
762
+ if (k) props[k.trim()] = v.join("=").trim();
763
+ }
764
+ const activeState = props["ActiveState"] ?? "unknown";
765
+ const subState = props["SubState"] ?? "unknown";
766
+ const mainPid = parseInt(props["MainPID"] ?? "0", 10);
767
+ const restarts = parseInt(props["NRestarts"] ?? "0", 10);
768
+ const result = props["Result"] ?? "unknown";
769
+ return {
770
+ unit,
771
+ activeState,
772
+ subState,
773
+ mainPid,
774
+ restarts,
775
+ result,
776
+ crashLooping: activeState === "activating" && subState === "auto-restart",
777
+ failed: activeState === "failed"
778
+ };
779
+ } catch {
780
+ return null;
781
+ }
782
+ }
783
+ function auditWorkers(config) {
784
+ if (!config.workersFile || !(0, import_node_fs.existsSync)(config.workersFile)) return null;
785
+ try {
786
+ const data = JSON.parse((0, import_node_fs.readFileSync)(config.workersFile, "utf8"));
787
+ const orchPid = data.pid ?? null;
788
+ const orchAlive = orchPid ? isPidAlive(orchPid) : false;
789
+ const workers = [];
790
+ for (const [name, info] of Object.entries(data.tools ?? {})) {
791
+ const w = info;
792
+ const wPid = w.pid ?? null;
793
+ const wAlive = wPid ? isPidAlive(wPid) : false;
794
+ workers.push({
795
+ name,
796
+ pid: wPid,
797
+ declaredStatus: w.status ?? "unknown",
798
+ alive: wAlive,
799
+ stale: w.status === "running" && !wAlive
800
+ });
801
+ }
802
+ return {
803
+ orchestratorPid: orchPid,
804
+ orchestratorAlive: orchAlive,
805
+ startedAt: data.started_at ?? "",
806
+ workers
807
+ };
808
+ } catch {
809
+ return null;
810
+ }
811
+ }
812
+ function getOsProcesses(processName) {
813
+ try {
814
+ const raw = (0, import_node_child_process.execSync)(`ps aux`, { encoding: "utf8", timeout: 5e3 });
815
+ return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
816
+ const parts = line.trim().split(/\s+/);
817
+ return {
818
+ pid: parseInt(parts[1] ?? "0", 10),
819
+ cpu: parts[2] ?? "0",
820
+ mem: parts[3] ?? "0",
821
+ command: parts.slice(10).join(" ")
822
+ };
823
+ }).filter((p) => !isNaN(p.pid) && p.pid > 0);
824
+ } catch {
825
+ return [];
826
+ }
827
+ }
828
+ function discoverProcessConfig(dirs) {
829
+ let pidFile;
830
+ let workersFile;
831
+ let processName = "";
832
+ for (const dir of dirs) {
833
+ if (!(0, import_node_fs.existsSync)(dir)) continue;
834
+ let entries;
835
+ try {
836
+ entries = (0, import_node_fs.readdirSync)(dir);
837
+ } catch {
838
+ continue;
839
+ }
840
+ for (const f of entries) {
841
+ const fp = (0, import_node_path.join)(dir, f);
842
+ try {
843
+ if (!(0, import_node_fs.statSync)(fp).isFile()) continue;
844
+ } catch {
845
+ continue;
846
+ }
847
+ if (f.endsWith(".pid") && !pidFile) {
848
+ pidFile = fp;
849
+ if (!processName) {
850
+ processName = (0, import_node_path.basename)(f, ".pid");
851
+ }
852
+ }
853
+ if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
854
+ workersFile = fp;
855
+ if (!processName && f !== "workers.json") {
856
+ processName = (0, import_node_path.basename)(f, "-workers.json");
857
+ }
858
+ }
859
+ }
860
+ }
861
+ if (!processName && !pidFile && !workersFile) return null;
862
+ if (!processName) processName = "agent";
863
+ return { processName, pidFile, workersFile };
864
+ }
865
+ function auditProcesses(config) {
866
+ const pidFile = auditPidFile(config);
867
+ const systemd = auditSystemd(config);
868
+ const workers = auditWorkers(config);
869
+ const osProcesses = getOsProcesses(config.processName);
870
+ const knownPids = /* @__PURE__ */ new Set();
871
+ if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
872
+ if (workers) {
873
+ if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
874
+ for (const w of workers.workers) {
875
+ if (w.pid) knownPids.add(w.pid);
876
+ }
877
+ }
878
+ if (systemd?.mainPid) knownPids.add(systemd.mainPid);
879
+ const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
880
+ const problems = [];
881
+ if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
882
+ if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
883
+ if (systemd?.failed) problems.push("Systemd unit has failed");
884
+ if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
885
+ if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
886
+ problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
887
+ }
888
+ if (workers) {
889
+ for (const w of workers.workers) {
890
+ if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
891
+ }
892
+ }
893
+ if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
894
+ return { pidFile, systemd, workers, osProcesses, orphans, problems };
895
+ }
896
+ function formatAuditReport(result) {
897
+ const lines = [];
898
+ lines.push("");
899
+ lines.push("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
900
+ lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
901
+ lines.push("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
902
+ if (result.pidFile) {
903
+ const pf = result.pidFile;
904
+ const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
905
+ lines.push(`
906
+ PID File: ${pf.path}`);
907
+ lines.push(` ${icon} ${pf.reason}`);
908
+ }
909
+ if (result.systemd) {
910
+ const sd = result.systemd;
911
+ const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
912
+ lines.push(`
913
+ Systemd: ${sd.unit}`);
914
+ lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
915
+ lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
916
+ }
917
+ if (result.workers) {
918
+ const w = result.workers;
919
+ lines.push(`
920
+ Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
921
+ for (const worker of w.workers) {
922
+ const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
923
+ lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
924
+ }
925
+ }
926
+ if (result.osProcesses.length > 0) {
927
+ lines.push(`
928
+ OS Processes (${result.osProcesses.length} total)`);
929
+ for (const p of result.osProcesses) {
930
+ lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
931
+ }
932
+ }
933
+ if (result.orphans.length > 0) {
934
+ lines.push(`
935
+ \u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
936
+ for (const p of result.orphans) {
937
+ lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
938
+ }
939
+ }
940
+ lines.push("");
941
+ if (result.problems.length === 0) {
942
+ lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
943
+ } else {
944
+ lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
945
+ for (const p of result.problems) {
946
+ lines.push(` \u2022 ${p}`);
947
+ }
948
+ }
949
+ lines.push("");
950
+ return lines.join("\n");
951
+ }
952
+
695
953
  // src/live.ts
696
954
  var C = {
697
955
  reset: "\x1B[0m",
@@ -725,13 +983,13 @@ function parseArgs(argv) {
725
983
  config.recursive = true;
726
984
  i++;
727
985
  } else if (!arg.startsWith("-")) {
728
- config.dirs.push((0, import_node_path.resolve)(arg));
986
+ config.dirs.push((0, import_node_path2.resolve)(arg));
729
987
  i++;
730
988
  } else {
731
989
  i++;
732
990
  }
733
991
  }
734
- if (config.dirs.length === 0) config.dirs.push((0, import_node_path.resolve)("."));
992
+ if (config.dirs.length === 0) config.dirs.push((0, import_node_path2.resolve)("."));
735
993
  return config;
736
994
  }
737
995
  function printUsage() {
@@ -767,7 +1025,7 @@ function scanFiles(dirs, recursive) {
767
1025
  const seen = /* @__PURE__ */ new Set();
768
1026
  function scanDir(d, topLevel) {
769
1027
  try {
770
- const dirStat = (0, import_node_fs.statSync)(d);
1028
+ const dirStat = (0, import_node_fs2.statSync)(d);
771
1029
  const dirMtime = dirStat.mtime.getTime();
772
1030
  const cachedMtime = dirMtimeCache.get(d);
773
1031
  if (cachedMtime === dirMtime) {
@@ -783,13 +1041,13 @@ function scanFiles(dirs, recursive) {
783
1041
  }
784
1042
  }
785
1043
  const dirResults = [];
786
- for (const f of (0, import_node_fs.readdirSync)(d)) {
1044
+ for (const f of (0, import_node_fs2.readdirSync)(d)) {
787
1045
  if (f.startsWith(".")) continue;
788
- const fp = (0, import_node_path.join)(d, f);
1046
+ const fp = (0, import_node_path2.join)(d, f);
789
1047
  if (seen.has(fp)) continue;
790
1048
  let stat;
791
1049
  try {
792
- stat = (0, import_node_fs.statSync)(fp);
1050
+ stat = (0, import_node_fs2.statSync)(fp);
793
1051
  } catch {
794
1052
  continue;
795
1053
  }
@@ -821,13 +1079,13 @@ function scanFiles(dirs, recursive) {
821
1079
  }
822
1080
  function safeReadJson(fp) {
823
1081
  try {
824
- return JSON.parse((0, import_node_fs.readFileSync)(fp, "utf8"));
1082
+ return JSON.parse((0, import_node_fs2.readFileSync)(fp, "utf8"));
825
1083
  } catch {
826
1084
  return null;
827
1085
  }
828
1086
  }
829
1087
  function nameFromFile(filename) {
830
- return (0, import_node_path.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
1088
+ return (0, import_node_path2.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
831
1089
  }
832
1090
  function normalizeStatus(val) {
833
1091
  if (typeof val !== "string") return "unknown";
@@ -963,18 +1221,20 @@ function processJsonFile(file) {
963
1221
  const w = info;
964
1222
  const status2 = findStatus(w);
965
1223
  const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
966
- const pid = w.pid;
1224
+ const rawPid = w.pid;
1225
+ const pid = typeof rawPid === "number" ? rawPid : Number(rawPid);
1226
+ const validPid = Number.isFinite(pid) && pid > 0;
967
1227
  let validatedStatus = status2;
968
1228
  let pidAlive = true;
969
- if (pid && (status2 === "running" || status2 === "ok")) {
1229
+ if (validPid && (status2 === "running" || status2 === "ok")) {
970
1230
  try {
971
- (0, import_node_child_process.execSync)(`kill -0 ${pid} 2>/dev/null`, { stdio: "ignore" });
1231
+ process.kill(pid, 0);
972
1232
  } catch {
973
1233
  pidAlive = false;
974
1234
  validatedStatus = "error";
975
1235
  }
976
1236
  }
977
- const pidLabel = pid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
1237
+ const pidLabel = validPid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
978
1238
  const detail2 = pidLabel || extractDetail(w);
979
1239
  records.push({
980
1240
  id: name,
@@ -1003,7 +1263,7 @@ function processJsonFile(file) {
1003
1263
  }
1004
1264
  function processJsonlFile(file) {
1005
1265
  try {
1006
- const content = (0, import_node_fs.readFileSync)(file.path, "utf8").trim();
1266
+ const content = (0, import_node_fs2.readFileSync)(file.path, "utf8").trim();
1007
1267
  if (!content) return [];
1008
1268
  const lines = content.split("\n");
1009
1269
  const lineCount = lines.length;
@@ -1155,6 +1415,9 @@ var prevFileCount = 0;
1155
1415
  var newExecCount = 0;
1156
1416
  var sessionStart = Date.now();
1157
1417
  var firstRender = true;
1418
+ var cachedAuditConfig = null;
1419
+ var cachedAuditResult = null;
1420
+ var lastAuditTime = 0;
1158
1421
  var fileCache = /* @__PURE__ */ new Map();
1159
1422
  function getRecordsCached(f) {
1160
1423
  const cached = fileCache.get(f.path);
@@ -1274,6 +1537,22 @@ function render(config) {
1274
1537
  const level = Math.round(v / maxBucket * 8);
1275
1538
  return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
1276
1539
  }).join("");
1540
+ let auditResult = null;
1541
+ if (now - lastAuditTime > 1e4) {
1542
+ if (!cachedAuditConfig) {
1543
+ cachedAuditConfig = discoverProcessConfig(config.dirs);
1544
+ }
1545
+ if (cachedAuditConfig) {
1546
+ try {
1547
+ auditResult = auditProcesses(cachedAuditConfig);
1548
+ cachedAuditResult = auditResult;
1549
+ lastAuditTime = now;
1550
+ } catch {
1551
+ }
1552
+ }
1553
+ } else {
1554
+ auditResult = cachedAuditResult;
1555
+ }
1277
1556
  const distributedTraces = [];
1278
1557
  if (allTraces.length > 1) {
1279
1558
  const traceGroups = groupByTraceId(allTraces);
@@ -1354,6 +1633,41 @@ function render(config) {
1354
1633
  );
1355
1634
  writeLine(L, "");
1356
1635
  writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1636
+ if (auditResult) {
1637
+ const ar = auditResult;
1638
+ const healthy = ar.problems.length === 0;
1639
+ const healthIcon = healthy ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
1640
+ const healthLabel = healthy ? `${C.green}healthy${C.reset}` : `${C.red}${ar.problems.length} issue(s)${C.reset}`;
1641
+ const workerParts = [];
1642
+ if (ar.workers) {
1643
+ for (const w of ar.workers.workers) {
1644
+ const wIcon = w.declaredStatus === "running" && w.alive ? `${C.green}\u25CF${C.reset}` : w.stale ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1645
+ workerParts.push(`${wIcon} ${w.name}`);
1646
+ }
1647
+ }
1648
+ let sysdLabel = "";
1649
+ if (ar.systemd) {
1650
+ const si = ar.systemd.activeState === "active" ? `${C.green}\u25CF${C.reset}` : ar.systemd.crashLooping ? `${C.yellow}\u25CF${C.reset}` : ar.systemd.failed ? `${C.red}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1651
+ sysdLabel = ` ${C.bold}Systemd${C.reset} ${si} ${ar.systemd.activeState}`;
1652
+ if (ar.systemd.restarts > 0) sysdLabel += ` ${C.dim}(${ar.systemd.restarts} restarts)${C.reset}`;
1653
+ }
1654
+ let pidLabel = "";
1655
+ if (ar.pidFile?.pid) {
1656
+ const pi = ar.pidFile.alive && ar.pidFile.matchesProcess ? `${C.green}\u25CF${C.reset}` : `${C.red}\u25CF${C.reset}`;
1657
+ pidLabel = ` ${C.bold}PID${C.reset} ${pi} ${ar.pidFile.pid}`;
1658
+ }
1659
+ writeLine(L, "");
1660
+ writeLine(L, ` ${C.bold}${C.under}Process Health${C.reset}`);
1661
+ writeLine(L, ` ${healthIcon} ${healthLabel}${pidLabel}${sysdLabel} ${C.bold}Procs${C.reset} ${C.dim}${ar.osProcesses.length}${C.reset} ${ar.orphans.length > 0 ? `${C.red}Orphans ${ar.orphans.length}${C.reset}` : `${C.dim}Orphans 0${C.reset}`}`);
1662
+ if (workerParts.length > 0) {
1663
+ writeLine(L, ` ${C.dim}Workers${C.reset} ${workerParts.join(" ")}`);
1664
+ }
1665
+ if (!healthy) {
1666
+ for (const p of ar.problems.slice(0, 3)) {
1667
+ writeLine(L, ` ${C.red}\u2022${C.reset} ${C.dim}${p}${C.reset}`);
1668
+ }
1669
+ }
1670
+ }
1357
1671
  writeLine(L, "");
1358
1672
  writeLine(
1359
1673
  L,
@@ -1461,21 +1775,24 @@ function render(config) {
1461
1775
  writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
1462
1776
  flushLines(L);
1463
1777
  }
1464
- function getDistDepth(dt, spanId) {
1778
+ function getDistDepth(dt, spanId, visited) {
1465
1779
  if (!spanId) return 0;
1780
+ const seen = visited ?? /* @__PURE__ */ new Set();
1781
+ if (seen.has(spanId)) return 0;
1782
+ seen.add(spanId);
1466
1783
  const g = dt.graphs.get(spanId);
1467
1784
  if (!g || !g.parentSpanId) return 0;
1468
- return 1 + getDistDepth(dt, g.parentSpanId);
1785
+ return 1 + getDistDepth(dt, g.parentSpanId, seen);
1469
1786
  }
1470
1787
  function startLive(argv) {
1471
1788
  const config = parseArgs(argv);
1472
- const valid = config.dirs.filter((d) => (0, import_node_fs.existsSync)(d));
1789
+ const valid = config.dirs.filter((d) => (0, import_node_fs2.existsSync)(d));
1473
1790
  if (valid.length === 0) {
1474
1791
  console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1475
1792
  console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
1476
1793
  process.exit(1);
1477
1794
  }
1478
- const invalid = config.dirs.filter((d) => !(0, import_node_fs.existsSync)(d));
1795
+ const invalid = config.dirs.filter((d) => !(0, import_node_fs2.existsSync)(d));
1479
1796
  if (invalid.length > 0) {
1480
1797
  console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1481
1798
  }
@@ -1484,7 +1801,7 @@ function startLive(argv) {
1484
1801
  let debounce = null;
1485
1802
  for (const dir of config.dirs) {
1486
1803
  try {
1487
- (0, import_node_fs.watch)(dir, { recursive: config.recursive }, () => {
1804
+ (0, import_node_fs2.watch)(dir, { recursive: config.recursive }, () => {
1488
1805
  if (debounce) clearTimeout(debounce);
1489
1806
  debounce = setTimeout(() => render(config), 500);
1490
1807
  });
@@ -1500,20 +1817,20 @@ function startLive(argv) {
1500
1817
 
1501
1818
  // src/runner.ts
1502
1819
  var import_node_child_process2 = require("child_process");
1503
- var import_node_fs2 = require("fs");
1504
- var import_node_path2 = require("path");
1820
+ var import_node_fs3 = require("fs");
1821
+ var import_node_path3 = require("path");
1505
1822
  function globToRegex(pattern) {
1506
1823
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1507
1824
  return new RegExp(`^${escaped}$`);
1508
1825
  }
1509
1826
  function snapshotDir(dir, patterns) {
1510
1827
  const result = /* @__PURE__ */ new Map();
1511
- if (!(0, import_node_fs2.existsSync)(dir)) return result;
1512
- for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
1828
+ if (!(0, import_node_fs3.existsSync)(dir)) return result;
1829
+ for (const entry of (0, import_node_fs3.readdirSync)(dir)) {
1513
1830
  if (!patterns.some((re) => re.test(entry))) continue;
1514
- const full = (0, import_node_path2.join)(dir, entry);
1831
+ const full = (0, import_node_path3.join)(dir, entry);
1515
1832
  try {
1516
- const stat = (0, import_node_fs2.statSync)(full);
1833
+ const stat = (0, import_node_fs3.statSync)(full);
1517
1834
  if (stat.isFile()) {
1518
1835
  result.set(full, stat.mtimeMs);
1519
1836
  }
@@ -1523,7 +1840,7 @@ function snapshotDir(dir, patterns) {
1523
1840
  return result;
1524
1841
  }
1525
1842
  function agentIdFromFilename(filePath) {
1526
- const base = (0, import_node_path2.basename)(filePath, ".json");
1843
+ const base = (0, import_node_path3.basename)(filePath, ".json");
1527
1844
  const cleaned = base.replace(/-state$/, "");
1528
1845
  return `alfred-${cleaned}`;
1529
1846
  }
@@ -1545,7 +1862,7 @@ async function runTraced(config) {
1545
1862
  if (command.length === 0) {
1546
1863
  throw new Error("runTraced: command must not be empty");
1547
1864
  }
1548
- const resolvedTracesDir = (0, import_node_path2.resolve)(tracesDir);
1865
+ const resolvedTracesDir = (0, import_node_path3.resolve)(tracesDir);
1549
1866
  const patterns = watchPatterns.map(globToRegex);
1550
1867
  const orchestrator = createGraphBuilder({ agentId, trigger });
1551
1868
  const { traceId, spanId } = orchestrator.traceContext;
@@ -1628,15 +1945,19 @@ async function runTraced(config) {
1628
1945
  childBuilder.endNode(childRootId);
1629
1946
  allGraphs.push(childBuilder.build());
1630
1947
  }
1631
- if (!(0, import_node_fs2.existsSync)(resolvedTracesDir)) {
1632
- (0, import_node_fs2.mkdirSync)(resolvedTracesDir, { recursive: true });
1948
+ if (!(0, import_node_fs3.existsSync)(resolvedTracesDir)) {
1949
+ (0, import_node_fs3.mkdirSync)(resolvedTracesDir, { recursive: true });
1633
1950
  }
1634
1951
  const ts = fileTimestamp();
1635
1952
  const tracePaths = [];
1636
1953
  for (const graph of allGraphs) {
1637
1954
  const filename = `${graph.agentId}-${ts}.json`;
1638
- const outPath = (0, import_node_path2.join)(resolvedTracesDir, filename);
1639
- (0, import_node_fs2.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
1955
+ const outPath = (0, import_node_path3.join)(resolvedTracesDir, filename);
1956
+ const resolvedOut = (0, import_node_path3.resolve)(outPath);
1957
+ if (!resolvedOut.startsWith(resolvedTracesDir + "/") && resolvedOut !== resolvedTracesDir) {
1958
+ throw new Error(`Path traversal detected: agentId "${graph.agentId}" escapes traces directory`);
1959
+ }
1960
+ (0, import_node_fs3.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
1640
1961
  tracePaths.push(outPath);
1641
1962
  }
1642
1963
  if (tracePaths.length > 0) {
@@ -1684,6 +2005,11 @@ function createTraceStore(dir) {
1684
2005
  await ensureDir();
1685
2006
  const json = graphToJson(graph);
1686
2007
  const filePath = (0, import_path.join)(dir, `${graph.id}.json`);
2008
+ const resolvedBase = (0, import_path.resolve)(dir);
2009
+ const resolvedPath = (0, import_path.resolve)(filePath);
2010
+ if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
2011
+ throw new Error(`Path traversal detected: "${graph.id}" escapes base directory`);
2012
+ }
1687
2013
  await (0, import_promises.writeFile)(filePath, JSON.stringify(json, null, 2), "utf-8");
1688
2014
  return filePath;
1689
2015
  },
@@ -1882,9 +2208,9 @@ function toTimeline(graph) {
1882
2208
  }
1883
2209
 
1884
2210
  // src/watch.ts
1885
- var import_node_fs4 = require("fs");
2211
+ var import_node_fs5 = require("fs");
1886
2212
  var import_node_os = require("os");
1887
- var import_node_path3 = require("path");
2213
+ var import_node_path4 = require("path");
1888
2214
 
1889
2215
  // src/watch-alerts.ts
1890
2216
  var import_node_child_process3 = require("child_process");
@@ -1942,7 +2268,7 @@ function sendTelegram(payload, botToken, chatId) {
1942
2268
  text: formatTelegram(payload),
1943
2269
  parse_mode: "Markdown"
1944
2270
  });
1945
- return new Promise((resolve4, reject) => {
2271
+ return new Promise((resolve5, reject) => {
1946
2272
  const req = (0, import_node_https.request)(
1947
2273
  `https://api.telegram.org/bot${botToken}/sendMessage`,
1948
2274
  {
@@ -1951,7 +2277,7 @@ function sendTelegram(payload, botToken, chatId) {
1951
2277
  },
1952
2278
  (res) => {
1953
2279
  res.resume();
1954
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
2280
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1955
2281
  else reject(new Error(`Telegram API returned ${res.statusCode}`));
1956
2282
  }
1957
2283
  );
@@ -1964,7 +2290,7 @@ function sendWebhook(payload, url) {
1964
2290
  const body = JSON.stringify(payload);
1965
2291
  const isHttps = url.startsWith("https");
1966
2292
  const doRequest = isHttps ? import_node_https.request : import_node_http.request;
1967
- return new Promise((resolve4, reject) => {
2293
+ return new Promise((resolve5, reject) => {
1968
2294
  const req = doRequest(
1969
2295
  url,
1970
2296
  {
@@ -1973,7 +2299,7 @@ function sendWebhook(payload, url) {
1973
2299
  },
1974
2300
  (res) => {
1975
2301
  res.resume();
1976
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
2302
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1977
2303
  else reject(new Error(`Webhook returned ${res.statusCode}`));
1978
2304
  }
1979
2305
  );
@@ -1986,7 +2312,7 @@ function sendWebhook(payload, url) {
1986
2312
  });
1987
2313
  }
1988
2314
  function sendCommand(payload, cmd) {
1989
- return new Promise((resolve4, reject) => {
2315
+ return new Promise((resolve5, reject) => {
1990
2316
  const env = {
1991
2317
  ...process.env,
1992
2318
  AGENTFLOW_ALERT_AGENT: payload.agentId,
@@ -1999,13 +2325,13 @@ function sendCommand(payload, cmd) {
1999
2325
  };
2000
2326
  (0, import_node_child_process3.exec)(cmd, { env, timeout: 3e4 }, (err) => {
2001
2327
  if (err) reject(err);
2002
- else resolve4();
2328
+ else resolve5();
2003
2329
  });
2004
2330
  });
2005
2331
  }
2006
2332
 
2007
2333
  // src/watch-state.ts
2008
- var import_node_fs3 = require("fs");
2334
+ var import_node_fs4 = require("fs");
2009
2335
  function parseDuration(input) {
2010
2336
  const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
2011
2337
  if (!match) {
@@ -2030,9 +2356,9 @@ function emptyState() {
2030
2356
  return { version: 1, agents: {}, lastPollTime: 0 };
2031
2357
  }
2032
2358
  function loadWatchState(filePath) {
2033
- if (!(0, import_node_fs3.existsSync)(filePath)) return emptyState();
2359
+ if (!(0, import_node_fs4.existsSync)(filePath)) return emptyState();
2034
2360
  try {
2035
- const raw = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
2361
+ const raw = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
2036
2362
  if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
2037
2363
  return raw;
2038
2364
  } catch {
@@ -2042,11 +2368,11 @@ function loadWatchState(filePath) {
2042
2368
  function saveWatchState(filePath, state) {
2043
2369
  const tmp = filePath + ".tmp";
2044
2370
  try {
2045
- (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify(state, null, 2), "utf8");
2046
- (0, import_node_fs3.renameSync)(tmp, filePath);
2371
+ (0, import_node_fs4.writeFileSync)(tmp, JSON.stringify(state, null, 2), "utf8");
2372
+ (0, import_node_fs4.renameSync)(tmp, filePath);
2047
2373
  } catch {
2048
2374
  try {
2049
- (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(state, null, 2), "utf8");
2375
+ (0, import_node_fs4.writeFileSync)(filePath, JSON.stringify(state, null, 2), "utf8");
2050
2376
  } catch {
2051
2377
  }
2052
2378
  }
@@ -2274,20 +2600,20 @@ function parseWatchArgs(argv) {
2274
2600
  recursive = true;
2275
2601
  i++;
2276
2602
  } else if (!arg.startsWith("-")) {
2277
- dirs.push((0, import_node_path3.resolve)(arg));
2603
+ dirs.push((0, import_node_path4.resolve)(arg));
2278
2604
  i++;
2279
2605
  } else {
2280
2606
  i++;
2281
2607
  }
2282
2608
  }
2283
- if (dirs.length === 0) dirs.push((0, import_node_path3.resolve)("."));
2609
+ if (dirs.length === 0) dirs.push((0, import_node_path4.resolve)("."));
2284
2610
  if (alertConditions.length === 0) {
2285
2611
  alertConditions.push({ type: "error" });
2286
2612
  alertConditions.push({ type: "recovery" });
2287
2613
  }
2288
2614
  notifyChannels.unshift({ type: "stdout" });
2289
2615
  if (!stateFilePath) {
2290
- stateFilePath = (0, import_node_path3.join)(dirs[0], ".agentflow-watch-state.json");
2616
+ stateFilePath = (0, import_node_path4.join)(dirs[0], ".agentflow-watch-state.json");
2291
2617
  }
2292
2618
  return {
2293
2619
  dirs,
@@ -2295,7 +2621,7 @@ function parseWatchArgs(argv) {
2295
2621
  pollIntervalMs,
2296
2622
  alertConditions,
2297
2623
  notifyChannels,
2298
- stateFilePath: (0, import_node_path3.resolve)(stateFilePath),
2624
+ stateFilePath: (0, import_node_path4.resolve)(stateFilePath),
2299
2625
  cooldownMs
2300
2626
  };
2301
2627
  }
@@ -2349,12 +2675,12 @@ Examples:
2349
2675
  }
2350
2676
  function startWatch(argv) {
2351
2677
  const config = parseWatchArgs(argv);
2352
- const valid = config.dirs.filter((d) => (0, import_node_fs4.existsSync)(d));
2678
+ const valid = config.dirs.filter((d) => (0, import_node_fs5.existsSync)(d));
2353
2679
  if (valid.length === 0) {
2354
2680
  console.error(`No valid directories found: ${config.dirs.join(", ")}`);
2355
2681
  process.exit(1);
2356
2682
  }
2357
- const invalid = config.dirs.filter((d) => !(0, import_node_fs4.existsSync)(d));
2683
+ const invalid = config.dirs.filter((d) => !(0, import_node_fs5.existsSync)(d));
2358
2684
  if (invalid.length > 0) {
2359
2685
  console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
2360
2686
  }
@@ -2433,10 +2759,13 @@ agentflow watch started`);
2433
2759
  }
2434
2760
  // Annotate the CommonJS export names for ESM import in node:
2435
2761
  0 && (module.exports = {
2762
+ auditProcesses,
2436
2763
  checkGuards,
2437
2764
  createGraphBuilder,
2438
2765
  createTraceStore,
2766
+ discoverProcessConfig,
2439
2767
  findWaitingOn,
2768
+ formatAuditReport,
2440
2769
  getChildren,
2441
2770
  getCriticalPath,
2442
2771
  getDepth,