agentflow-core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -209,7 +209,6 @@ function getTraceTree(trace) {
209
209
  // src/live.ts
210
210
  import { existsSync, readdirSync, readFileSync, statSync, watch } from "fs";
211
211
  import { basename, join, resolve } from "path";
212
- import { execSync } from "child_process";
213
212
  var C = {
214
213
  reset: "\x1B[0m",
215
214
  bold: "\x1B[1m",
@@ -480,18 +479,20 @@ function processJsonFile(file) {
480
479
  const w = info;
481
480
  const status2 = findStatus(w);
482
481
  const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
483
- const pid = w.pid;
482
+ const rawPid = w.pid;
483
+ const pid = typeof rawPid === "number" ? rawPid : Number(rawPid);
484
+ const validPid = Number.isFinite(pid) && pid > 0;
484
485
  let validatedStatus = status2;
485
486
  let pidAlive = true;
486
- if (pid && (status2 === "running" || status2 === "ok")) {
487
+ if (validPid && (status2 === "running" || status2 === "ok")) {
487
488
  try {
488
- execSync(`kill -0 ${pid} 2>/dev/null`, { stdio: "ignore" });
489
+ process.kill(pid, 0);
489
490
  } catch {
490
491
  pidAlive = false;
491
492
  validatedStatus = "error";
492
493
  }
493
494
  }
494
- const pidLabel = pid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
495
+ const pidLabel = validPid ? pidAlive ? `pid: ${pid}` : `pid: ${pid} (dead)` : "";
495
496
  const detail2 = pidLabel || extractDetail(w);
496
497
  records.push({
497
498
  id: name,
@@ -978,11 +979,14 @@ function render(config) {
978
979
  writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
979
980
  flushLines(L);
980
981
  }
981
- function getDistDepth(dt, spanId) {
982
+ function getDistDepth(dt, spanId, visited) {
982
983
  if (!spanId) return 0;
984
+ const seen = visited ?? /* @__PURE__ */ new Set();
985
+ if (seen.has(spanId)) return 0;
986
+ seen.add(spanId);
983
987
  const g = dt.graphs.get(spanId);
984
988
  if (!g || !g.parentSpanId) return 0;
985
- return 1 + getDistDepth(dt, g.parentSpanId);
989
+ return 1 + getDistDepth(dt, g.parentSpanId, seen);
986
990
  }
987
991
  function startLive(argv) {
988
992
  const config = parseArgs(argv);
@@ -1015,6 +1019,262 @@ function startLive(argv) {
1015
1019
  });
1016
1020
  }
1017
1021
 
1022
+ // src/process-audit.ts
1023
+ import { execSync } from "child_process";
1024
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
1025
+ import { basename as basename2, join as join2 } from "path";
1026
+ function isPidAlive(pid) {
1027
+ try {
1028
+ process.kill(pid, 0);
1029
+ return true;
1030
+ } catch {
1031
+ return false;
1032
+ }
1033
+ }
1034
+ function pidMatchesName(pid, name) {
1035
+ try {
1036
+ const cmdline = readFileSync2(`/proc/${pid}/cmdline`, "utf8");
1037
+ return cmdline.includes(name);
1038
+ } catch {
1039
+ return false;
1040
+ }
1041
+ }
1042
+ function readPidFile(path) {
1043
+ try {
1044
+ const pid = parseInt(readFileSync2(path, "utf8").trim(), 10);
1045
+ return isNaN(pid) ? null : pid;
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ }
1050
+ function auditPidFile(config) {
1051
+ if (!config.pidFile) return null;
1052
+ const pid = readPidFile(config.pidFile);
1053
+ if (pid === null) {
1054
+ return {
1055
+ path: config.pidFile,
1056
+ pid: null,
1057
+ alive: false,
1058
+ matchesProcess: false,
1059
+ stale: !existsSync2(config.pidFile),
1060
+ reason: existsSync2(config.pidFile) ? "PID file exists but content is invalid" : "No PID file found"
1061
+ };
1062
+ }
1063
+ const alive = isPidAlive(pid);
1064
+ const matchesProcess = alive ? pidMatchesName(pid, config.processName) : false;
1065
+ const stale = !alive || alive && !matchesProcess;
1066
+ let reason;
1067
+ if (alive && matchesProcess) {
1068
+ reason = `PID ${pid} alive and matches ${config.processName}`;
1069
+ } else if (alive && !matchesProcess) {
1070
+ reason = `PID ${pid} alive but is NOT ${config.processName} (PID reused by another process)`;
1071
+ } else {
1072
+ reason = `PID ${pid} no longer exists`;
1073
+ }
1074
+ return { path: config.pidFile, pid, alive, matchesProcess, stale, reason };
1075
+ }
1076
+ function auditSystemd(config) {
1077
+ if (config.systemdUnit === null || config.systemdUnit === void 0) return null;
1078
+ const unit = config.systemdUnit;
1079
+ try {
1080
+ const raw = execSync(
1081
+ `systemctl --user show ${unit} --property=ActiveState,SubState,MainPID,NRestarts,Result --no-pager 2>/dev/null`,
1082
+ { encoding: "utf8", timeout: 5e3 }
1083
+ );
1084
+ const props = {};
1085
+ for (const line of raw.trim().split("\n")) {
1086
+ const [k, ...v] = line.split("=");
1087
+ if (k) props[k.trim()] = v.join("=").trim();
1088
+ }
1089
+ const activeState = props["ActiveState"] ?? "unknown";
1090
+ const subState = props["SubState"] ?? "unknown";
1091
+ const mainPid = parseInt(props["MainPID"] ?? "0", 10);
1092
+ const restarts = parseInt(props["NRestarts"] ?? "0", 10);
1093
+ const result = props["Result"] ?? "unknown";
1094
+ return {
1095
+ unit,
1096
+ activeState,
1097
+ subState,
1098
+ mainPid,
1099
+ restarts,
1100
+ result,
1101
+ crashLooping: activeState === "activating" && subState === "auto-restart",
1102
+ failed: activeState === "failed"
1103
+ };
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function auditWorkers(config) {
1109
+ if (!config.workersFile || !existsSync2(config.workersFile)) return null;
1110
+ try {
1111
+ const data = JSON.parse(readFileSync2(config.workersFile, "utf8"));
1112
+ const orchPid = data.pid ?? null;
1113
+ const orchAlive = orchPid ? isPidAlive(orchPid) : false;
1114
+ const workers = [];
1115
+ for (const [name, info] of Object.entries(data.tools ?? {})) {
1116
+ const w = info;
1117
+ const wPid = w.pid ?? null;
1118
+ const wAlive = wPid ? isPidAlive(wPid) : false;
1119
+ workers.push({
1120
+ name,
1121
+ pid: wPid,
1122
+ declaredStatus: w.status ?? "unknown",
1123
+ alive: wAlive,
1124
+ stale: w.status === "running" && !wAlive
1125
+ });
1126
+ }
1127
+ return {
1128
+ orchestratorPid: orchPid,
1129
+ orchestratorAlive: orchAlive,
1130
+ startedAt: data.started_at ?? "",
1131
+ workers
1132
+ };
1133
+ } catch {
1134
+ return null;
1135
+ }
1136
+ }
1137
+ function getOsProcesses(processName) {
1138
+ try {
1139
+ const raw = execSync(`ps aux`, { encoding: "utf8", timeout: 5e3 });
1140
+ return raw.split("\n").filter((line) => line.includes(processName) && !line.includes("process-audit") && !line.includes("grep")).map((line) => {
1141
+ const parts = line.trim().split(/\s+/);
1142
+ return {
1143
+ pid: parseInt(parts[1] ?? "0", 10),
1144
+ cpu: parts[2] ?? "0",
1145
+ mem: parts[3] ?? "0",
1146
+ command: parts.slice(10).join(" ")
1147
+ };
1148
+ }).filter((p) => !isNaN(p.pid) && p.pid > 0);
1149
+ } catch {
1150
+ return [];
1151
+ }
1152
+ }
1153
+ function discoverProcessConfig(dirs) {
1154
+ let pidFile;
1155
+ let workersFile;
1156
+ let processName = "";
1157
+ for (const dir of dirs) {
1158
+ if (!existsSync2(dir)) continue;
1159
+ let entries;
1160
+ try {
1161
+ entries = readdirSync2(dir);
1162
+ } catch {
1163
+ continue;
1164
+ }
1165
+ for (const f of entries) {
1166
+ const fp = join2(dir, f);
1167
+ try {
1168
+ if (!statSync2(fp).isFile()) continue;
1169
+ } catch {
1170
+ continue;
1171
+ }
1172
+ if (f.endsWith(".pid") && !pidFile) {
1173
+ pidFile = fp;
1174
+ if (!processName) {
1175
+ processName = basename2(f, ".pid");
1176
+ }
1177
+ }
1178
+ if ((f === "workers.json" || f.endsWith("-workers.json")) && !workersFile) {
1179
+ workersFile = fp;
1180
+ if (!processName && f !== "workers.json") {
1181
+ processName = basename2(f, "-workers.json");
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+ if (!processName && !pidFile && !workersFile) return null;
1187
+ if (!processName) processName = "agent";
1188
+ return { processName, pidFile, workersFile };
1189
+ }
1190
+ function auditProcesses(config) {
1191
+ const pidFile = auditPidFile(config);
1192
+ const systemd = auditSystemd(config);
1193
+ const workers = auditWorkers(config);
1194
+ const osProcesses = getOsProcesses(config.processName);
1195
+ const knownPids = /* @__PURE__ */ new Set();
1196
+ if (pidFile?.pid && !pidFile.stale) knownPids.add(pidFile.pid);
1197
+ if (workers) {
1198
+ if (workers.orchestratorPid) knownPids.add(workers.orchestratorPid);
1199
+ for (const w of workers.workers) {
1200
+ if (w.pid) knownPids.add(w.pid);
1201
+ }
1202
+ }
1203
+ if (systemd?.mainPid) knownPids.add(systemd.mainPid);
1204
+ const orphans = osProcesses.filter((p) => !knownPids.has(p.pid));
1205
+ const problems = [];
1206
+ if (pidFile?.stale) problems.push(`Stale PID file: ${pidFile.reason}`);
1207
+ if (systemd?.crashLooping) problems.push("Systemd unit is crash-looping (auto-restart)");
1208
+ if (systemd?.failed) problems.push("Systemd unit has failed");
1209
+ if (systemd && systemd.restarts > 10) problems.push(`High systemd restart count: ${systemd.restarts}`);
1210
+ if (pidFile?.pid && systemd?.mainPid && pidFile.pid !== systemd.mainPid) {
1211
+ problems.push(`PID mismatch: file says ${pidFile.pid}, systemd says ${systemd.mainPid}`);
1212
+ }
1213
+ if (workers) {
1214
+ for (const w of workers.workers) {
1215
+ if (w.stale) problems.push(`Worker "${w.name}" (pid ${w.pid}) declares running but is dead`);
1216
+ }
1217
+ }
1218
+ if (orphans.length > 0) problems.push(`${orphans.length} orphan process(es) not tracked by PID file or workers registry`);
1219
+ return { pidFile, systemd, workers, osProcesses, orphans, problems };
1220
+ }
1221
+ function formatAuditReport(result) {
1222
+ const lines = [];
1223
+ lines.push("");
1224
+ 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");
1225
+ lines.push("\u2551 \u{1F50D} P R O C E S S A U D I T \u2551");
1226
+ 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");
1227
+ if (result.pidFile) {
1228
+ const pf = result.pidFile;
1229
+ const icon = pf.pid && pf.alive && pf.matchesProcess ? "\u2705" : pf.stale ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
1230
+ lines.push(`
1231
+ PID File: ${pf.path}`);
1232
+ lines.push(` ${icon} ${pf.reason}`);
1233
+ }
1234
+ if (result.systemd) {
1235
+ const sd = result.systemd;
1236
+ const icon = sd.activeState === "active" ? "\u{1F7E2}" : sd.crashLooping ? "\u{1F7E1}" : sd.failed ? "\u{1F534}" : "\u26AA";
1237
+ lines.push(`
1238
+ Systemd: ${sd.unit}`);
1239
+ lines.push(` ${icon} State: ${sd.activeState} (${sd.subState}) Result: ${sd.result}`);
1240
+ lines.push(` Main PID: ${sd.mainPid || "none"} Restarts: ${sd.restarts}`);
1241
+ }
1242
+ if (result.workers) {
1243
+ const w = result.workers;
1244
+ lines.push(`
1245
+ Workers (orchestrator pid ${w.orchestratorPid ?? "unknown"} ${w.orchestratorAlive ? "\u2705" : "\u274C"})`);
1246
+ for (const worker of w.workers) {
1247
+ const icon = worker.declaredStatus === "running" && worker.alive ? "\u{1F7E2}" : worker.stale ? "\u{1F534} STALE" : "\u26AA";
1248
+ lines.push(` ${icon} ${worker.name.padEnd(14)} pid=${String(worker.pid ?? "-").padEnd(8)} status=${worker.declaredStatus}`);
1249
+ }
1250
+ }
1251
+ if (result.osProcesses.length > 0) {
1252
+ lines.push(`
1253
+ OS Processes (${result.osProcesses.length} total)`);
1254
+ for (const p of result.osProcesses) {
1255
+ lines.push(` PID ${String(p.pid).padEnd(8)} CPU=${p.cpu.padEnd(6)} MEM=${p.mem.padEnd(6)} ${p.command.substring(0, 55)}`);
1256
+ }
1257
+ }
1258
+ if (result.orphans.length > 0) {
1259
+ lines.push(`
1260
+ \u26A0\uFE0F ${result.orphans.length} ORPHAN PROCESS(ES):`);
1261
+ for (const p of result.orphans) {
1262
+ lines.push(` PID ${p.pid} \u2014 not tracked by PID file or workers registry`);
1263
+ }
1264
+ }
1265
+ lines.push("");
1266
+ if (result.problems.length === 0) {
1267
+ lines.push(" \u2705 All checks passed \u2014 no process issues detected.");
1268
+ } else {
1269
+ lines.push(` \u26A0\uFE0F ${result.problems.length} issue(s):`);
1270
+ for (const p of result.problems) {
1271
+ lines.push(` \u2022 ${p}`);
1272
+ }
1273
+ }
1274
+ lines.push("");
1275
+ return lines.join("\n");
1276
+ }
1277
+
1018
1278
  // src/graph-builder.ts
1019
1279
  import { randomUUID } from "crypto";
1020
1280
  function deepFreeze(obj) {
@@ -1243,20 +1503,20 @@ function createGraphBuilder(config) {
1243
1503
 
1244
1504
  // src/runner.ts
1245
1505
  import { spawnSync } from "child_process";
1246
- import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "fs";
1247
- import { basename as basename2, join as join2, resolve as resolve2 } from "path";
1506
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, statSync as statSync3, writeFileSync } from "fs";
1507
+ import { basename as basename3, join as join3, resolve as resolve2 } from "path";
1248
1508
  function globToRegex(pattern) {
1249
1509
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1250
1510
  return new RegExp(`^${escaped}$`);
1251
1511
  }
1252
1512
  function snapshotDir(dir, patterns) {
1253
1513
  const result = /* @__PURE__ */ new Map();
1254
- if (!existsSync2(dir)) return result;
1255
- for (const entry of readdirSync2(dir)) {
1514
+ if (!existsSync3(dir)) return result;
1515
+ for (const entry of readdirSync3(dir)) {
1256
1516
  if (!patterns.some((re) => re.test(entry))) continue;
1257
- const full = join2(dir, entry);
1517
+ const full = join3(dir, entry);
1258
1518
  try {
1259
- const stat = statSync2(full);
1519
+ const stat = statSync3(full);
1260
1520
  if (stat.isFile()) {
1261
1521
  result.set(full, stat.mtimeMs);
1262
1522
  }
@@ -1266,7 +1526,7 @@ function snapshotDir(dir, patterns) {
1266
1526
  return result;
1267
1527
  }
1268
1528
  function agentIdFromFilename(filePath) {
1269
- const base = basename2(filePath, ".json");
1529
+ const base = basename3(filePath, ".json");
1270
1530
  const cleaned = base.replace(/-state$/, "");
1271
1531
  return `alfred-${cleaned}`;
1272
1532
  }
@@ -1371,14 +1631,18 @@ async function runTraced(config) {
1371
1631
  childBuilder.endNode(childRootId);
1372
1632
  allGraphs.push(childBuilder.build());
1373
1633
  }
1374
- if (!existsSync2(resolvedTracesDir)) {
1634
+ if (!existsSync3(resolvedTracesDir)) {
1375
1635
  mkdirSync(resolvedTracesDir, { recursive: true });
1376
1636
  }
1377
1637
  const ts = fileTimestamp();
1378
1638
  const tracePaths = [];
1379
1639
  for (const graph of allGraphs) {
1380
1640
  const filename = `${graph.agentId}-${ts}.json`;
1381
- const outPath = join2(resolvedTracesDir, filename);
1641
+ const outPath = join3(resolvedTracesDir, filename);
1642
+ const resolvedOut = resolve2(outPath);
1643
+ if (!resolvedOut.startsWith(resolvedTracesDir + "/") && resolvedOut !== resolvedTracesDir) {
1644
+ throw new Error(`Path traversal detected: agentId "${graph.agentId}" escapes traces directory`);
1645
+ }
1382
1646
  writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
1383
1647
  tracePaths.push(outPath);
1384
1648
  }
@@ -1397,7 +1661,7 @@ async function runTraced(config) {
1397
1661
 
1398
1662
  // src/trace-store.ts
1399
1663
  import { mkdir, readdir, readFile, writeFile } from "fs/promises";
1400
- import { join as join3 } from "path";
1664
+ import { join as join4, resolve as resolve3 } from "path";
1401
1665
  function createTraceStore(dir) {
1402
1666
  async function ensureDir() {
1403
1667
  await mkdir(dir, { recursive: true });
@@ -1414,7 +1678,7 @@ function createTraceStore(dir) {
1414
1678
  for (const file of files) {
1415
1679
  if (!file.endsWith(".json")) continue;
1416
1680
  try {
1417
- const content = await readFile(join3(dir, file), "utf-8");
1681
+ const content = await readFile(join4(dir, file), "utf-8");
1418
1682
  const graph = loadGraph(content);
1419
1683
  graphs.push(graph);
1420
1684
  } catch {
@@ -1426,13 +1690,18 @@ function createTraceStore(dir) {
1426
1690
  async save(graph) {
1427
1691
  await ensureDir();
1428
1692
  const json = graphToJson(graph);
1429
- const filePath = join3(dir, `${graph.id}.json`);
1693
+ const filePath = join4(dir, `${graph.id}.json`);
1694
+ const resolvedBase = resolve3(dir);
1695
+ const resolvedPath = resolve3(filePath);
1696
+ if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
1697
+ throw new Error(`Path traversal detected: "${graph.id}" escapes base directory`);
1698
+ }
1430
1699
  await writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
1431
1700
  return filePath;
1432
1701
  },
1433
1702
  async get(graphId) {
1434
1703
  await ensureDir();
1435
- const filePath = join3(dir, `${graphId}.json`);
1704
+ const filePath = join4(dir, `${graphId}.json`);
1436
1705
  try {
1437
1706
  const content = await readFile(filePath, "utf-8");
1438
1707
  return loadGraph(content);
@@ -1625,9 +1894,9 @@ function toTimeline(graph) {
1625
1894
  }
1626
1895
 
1627
1896
  // src/watch.ts
1628
- import { existsSync as existsSync4 } from "fs";
1897
+ import { existsSync as existsSync5 } from "fs";
1629
1898
  import { hostname } from "os";
1630
- import { join as join4, resolve as resolve3 } from "path";
1899
+ import { join as join5, resolve as resolve4 } from "path";
1631
1900
 
1632
1901
  // src/watch-alerts.ts
1633
1902
  import { exec } from "child_process";
@@ -1685,7 +1954,7 @@ function sendTelegram(payload, botToken, chatId) {
1685
1954
  text: formatTelegram(payload),
1686
1955
  parse_mode: "Markdown"
1687
1956
  });
1688
- return new Promise((resolve4, reject) => {
1957
+ return new Promise((resolve5, reject) => {
1689
1958
  const req = httpsRequest(
1690
1959
  `https://api.telegram.org/bot${botToken}/sendMessage`,
1691
1960
  {
@@ -1694,7 +1963,7 @@ function sendTelegram(payload, botToken, chatId) {
1694
1963
  },
1695
1964
  (res) => {
1696
1965
  res.resume();
1697
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1966
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1698
1967
  else reject(new Error(`Telegram API returned ${res.statusCode}`));
1699
1968
  }
1700
1969
  );
@@ -1707,7 +1976,7 @@ function sendWebhook(payload, url) {
1707
1976
  const body = JSON.stringify(payload);
1708
1977
  const isHttps = url.startsWith("https");
1709
1978
  const doRequest = isHttps ? httpsRequest : httpRequest;
1710
- return new Promise((resolve4, reject) => {
1979
+ return new Promise((resolve5, reject) => {
1711
1980
  const req = doRequest(
1712
1981
  url,
1713
1982
  {
@@ -1716,7 +1985,7 @@ function sendWebhook(payload, url) {
1716
1985
  },
1717
1986
  (res) => {
1718
1987
  res.resume();
1719
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1988
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
1720
1989
  else reject(new Error(`Webhook returned ${res.statusCode}`));
1721
1990
  }
1722
1991
  );
@@ -1729,7 +1998,7 @@ function sendWebhook(payload, url) {
1729
1998
  });
1730
1999
  }
1731
2000
  function sendCommand(payload, cmd) {
1732
- return new Promise((resolve4, reject) => {
2001
+ return new Promise((resolve5, reject) => {
1733
2002
  const env = {
1734
2003
  ...process.env,
1735
2004
  AGENTFLOW_ALERT_AGENT: payload.agentId,
@@ -1742,13 +2011,13 @@ function sendCommand(payload, cmd) {
1742
2011
  };
1743
2012
  exec(cmd, { env, timeout: 3e4 }, (err) => {
1744
2013
  if (err) reject(err);
1745
- else resolve4();
2014
+ else resolve5();
1746
2015
  });
1747
2016
  });
1748
2017
  }
1749
2018
 
1750
2019
  // src/watch-state.ts
1751
- import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
2020
+ import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
1752
2021
  function parseDuration(input) {
1753
2022
  const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
1754
2023
  if (!match) {
@@ -1773,9 +2042,9 @@ function emptyState() {
1773
2042
  return { version: 1, agents: {}, lastPollTime: 0 };
1774
2043
  }
1775
2044
  function loadWatchState(filePath) {
1776
- if (!existsSync3(filePath)) return emptyState();
2045
+ if (!existsSync4(filePath)) return emptyState();
1777
2046
  try {
1778
- const raw = JSON.parse(readFileSync2(filePath, "utf8"));
2047
+ const raw = JSON.parse(readFileSync3(filePath, "utf8"));
1779
2048
  if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
1780
2049
  return raw;
1781
2050
  } catch {
@@ -2017,20 +2286,20 @@ function parseWatchArgs(argv) {
2017
2286
  recursive = true;
2018
2287
  i++;
2019
2288
  } else if (!arg.startsWith("-")) {
2020
- dirs.push(resolve3(arg));
2289
+ dirs.push(resolve4(arg));
2021
2290
  i++;
2022
2291
  } else {
2023
2292
  i++;
2024
2293
  }
2025
2294
  }
2026
- if (dirs.length === 0) dirs.push(resolve3("."));
2295
+ if (dirs.length === 0) dirs.push(resolve4("."));
2027
2296
  if (alertConditions.length === 0) {
2028
2297
  alertConditions.push({ type: "error" });
2029
2298
  alertConditions.push({ type: "recovery" });
2030
2299
  }
2031
2300
  notifyChannels.unshift({ type: "stdout" });
2032
2301
  if (!stateFilePath) {
2033
- stateFilePath = join4(dirs[0], ".agentflow-watch-state.json");
2302
+ stateFilePath = join5(dirs[0], ".agentflow-watch-state.json");
2034
2303
  }
2035
2304
  return {
2036
2305
  dirs,
@@ -2038,7 +2307,7 @@ function parseWatchArgs(argv) {
2038
2307
  pollIntervalMs,
2039
2308
  alertConditions,
2040
2309
  notifyChannels,
2041
- stateFilePath: resolve3(stateFilePath),
2310
+ stateFilePath: resolve4(stateFilePath),
2042
2311
  cooldownMs
2043
2312
  };
2044
2313
  }
@@ -2092,12 +2361,12 @@ Examples:
2092
2361
  }
2093
2362
  function startWatch(argv) {
2094
2363
  const config = parseWatchArgs(argv);
2095
- const valid = config.dirs.filter((d) => existsSync4(d));
2364
+ const valid = config.dirs.filter((d) => existsSync5(d));
2096
2365
  if (valid.length === 0) {
2097
2366
  console.error(`No valid directories found: ${config.dirs.join(", ")}`);
2098
2367
  process.exit(1);
2099
2368
  }
2100
- const invalid = config.dirs.filter((d) => !existsSync4(d));
2369
+ const invalid = config.dirs.filter((d) => !existsSync5(d));
2101
2370
  if (invalid.length > 0) {
2102
2371
  console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
2103
2372
  }
@@ -2135,9 +2404,17 @@ agentflow watch started`);
2135
2404
  records.push(...recs);
2136
2405
  }
2137
2406
  const alerts = detectTransitions(state, records, config, now);
2138
- for (const alert of alerts) {
2139
- for (const channel of config.notifyChannels) {
2140
- await sendAlert(alert, channel);
2407
+ const isBootstrap = pollCount === 1 && Object.keys(state.agents).length === 0;
2408
+ if (isBootstrap) {
2409
+ const suppressed = alerts.length;
2410
+ if (suppressed > 0) {
2411
+ console.log(`[bootstrap] Suppressed ${suppressed} initial alerts (baseline scan)`);
2412
+ }
2413
+ } else {
2414
+ for (const alert of alerts) {
2415
+ for (const channel of config.notifyChannels) {
2416
+ await sendAlert(alert, channel);
2417
+ }
2141
2418
  }
2142
2419
  }
2143
2420
  state = updateWatchState(state, records, alerts, now);
@@ -2183,6 +2460,9 @@ export {
2183
2460
  stitchTrace,
2184
2461
  getTraceTree,
2185
2462
  startLive,
2463
+ discoverProcessConfig,
2464
+ auditProcesses,
2465
+ formatAuditReport,
2186
2466
  createGraphBuilder,
2187
2467
  runTraced,
2188
2468
  createTraceStore,