@integrity-labs/agt-cli 0.19.14 → 0.19.16

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.
@@ -100,7 +100,7 @@ async function spawnPairSession(session) {
100
100
  return { ok: true };
101
101
  } catch {
102
102
  }
103
- const { resolveClaudeBinary } = await import("./persistent-session-CFDGW7QE.js");
103
+ const { resolveClaudeBinary } = await import("./persistent-session-M2GVL6Z6.js");
104
104
  const claudeBin = resolveClaudeBinary();
105
105
  try {
106
106
  await execFileAsync("tmux", [
@@ -357,4 +357,4 @@ export {
357
357
  startClaudePair,
358
358
  submitClaudePairCode
359
359
  };
360
- //# sourceMappingURL=claude-pair-runtime-VXN4NVGR.js.map
360
+ //# sourceMappingURL=claude-pair-runtime-UF4OMFCA.js.map
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-KLNFWXOI.js";
25
+ } from "../chunk-NT26H4DO.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -39,6 +39,7 @@ import {
39
39
  isAgentIdle,
40
40
  isSessionHealthy,
41
41
  isStaleForToday,
42
+ parsePsRows,
42
43
  peekCurrentSession,
43
44
  prepareForRespawn,
44
45
  reapOrphanChannelMcps,
@@ -48,7 +49,7 @@ import {
48
49
  startPersistentSession,
49
50
  stopAllSessionsAndWait,
50
51
  stopPersistentSession
51
- } from "../chunk-HA4IUBVC.js";
52
+ } from "../chunk-3K3RO5NS.js";
52
53
 
53
54
  // src/lib/manager-worker.ts
54
55
  import { createHash } from "crypto";
@@ -59,6 +60,220 @@ import { join as join4, dirname } from "path";
59
60
  import { homedir as homedir3 } from "os";
60
61
  import { fileURLToPath } from "url";
61
62
 
63
+ // src/lib/mcp-config-drift.ts
64
+ function decideMcpDriftAction(currentHash, knownHash) {
65
+ if (!currentHash) return { kind: "no-config" };
66
+ if (!knownHash) return { kind: "baseline", hash: currentHash };
67
+ if (knownHash === currentHash) return { kind: "no-drift" };
68
+ return { kind: "drift", previous: knownHash, current: currentHash };
69
+ }
70
+
71
+ // src/lib/stale-mcp-reaper.ts
72
+ import { execFileSync } from "child_process";
73
+ function parseEnvIntegrationsEntries(content) {
74
+ const entries = /* @__PURE__ */ new Map();
75
+ for (const raw of content.split(/\r?\n/)) {
76
+ const line = raw.trim();
77
+ if (!line || line.startsWith("#")) continue;
78
+ const eq = line.indexOf("=");
79
+ if (eq <= 0) continue;
80
+ const name = line.slice(0, eq).trim();
81
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
82
+ const value = line.slice(eq + 1);
83
+ entries.set(name, value);
84
+ }
85
+ return entries;
86
+ }
87
+ function diffEnvIntegrations(oldContent, newContent) {
88
+ const oldEntries = oldContent === void 0 ? /* @__PURE__ */ new Map() : parseEnvIntegrationsEntries(oldContent);
89
+ const newEntries = parseEnvIntegrationsEntries(newContent);
90
+ const changed = /* @__PURE__ */ new Set();
91
+ for (const [name, value] of newEntries) {
92
+ if (oldEntries.get(name) !== value) changed.add(name);
93
+ }
94
+ for (const name of oldEntries.keys()) {
95
+ if (!newEntries.has(name)) changed.add(name);
96
+ }
97
+ return [...changed];
98
+ }
99
+ function findMcpServersUsingVars(mcp, changedVars) {
100
+ const changedSet = new Set(changedVars);
101
+ if (!mcp?.mcpServers || changedSet.size === 0) return [];
102
+ const result = [];
103
+ for (const [serverKey, entry] of Object.entries(mcp.mcpServers)) {
104
+ if (!entry || typeof entry !== "object") continue;
105
+ const env = entry.env;
106
+ if (!env || typeof env !== "object") continue;
107
+ let matches = false;
108
+ for (const value of Object.values(env)) {
109
+ if (typeof value !== "string") continue;
110
+ const placeholderMatches = value.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g);
111
+ if (placeholderMatches) {
112
+ for (const ph of placeholderMatches) {
113
+ const name = ph.slice(2, -1);
114
+ if (changedSet.has(name)) {
115
+ matches = true;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ if (matches) break;
121
+ }
122
+ if (matches) result.push(serverKey);
123
+ }
124
+ return result;
125
+ }
126
+ function buildArgvMatchersForEntry(key, entry) {
127
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
128
+ const patterns = [];
129
+ const args = entry?.args ?? [];
130
+ for (let i = args.length - 1; i >= 0; i--) {
131
+ const arg = args[i];
132
+ if (typeof arg !== "string" || !arg) continue;
133
+ if (arg.startsWith("-")) continue;
134
+ const stripped = arg.replace(
135
+ /@[^/@]*$/,
136
+ (m) => (
137
+ // @latest, @1.2.3, etc. — drop. But @scope at the START of a
138
+ // package spec must NOT be stripped, so we only strip a tail
139
+ // `@...` if it doesn't itself start with a slash inside.
140
+ // The regex above only matches a single trailing `@...`
141
+ // segment after the last `/`, so this is safe for `@scope/pkg`.
142
+ m.includes("/") ? m : ""
143
+ )
144
+ );
145
+ const tail = "(?![A-Za-z0-9_-])";
146
+ if (/^@?[a-z0-9]([a-z0-9._-]*\/)?[a-z0-9._-]+$/i.test(stripped)) {
147
+ patterns.push(new RegExp(`${escapeRe(stripped)}${tail}`));
148
+ const basename = stripped.split("/").pop();
149
+ if (basename && basename !== stripped) {
150
+ patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));
151
+ }
152
+ break;
153
+ }
154
+ if (stripped.includes("/")) {
155
+ const basename = stripped.split("/").pop();
156
+ if (basename) {
157
+ patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ if (patterns.length === 0) {
163
+ const safe = escapeRe(key);
164
+ patterns.push(new RegExp(`(?<![A-Za-z0-9_])${safe}(?![A-Za-z0-9_])`));
165
+ if (safe.includes("_")) {
166
+ const dashed = safe.replace(/_/g, "-");
167
+ patterns.push(new RegExp(`(?<![A-Za-z0-9_])${dashed}(?![A-Za-z0-9_])`));
168
+ }
169
+ }
170
+ return patterns;
171
+ }
172
+ function buildClaudeAgentMatcher(codeName) {
173
+ const safe = codeName.replace(/[^A-Za-z0-9_-]/g, "");
174
+ return new RegExp(`\\bclaude\\b.*--name\\s+agt-${safe}(?=\\s|$)`);
175
+ }
176
+ function findMcpChildrenForAgent(args) {
177
+ const { rows, codeName, serverKeys, mcpJson } = args;
178
+ const maxDepth = args.maxDepth ?? 8;
179
+ if (serverKeys.length === 0 || rows.length === 0) return [];
180
+ const claudeMatcher = buildClaudeAgentMatcher(codeName);
181
+ const argvMatchers = [];
182
+ for (const key of serverKeys) {
183
+ const entry = mcpJson?.mcpServers?.[key];
184
+ argvMatchers.push(...buildArgvMatchersForEntry(key, entry));
185
+ }
186
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
187
+ const matched = [];
188
+ for (const row of rows) {
189
+ if (!argvMatchers.some((re) => re.test(row.args))) continue;
190
+ if (/\bclaude\b/.test(row.args) && row.args.includes(`--name agt-${codeName}`)) continue;
191
+ let cur = byPid.get(row.ppid);
192
+ let belongs = false;
193
+ for (let depth = 0; depth < maxDepth && cur; depth++) {
194
+ if (claudeMatcher.test(cur.args)) {
195
+ belongs = true;
196
+ break;
197
+ }
198
+ if (cur.pid === 1) break;
199
+ cur = byPid.get(cur.ppid);
200
+ }
201
+ if (belongs) matched.push(row.pid);
202
+ }
203
+ return matched;
204
+ }
205
+ function reapStaleMcpChildren(args) {
206
+ const { log: log2, codeName, serverKeys, mcpJson, graceMs = 5e3 } = args;
207
+ if (serverKeys.length === 0) return [];
208
+ const runPs = args.runPs ?? (() => execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
209
+ const killProcess = args.killProcess ?? ((pid, signal) => {
210
+ try {
211
+ process.kill(pid, signal);
212
+ } catch {
213
+ }
214
+ });
215
+ const isAlive = args.isAlive ?? ((pid) => {
216
+ try {
217
+ process.kill(pid, 0);
218
+ return true;
219
+ } catch {
220
+ return false;
221
+ }
222
+ });
223
+ let psOutput;
224
+ try {
225
+ psOutput = runPs();
226
+ } catch (err) {
227
+ log2(`[stale-mcp-reaper] ps invocation failed for '${codeName}': ${err.message} \u2014 skipping reap`);
228
+ return [];
229
+ }
230
+ const rows = parsePsRows(psOutput);
231
+ const targets = findMcpChildrenForAgent({ rows, codeName, serverKeys, mcpJson });
232
+ if (targets.length === 0) return [];
233
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
234
+ const describe = (pid) => {
235
+ const argv = byPid.get(pid)?.args ?? "";
236
+ const pkgMatch = argv.match(/(@[a-z0-9_-]+\/[a-z0-9_-]+|[a-z0-9_-]+-mcp-server|[a-z0-9_-]+-mcp\b)/i);
237
+ return pkgMatch ? `${pkgMatch[1]} (pid ${pid})` : `pid ${pid}`;
238
+ };
239
+ log2(
240
+ `[stale-mcp-reaper] '${codeName}': rotating ${targets.length} stale MCP child(ren) for [${serverKeys.join(", ")}]: ${targets.map(describe).join(", ")}`
241
+ );
242
+ for (const pid of targets) {
243
+ killProcess(pid, "SIGTERM");
244
+ }
245
+ setTimeout(() => {
246
+ try {
247
+ let freshPsOutput;
248
+ try {
249
+ freshPsOutput = runPs();
250
+ } catch (err) {
251
+ log2(`[stale-mcp-reaper] '${codeName}': fresh ps for SIGKILL re-verify failed: ${err.message} \u2014 skipping SIGKILL pass`);
252
+ return;
253
+ }
254
+ const stillOwned = new Set(
255
+ findMcpChildrenForAgent({
256
+ rows: parsePsRows(freshPsOutput),
257
+ codeName,
258
+ serverKeys,
259
+ mcpJson
260
+ })
261
+ );
262
+ const stragglers = targets.filter((pid) => isAlive(pid) && stillOwned.has(pid));
263
+ if (stragglers.length === 0) return;
264
+ log2(
265
+ `[stale-mcp-reaper] '${codeName}': ${stragglers.length} child(ren) survived SIGTERM; sending SIGKILL: ${stragglers.map(describe).join(", ")}`
266
+ );
267
+ for (const pid of stragglers) {
268
+ killProcess(pid, "SIGKILL");
269
+ }
270
+ } catch (err) {
271
+ log2(`[stale-mcp-reaper] '${codeName}': error in SIGKILL pass: ${err.message}`);
272
+ }
273
+ }, graceMs).unref();
274
+ return targets;
275
+ }
276
+
62
277
  // src/lib/channel-restart-decision.ts
63
278
  function launchableChannelIds(channelConfigs) {
64
279
  if (!channelConfigs) return /* @__PURE__ */ new Set();
@@ -649,7 +864,7 @@ function saveChannelHashCache(source, configDir) {
649
864
  }
650
865
 
651
866
  // src/lib/channel-sweep.ts
652
- import { execFileSync } from "child_process";
867
+ import { execFileSync as execFileSync2 } from "child_process";
653
868
  var CHANNEL_BASENAMES = [
654
869
  "slack-channel",
655
870
  "direct-chat-channel",
@@ -812,7 +1027,7 @@ function resolveLiveAnchorPids(agentCodeNames) {
812
1027
  for (const codeName of agentCodeNames) {
813
1028
  const pids = /* @__PURE__ */ new Set();
814
1029
  try {
815
- const out = execFileSync("tmux", ["list-panes", "-t", `agt-${codeName}`, "-F", "#{pane_pid}"], {
1030
+ const out = execFileSync2("tmux", ["list-panes", "-t", `agt-${codeName}`, "-F", "#{pane_pid}"], {
816
1031
  encoding: "utf-8",
817
1032
  timeout: 2e3,
818
1033
  stdio: ["ignore", "pipe", "ignore"]
@@ -832,7 +1047,7 @@ async function sweepChannelProcesses(opts) {
832
1047
  const kill = opts.killFn ?? defaultKill;
833
1048
  let psOutput = "";
834
1049
  try {
835
- psOutput = execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
1050
+ psOutput = execFileSync2("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
836
1051
  encoding: "utf-8",
837
1052
  timeout: 5e3,
838
1053
  maxBuffer: 10 * 1024 * 1024
@@ -869,7 +1084,7 @@ function defaultKillSignal(pid, signal) {
869
1084
  }
870
1085
  }
871
1086
  function defaultPs() {
872
- return execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
1087
+ return execFileSync2("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
873
1088
  encoding: "utf-8",
874
1089
  timeout: 5e3,
875
1090
  maxBuffer: 10 * 1024 * 1024
@@ -1593,6 +1808,7 @@ function scheduleSessionRestart(codeName, delayMs, reason) {
1593
1808
  const timer = setTimeout(() => {
1594
1809
  pendingSessionRestarts.delete(codeName);
1595
1810
  stopPersistentSession(codeName, log);
1811
+ runningMcpHashes.delete(codeName);
1596
1812
  log(`[hot-reload] Session stopped for '${codeName}' \u2014 will respawn with ${reason}`);
1597
1813
  }, delayMs);
1598
1814
  timer.unref?.();
@@ -1607,6 +1823,38 @@ function cancelPendingSessionRestart(codeName) {
1607
1823
  }
1608
1824
  var writtenHashes = /* @__PURE__ */ new Map();
1609
1825
  var knownSecretsHashes = /* @__PURE__ */ new Map();
1826
+ var runningMcpHashes = /* @__PURE__ */ new Map();
1827
+ function projectMcpHash(codeName, projectDir) {
1828
+ try {
1829
+ return createHash("sha256").update(readFileSync3(join4(projectDir, ".mcp.json"))).digest("hex");
1830
+ } catch {
1831
+ return null;
1832
+ }
1833
+ }
1834
+ function stopPersistentSessionAndForgetMcpBaseline(codeName) {
1835
+ cancelPendingSessionRestart(codeName);
1836
+ stopPersistentSession(codeName, log);
1837
+ runningMcpHashes.delete(codeName);
1838
+ }
1839
+ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
1840
+ const currentHash = projectMcpHash(codeName, projectDir);
1841
+ const action = decideMcpDriftAction(currentHash, runningMcpHashes.get(codeName));
1842
+ switch (action.kind) {
1843
+ case "no-config":
1844
+ case "no-drift":
1845
+ return;
1846
+ case "baseline":
1847
+ runningMcpHashes.set(codeName, action.hash);
1848
+ return;
1849
+ case "drift":
1850
+ log(
1851
+ `[hot-reload] .mcp.json content changed for '${codeName}' (${action.previous.slice(0, 12)} \u2192 ${action.current.slice(0, 12)}); scheduling restart (ENG-4897)`
1852
+ );
1853
+ scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
1854
+ runningMcpHashes.delete(codeName);
1855
+ return;
1856
+ }
1857
+ }
1610
1858
  var knownChannelConfigHashes = /* @__PURE__ */ new Map();
1611
1859
  var knownModels = /* @__PURE__ */ new Map();
1612
1860
  var knownTasksHashes = /* @__PURE__ */ new Map();
@@ -1681,10 +1929,10 @@ function clearAgentCaches(agentId, codeName) {
1681
1929
  var cachedFrameworkVersion = null;
1682
1930
  var lastVersionCheckAt = 0;
1683
1931
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
1684
- var agtCliVersion = true ? "0.19.14" : "dev";
1685
- function resolveBrewPath(execFileSync2) {
1932
+ var agtCliVersion = true ? "0.19.16" : "dev";
1933
+ function resolveBrewPath(execFileSync3) {
1686
1934
  try {
1687
- const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
1935
+ const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
1688
1936
  if (out) return out;
1689
1937
  } catch {
1690
1938
  }
@@ -1726,9 +1974,9 @@ async function ensureToolkitCli(toolkitSlug) {
1726
1974
  }
1727
1975
  const { binary, installer, package: pkg, script } = integration.cli_tool;
1728
1976
  const resolvedInstaller = installer ?? "manual";
1729
- const { execFileSync: execFileSync2, execSync } = await import("child_process");
1977
+ const { execFileSync: execFileSync3, execSync } = await import("child_process");
1730
1978
  try {
1731
- execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
1979
+ execFileSync3("which", [binary], { timeout: 5e3, stdio: "pipe" });
1732
1980
  toolkitCliEnsured.add(toolkitSlug);
1733
1981
  toolkitCliRetryAfter.delete(toolkitSlug);
1734
1982
  toolkitCliFailureCount.delete(toolkitSlug);
@@ -1749,14 +1997,14 @@ async function ensureToolkitCli(toolkitSlug) {
1749
1997
  return;
1750
1998
  }
1751
1999
  log(`[toolkit-install] ${toolkitSlug}: installing via npm (${pkg})\u2026`);
1752
- execFileSync2("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
2000
+ execFileSync3("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
1753
2001
  } else if (resolvedInstaller === "brew") {
1754
2002
  if (!pkg) {
1755
2003
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but no package declared`);
1756
2004
  toolkitCliEnsured.add(toolkitSlug);
1757
2005
  return;
1758
2006
  }
1759
- const brewPath = resolveBrewPath(execFileSync2);
2007
+ const brewPath = resolveBrewPath(execFileSync3);
1760
2008
  if (!brewPath) {
1761
2009
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but Homebrew not available \u2014 install manually: brew install ${pkg}`);
1762
2010
  toolkitCliEnsured.add(toolkitSlug);
@@ -1766,9 +2014,9 @@ async function ensureToolkitCli(toolkitSlug) {
1766
2014
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
1767
2015
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
1768
2016
  if (isRoot) {
1769
- execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe", cwd: "/tmp" });
2017
+ execFileSync3("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe", cwd: "/tmp" });
1770
2018
  } else {
1771
- execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
2019
+ execFileSync3(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
1772
2020
  }
1773
2021
  } else if (resolvedInstaller === "script") {
1774
2022
  if (!script) {
@@ -1788,7 +2036,7 @@ async function ensureToolkitCli(toolkitSlug) {
1788
2036
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
1789
2037
  }
1790
2038
  try {
1791
- execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
2039
+ execFileSync3("which", [binary], { timeout: 5e3, stdio: "pipe" });
1792
2040
  log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
1793
2041
  toolkitCliEnsured.add(toolkitSlug);
1794
2042
  toolkitCliRetryAfter.delete(toolkitSlug);
@@ -1841,8 +2089,8 @@ async function ensureFrameworkBinary(frameworkId) {
1841
2089
  if (frameworkId !== "claude-code") return;
1842
2090
  if (frameworkBinaryChecked.has(frameworkId)) return;
1843
2091
  frameworkBinaryChecked.add(frameworkId);
1844
- const { execFileSync: execFileSync2 } = await import("child_process");
1845
- const brewPath = resolveBrewPath(execFileSync2);
2092
+ const { execFileSync: execFileSync3 } = await import("child_process");
2093
+ const brewPath = resolveBrewPath(execFileSync3);
1846
2094
  if (!brewPath) {
1847
2095
  log("Homebrew not found (no `brew` on PATH, no /home/linuxbrew/.linuxbrew/bin/brew). Cannot auto-install Claude Code. Install manually: https://claude.ai/download");
1848
2096
  return;
@@ -1858,7 +2106,7 @@ async function ensureFrameworkBinary(frameworkId) {
1858
2106
  let claudeExists = existsSync3("/home/linuxbrew/.linuxbrew/bin/claude");
1859
2107
  if (!claudeExists) {
1860
2108
  try {
1861
- execFileSync2("which", ["claude"], { timeout: 5e3 });
2109
+ execFileSync3("which", ["claude"], { timeout: 5e3 });
1862
2110
  claudeExists = true;
1863
2111
  } catch {
1864
2112
  }
@@ -1950,16 +2198,16 @@ async function checkAndUpdateCli() {
1950
2198
  }
1951
2199
  }
1952
2200
  async function checkAndUpdateCliViaBrew() {
1953
- const { execFileSync: execFileSync2 } = await import("child_process");
1954
- const brewPath = resolveBrewPath(execFileSync2);
2201
+ const { execFileSync: execFileSync3 } = await import("child_process");
2202
+ const brewPath = resolveBrewPath(execFileSync3);
1955
2203
  if (!brewPath) return;
1956
2204
  try {
1957
- execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
2205
+ execFileSync3(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
1958
2206
  } catch (err) {
1959
2207
  log(`[self-update] brew update failed (continuing with stale cache): ${err.message}`);
1960
2208
  }
1961
2209
  try {
1962
- const outdated = execFileSync2(brewPath, ["outdated", "--json=v2"], {
2210
+ const outdated = execFileSync3(brewPath, ["outdated", "--json=v2"], {
1963
2211
  timeout: 3e4,
1964
2212
  encoding: "utf-8"
1965
2213
  });
@@ -1970,7 +2218,7 @@ async function checkAndUpdateCliViaBrew() {
1970
2218
  const latest = agtOutdated.current_version ?? "unknown";
1971
2219
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading via brew...`);
1972
2220
  try {
1973
- execFileSync2(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
2221
+ execFileSync3(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1974
2222
  timeout: 12e4,
1975
2223
  stdio: "pipe"
1976
2224
  });
@@ -1989,7 +2237,7 @@ async function checkAndUpdateCliViaBrew() {
1989
2237
  }
1990
2238
  }
1991
2239
  async function checkAndUpdateCliViaNpm() {
1992
- const { execFileSync: execFileSync2 } = await import("child_process");
2240
+ const { execFileSync: execFileSync3 } = await import("child_process");
1993
2241
  if (agtCliVersion === "dev") return;
1994
2242
  let latest;
1995
2243
  try {
@@ -2038,7 +2286,7 @@ async function checkAndUpdateCliViaNpm() {
2038
2286
  "--registry=https://registry.npmjs.org"
2039
2287
  ];
2040
2288
  try {
2041
- execFileSync2(cmd, args, { timeout: 18e4, stdio: "pipe" });
2289
+ execFileSync3(cmd, args, { timeout: 18e4, stdio: "pipe" });
2042
2290
  log(`[self-update] agt CLI upgraded to ${latest}. Scheduling manager restart so the new binary takes effect.`);
2043
2291
  restartAfterUpgrade = true;
2044
2292
  pendingUpgradeVersion = latest;
@@ -2524,8 +2772,7 @@ async function pollCycle() {
2524
2772
  log,
2525
2773
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
2526
2774
  stopSession: (codeName) => {
2527
- cancelPendingSessionRestart(codeName);
2528
- stopPersistentSession(codeName, log);
2775
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
2529
2776
  persistentSessionAgents.delete(codeName);
2530
2777
  claudeAuthTupleBySession.delete(codeName);
2531
2778
  },
@@ -2585,7 +2832,7 @@ async function pollCycle() {
2585
2832
  }
2586
2833
  try {
2587
2834
  const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
2588
- const { collectDiagnostics } = await import("../persistent-session-CFDGW7QE.js");
2835
+ const { collectDiagnostics } = await import("../persistent-session-M2GVL6Z6.js");
2589
2836
  const diagCodeNames = [...persistentSessionAgents];
2590
2837
  const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
2591
2838
  let tailscaleHostname;
@@ -2685,8 +2932,7 @@ async function pollCycle() {
2685
2932
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
2686
2933
  const adapter = resolveAgentFramework(prev.codeName);
2687
2934
  await stopGatewayIfRunning(prev.codeName, adapter);
2688
- cancelPendingSessionRestart(prev.codeName);
2689
- stopPersistentSession(prev.codeName, log);
2935
+ stopPersistentSessionAndForgetMcpBaseline(prev.codeName);
2690
2936
  try {
2691
2937
  const { execSync: es } = await import("child_process");
2692
2938
  es(`tmux kill-session -t agt-${prev.codeName} 2>/dev/null`, { stdio: "ignore" });
@@ -2823,8 +3069,7 @@ async function processAgent(agent, agentStates) {
2823
3069
  if (agent.status === "draft" || agent.status === "paused") {
2824
3070
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
2825
3071
  await stopGatewayIfRunning(agent.code_name, adapter);
2826
- cancelPendingSessionRestart(agent.code_name);
2827
- stopPersistentSession(agent.code_name, log);
3072
+ stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
2828
3073
  try {
2829
3074
  const { execSync: es } = await import("child_process");
2830
3075
  es(`tmux kill-session -t agt-${agent.code_name} 2>/dev/null`, { stdio: "ignore" });
@@ -2852,8 +3097,7 @@ async function processAgent(agent, agentStates) {
2852
3097
  if (agent.status === "revoked") {
2853
3098
  log(`Agent '${agent.code_name}' is revoked, cleaning up`);
2854
3099
  await stopGatewayIfRunning(agent.code_name, adapter);
2855
- cancelPendingSessionRestart(agent.code_name);
2856
- stopPersistentSession(agent.code_name, log);
3100
+ stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
2857
3101
  try {
2858
3102
  const { execSync: es } = await import("child_process");
2859
3103
  es(`tmux kill-session -t agt-${agent.code_name} 2>/dev/null`, { stdio: "ignore" });
@@ -3326,19 +3570,50 @@ async function processAgent(agent, agentStates) {
3326
3570
  const intHash = createHash("sha256").update(JSON.stringify(integrations.map((i) => `${i.definition_id}:${JSON.stringify(i.credentials)}`))).digest("hex").slice(0, 16);
3327
3571
  const prevIntHash = knownIntegrationHashes.get(agent.agent_id);
3328
3572
  if (intHash !== prevIntHash) {
3573
+ const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
3574
+ const envIntPath = join4(projectDir, ".env.integrations");
3575
+ let preWriteEnv;
3576
+ try {
3577
+ preWriteEnv = readFileSync3(envIntPath, "utf-8");
3578
+ } catch {
3579
+ preWriteEnv = void 0;
3580
+ }
3329
3581
  if (frameworkAdapter.writeIntegrations) {
3330
3582
  frameworkAdapter.writeIntegrations(agent.code_name, integrations);
3331
3583
  }
3332
- knownIntegrationHashes.set(agent.agent_id, intHash);
3333
3584
  log(`Integrations provisioned for '${agent.code_name}' (${integrations.length} integration(s))`);
3334
3585
  const fw = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
3586
+ let rotationHandled = true;
3335
3587
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
3336
- const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
3337
- injectMessage(agent.code_name, "system", `Your integrations have been updated. You now have access to: ${names}. Your .env.integrations and CLAUDE.md have been refreshed \u2014 credentials are available immediately via environment variables.`, {
3338
- task_name: "integration-update"
3339
- }, log).catch(() => {
3340
- });
3341
- log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names}`);
3588
+ try {
3589
+ const projectMcpPath = join4(projectDir, ".mcp.json");
3590
+ const postWriteEnv = readFileSync3(envIntPath, "utf-8");
3591
+ const mcpContent = readFileSync3(projectMcpPath, "utf-8");
3592
+ const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
3593
+ const mcpJsonForReap = JSON.parse(mcpContent);
3594
+ const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
3595
+ if (affectedServerKeys.length > 0) {
3596
+ reapStaleMcpChildren({
3597
+ log,
3598
+ codeName: agent.code_name,
3599
+ serverKeys: affectedServerKeys,
3600
+ mcpJson: mcpJsonForReap
3601
+ });
3602
+ }
3603
+ const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
3604
+ const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
3605
+ injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
3606
+ task_name: "integration-update"
3607
+ }, log).catch(() => {
3608
+ });
3609
+ log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
3610
+ } catch (err) {
3611
+ rotationHandled = false;
3612
+ log(`[hot-reload] Failed to compute / reap affected MCP servers for '${agent.code_name}': ${err.message} \u2014 will retry next tick`);
3613
+ }
3614
+ }
3615
+ if (rotationHandled) {
3616
+ knownIntegrationHashes.set(agent.agent_id, intHash);
3342
3617
  }
3343
3618
  needsGatewayRestart = true;
3344
3619
  }
@@ -3656,6 +3931,11 @@ async function processAgent(agent, agentStates) {
3656
3931
  const sessionMode = refreshData.agent.session_mode ?? "oneshot";
3657
3932
  if (agentFw === "claude-code" && sessionMode === "persistent") {
3658
3933
  await ensurePersistentSession(agent, tasks, boardItems, refreshData);
3934
+ try {
3935
+ checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir2(agent.code_name));
3936
+ } catch (err) {
3937
+ log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
3938
+ }
3659
3939
  } else if (agentFw === "claude-code" && tasks.length > 0) {
3660
3940
  await syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData);
3661
3941
  } else if (frameworkAdapter.syncScheduledTasks && gatewayRunning && gatewayPort) {
@@ -4407,8 +4687,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4407
4687
  const msg = err.message;
4408
4688
  log(`[persistent-session] Failed to resolve auth for '${codeName}': ${msg} \u2014 refusing to spawn`);
4409
4689
  if (isSessionHealthy(codeName)) {
4410
- cancelPendingSessionRestart(codeName);
4411
- stopPersistentSession(codeName, log);
4690
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4412
4691
  persistentSessionAgents.delete(codeName);
4413
4692
  claudeAuthTupleBySession.delete(codeName);
4414
4693
  }
@@ -4430,8 +4709,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4430
4709
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
4431
4710
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
4432
4711
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
4433
- cancelPendingSessionRestart(codeName);
4434
- stopPersistentSession(codeName, log);
4712
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4435
4713
  persistentSessionAgents.delete(codeName);
4436
4714
  }
4437
4715
  if (isStaleForToday(codeName) && isSessionHealthy(codeName)) {
@@ -4442,8 +4720,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4442
4720
  log(
4443
4721
  `[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
4444
4722
  );
4445
- cancelPendingSessionRestart(codeName);
4446
- stopPersistentSession(codeName, log);
4723
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4447
4724
  persistentSessionAgents.delete(codeName);
4448
4725
  } else {
4449
4726
  log(
@@ -5822,7 +6099,7 @@ async function processClaudePairSessions(agents) {
5822
6099
  killPairSession,
5823
6100
  pairTmuxSession,
5824
6101
  finalizeClaudePairOnboarding
5825
- } = await import("../claude-pair-runtime-VXN4NVGR.js");
6102
+ } = await import("../claude-pair-runtime-UF4OMFCA.js");
5826
6103
  for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
5827
6104
  log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
5828
6105
  const killed = await killPairSession(pairTmuxSession(pairId));