@pushpalsdev/cli 1.0.11 → 1.0.13

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.
@@ -7,7 +7,9 @@ import {
7
7
  chmodSync,
8
8
  cpSync,
9
9
  existsSync as existsSync4,
10
+ lstatSync,
10
11
  mkdirSync,
12
+ readdirSync,
11
13
  readFileSync as readFileSync4,
12
14
  writeFileSync
13
15
  } from "fs";
@@ -616,7 +618,7 @@ function loadPushPalsConfig(options = {}) {
616
618
  retentionDays: remoteMemoryRetentionDays
617
619
  },
618
620
  autonomy: {
619
- enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, false),
621
+ enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, true),
620
622
  killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
621
623
  tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
622
624
  heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
@@ -1104,6 +1106,17 @@ function formatClientRuntimePreflightLines(result, prefix) {
1104
1106
  return lines;
1105
1107
  }
1106
1108
 
1109
+ // ../shared/src/communication.ts
1110
+ function stripPresenceSourcePrefix(value) {
1111
+ return value.replace(/^(agent|client)(?:[\s:./_-]+)+/i, "");
1112
+ }
1113
+ function normalizePresenceClientLabel(value) {
1114
+ return stripPresenceSourcePrefix(String(value ?? "")).replace(/\s+/g, " ").trim();
1115
+ }
1116
+ function normalizePresenceLookupToken(value) {
1117
+ return normalizePresenceClientLabel(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
1118
+ }
1119
+
1107
1120
  // ../shared/src/repo.ts
1108
1121
  import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
1109
1122
  import { resolve as resolve3 } from "path";
@@ -1144,6 +1157,21 @@ function resolveGitStateFilePath(repoRoot, fileName) {
1144
1157
  return resolve3(gitMetadataDir, normalizedFileName);
1145
1158
  }
1146
1159
 
1160
+ // ../shared/src/session_event_visibility.ts
1161
+ var HEARTBEAT_STATUS_RE = /\bheartbeat\b/i;
1162
+ function isHeartbeatStatusSessionEvent(event) {
1163
+ const type = String(event?.type ?? "").trim().toLowerCase();
1164
+ if (type !== "status")
1165
+ return false;
1166
+ const payload = event?.payload ?? {};
1167
+ const detail = typeof payload.detail === "string" ? payload.detail.trim() : "";
1168
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
1169
+ return HEARTBEAT_STATUS_RE.test(detail) || HEARTBEAT_STATUS_RE.test(message);
1170
+ }
1171
+ function shouldDisplayInteractiveSessionEvent(event) {
1172
+ return !isHeartbeatStatusSessionEvent(event);
1173
+ }
1174
+
1147
1175
  // ../../scripts/pushpals-cli.ts
1148
1176
  var DEFAULT_MONITOR_PORT = 8081;
1149
1177
  var MONITOR_SCAN_PORTS = 32;
@@ -1163,6 +1191,7 @@ var GITHUB_HEADERS = {
1163
1191
  Accept: "application/vnd.github+json",
1164
1192
  "User-Agent": "pushpals-cli"
1165
1193
  };
1194
+ var ASK_REMOTE_BUDDY_COMMAND = "/ask_remote_buddy";
1166
1195
  var stateVersion = 1;
1167
1196
  var cliTimestampedConsoleInstalled = false;
1168
1197
  function formatTimestampedCliLine(line, at = new Date) {
@@ -1172,6 +1201,21 @@ function formatTimestampedCliLine(line, at = new Date) {
1172
1201
  }
1173
1202
  return `[${at.toISOString()}]${text}`;
1174
1203
  }
1204
+ function normalizeCliInteractiveMessage(input) {
1205
+ const trimmed = String(input ?? "").trim();
1206
+ const command = ASK_REMOTE_BUDDY_COMMAND.toLowerCase();
1207
+ if (!trimmed.toLowerCase().startsWith(command)) {
1208
+ return { text: trimmed };
1209
+ }
1210
+ const rest = trimmed.slice(command.length).replace(/^[:\-]\s*/, "").trim();
1211
+ if (!rest) {
1212
+ return {
1213
+ text: "",
1214
+ usageMessage: "Usage: /ask_remote_buddy <request>. Example: /ask_remote_buddy fix the failing job status in the dashboard."
1215
+ };
1216
+ }
1217
+ return { text: rest };
1218
+ }
1175
1219
  function installTimestampedCliConsole() {
1176
1220
  if (cliTimestampedConsoleInstalled)
1177
1221
  return;
@@ -1205,12 +1249,12 @@ function printUsage() {
1205
1249
  console.log("");
1206
1250
  console.log("Options:");
1207
1251
  console.log(" --server-url <url> Override PushPals server URL");
1208
- console.log(" --local-agent-url <url> Override LocalBuddy URL");
1252
+ console.log(" --local-agent-url <url> Override LocalBuddy URL for monitoring/runtime state");
1209
1253
  console.log(" --session-id <id> Override session ID");
1210
1254
  console.log(" --hub-url <url> Override monitoring hub URL");
1211
1255
  console.log(" --runtime-root <path> Override embedded runtime directory for auto-start");
1212
1256
  console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
1213
- console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
1257
+ console.log(" --no-auto-start Disable runtime auto-start when the server is down");
1214
1258
  console.log(" --no-stream Disable live session event stream");
1215
1259
  console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
1216
1260
  console.log(" -h, --help Show this help");
@@ -1223,8 +1267,8 @@ function printUsage() {
1223
1267
  console.log("");
1224
1268
  console.log("Notes:");
1225
1269
  console.log(" - Must be run from inside a git repository.");
1226
- console.log(" - Auto-start can bootstrap server/localbuddy/remotebuddy/source_control_manager.");
1227
- console.log(" - LocalBuddy must be attached to the same repo root.");
1270
+ console.log(" - Auto-start can bootstrap server/remotebuddy/source_control_manager and LocalBuddy when runtime config enables it.");
1271
+ console.log(" - Interactive CLI talks directly to server sessions; LocalBuddy is optional.");
1228
1272
  }
1229
1273
  function parseArgs(argv) {
1230
1274
  const options = { noAutoStart: false, noStream: false, runtimeOnly: false };
@@ -1311,12 +1355,6 @@ function parsePositiveInt(value, fallback) {
1311
1355
  return fallback;
1312
1356
  return parsed;
1313
1357
  }
1314
- function normalizePath(value) {
1315
- const normalized = resolve4(value).replace(/\\/g, "/").replace(/\/+$/, "");
1316
- if (process.platform === "win32")
1317
- return normalized.toLowerCase();
1318
- return normalized;
1319
- }
1320
1358
  function jsonHtmlBootstrap(value) {
1321
1359
  return JSON.stringify(value).replace(/</g, "\\u003c");
1322
1360
  }
@@ -1374,6 +1412,42 @@ function resolveBundledRuntimeAssetSource() {
1374
1412
  function looksLikeMonitoringHubBuild(root) {
1375
1413
  return existsSync4(join2(root, "index.html")) && existsSync4(join2(root, "_expo"));
1376
1414
  }
1415
+ function latestPathMtimeMs(pathValue) {
1416
+ if (!existsSync4(pathValue))
1417
+ return 0;
1418
+ const stat = lstatSync(pathValue);
1419
+ let latest = stat.mtimeMs;
1420
+ if (!stat.isDirectory())
1421
+ return latest;
1422
+ for (const entry of readdirSync(pathValue)) {
1423
+ latest = Math.max(latest, latestPathMtimeMs(join2(pathValue, entry)));
1424
+ }
1425
+ return latest;
1426
+ }
1427
+ function bundledMonitoringHubSourceWatchPaths(sourceRoot) {
1428
+ return [
1429
+ join2(sourceRoot, "apps", "client", "app"),
1430
+ join2(sourceRoot, "apps", "client", "assets"),
1431
+ join2(sourceRoot, "apps", "client", "components"),
1432
+ join2(sourceRoot, "apps", "client", "constants"),
1433
+ join2(sourceRoot, "apps", "client", "hooks"),
1434
+ join2(sourceRoot, "apps", "client", "scripts"),
1435
+ join2(sourceRoot, "apps", "client", "src"),
1436
+ join2(sourceRoot, "apps", "client", "app.json"),
1437
+ join2(sourceRoot, "apps", "client", "package.json"),
1438
+ join2(sourceRoot, "packages", "shared", "src"),
1439
+ join2(sourceRoot, "scripts", "sync-cli-monitor-ui.ts")
1440
+ ];
1441
+ }
1442
+ function bundledMonitoringHubNeedsRefresh(existingRoot, sourceRoot) {
1443
+ if (!looksLikeMonitoringHubBuild(existingRoot))
1444
+ return true;
1445
+ const bundleMtimeMs = latestPathMtimeMs(existingRoot);
1446
+ if (bundleMtimeMs <= 0)
1447
+ return true;
1448
+ const sourceMtimeMs = bundledMonitoringHubSourceWatchPaths(sourceRoot).reduce((latest, pathValue) => Math.max(latest, latestPathMtimeMs(pathValue)), 0);
1449
+ return sourceMtimeMs > bundleMtimeMs;
1450
+ }
1377
1451
  function resolveBundledMonitoringHubRoot() {
1378
1452
  const candidates = [
1379
1453
  resolve4(import.meta.dir, "..", "monitor-ui"),
@@ -1413,11 +1487,15 @@ function exportBundledMonitoringHubFromSourceCheckout(sourceRoot) {
1413
1487
  }
1414
1488
  async function ensureBundledMonitoringHubRoot() {
1415
1489
  const existingRoot = resolveBundledMonitoringHubRoot();
1416
- if (existingRoot)
1417
- return existingRoot;
1418
1490
  const sourceRoot = resolveCliSourceCheckoutRoot();
1419
1491
  if (!sourceRoot)
1420
- return null;
1492
+ return existingRoot;
1493
+ if (existingRoot && !bundledMonitoringHubNeedsRefresh(existingRoot, sourceRoot)) {
1494
+ return existingRoot;
1495
+ }
1496
+ if (existingRoot) {
1497
+ console.log("[pushpals] Packaged monitor UI is stale; refreshing the exported client monitor...");
1498
+ }
1421
1499
  exportBundledMonitoringHubFromSourceCheckout(sourceRoot);
1422
1500
  return resolveBundledMonitoringHubRoot();
1423
1501
  }
@@ -1637,8 +1715,9 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1637
1715
  PUSHPALS_PROMPTS_ROOT_OVERRIDE: opts.repoRoot
1638
1716
  },
1639
1717
  PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
1640
- ...opts.forceLocalBuddyEnabled ? { LOCALBUDDY_ENABLED: "1" } : {},
1641
- ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {}
1718
+ ...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
1719
+ ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {},
1720
+ ...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {}
1642
1721
  };
1643
1722
  }
1644
1723
  function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
@@ -1692,6 +1771,23 @@ async function resolveCommandPath(command, cwd, env) {
1692
1771
  function timestampFileToken() {
1693
1772
  return new Date().toISOString().replace(/[:.]/g, "-");
1694
1773
  }
1774
+ function buildRuntimeServiceLogPaths(logDir, runToken) {
1775
+ return {
1776
+ server: join2(logDir, `${runToken}-server.log`),
1777
+ localbuddy: join2(logDir, `${runToken}-localbuddy.log`),
1778
+ remotebuddy: join2(logDir, `${runToken}-remotebuddy.log`),
1779
+ source_control_manager: join2(logDir, `${runToken}-source_control_manager.log`)
1780
+ };
1781
+ }
1782
+ function appendRuntimeServicesLogLine(logPath, line) {
1783
+ const text = String(line ?? "").trim();
1784
+ if (!text)
1785
+ return;
1786
+ try {
1787
+ appendFileSync(logPath, `${new Date().toISOString()} ${text}
1788
+ `, "utf8");
1789
+ } catch {}
1790
+ }
1695
1791
  function readLogTail(logPath, maxLines = 40) {
1696
1792
  if (!existsSync4(logPath))
1697
1793
  return "";
@@ -1702,6 +1798,31 @@ function readLogTail(logPath, maxLines = 40) {
1702
1798
  return lines.slice(-maxLines).join(`
1703
1799
  `);
1704
1800
  }
1801
+ function extractRemoteBuddyAutonomousEngineState(logText) {
1802
+ const text = String(logText ?? "");
1803
+ if (!text)
1804
+ return "unknown";
1805
+ let state = "unknown";
1806
+ for (const line of text.split(/\r?\n/)) {
1807
+ if (/Autonomous engine:\s*enabled\b/i.test(line)) {
1808
+ state = "enabled";
1809
+ continue;
1810
+ }
1811
+ if (/Autonomous engine:\s*disabled\b/i.test(line)) {
1812
+ state = "disabled";
1813
+ }
1814
+ }
1815
+ return state;
1816
+ }
1817
+ function readRemoteBuddyAutonomousEngineState(logPath) {
1818
+ if (!existsSync4(logPath))
1819
+ return "unknown";
1820
+ try {
1821
+ return extractRemoteBuddyAutonomousEngineState(readFileSync4(logPath, "utf8"));
1822
+ } catch {
1823
+ return "unknown";
1824
+ }
1825
+ }
1705
1826
  async function downloadBinaryAsset(tag, assetName, outPath) {
1706
1827
  console.log(`[pushpals] Downloading embedded runtime binary ${assetName} from ${tag}...`);
1707
1828
  const url = `${GITHUB_RELEASE_URL}/${encodeURIComponent(tag)}/${assetName}`;
@@ -1753,9 +1874,13 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1753
1874
  console.log("[pushpals] Embedded runtime binaries are ready.");
1754
1875
  return runtimeBinaries;
1755
1876
  }
1756
- function spawnRuntimeService(name, command, cwd, env, logPath) {
1757
- writeFileSync(logPath, `[pushpals] service=${name} command=${command.join(" ")} cwd=${cwd}
1877
+ function spawnRuntimeService(name, command, cwd, env, logPath, runtimeServicesLogPath) {
1878
+ const header = `[pushpals] service=${name} command=${command.join(" ")} cwd=${cwd}`;
1879
+ writeFileSync(logPath, `${header}
1758
1880
  `, "utf8");
1881
+ if (runtimeServicesLogPath) {
1882
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, header);
1883
+ }
1759
1884
  const proc = Bun.spawn(command, {
1760
1885
  cwd,
1761
1886
  env,
@@ -1779,16 +1904,24 @@ function spawnRuntimeService(name, command, cwd, env, logPath) {
1779
1904
  const lines = pending.split(/\r?\n/);
1780
1905
  pending = lines.pop() ?? "";
1781
1906
  for (const line of lines) {
1782
- appendFileSync(logPath, `[${channel}] ${line}
1907
+ const serviceLine = `[${channel}] ${line}`;
1908
+ appendFileSync(logPath, `${serviceLine}
1783
1909
  `, "utf8");
1910
+ if (runtimeServicesLogPath) {
1911
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
1912
+ }
1784
1913
  }
1785
1914
  }
1786
1915
  const rest = decoder.decode();
1787
1916
  if (rest)
1788
1917
  pending += rest;
1789
1918
  if (pending.trim().length > 0) {
1790
- appendFileSync(logPath, `[${channel}] ${pending.trimEnd()}
1919
+ const serviceLine = `[${channel}] ${pending.trimEnd()}`;
1920
+ appendFileSync(logPath, `${serviceLine}
1791
1921
  `, "utf8");
1922
+ if (runtimeServicesLogPath) {
1923
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[${name}] ${serviceLine}`);
1924
+ }
1792
1925
  }
1793
1926
  };
1794
1927
  pipeToLog(proc.stdout, "stdout");
@@ -1838,7 +1971,7 @@ function prependExecutableDirToPath(env, executablePath, platform = process.plat
1838
1971
  const executableDir = dirname(resolvedPath);
1839
1972
  const existingPath = platform === "win32" ? String(env.Path ?? env.PATH ?? "") : String(env.PATH ?? "");
1840
1973
  const pathEntries = existingPath.split(delimiter).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1841
- const hasDir = pathEntries.some((entry) => entry.toLowerCase() === executableDir.toLowerCase());
1974
+ const hasDir = pathEntries.some((entry) => platform === "win32" ? entry.toLowerCase() === executableDir.toLowerCase() : entry === executableDir);
1842
1975
  const nextPath = hasDir ? existingPath : [executableDir, ...pathEntries].join(delimiter);
1843
1976
  if (platform === "win32") {
1844
1977
  env.Path = nextPath;
@@ -1854,6 +1987,11 @@ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = p
1854
1987
  return env;
1855
1988
  prependExecutableDirToPath(env, resolvedPath, platform);
1856
1989
  env.PUSHPALS_GIT_BIN = basename(resolvedPath);
1990
+ if (resolvedPath.includes("/") || resolvedPath.includes("\\")) {
1991
+ env.PUSHPALS_GIT_BIN_ABSOLUTE = resolvedPath;
1992
+ } else {
1993
+ delete env.PUSHPALS_GIT_BIN_ABSOLUTE;
1994
+ }
1857
1995
  return env;
1858
1996
  }
1859
1997
  function isOptionalEmbeddedService(name) {
@@ -1889,6 +2027,111 @@ async function probeServer(serverUrl) {
1889
2027
  return false;
1890
2028
  }
1891
2029
  }
2030
+ function normalizeRepoPathForComparison(repoPath) {
2031
+ const normalized = resolve4(String(repoPath ?? "")).replace(/\\/g, "/").replace(/\/+$/, "");
2032
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
2033
+ }
2034
+ async function fetchServerRepoRoot(serverUrl) {
2035
+ const response = await fetchWithTimeout(`${serverUrl}/system/status`, {}, 1e4);
2036
+ if (!response.ok) {
2037
+ throw new Error(`status probe failed with HTTP ${response.status}`);
2038
+ }
2039
+ const payload = await response.json().catch(() => ({}));
2040
+ const repoRoot = payload?.repo && typeof payload.repo.root === "string" ? payload.repo.root.trim() : "";
2041
+ if (!repoRoot) {
2042
+ throw new Error("server did not report repo.root in /system/status");
2043
+ }
2044
+ return repoRoot;
2045
+ }
2046
+ async function ensureServerRepoAffinity(serverUrl, currentRepoRoot) {
2047
+ const serverRepoRoot = await fetchServerRepoRoot(serverUrl);
2048
+ if (normalizeRepoPathForComparison(serverRepoRoot) === normalizeRepoPathForComparison(currentRepoRoot)) {
2049
+ return;
2050
+ }
2051
+ throw new Error(`repo mismatch: currentRepo=${currentRepoRoot} serverRepo=${serverRepoRoot}. Stop the existing runtime or switch to the matching repo.`);
2052
+ }
2053
+ function isRemoteBuddyClientRow(row) {
2054
+ const clientId = normalizePresenceLookupToken(row.clientId);
2055
+ const label = normalizePresenceLookupToken(row.label);
2056
+ return clientId.includes("remotebuddy") || label.includes("remotebuddy");
2057
+ }
2058
+ function extractRemoteBuddySessionConsumerHealth(statusPayload, sessionId) {
2059
+ const rows = Array.isArray(statusPayload?.clients?.items) ? statusPayload.clients?.items ?? [] : [];
2060
+ const sessionRows = rows.filter((row) => {
2061
+ if (!row || typeof row !== "object" || Array.isArray(row))
2062
+ return false;
2063
+ return String(row.sessionId ?? "").trim() === sessionId;
2064
+ });
2065
+ const remotebuddyRows = sessionRows.filter(isRemoteBuddyClientRow);
2066
+ const connectedRow = remotebuddyRows.find((row) => String(row.status ?? "").trim().toLowerCase() === "connected");
2067
+ if (connectedRow) {
2068
+ return {
2069
+ ok: true,
2070
+ detail: `RemoteBuddy session consumer connected (${String(connectedRow.clientId ?? "").trim()})`,
2071
+ clientId: String(connectedRow.clientId ?? "").trim() || undefined,
2072
+ sessionId
2073
+ };
2074
+ }
2075
+ const anyRemoteBuddyRows = rows.filter((row) => {
2076
+ if (!row || typeof row !== "object" || Array.isArray(row))
2077
+ return false;
2078
+ return isRemoteBuddyClientRow(row);
2079
+ });
2080
+ const connectedOtherSession = anyRemoteBuddyRows.find((row) => {
2081
+ const rowSessionId = String(row.sessionId ?? "").trim();
2082
+ if (!rowSessionId || rowSessionId === sessionId)
2083
+ return false;
2084
+ return String(row.status ?? "").trim().toLowerCase() === "connected";
2085
+ });
2086
+ if (connectedOtherSession) {
2087
+ const otherSessionId = String(connectedOtherSession.sessionId ?? "").trim();
2088
+ const otherClientId = String(connectedOtherSession.clientId ?? "").trim();
2089
+ return {
2090
+ ok: false,
2091
+ detail: `RemoteBuddy is connected to session ${otherSessionId || "unknown"} ` + `(${otherClientId || "unknown client"}), not ${sessionId}`,
2092
+ clientId: otherClientId || undefined,
2093
+ sessionId: otherSessionId || undefined
2094
+ };
2095
+ }
2096
+ if (remotebuddyRows.length > 0) {
2097
+ return {
2098
+ ok: false,
2099
+ detail: `RemoteBuddy session consumer exists for ${sessionId} but is not connected`,
2100
+ clientId: String(remotebuddyRows[0]?.clientId ?? "").trim() || undefined,
2101
+ sessionId
2102
+ };
2103
+ }
2104
+ if (anyRemoteBuddyRows.length > 0) {
2105
+ const knownSessions = [...new Set(anyRemoteBuddyRows.map((row) => String(row.sessionId ?? "").trim()))].filter(Boolean).sort();
2106
+ const suffix = knownSessions.length > 0 ? ` Known RemoteBuddy sessions: ${knownSessions.join(", ")}.` : "";
2107
+ return {
2108
+ ok: false,
2109
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}.${suffix}`.trim()
2110
+ };
2111
+ }
2112
+ return {
2113
+ ok: false,
2114
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
2115
+ };
2116
+ }
2117
+ async function probeRemoteBuddySessionConsumer(serverUrl, sessionId) {
2118
+ try {
2119
+ const response = await fetchWithTimeout(`${serverUrl}/system/status`, {}, 1e4);
2120
+ if (!response.ok) {
2121
+ return {
2122
+ ok: false,
2123
+ detail: `system status probe failed with HTTP ${response.status}`
2124
+ };
2125
+ }
2126
+ const payload = await response.json().catch(() => ({}));
2127
+ return extractRemoteBuddySessionConsumerHealth(payload, sessionId);
2128
+ } catch (err) {
2129
+ return {
2130
+ ok: false,
2131
+ detail: `system status probe failed: ${String(err)}`
2132
+ };
2133
+ }
2134
+ }
1892
2135
  async function probeSourceControlManager(port) {
1893
2136
  if (!Number.isFinite(port) || port <= 0)
1894
2137
  return false;
@@ -1940,13 +2183,43 @@ function createRuntimeClientId(prefix) {
1940
2183
  async function probeLocalBuddy(localAgentUrl) {
1941
2184
  return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, {}, LOCALBUDDY_TIMEOUT_MS);
1942
2185
  }
2186
+ function resolveCliLocalBuddyAutostart(runtimeOnly, runtimeConfigEnabled) {
2187
+ return runtimeOnly ? runtimeConfigEnabled : false;
2188
+ }
2189
+ async function ensureServerSession(serverUrl, requestedSessionId, client) {
2190
+ const response = await fetchWithTimeout(`${serverUrl}/sessions`, {
2191
+ method: "POST",
2192
+ headers: { "Content-Type": "application/json" },
2193
+ body: JSON.stringify({
2194
+ sessionId: requestedSessionId,
2195
+ client: {
2196
+ clientId: client.clientId,
2197
+ kind: client.kind,
2198
+ label: client.label,
2199
+ version: client.version,
2200
+ platform: client.platform,
2201
+ repoRoot: client.repoRoot
2202
+ }
2203
+ })
2204
+ }, 15000);
2205
+ if (!response.ok) {
2206
+ const detail = await response.text().catch(() => "");
2207
+ throw new Error(`Failed to create or join session ${requestedSessionId}: HTTP ${response.status}${detail ? ` ${detail}` : ""}`);
2208
+ }
2209
+ const payload = await response.json().catch(() => ({}));
2210
+ const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : "";
2211
+ if (!sessionId) {
2212
+ throw new Error("Server session bootstrap returned no sessionId.");
2213
+ }
2214
+ return sessionId;
2215
+ }
1943
2216
  async function autoStartRuntimeServices(opts) {
1944
2217
  const { runtimePreflight } = opts.preparedRuntime;
1945
2218
  const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1946
2219
  const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1947
- const requireLocalBuddy = opts.requireLocalBuddy ?? true;
1948
- const localBuddyEnabled = requireLocalBuddy || Boolean(runtimePreflight.config.localbuddy.enabled);
1949
- console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
2220
+ const startLocalBuddy = opts.startLocalBuddy ?? Boolean(runtimePreflight.config.localbuddy.enabled);
2221
+ const localBuddyEnabled = startLocalBuddy;
2222
+ console.log(`[pushpals] Runtime unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1950
2223
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1951
2224
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
1952
2225
  if (!runtimePreflight.ok) {
@@ -1958,12 +2231,13 @@ async function autoStartRuntimeServices(opts) {
1958
2231
  repoRoot: opts.repoRoot,
1959
2232
  runtimeRoot,
1960
2233
  useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
1961
- forceLocalBuddyEnabled: requireLocalBuddy
2234
+ sessionId: opts.sessionId
1962
2235
  });
1963
2236
  if (runtimeEnv.PUSHPALS_GIT_BIN) {
1964
2237
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
1965
2238
  }
1966
- const resolvedGitBinary = await resolveCommandPath("git", opts.repoRoot, normalizeChildProcessEnv(process.env));
2239
+ const gitLookupCommand = typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? runtimeEnv.PUSHPALS_GIT_BIN.trim() : "git";
2240
+ const resolvedGitBinary = await resolveCommandPath(gitLookupCommand, opts.repoRoot, runtimeEnv);
1967
2241
  if (resolvedGitBinary) {
1968
2242
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, resolvedGitBinary);
1969
2243
  }
@@ -1971,11 +2245,21 @@ async function autoStartRuntimeServices(opts) {
1971
2245
  const runToken = timestampFileToken();
1972
2246
  const logDir = join2(runtimeRoot, "logs", "bootstrap");
1973
2247
  mkdirSync(logDir, { recursive: true });
1974
- const logPathFor = (name) => join2(logDir, `${runToken}-${name}.log`);
2248
+ const serviceLogPaths = buildRuntimeServiceLogPaths(logDir, runToken);
2249
+ const runtimeServicesLogPath = join2(logDir, `${runToken}-runtime-services.log`);
2250
+ writeFileSync(runtimeServicesLogPath, "", "utf8");
2251
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] runtimeRoot=${runtimeRoot}`);
2252
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] runtimeTag=${runtimeTag}`);
2253
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] repoRoot=${opts.repoRoot}`);
2254
+ console.log(`[pushpals] runtime services log: ${runtimeServicesLogPath}`);
2255
+ console.log(`[pushpals] service log (server)=${serviceLogPaths.server}`);
2256
+ console.log(`[pushpals] service log (localbuddy)=${serviceLogPaths.localbuddy}`);
2257
+ console.log(`[pushpals] service log (remotebuddy)=${serviceLogPaths.remotebuddy}`);
2258
+ console.log(`[pushpals] service log (source_control_manager)=${serviceLogPaths.source_control_manager}`);
1975
2259
  const serverHealthy = await probeServer(opts.serverUrl);
1976
2260
  if (!serverHealthy) {
1977
2261
  console.log("[pushpals] Starting embedded server...");
1978
- const serverService = spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv, logPathFor("server"));
2262
+ const serverService = spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv, serviceLogPaths.server, runtimeServicesLogPath);
1979
2263
  services.push(serverService);
1980
2264
  console.log(`[pushpals] server log: ${serverService.logPath}`);
1981
2265
  const serverDeadline = Date.now() + DEFAULT_SERVER_BOOT_TIMEOUT_MS;
@@ -1983,6 +2267,7 @@ async function autoStartRuntimeServices(opts) {
1983
2267
  while (Date.now() < serverDeadline) {
1984
2268
  if (serverService.exited) {
1985
2269
  const tail = readLogTail(serverService.logPath);
2270
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}).`);
1986
2271
  stopRuntimeServices(services);
1987
2272
  throw new Error(`Embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}). ` + `See ${serverService.logPath}${tail ? `
1988
2273
  --- server log tail ---
@@ -1996,6 +2281,7 @@ ${tail}` : ""}`);
1996
2281
  }
1997
2282
  if (!serverIsReady) {
1998
2283
  const tail = readLogTail(serverService.logPath);
2284
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms.`);
1999
2285
  stopRuntimeServices(services);
2000
2286
  throw new Error(`Embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms. ` + `See ${serverService.logPath}${tail ? `
2001
2287
  --- server log tail ---
@@ -2004,61 +2290,84 @@ ${tail}` : ""}`);
2004
2290
  console.log("[pushpals] Embedded server is healthy.");
2005
2291
  } else {
2006
2292
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
2293
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] server already healthy; embedded server start skipped.");
2007
2294
  }
2008
2295
  if (localBuddyEnabled) {
2009
- if (requireLocalBuddy && !runtimePreflight.config.localbuddy.enabled) {
2010
- console.log("[pushpals] LocalBuddy is disabled in config; forcing it on for this CLI session.");
2011
- }
2012
2296
  console.log("[pushpals] Starting embedded LocalBuddy...");
2013
- const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, logPathFor("localbuddy"));
2297
+ const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, serviceLogPaths.localbuddy, runtimeServicesLogPath);
2014
2298
  services.push(localbuddyService);
2015
2299
  console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
2016
2300
  } else {
2017
- console.log("[pushpals] Embedded LocalBuddy disabled by runtime config; skipping start.");
2301
+ console.log("[pushpals] Embedded LocalBuddy disabled for this CLI session; skipping start.");
2302
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] localbuddy disabled for this CLI session; embedded localbuddy start skipped.");
2018
2303
  }
2019
2304
  console.log("[pushpals] Starting embedded RemoteBuddy...");
2020
- const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, logPathFor("remotebuddy"));
2305
+ const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, serviceLogPaths.remotebuddy, runtimeServicesLogPath);
2021
2306
  services.push(remotebuddyService);
2022
2307
  console.log(`[pushpals] remotebuddy log: ${remotebuddyService.logPath}`);
2308
+ let lastReportedRemoteBuddyAutonomyState = "unknown";
2309
+ const reportRemoteBuddyAutonomousEngineState = () => {
2310
+ const autonomyState = readRemoteBuddyAutonomousEngineState(remotebuddyService.logPath);
2311
+ if (autonomyState === "unknown" || autonomyState === lastReportedRemoteBuddyAutonomyState) {
2312
+ return;
2313
+ }
2314
+ lastReportedRemoteBuddyAutonomyState = autonomyState;
2315
+ if (autonomyState === "enabled") {
2316
+ console.log("[pushpals] Embedded RemoteBuddy autonomous engine is enabled.");
2317
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded remotebuddy autonomous engine is enabled.");
2318
+ return;
2319
+ }
2320
+ console.warn("[pushpals] Embedded RemoteBuddy autonomous engine is disabled (remotebuddy.autonomy.enabled=false).");
2321
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded remotebuddy autonomous engine is disabled (remotebuddy.autonomy.enabled=false).");
2322
+ };
2323
+ reportRemoteBuddyAutonomousEngineState();
2023
2324
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
2024
2325
  const scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
2025
- const gitProbeCommand = typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? [runtimeEnv.PUSHPALS_GIT_BIN.trim(), "--version"] : ["git", "--version"];
2326
+ const gitForScm = typeof runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE.trim() : typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? runtimeEnv.PUSHPALS_GIT_BIN.trim() : "git";
2327
+ const gitProbeCommand = [gitForScm, "--version"];
2026
2328
  const gitAvailableForScm = await canSpawnCommand(gitProbeCommand, opts.repoRoot, runtimeEnv);
2027
2329
  if (!scmHealthy && scmRemoteAvailable) {
2028
2330
  if (!gitAvailableForScm) {
2029
2331
  console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
2332
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
2030
2333
  } else {
2031
- if (runtimeEnv.PUSHPALS_GIT_BIN) {
2032
- console.log(`[pushpals] Embedded SourceControlManager git=${runtimeEnv.PUSHPALS_GIT_BIN}`);
2033
- }
2334
+ console.log(`[pushpals] Embedded SourceControlManager git=${gitForScm}`);
2034
2335
  console.log("[pushpals] Starting embedded SourceControlManager...");
2035
- const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, logPathFor("source_control_manager"));
2336
+ const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, serviceLogPaths.source_control_manager, runtimeServicesLogPath);
2036
2337
  services.push(sourceControlManagerService);
2037
2338
  console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
2038
2339
  }
2039
2340
  } else if (!scmRemoteAvailable) {
2040
2341
  console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
2342
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
2041
2343
  } else if (!gitAvailableForScm) {
2042
2344
  console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
2345
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
2043
2346
  } else {
2044
2347
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
2348
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager already healthy; embedded start skipped.");
2045
2349
  }
2046
2350
  const deadline = Date.now() + DEFAULT_RUNTIME_BOOT_TIMEOUT_MS;
2047
2351
  while (Date.now() < deadline) {
2352
+ reportRemoteBuddyAutonomousEngineState();
2048
2353
  for (let i = services.length - 1;i >= 0; i--) {
2049
2354
  const service = services[i];
2050
2355
  if (service.exited) {
2051
2356
  if (isOptionalEmbeddedService(service.name)) {
2052
2357
  console.warn(`[pushpals] Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); continuing without SCM.`);
2358
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); continuing.`);
2053
2359
  const tail2 = readLogTail(service.logPath);
2054
2360
  if (tail2) {
2055
2361
  console.warn(`[pushpals] ${service.name} log tail:
2362
+ ${tail2}`);
2363
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] ${service.name} log tail:
2056
2364
  ${tail2}`);
2057
2365
  }
2058
2366
  services.splice(i, 1);
2059
2367
  continue;
2060
2368
  }
2061
2369
  const tail = readLogTail(service.logPath);
2370
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}).`);
2062
2371
  stopRuntimeServices(services);
2063
2372
  throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
2064
2373
  --- ${service.name} log tail ---
@@ -2066,9 +2375,12 @@ ${tail}` : ""}`);
2066
2375
  }
2067
2376
  }
2068
2377
  const health = localBuddyEnabled ? await probeLocalBuddy(opts.localAgentUrl) : null;
2069
- if (!requireLocalBuddy || localBuddyEnabled && health?.ok) {
2378
+ const remoteBuddyHealth2 = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
2379
+ if ((!localBuddyEnabled || health?.ok) && remoteBuddyHealth2.ok) {
2380
+ reportRemoteBuddyAutonomousEngineState();
2070
2381
  const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
2071
2382
  while (Date.now() < stabilityDeadline) {
2383
+ reportRemoteBuddyAutonomousEngineState();
2072
2384
  for (let i = services.length - 1;i >= 0; i--) {
2073
2385
  const service = services[i];
2074
2386
  if (!service.exited)
@@ -2076,14 +2388,18 @@ ${tail}` : ""}`);
2076
2388
  if (isOptionalEmbeddedService(service.name)) {
2077
2389
  const tail2 = readLogTail(service.logPath);
2078
2390
  console.warn(`[pushpals] Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}); continuing without SCM.`);
2391
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}); continuing.`);
2079
2392
  if (tail2) {
2080
2393
  console.warn(`[pushpals] ${service.name} log tail:
2394
+ ${tail2}`);
2395
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] ${service.name} log tail:
2081
2396
  ${tail2}`);
2082
2397
  }
2083
2398
  services.splice(i, 1);
2084
2399
  continue;
2085
2400
  }
2086
2401
  const tail = readLogTail(service.logPath);
2402
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}).`);
2087
2403
  stopRuntimeServices(services);
2088
2404
  throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
2089
2405
  --- ${service.name} log tail ---
@@ -2092,15 +2408,23 @@ ${tail}` : ""}`);
2092
2408
  await Bun.sleep(250);
2093
2409
  }
2094
2410
  console.log("[pushpals] Embedded runtime is ready.");
2411
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded runtime is ready.");
2095
2412
  return services;
2096
2413
  }
2097
2414
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
2098
2415
  }
2099
2416
  stopRuntimeServices(services);
2417
+ const remoteBuddyHealth = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
2418
+ if (!localBuddyEnabled && !remoteBuddyHealth.ok) {
2419
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail}).`);
2420
+ throw new Error(`Timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail})`);
2421
+ }
2100
2422
  if (!localBuddyEnabled) {
2423
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms.`);
2101
2424
  throw new Error(`Timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2102
2425
  }
2103
- throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2426
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms.`);
2427
+ throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
2104
2428
  }
2105
2429
  function readCliState(pathValue) {
2106
2430
  if (!existsSync4(pathValue))
@@ -2153,7 +2477,6 @@ async function looksLikeMonitoringHub(url) {
2153
2477
  function buildMonitoringHubRuntimeBootstrap(opts) {
2154
2478
  return {
2155
2479
  serverUrl: opts.serverUrl,
2156
- localAgentUrl: opts.localAgentUrl,
2157
2480
  sessionId: opts.sessionId,
2158
2481
  clientId: `cli-monitor-${opts.sessionId}`,
2159
2482
  clientKind: "cli_monitor",
@@ -2238,7 +2561,10 @@ async function serveBundledMonitoringHub(assetRoot, pathname, bootstrap) {
2238
2561
  });
2239
2562
  }
2240
2563
  function buildEmbeddedMonitoringHubHtml(opts) {
2241
- const bootstrap = jsonHtmlBootstrap(opts);
2564
+ const bootstrap = jsonHtmlBootstrap({
2565
+ serverUrl: opts.serverUrl,
2566
+ sessionId: opts.sessionId
2567
+ });
2242
2568
  return `<!doctype html>
2243
2569
  <html lang="en">
2244
2570
  <head>
@@ -2334,7 +2660,6 @@ function buildEmbeddedMonitoringHubHtml(opts) {
2334
2660
  cardsEl.innerHTML = cards.map((card) => '<div class="card"><div class="label">' + esc(card.label) + '</div><div class="value">' + esc(card.value) + '</div><div class="sub">' + esc(card.sub) + '</div></div>').join("");
2335
2661
  metaEl.innerHTML = [
2336
2662
  '<span class="pill">server ' + esc(boot.serverUrl) + '</span>',
2337
- '<span class="pill">localbuddy ' + esc(boot.localAgentUrl) + '</span>',
2338
2663
  '<span class="pill">session ' + esc(boot.sessionId) + '</span>',
2339
2664
  '<span class="pill">repo ' + esc(repo?.root ?? repo?.remoteUrl ?? "current repo") + '</span>'
2340
2665
  ].join("");
@@ -2391,7 +2716,6 @@ async function startEmbeddedMonitoringHub(opts) {
2391
2716
  }
2392
2717
  const bootstrap = buildMonitoringHubRuntimeBootstrap({
2393
2718
  serverUrl: opts.serverUrl,
2394
- localAgentUrl: opts.localAgentUrl,
2395
2719
  sessionId: opts.sessionId
2396
2720
  });
2397
2721
  const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
@@ -2462,72 +2786,30 @@ async function resolveMonitoringHub(opts) {
2462
2786
  }
2463
2787
  return embedded;
2464
2788
  }
2465
- async function sendMessageToLocalBuddy(localAgentUrl, text) {
2466
- let response;
2789
+ async function sendMessageToServerSession(serverUrl, sessionId, text) {
2467
2790
  try {
2468
- response = await fetchWithTimeout(`${localAgentUrl}/message`, {
2791
+ const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/message`, {
2469
2792
  method: "POST",
2470
2793
  headers: { "Content-Type": "application/json" },
2471
2794
  body: JSON.stringify({ text })
2472
- }, 30000);
2795
+ }, 15000);
2796
+ if (!response.ok) {
2797
+ const detail = await response.text().catch(() => "");
2798
+ console.error(`[pushpals] Session message rejected: HTTP ${response.status}${detail ? ` ${detail}` : ""}`);
2799
+ return false;
2800
+ }
2801
+ return true;
2473
2802
  } catch (err) {
2474
- console.error(`[pushpals] Failed to reach LocalBuddy: ${String(err)}`);
2475
- return false;
2476
- }
2477
- if (!response.ok) {
2478
- const detail = await response.text().catch(() => "");
2479
- console.error(`[pushpals] LocalBuddy rejected message: HTTP ${response.status} ${detail}`);
2480
- return false;
2481
- }
2482
- const reader = response.body?.getReader();
2483
- if (!reader) {
2484
- console.error("[pushpals] LocalBuddy response stream missing.");
2803
+ console.error(`[pushpals] Failed to reach server session endpoint: ${String(err)}`);
2485
2804
  return false;
2486
2805
  }
2487
- let buffer = "";
2488
- const decoder = new TextDecoder;
2489
- let complete = false;
2490
- let ok = true;
2491
- while (true) {
2492
- const { done, value } = await reader.read();
2493
- if (done)
2494
- break;
2495
- buffer += decoder.decode(value, { stream: true });
2496
- const chunks = buffer.split(`
2497
-
2498
- `);
2499
- buffer = chunks.pop() ?? "";
2500
- for (const chunk of chunks) {
2501
- const dataLine = chunk.split(/\r?\n/).map((line) => line.trim()).find((line) => line.startsWith("data: "));
2502
- if (!dataLine)
2503
- continue;
2504
- try {
2505
- const payload = JSON.parse(dataLine.slice(6));
2506
- const type = String(payload.type ?? "").trim().toLowerCase();
2507
- const message = String(payload.message ?? "").trim();
2508
- if (type === "status" && message) {
2509
- console.log(`[localbuddy] ${message}`);
2510
- } else if (type === "error") {
2511
- ok = false;
2512
- console.log(`[localbuddy] ERROR: ${message || "Unknown failure"}`);
2513
- } else if (type === "complete") {
2514
- complete = true;
2515
- const requestId = payload.data && typeof payload.data.requestId === "string" ? payload.data.requestId : "";
2516
- if (requestId) {
2517
- console.log(`[localbuddy] requestId=${requestId}`);
2518
- } else if (message) {
2519
- console.log(`[localbuddy] ${message}`);
2520
- }
2521
- }
2522
- } catch {}
2523
- }
2524
- }
2525
- return ok && complete;
2526
2806
  }
2527
2807
  function formatSessionEventLine(event) {
2528
2808
  const type = String(event.type ?? "").toLowerCase();
2529
2809
  const from = String(event.from ?? "");
2530
2810
  const payload = event.payload ?? {};
2811
+ if (!shouldDisplayInteractiveSessionEvent(event))
2812
+ return null;
2531
2813
  if (type === "message")
2532
2814
  return null;
2533
2815
  if (type === "assistant_message") {
@@ -2686,9 +2968,23 @@ async function main() {
2686
2968
  process.exit(1);
2687
2969
  }
2688
2970
  const config = preparedRuntime.runtimePreflight.config;
2971
+ if (config.remotebuddy.autonomy.enabled) {
2972
+ console.log("[pushpals] RemoteBuddy autonomy is enabled for CLI.");
2973
+ } else {
2974
+ console.warn("[pushpals] RemoteBuddy autonomy is disabled in config (remotebuddy.autonomy.enabled=false); continuing.");
2975
+ }
2689
2976
  const serverUrl = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
2690
2977
  const localAgentUrl = normalizeLoopbackUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
2691
2978
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
2979
+ const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
2980
+ const cliClient = {
2981
+ clientId: createRuntimeClientId("cli"),
2982
+ kind: "cli",
2983
+ label: "CLI",
2984
+ version: cliVersion,
2985
+ platform: `${process.platform}/${process.arch}`,
2986
+ repoRoot
2987
+ };
2692
2988
  let autoStartedServices = [];
2693
2989
  const stopAutoStartedServices = () => {
2694
2990
  if (autoStartedServices.length === 0)
@@ -2697,66 +2993,76 @@ async function main() {
2697
2993
  autoStartedServices = [];
2698
2994
  };
2699
2995
  let serverHealthy = await probeServer(serverUrl);
2700
- let health = await probeLocalBuddy(localAgentUrl);
2701
- const runtimeNeedsAutoStart = parsed.runtimeOnly ? !serverHealthy : !health?.ok;
2702
- if (runtimeNeedsAutoStart && !parsed.noAutoStart) {
2996
+ const serverWasAlreadyHealthy = serverHealthy;
2997
+ let remoteBuddyConsumerHealth = {
2998
+ ok: false,
2999
+ detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
3000
+ };
3001
+ if (!serverHealthy) {
3002
+ if (!parsed.noAutoStart) {
3003
+ try {
3004
+ autoStartedServices = await autoStartRuntimeServices({
3005
+ repoRoot,
3006
+ serverUrl,
3007
+ localAgentUrl,
3008
+ sessionId,
3009
+ sourceControlManagerPort: config.sourceControlManager.port,
3010
+ sourceControlManagerRemote: config.sourceControlManager.remote,
3011
+ preparedRuntime,
3012
+ requestedRuntimeTag: parsed.runtimeTag,
3013
+ startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled))
3014
+ });
3015
+ serverHealthy = await probeServer(serverUrl);
3016
+ } catch (err) {
3017
+ console.error(`[pushpals] Auto-start failed: ${String(err)}`);
3018
+ stopAutoStartedServices();
3019
+ }
3020
+ }
3021
+ if (!serverHealthy) {
3022
+ console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
3023
+ if (parsed.noAutoStart) {
3024
+ console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
3025
+ } else {
3026
+ console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
3027
+ }
3028
+ process.exit(1);
3029
+ }
3030
+ }
3031
+ try {
3032
+ await ensureServerRepoAffinity(serverUrl, repoRoot);
3033
+ } catch (err) {
3034
+ stopAutoStartedServices();
3035
+ console.error(`[pushpals] Repo affinity check failed: ${String(err)}`);
3036
+ process.exit(1);
3037
+ }
3038
+ let activeSessionId = sessionId;
3039
+ if (!parsed.runtimeOnly) {
2703
3040
  try {
2704
- autoStartedServices = await autoStartRuntimeServices({
2705
- repoRoot,
2706
- serverUrl,
2707
- localAgentUrl,
2708
- sourceControlManagerPort: config.sourceControlManager.port,
2709
- sourceControlManagerRemote: config.sourceControlManager.remote,
2710
- preparedRuntime,
2711
- requestedRuntimeTag: parsed.runtimeTag,
2712
- requireLocalBuddy: !parsed.runtimeOnly
2713
- });
2714
- serverHealthy = await probeServer(serverUrl);
2715
- health = await probeLocalBuddy(localAgentUrl);
3041
+ activeSessionId = await ensureServerSession(serverUrl, sessionId, cliClient);
2716
3042
  } catch (err) {
2717
- console.error(`[pushpals] Auto-start failed: ${String(err)}`);
2718
3043
  stopAutoStartedServices();
3044
+ console.error(`[pushpals] Session bootstrap failed: ${String(err)}`);
3045
+ process.exit(1);
2719
3046
  }
2720
3047
  }
2721
- if (parsed.runtimeOnly && !serverHealthy) {
3048
+ remoteBuddyConsumerHealth = await probeRemoteBuddySessionConsumer(serverUrl, activeSessionId);
3049
+ if (!serverHealthy) {
2722
3050
  console.error(`[pushpals] Server is unavailable at ${serverUrl}.`);
2723
- if (parsed.noAutoStart) {
2724
- console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2725
- } else {
2726
- console.error("[pushpals] Auto-start could not bring the embedded runtime online.");
2727
- }
2728
3051
  process.exit(1);
2729
3052
  }
2730
- if (!parsed.runtimeOnly && !health?.ok) {
2731
- console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
2732
- if (parsed.noAutoStart) {
3053
+ if (!remoteBuddyConsumerHealth.ok) {
3054
+ stopAutoStartedServices();
3055
+ console.error(`[pushpals] RemoteBuddy is not ready for session ${activeSessionId}: ${remoteBuddyConsumerHealth.detail}`);
3056
+ if (serverWasAlreadyHealthy) {
3057
+ console.error("[pushpals] A PushPals runtime is already serving this repo, but it does not have a connected RemoteBuddy consumer for this session.");
3058
+ console.error("[pushpals] Refusing to start another embedded RemoteBuddy against the same runtime. Restart or stop the existing runtime before retrying.");
3059
+ } else if (parsed.noAutoStart) {
2733
3060
  console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
2734
3061
  } else {
2735
- console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
3062
+ console.error("[pushpals] Auto-start could not bring the embedded runtime into a usable state.");
2736
3063
  }
2737
3064
  process.exit(1);
2738
3065
  }
2739
- let localBuddySessionId = sessionId;
2740
- if (!parsed.runtimeOnly) {
2741
- const localBuddyRepo = health?.repo ? resolve4(health.repo) : "";
2742
- if (!localBuddyRepo) {
2743
- stopAutoStartedServices();
2744
- console.error("[pushpals] LocalBuddy health response did not include repo path.");
2745
- process.exit(1);
2746
- }
2747
- if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
2748
- stopAutoStartedServices();
2749
- console.error("[pushpals] Repo mismatch detected.");
2750
- console.error(`[pushpals] currentRepo=${repoRoot}`);
2751
- console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
2752
- console.error("[pushpals] LocalBuddy must run against the same repo. Start PushPals from this repo and retry.");
2753
- process.exit(1);
2754
- }
2755
- localBuddySessionId = health?.sessionId && String(health.sessionId).trim() ? String(health.sessionId).trim() : sessionId;
2756
- if (sessionId && sessionId !== localBuddySessionId) {
2757
- console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
2758
- }
2759
- }
2760
3066
  const statePath = resolveCliStatePath(repoRoot);
2761
3067
  const saved = statePath ? readCliState(statePath) : {};
2762
3068
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -2765,8 +3071,7 @@ async function main() {
2765
3071
  preferredUrl: preferredHubUrl,
2766
3072
  fallbackPort: monitorPort,
2767
3073
  serverUrl,
2768
- localAgentUrl,
2769
- sessionId: localBuddySessionId
3074
+ sessionId: activeSessionId
2770
3075
  });
2771
3076
  const monitoringHubUrl = monitoringHub?.url ?? "";
2772
3077
  if (statePath) {
@@ -2774,7 +3079,7 @@ async function main() {
2774
3079
  monitoringHubUrl: monitoringHubUrl || undefined,
2775
3080
  serverUrl,
2776
3081
  localAgentUrl,
2777
- sessionId: localBuddySessionId,
3082
+ sessionId: activeSessionId,
2778
3083
  repoRoot
2779
3084
  });
2780
3085
  } else {
@@ -2790,8 +3095,7 @@ async function main() {
2790
3095
  console.log("[pushpals] monitoringHubUrl=unavailable");
2791
3096
  }
2792
3097
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2793
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2794
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
3098
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2795
3099
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2796
3100
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
2797
3101
  if (parsed.runtimeOnly) {
@@ -2799,15 +3103,6 @@ async function main() {
2799
3103
  } else {
2800
3104
  console.log("[pushpals] Type a message and press Enter. Use /exit or exit to quit.");
2801
3105
  }
2802
- const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
2803
- const cliClient = {
2804
- clientId: createRuntimeClientId("cli"),
2805
- kind: "cli",
2806
- label: "CLI",
2807
- version: cliVersion,
2808
- platform: `${process.platform}/${process.arch}`,
2809
- repoRoot
2810
- };
2811
3106
  const streamAbort = new AbortController;
2812
3107
  let rl = null;
2813
3108
  const printIncoming = (line) => {
@@ -2822,7 +3117,7 @@ ${line}
2822
3117
  }
2823
3118
  console.log(line);
2824
3119
  };
2825
- const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, localBuddySessionId, cliClient, printIncoming, streamAbort.signal);
3120
+ const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
2826
3121
  let shuttingDown = false;
2827
3122
  const requestStop = () => {
2828
3123
  if (shuttingDown)
@@ -2900,8 +3195,7 @@ ${line}
2900
3195
  }
2901
3196
  if (text === "/status") {
2902
3197
  console.log(`[pushpals] serverUrl=${serverUrl}`);
2903
- console.log(`[pushpals] localAgentUrl=${localAgentUrl}`);
2904
- console.log(`[pushpals] sessionId=${localBuddySessionId}`);
3198
+ console.log(`[pushpals] sessionId=${activeSessionId}`);
2905
3199
  console.log(`[pushpals] repoRoot=${repoRoot}`);
2906
3200
  console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
2907
3201
  rl.prompt();
@@ -2918,7 +3212,13 @@ ${line}
2918
3212
  rl.prompt();
2919
3213
  continue;
2920
3214
  }
2921
- const ok = await sendMessageToLocalBuddy(localAgentUrl, text);
3215
+ const normalized = normalizeCliInteractiveMessage(text);
3216
+ if (normalized.usageMessage) {
3217
+ console.log(`[pushpals] ${normalized.usageMessage}`);
3218
+ rl.prompt();
3219
+ continue;
3220
+ }
3221
+ const ok = await sendMessageToServerSession(serverUrl, activeSessionId, normalized.text);
2922
3222
  if (!ok) {
2923
3223
  console.log("[pushpals] Message failed.");
2924
3224
  }
@@ -2937,14 +3237,22 @@ export {
2937
3237
  startEmbeddedMonitoringHub,
2938
3238
  resolveCommandPath,
2939
3239
  resolveCliStatePath,
3240
+ resolveCliLocalBuddyAutostart,
2940
3241
  resolveBundledRuntimeAssetSource,
2941
3242
  resolveBundledMonitoringHubRoot,
2942
3243
  prepareCliRuntime,
3244
+ normalizeRepoPathForComparison,
3245
+ normalizeCliInteractiveMessage,
2943
3246
  normalizeChildProcessEnv,
2944
3247
  isCliExitCommand,
2945
3248
  injectMonitoringHubBootstrap,
2946
3249
  formatTimestampedCliLine,
3250
+ formatSessionEventLine,
3251
+ extractRemoteBuddySessionConsumerHealth,
3252
+ extractRemoteBuddyAutonomousEngineState,
3253
+ bundledMonitoringHubNeedsRefresh,
2947
3254
  buildServiceStopCommand,
3255
+ buildRuntimeServiceLogPaths,
2948
3256
  buildOpenMonitoringHubCommand,
2949
3257
  buildEmbeddedRuntimeEnv,
2950
3258
  buildEmbeddedMonitoringHubHtml,