@bondsmith/bondsmithd 0.1.14 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +43 -15
  2. package/dist/index.js +557 -243
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,20 +4,23 @@
4
4
  import { createServer } from "http";
5
5
  import { existsSync } from "fs";
6
6
  import { hostname as hostname2 } from "os";
7
- import { dirname as dirname5, join as join12 } from "path";
7
+ import { dirname as dirname6, join as join13 } from "path";
8
8
  import { fileURLToPath } from "url";
9
9
 
10
10
  // ../../packages/local-agent-protocol/src/session.ts
11
11
  var SESSION_STATUSES = ["running", "exited"];
12
12
  var AGENT_STATES = ["working", "waiting", "done"];
13
+ var AGENT_IDS = ["claude", "codex", "opencode"];
14
+ var TASK_STATUSES = ["pending", "in_progress", "completed"];
13
15
  var MAX_SKIPPED_FILES_REPORTED = 20;
16
+ var MAX_SESSION_PULL_REQUESTS = 20;
14
17
 
15
18
  // ../../packages/local-agent-protocol/src/messages.ts
16
19
  var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
17
20
  var INVALID_MESSAGE_ERROR = "invalid message";
18
21
 
19
22
  // ../../packages/local-agent-protocol/src/protocol-version.ts
20
- var PROTOCOL_VERSION = 17;
23
+ var PROTOCOL_VERSION = 20;
21
24
 
22
25
  // ../../packages/local-agent-protocol/src/branch-name.ts
23
26
  var MAX_BRANCH_NAME_LENGTH = 250;
@@ -184,6 +187,8 @@ var MAX_WIRE_MESSAGE_BYTES = 256 * 1024;
184
187
  var MAX_DAEMON_REPLY_BYTES = 200 * 1024;
185
188
  var MAX_RANGE_LINES = 500;
186
189
  var MAX_PLAN_CHARS = 32 * 1024;
190
+ var MAX_PLAN_TASKS = 50;
191
+ var MAX_TASK_TEXT_CHARS = 120;
187
192
  var MAX_TERMINAL_DIMENSION = 1e3;
188
193
  var terminalSizeSchema = z3.object({
189
194
  cols: z3.number().int().min(1).max(MAX_TERMINAL_DIMENSION),
@@ -204,9 +209,10 @@ var localSessionSchema = z3.object({
204
209
  worktreeOf: z3.string().nullable(),
205
210
  // .catch(undefined): a state this version doesn't know must never drop the whole session message.
206
211
  agentState: z3.enum(AGENT_STATES).nullable().optional().catch(void 0),
212
+ agent: z3.enum(AGENT_IDS).nullable().optional().catch(void 0),
207
213
  worktreeSetupFailed: z3.boolean().optional().catch(void 0),
208
214
  worktreeSetupSkipped: z3.array(z3.string().max(300)).max(MAX_SKIPPED_FILES_REPORTED).optional().catch(void 0),
209
- headline: z3.string().nullable().optional().catch(void 0)
215
+ pullRequestUrls: z3.array(z3.string().max(300)).max(MAX_SESSION_PULL_REQUESTS).optional().catch(void 0)
210
216
  });
211
217
  var workspaceFileEntrySchema = z3.object({
212
218
  path: z3.string(),
@@ -317,6 +323,11 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
317
323
  sessionId: z3.string(),
318
324
  ...terminalSizeSchema.shape
319
325
  }),
326
+ z3.object({
327
+ type: z3.literal("session.setMouseMode"),
328
+ sessionId: z3.string(),
329
+ enabled: z3.boolean()
330
+ }),
320
331
  z3.object({ type: z3.literal("session.stop"), sessionId: z3.string() }),
321
332
  z3.object({
322
333
  type: z3.literal("workspace.getStatus"),
@@ -381,6 +392,11 @@ var connectionAuthenticationFailureReasonSchema = z3.enum([
381
392
  "key_agreement_failed"
382
393
  ]);
383
394
  var connectionRejectionReasonSchema = z3.enum(["origin_not_allowed", "token_invalid"]);
395
+ var sessionTaskSchema = z3.object({
396
+ subject: z3.string().max(MAX_TASK_TEXT_CHARS),
397
+ activeForm: z3.string().max(MAX_TASK_TEXT_CHARS).optional(),
398
+ status: z3.enum(TASK_STATUSES)
399
+ });
384
400
  var daemonMessageSchema = z3.discriminatedUnion("type", [
385
401
  z3.object({ type: z3.literal("session.list"), sessions: z3.array(localSessionSchema) }),
386
402
  z3.object({
@@ -438,7 +454,8 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
438
454
  type: z3.literal("workspace.plan"),
439
455
  sessionId: z3.string(),
440
456
  requestId: z3.string(),
441
- plan: z3.string().max(MAX_PLAN_CHARS).nullable()
457
+ plan: z3.string().max(MAX_PLAN_CHARS).nullable(),
458
+ tasks: z3.array(sessionTaskSchema).max(MAX_PLAN_TASKS).default([])
442
459
  }),
443
460
  z3.object({
444
461
  type: z3.literal("project.pathValidated"),
@@ -527,6 +544,7 @@ var SIGNABLE_ACTIONS = [
527
544
  "session.input",
528
545
  "session.insertText",
529
546
  "session.resize",
547
+ "session.setMouseMode",
530
548
  "session.stop",
531
549
  "session.list",
532
550
  "workspace.getStatus",
@@ -1035,7 +1053,7 @@ async function callTRPCQuery(baseUrl, procedure, input) {
1035
1053
  // src/pairing/pairing.ts
1036
1054
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
1037
1055
  function sleep(ms) {
1038
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1056
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1039
1057
  }
1040
1058
  async function ensurePaired(options) {
1041
1059
  const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
@@ -1173,8 +1191,8 @@ async function bindFirstController(params, response) {
1173
1191
  }
1174
1192
 
1175
1193
  // src/projects/validate-path.ts
1176
- import { stat as stat2 } from "fs/promises";
1177
- import { isAbsolute as isAbsolute2, resolve } from "path";
1194
+ import { stat } from "fs/promises";
1195
+ import { isAbsolute, resolve } from "path";
1178
1196
 
1179
1197
  // src/sessions/expand-home.ts
1180
1198
  import { homedir } from "os";
@@ -1244,7 +1262,7 @@ function runGitDiffAllowingExitCodes(args, cwd, allowedExitCodes) {
1244
1262
  var GIT_INPUT_TIMEOUT_MS = 3e4;
1245
1263
  async function runGitWithInput(args, cwd, input, options = {}) {
1246
1264
  const { allowedExitCodes = [], timeoutMs = GIT_INPUT_TIMEOUT_MS } = options;
1247
- return new Promise((resolve2, reject) => {
1265
+ return new Promise((resolve3, reject) => {
1248
1266
  const child = spawn2("git", [...SAFE_GIT_CONFIG, ...args], { cwd });
1249
1267
  const stdout = [];
1250
1268
  const stderr = [];
@@ -1268,7 +1286,7 @@ async function runGitWithInput(args, cwd, input, options = {}) {
1268
1286
  "close",
1269
1287
  (code) => settle(() => {
1270
1288
  if (code === 0 || code !== null && allowedExitCodes.includes(code)) {
1271
- resolve2(Buffer.concat(stdout).toString("utf8"));
1289
+ resolve3(Buffer.concat(stdout).toString("utf8"));
1272
1290
  return;
1273
1291
  }
1274
1292
  const message = Buffer.concat(stderr).toString("utf8").trim();
@@ -1279,134 +1297,13 @@ async function runGitWithInput(args, cwd, input, options = {}) {
1279
1297
  });
1280
1298
  }
1281
1299
 
1282
- // src/sessions/files/plan-file.ts
1283
- import { constants as constants2 } from "fs";
1284
- import { open as open2 } from "fs/promises";
1300
+ // src/sessions/agent-reports/layout.ts
1285
1301
  import { homedir as homedir2 } from "os";
1286
- import { join as join3 } from "path";
1287
-
1288
- // src/sessions/files/read-range.ts
1289
- import { constants } from "fs";
1290
- import { lstat, open, realpath, stat } from "fs/promises";
1291
- import { isAbsolute, join as join2, sep } from "path";
1292
- var MAX_RANGE_FILE_BYTES = 2 * 1024 * 1024;
1293
- var REPLY_ENVELOPE_BYTES = 8 * 1024;
1294
- var BINARY_SNIFF_BYTES = 8e3;
1295
- function fileVersion(stats) {
1296
- return `${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.ino}`;
1297
- }
1298
- async function locateInside(cwd, path) {
1299
- if (isAbsolute(path)) return { refused: "not_allowed" };
1300
- try {
1301
- const root = await realpath(cwd);
1302
- const lexical = join2(root, path);
1303
- if ((await lstat(lexical)).isSymbolicLink()) return { refused: "not_allowed" };
1304
- const real = await realpath(lexical);
1305
- return real === root || real.startsWith(root + sep) ? { found: real } : { refused: "missing" };
1306
- } catch {
1307
- return { refused: "missing" };
1308
- }
1309
- }
1310
- async function readExactly(handle, size) {
1311
- const buffer = Buffer.allocUnsafe(size + 1);
1312
- let filled = 0;
1313
- while (filled < buffer.length) {
1314
- const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
1315
- if (bytesRead === 0) break;
1316
- filled += bytesRead;
1317
- }
1318
- return filled === size ? buffer.subarray(0, size) : void 0;
1319
- }
1320
- async function currentFileVersion(cwd, path) {
1321
- const located = await locateInside(cwd, path);
1322
- if (!("found" in located)) return void 0;
1323
- try {
1324
- return fileVersion(await stat(located.found));
1325
- } catch {
1326
- return void 0;
1327
- }
1328
- }
1329
- function splitLines(text) {
1330
- if (text === "") return [];
1331
- const lines = text.split("\n");
1332
- if (lines[lines.length - 1] === "") lines.pop();
1333
- return lines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
1334
- }
1335
- async function readFileRange(cwd, path, startLine, lineCount, version) {
1336
- const refuse = (reason) => ({ ok: false, path, reason });
1337
- const located = await locateInside(cwd, path);
1338
- if (!("found" in located)) return refuse(located.refused);
1339
- let content;
1340
- let handle;
1341
- try {
1342
- handle = await open(
1343
- located.found,
1344
- constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK
1345
- );
1346
- const before = await handle.stat();
1347
- if (!before.isFile()) return refuse("not_allowed");
1348
- if (fileVersion(before) !== version) return refuse("changed");
1349
- if (before.size > MAX_RANGE_FILE_BYTES) return refuse("too_large");
1350
- const bytes = await readExactly(handle, before.size);
1351
- if (bytes === void 0 || fileVersion(await handle.stat()) !== version) {
1352
- return refuse("changed");
1353
- }
1354
- content = bytes;
1355
- } catch {
1356
- return refuse("missing");
1357
- } finally {
1358
- await handle?.close();
1359
- }
1360
- if (content.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return refuse("binary");
1361
- const lines = splitLines(content.toString("utf8"));
1362
- const page = [];
1363
- let budget = MAX_DAEMON_REPLY_BYTES - REPLY_ENVELOPE_BYTES;
1364
- const wanted = Math.min(lineCount, MAX_RANGE_LINES);
1365
- for (let index = startLine - 1; index < lines.length && page.length < wanted; index++) {
1366
- const line = lines[index];
1367
- const cost = Buffer.byteLength(JSON.stringify(line)) + 1;
1368
- if (cost > budget) {
1369
- if (page.length === 0) return refuse("too_large");
1370
- break;
1371
- }
1372
- budget -= cost;
1373
- page.push(line);
1374
- }
1375
- return { ok: true, path, startLine, lines: page, totalLines: lines.length, version };
1376
- }
1377
-
1378
- // src/sessions/files/plan-file.ts
1302
+ import { join as join2 } from "path";
1303
+ var HOOK_PLANS_DIR = join2(homedir2(), ".bondsmith", "plans");
1304
+ var PLAN_FILE_SUFFIX = ".md";
1305
+ var TASKS_FILE_SUFFIX = ".tasks.jsonl";
1379
1306
  var PLAN_FILE_PATH = ".bondsmith/plan.md";
1380
- var MAX_PLAN_BYTES = MAX_PLAN_CHARS * 4;
1381
- var ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ELOOP", "ENOTDIR"]);
1382
- var HOOK_PLANS_DIR = join3(homedir2(), ".bondsmith", "plans");
1383
- async function readPlanAt(root, relativePath) {
1384
- const located = await locateInside(root, relativePath);
1385
- if (!("found" in located)) return null;
1386
- let handle;
1387
- try {
1388
- handle = await open2(
1389
- located.found,
1390
- constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
1391
- );
1392
- if (!(await handle.stat()).isFile()) return null;
1393
- const buffer = Buffer.alloc(MAX_PLAN_BYTES);
1394
- const { bytesRead } = await handle.read(buffer, 0, MAX_PLAN_BYTES, 0);
1395
- const plan = buffer.toString("utf8", 0, bytesRead).trim();
1396
- return plan ? plan.slice(0, MAX_PLAN_CHARS) : null;
1397
- } catch (error2) {
1398
- if (ABSENT_CODES.has(error2.code ?? "")) return null;
1399
- throw error2;
1400
- } finally {
1401
- await handle?.close();
1402
- }
1403
- }
1404
- function readPlanFile(cwd) {
1405
- return readPlanAt(cwd, PLAN_FILE_PATH);
1406
- }
1407
- function readHookPlan(sessionId, plansDir = HOOK_PLANS_DIR) {
1408
- return readPlanAt(plansDir, `${sessionId}.md`);
1409
- }
1410
1307
 
1411
1308
  // src/sessions/tmux/client.ts
1412
1309
  import { execFile as execFileCb2 } from "child_process";
@@ -1454,8 +1351,14 @@ async function runTmux(args, options) {
1454
1351
  throw error2;
1455
1352
  }
1456
1353
  }
1457
- async function sendKeysLiteral(tmuxSessionName, text) {
1458
- await runTmux(["send-keys", "-t", tmuxSessionName, "-l", text]);
1354
+ async function sendKeysLiteral(tmuxSessionName, text2) {
1355
+ await runTmux(["send-keys", "-t", tmuxSessionName, "-l", text2]);
1356
+ }
1357
+ async function setMouseMode(tmuxSessionName, enabled) {
1358
+ await runTmux(["set-option", "-t", tmuxSessionName, "mouse", enabled ? "on" : "off"]);
1359
+ }
1360
+ async function cancelCopyMode(tmuxSessionName) {
1361
+ await runTmux(["copy-mode", "-q", "-t", tmuxSessionName]);
1459
1362
  }
1460
1363
  async function capturePane(tmuxSessionName) {
1461
1364
  return runTmux(["capture-pane", "-t", tmuxSessionName, "-p", "-e"]);
@@ -1493,8 +1396,8 @@ var NO_SERVER_MESSAGE_PATTERNS = [
1493
1396
  function isNoServerError(error2) {
1494
1397
  if (typeof error2 !== "object" || error2 === null) return false;
1495
1398
  const { message, stderr } = error2;
1496
- const text = [message, stderr].filter((part) => typeof part === "string").join("\n");
1497
- return NO_SERVER_MESSAGE_PATTERNS.some((pattern) => pattern.test(text));
1399
+ const text2 = [message, stderr].filter((part) => typeof part === "string").join("\n");
1400
+ return NO_SERVER_MESSAGE_PATTERNS.some((pattern) => pattern.test(text2));
1498
1401
  }
1499
1402
 
1500
1403
  // src/sessions/git/base.ts
@@ -1531,14 +1434,14 @@ async function resolveComparisonBase(cwd) {
1531
1434
  }
1532
1435
 
1533
1436
  // src/sessions/git/baseline.ts
1534
- import { lstat as lstat2 } from "fs/promises";
1535
- import { join as join4 } from "path";
1437
+ import { lstat } from "fs/promises";
1438
+ import { join as join3 } from "path";
1536
1439
  var SESSION_START_REF = "session start";
1537
1440
  var MAX_PREEXISTING_FILES = 200;
1538
1441
  var DELETED = "deleted";
1539
1442
  async function fingerprintFile(cwd, path) {
1540
1443
  try {
1541
- const stats = await lstat2(join4(cwd, path));
1444
+ const stats = await lstat(join3(cwd, path));
1542
1445
  return `${stats.size}:${Math.trunc(stats.mtimeMs)}`;
1543
1446
  } catch {
1544
1447
  return DELETED;
@@ -1862,23 +1765,23 @@ function gitFailure(error2, path) {
1862
1765
  "git isn't installed, or isn't on the PATH of the terminal running bondsmithd."
1863
1766
  );
1864
1767
  }
1865
- const text = typeof stderr === "string" ? stderr : "";
1866
- if (text.includes("dubious ownership")) {
1768
+ const text2 = typeof stderr === "string" ? stderr : "";
1769
+ if (text2.includes("dubious ownership")) {
1867
1770
  return new Error(
1868
1771
  `git doesn't trust this folder because another user owns it. If you trust it, run: git config --global --add safe.directory ${shellQuote(path)}`
1869
1772
  );
1870
1773
  }
1871
- const fatal = text.split("\n").find((line) => line.startsWith("fatal:"));
1774
+ const fatal = text2.split("\n").find((line) => line.startsWith("fatal:"));
1872
1775
  return new Error(
1873
1776
  fatal ?? (error2 instanceof Error ? error2.message : "git failed to inspect that folder.")
1874
1777
  );
1875
1778
  }
1876
1779
  async function validateProjectPath(input) {
1877
1780
  const expanded = expandHome(input.trim());
1878
- if (!isAbsolute2(expanded)) return { valid: false, reason: "relative_path" };
1781
+ if (!isAbsolute(expanded)) return { valid: false, reason: "relative_path" };
1879
1782
  const path = resolve(expanded);
1880
1783
  try {
1881
- const stats = await stat2(path);
1784
+ const stats = await stat(path);
1882
1785
  if (!stats.isDirectory()) return { valid: false, reason: "not_a_directory" };
1883
1786
  } catch (error2) {
1884
1787
  const reason = invalidReasonForFsError(error2);
@@ -1905,7 +1808,7 @@ import { randomBytes, verify as verify2 } from "crypto";
1905
1808
  // src/security/trusted-controllers.ts
1906
1809
  import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
1907
1810
  import { homedir as homedir3 } from "os";
1908
- import { dirname, join as join5 } from "path";
1811
+ import { dirname, join as join4 } from "path";
1909
1812
  import { z as z12 } from "zod";
1910
1813
  var trustedControllerSchema = z12.object({
1911
1814
  id: z12.string(),
@@ -1928,7 +1831,7 @@ var trustedControllersFileSchema = z12.object({
1928
1831
  controllers: z12.array(trustedControllerSchema)
1929
1832
  });
1930
1833
  function defaultTrustedControllersPath() {
1931
- return join5(homedir3(), ".bondsmith", "trusted-controllers.json");
1834
+ return join4(homedir3(), ".bondsmith", "trusted-controllers.json");
1932
1835
  }
1933
1836
  async function loadTrustedControllers(path = defaultTrustedControllersPath()) {
1934
1837
  let raw;
@@ -2249,12 +2152,12 @@ var ConnectionAuthState = class {
2249
2152
  };
2250
2153
 
2251
2154
  // src/sessions/agents/installed-agents.ts
2252
- import { accessSync, constants as constants3, statSync } from "fs";
2253
- import { delimiter, join as join6 } from "path";
2254
- var KNOWN_AGENTS = ["claude", "codex", "opencode"];
2155
+ import { accessSync, constants, statSync } from "fs";
2156
+ import { delimiter, join as join5 } from "path";
2157
+ var KNOWN_AGENTS = AGENT_IDS;
2255
2158
  function isExecutableFile(path) {
2256
2159
  try {
2257
- accessSync(path, constants3.X_OK);
2160
+ accessSync(path, constants.X_OK);
2258
2161
  return statSync(path).isFile();
2259
2162
  } catch {
2260
2163
  return false;
@@ -2263,12 +2166,12 @@ function isExecutableFile(path) {
2263
2166
  function listInstalledAgents(env = process.env) {
2264
2167
  const directories = (env.PATH ?? "").split(delimiter).filter((directory) => directory !== "");
2265
2168
  return KNOWN_AGENTS.filter(
2266
- (agent) => directories.some((directory) => isExecutableFile(join6(directory, agent)))
2169
+ (agent) => directories.some((directory) => isExecutableFile(join5(directory, agent)))
2267
2170
  );
2268
2171
  }
2269
2172
 
2270
2173
  // src/version.ts
2271
- var DAEMON_VERSION = true ? "0.1.14" : "dev";
2174
+ var DAEMON_VERSION = true ? "0.1.16" : "dev";
2272
2175
  var VERSION_FLAGS = ["--version", "-v"];
2273
2176
  function versionLine() {
2274
2177
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -2277,6 +2180,241 @@ function isVersionRequest(argv) {
2277
2180
  return argv.some((arg) => VERSION_FLAGS.includes(arg));
2278
2181
  }
2279
2182
 
2183
+ // src/sessions/agent-reports/read-file.ts
2184
+ import { constants as constants3 } from "fs";
2185
+ import { open as open2 } from "fs/promises";
2186
+
2187
+ // src/sessions/files/read-range.ts
2188
+ import { constants as constants2 } from "fs";
2189
+ import { lstat as lstat2, open, realpath, stat as stat2 } from "fs/promises";
2190
+ import { isAbsolute as isAbsolute2, join as join6, sep } from "path";
2191
+ var MAX_RANGE_FILE_BYTES = 2 * 1024 * 1024;
2192
+ var REPLY_ENVELOPE_BYTES = 8 * 1024;
2193
+ var BINARY_SNIFF_BYTES = 8e3;
2194
+ function fileVersion(stats) {
2195
+ return `${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.ino}`;
2196
+ }
2197
+ async function locateInside(cwd, path) {
2198
+ if (isAbsolute2(path)) return { refused: "not_allowed" };
2199
+ try {
2200
+ const root = await realpath(cwd);
2201
+ const lexical = join6(root, path);
2202
+ if ((await lstat2(lexical)).isSymbolicLink()) return { refused: "not_allowed" };
2203
+ const real = await realpath(lexical);
2204
+ return real === root || real.startsWith(root + sep) ? { found: real } : { refused: "missing" };
2205
+ } catch {
2206
+ return { refused: "missing" };
2207
+ }
2208
+ }
2209
+ async function readExactly(handle, size) {
2210
+ const buffer = Buffer.allocUnsafe(size + 1);
2211
+ let filled = 0;
2212
+ while (filled < buffer.length) {
2213
+ const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
2214
+ if (bytesRead === 0) break;
2215
+ filled += bytesRead;
2216
+ }
2217
+ return filled === size ? buffer.subarray(0, size) : void 0;
2218
+ }
2219
+ async function currentFileVersion(cwd, path) {
2220
+ const located = await locateInside(cwd, path);
2221
+ if (!("found" in located)) return void 0;
2222
+ try {
2223
+ return fileVersion(await stat2(located.found));
2224
+ } catch {
2225
+ return void 0;
2226
+ }
2227
+ }
2228
+ function splitLines(text2) {
2229
+ if (text2 === "") return [];
2230
+ const lines = text2.split("\n");
2231
+ if (lines[lines.length - 1] === "") lines.pop();
2232
+ return lines.map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
2233
+ }
2234
+ async function readFileRange(cwd, path, startLine, lineCount, version) {
2235
+ const refuse = (reason) => ({ ok: false, path, reason });
2236
+ const located = await locateInside(cwd, path);
2237
+ if (!("found" in located)) return refuse(located.refused);
2238
+ let content;
2239
+ let handle;
2240
+ try {
2241
+ handle = await open(
2242
+ located.found,
2243
+ constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK
2244
+ );
2245
+ const before = await handle.stat();
2246
+ if (!before.isFile()) return refuse("not_allowed");
2247
+ if (fileVersion(before) !== version) return refuse("changed");
2248
+ if (before.size > MAX_RANGE_FILE_BYTES) return refuse("too_large");
2249
+ const bytes = await readExactly(handle, before.size);
2250
+ if (bytes === void 0 || fileVersion(await handle.stat()) !== version) {
2251
+ return refuse("changed");
2252
+ }
2253
+ content = bytes;
2254
+ } catch {
2255
+ return refuse("missing");
2256
+ } finally {
2257
+ await handle?.close();
2258
+ }
2259
+ if (content.subarray(0, BINARY_SNIFF_BYTES).includes(0)) return refuse("binary");
2260
+ const lines = splitLines(content.toString("utf8"));
2261
+ const page = [];
2262
+ let budget = MAX_DAEMON_REPLY_BYTES - REPLY_ENVELOPE_BYTES;
2263
+ const wanted = Math.min(lineCount, MAX_RANGE_LINES);
2264
+ for (let index = startLine - 1; index < lines.length && page.length < wanted; index++) {
2265
+ const line = lines[index];
2266
+ const cost = Buffer.byteLength(JSON.stringify(line)) + 1;
2267
+ if (cost > budget) {
2268
+ if (page.length === 0) return refuse("too_large");
2269
+ break;
2270
+ }
2271
+ budget -= cost;
2272
+ page.push(line);
2273
+ }
2274
+ return { ok: true, path, startLine, lines: page, totalLines: lines.length, version };
2275
+ }
2276
+
2277
+ // src/sessions/agent-reports/read-file.ts
2278
+ var ABSENT_CODES = /* @__PURE__ */ new Set(["ENOENT", "ELOOP", "ENOTDIR"]);
2279
+ async function readFileAt(root, relativePath, read) {
2280
+ const located = await locateInside(root, relativePath);
2281
+ if (!("found" in located)) return null;
2282
+ let handle;
2283
+ try {
2284
+ handle = await open2(
2285
+ located.found,
2286
+ constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
2287
+ );
2288
+ const info = await handle.stat();
2289
+ if (!info.isFile()) return null;
2290
+ return await read(handle, info.size);
2291
+ } catch (error2) {
2292
+ if (ABSENT_CODES.has(error2.code ?? "")) return null;
2293
+ throw error2;
2294
+ } finally {
2295
+ await handle?.close();
2296
+ }
2297
+ }
2298
+
2299
+ // src/sessions/agent-reports/task-list.ts
2300
+ import { z as z13 } from "zod";
2301
+ var text = z13.string().refine((value) => value.trim() !== "");
2302
+ var optionalText = text.optional().catch(void 0);
2303
+ var taskStatus = z13.enum(TASK_STATUSES).optional().catch(void 0);
2304
+ var taskId = z13.union([z13.string(), z13.number()]).transform(String);
2305
+ var taskEventSchema = z13.discriminatedUnion("op", [
2306
+ z13.object({
2307
+ op: z13.literal("add"),
2308
+ id: taskId.optional().catch(void 0),
2309
+ subject: text,
2310
+ activeForm: optionalText
2311
+ }),
2312
+ z13.object({
2313
+ op: z13.literal("update"),
2314
+ id: taskId,
2315
+ subject: optionalText,
2316
+ activeForm: optionalText,
2317
+ status: taskStatus
2318
+ }),
2319
+ z13.object({ op: z13.literal("remove"), id: taskId }),
2320
+ z13.object({ op: z13.literal("replace"), tasks: z13.array(z13.unknown()) })
2321
+ ]);
2322
+ var replacementTaskSchema = z13.object({
2323
+ subject: text,
2324
+ activeForm: optionalText,
2325
+ status: taskStatus
2326
+ });
2327
+ function applyEvent(tasks, event) {
2328
+ switch (event.op) {
2329
+ case "replace":
2330
+ return event.tasks.flatMap((candidate) => {
2331
+ const task = replacementTaskSchema.safeParse(candidate);
2332
+ if (!task.success) return [];
2333
+ const { subject, activeForm, status = "pending" } = task.data;
2334
+ return [{ id: null, subject, activeForm, status }];
2335
+ });
2336
+ case "add":
2337
+ return [
2338
+ ...tasks,
2339
+ {
2340
+ id: event.id ?? null,
2341
+ subject: event.subject,
2342
+ activeForm: event.activeForm,
2343
+ status: "pending"
2344
+ }
2345
+ ];
2346
+ case "update":
2347
+ return tasks.map(
2348
+ (task) => task.id === event.id ? {
2349
+ ...task,
2350
+ subject: event.subject ?? task.subject,
2351
+ activeForm: event.activeForm ?? task.activeForm,
2352
+ status: event.status ?? task.status
2353
+ } : task
2354
+ );
2355
+ case "remove":
2356
+ return tasks.filter((task) => task.id !== event.id);
2357
+ }
2358
+ }
2359
+ var cut = (value) => value.slice(0, MAX_TASK_TEXT_CHARS);
2360
+ function replayTasks(log) {
2361
+ let tasks = [];
2362
+ for (const line of log.split("\n")) {
2363
+ if (!line.trim()) continue;
2364
+ let raw;
2365
+ try {
2366
+ raw = JSON.parse(line);
2367
+ } catch {
2368
+ continue;
2369
+ }
2370
+ const event = taskEventSchema.safeParse(raw);
2371
+ if (event.success) tasks = applyEvent(tasks, event.data);
2372
+ }
2373
+ return tasks.slice(0, MAX_PLAN_TASKS).map(({ subject, activeForm, status }) => ({
2374
+ subject: cut(subject),
2375
+ ...activeForm && { activeForm: cut(activeForm) },
2376
+ status
2377
+ }));
2378
+ }
2379
+
2380
+ // src/sessions/agent-reports/readers.ts
2381
+ var MAX_PLAN_BYTES = MAX_PLAN_CHARS * 4;
2382
+ var MAX_TASK_LOG_BYTES = 1024 * 1024;
2383
+ async function readPlanAt(root, relativePath) {
2384
+ const text2 = await readFileAt(root, relativePath, async (handle) => {
2385
+ const buffer = Buffer.alloc(MAX_PLAN_BYTES);
2386
+ const { bytesRead } = await handle.read(buffer, 0, MAX_PLAN_BYTES, 0);
2387
+ return buffer.toString("utf8", 0, bytesRead).trim();
2388
+ });
2389
+ return text2 ? text2.slice(0, MAX_PLAN_CHARS) : null;
2390
+ }
2391
+ function readPlanFile(cwd) {
2392
+ return readPlanAt(cwd, PLAN_FILE_PATH);
2393
+ }
2394
+ function readHookPlan(sessionId, plansDir = HOOK_PLANS_DIR) {
2395
+ return readPlanAt(plansDir, `${sessionId}${PLAN_FILE_SUFFIX}`);
2396
+ }
2397
+ async function readHookTasks(sessionId, plansDir = HOOK_PLANS_DIR) {
2398
+ const log = await readFileAt(
2399
+ plansDir,
2400
+ `${sessionId}${TASKS_FILE_SUFFIX}`,
2401
+ async (handle, size) => {
2402
+ const length = Math.min(size, MAX_TASK_LOG_BYTES);
2403
+ const buffer = Buffer.alloc(length);
2404
+ const { bytesRead } = await handle.read(buffer, 0, length, size - length);
2405
+ const text2 = buffer.toString("utf8", 0, bytesRead);
2406
+ return length < size ? text2.slice(text2.indexOf("\n") + 1) : text2;
2407
+ }
2408
+ );
2409
+ return log === null ? [] : replayTasks(log);
2410
+ }
2411
+
2412
+ // src/sessions/agent-reports/session-report.ts
2413
+ async function readSessionReport(session) {
2414
+ const plan = await readHookPlan(session.id) ?? await readPlanFile(session.cwd) ?? await getSessionPlan(session.tmuxSessionName);
2415
+ return { plan, tasks: await readHookTasks(session.id) };
2416
+ }
2417
+
2280
2418
  // src/security/verify-command.ts
2281
2419
  import { verify as verify3 } from "crypto";
2282
2420
  var ENVELOPE_MAX_AGE_MS = 6e4;
@@ -2413,8 +2551,8 @@ function createOutputPacer(options) {
2413
2551
 
2414
2552
  // src/ws/sanitize-insert-text.ts
2415
2553
  var DISALLOWED_CHARACTERS = /[\x00-\x08\x0b-\x1f\x7f-\x9f‪-‮⁦-⁩]/g;
2416
- function sanitizeInsertText(text) {
2417
- const withLineFeeds = text.replace(/\r\n?/g, "\n");
2554
+ function sanitizeInsertText(text2) {
2555
+ const withLineFeeds = text2.replace(/\r\n?/g, "\n");
2418
2556
  const sanitized = withLineFeeds.replace(DISALLOWED_CHARACTERS, "");
2419
2557
  return { text: sanitized, strippedCount: withLineFeeds.length - sanitized.length };
2420
2558
  }
@@ -2445,6 +2583,7 @@ function verifyRejectionMessage(reason) {
2445
2583
  var REVOKED_REJECTION_MESSAGE = "connection has not completed its handshake";
2446
2584
  var BRACKETED_PASTE_START = "\x1B[200~";
2447
2585
  var BRACKETED_PASTE_END = "\x1B[201~";
2586
+ var WHEEL_ONLY_INPUT = /^(?:\x1b\[<6[45];\d+;\d+M)+$/;
2448
2587
  function releaseSession(conn, ptyAttachments, sessionId) {
2449
2588
  const listener = conn.getOutputListener(sessionId);
2450
2589
  if (listener) ptyAttachments.release(sessionId, listener);
@@ -2458,6 +2597,7 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
2458
2597
  let admissionChain = Promise.resolve();
2459
2598
  const conn = {
2460
2599
  subscriptions: /* @__PURE__ */ new Set(),
2600
+ mouseModeSessions: /* @__PURE__ */ new Map(),
2461
2601
  send,
2462
2602
  bufferedBytes,
2463
2603
  isClosed: () => closed,
@@ -2496,6 +2636,11 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
2496
2636
  for (const sessionId of [...conn.subscriptions]) {
2497
2637
  releaseSession(conn, ptyAttachments, sessionId);
2498
2638
  }
2639
+ for (const tmuxSessionName of conn.mouseModeSessions.values()) {
2640
+ setMouseMode(tmuxSessionName, false).catch(() => {
2641
+ });
2642
+ }
2643
+ conn.mouseModeSessions.clear();
2499
2644
  }
2500
2645
  return { conn, releaseAllSubscriptions };
2501
2646
  }
@@ -2729,6 +2874,11 @@ async function dispatch(conn, message, {
2729
2874
  return;
2730
2875
  }
2731
2876
  sessionManager.acknowledge(message.sessionId);
2877
+ const tmuxSessionName = conn.mouseModeSessions.get(message.sessionId);
2878
+ if (tmuxSessionName && !WHEEL_ONLY_INPUT.test(message.data)) {
2879
+ await cancelCopyMode(tmuxSessionName).catch(() => {
2880
+ });
2881
+ }
2732
2882
  ptyAttachments.write(message.sessionId, message.data);
2733
2883
  return;
2734
2884
  }
@@ -2736,14 +2886,14 @@ async function dispatch(conn, message, {
2736
2886
  const session = getSessionOrError(sessionManager, conn, message.sessionId);
2737
2887
  if (!session) return;
2738
2888
  sessionManager.acknowledge(message.sessionId);
2739
- const { text, strippedCount } = sanitizeInsertText(message.text);
2889
+ const { text: text2, strippedCount } = sanitizeInsertText(message.text);
2740
2890
  if (strippedCount > 0) {
2741
2891
  console.warn(`bondsmithd: stripped ${strippedCount} control characters from inserted text`);
2742
2892
  }
2743
2893
  try {
2744
2894
  await sendKeysLiteral(
2745
2895
  session.tmuxSessionName,
2746
- `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`
2896
+ `${BRACKETED_PASTE_START}${text2}${BRACKETED_PASTE_END}`
2747
2897
  );
2748
2898
  } catch (err) {
2749
2899
  conn.send(error(errorMessageOf(err, "failed to insert text")));
@@ -2759,6 +2909,20 @@ async function dispatch(conn, message, {
2759
2909
  ptyAttachments.resize(message.sessionId, message.cols, message.rows);
2760
2910
  return;
2761
2911
  }
2912
+ case "session.setMouseMode": {
2913
+ const session = sessionManager.get(message.sessionId);
2914
+ if (!session) return;
2915
+ if (message.enabled) conn.mouseModeSessions.set(session.id, session.tmuxSessionName);
2916
+ else conn.mouseModeSessions.delete(session.id);
2917
+ try {
2918
+ await setMouseMode(session.tmuxSessionName, message.enabled);
2919
+ if (message.enabled && conn.isClosed()) await setMouseMode(session.tmuxSessionName, false);
2920
+ } catch (err) {
2921
+ if (message.enabled) conn.mouseModeSessions.delete(session.id);
2922
+ console.warn(`bondsmithd: ${errorMessageOf(err, "failed to set mouse mode")}`);
2923
+ }
2924
+ return;
2925
+ }
2762
2926
  case "session.stop": {
2763
2927
  try {
2764
2928
  if (!await sessionManager.stop(message.sessionId)) {
@@ -2840,12 +3004,13 @@ async function dispatch(conn, message, {
2840
3004
  const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
2841
3005
  if (!session) return;
2842
3006
  try {
2843
- const plan = await readHookPlan(session.id) ?? await readPlanFile(session.cwd) ?? await getSessionPlan(session.tmuxSessionName);
3007
+ const { plan, tasks } = await readSessionReport(session);
2844
3008
  conn.send({
2845
3009
  type: "workspace.plan",
2846
3010
  sessionId: message.sessionId,
2847
3011
  requestId: message.requestId,
2848
- plan
3012
+ plan,
3013
+ tasks
2849
3014
  });
2850
3015
  } catch (err) {
2851
3016
  conn.send(error(errorMessageOf(err, "failed to read the session plan"), message.requestId));
@@ -3279,9 +3444,9 @@ function isMatchingKeyPair(publicKeyPem, privateKeyPem) {
3279
3444
  return false;
3280
3445
  }
3281
3446
  }
3282
- function extractPemBlock(text, label) {
3447
+ function extractPemBlock(text2, label) {
3283
3448
  const pattern = new RegExp(`-----BEGIN ${label}-----[\\s\\S]*?-----END ${label}-----\\n?`);
3284
- return text.match(pattern)?.[0] ?? null;
3449
+ return text2.match(pattern)?.[0] ?? null;
3285
3450
  }
3286
3451
 
3287
3452
  // src/security/daemon-proof.ts
@@ -3420,8 +3585,8 @@ var PendingAuthorizationStore = class {
3420
3585
  requestToken: randomUUID()
3421
3586
  };
3422
3587
  let settleFn;
3423
- const resultPromise = new Promise((resolve2) => {
3424
- settleFn = resolve2;
3588
+ const resultPromise = new Promise((resolve3) => {
3589
+ settleFn = resolve3;
3425
3590
  });
3426
3591
  const entry = {
3427
3592
  authorization,
@@ -3532,16 +3697,16 @@ var PendingAuthorizationStore = class {
3532
3697
  };
3533
3698
 
3534
3699
  // src/security/revocation-sync.ts
3535
- import { z as z13 } from "zod";
3700
+ import { z as z14 } from "zod";
3536
3701
  var POLL_INTERVAL_MS = 3e4;
3537
3702
  var POLL_JITTER_MS = 6e3;
3538
3703
  var POLL_REQUEST_TIMEOUT_MS = 1e4;
3539
3704
  var BACKOFF_BASE_MS = 2e3;
3540
3705
  var BACKOFF_MAX_MS = 2e4;
3541
3706
  var MAX_CONSECUTIVE_FAILURE_BACKOFFS = 4;
3542
- var checkRevocationStatusResponseSchema = z13.object({
3543
- pairingRevoked: z13.boolean(),
3544
- revokedControllerPublicKeys: z13.array(z13.string())
3707
+ var checkRevocationStatusResponseSchema = z14.object({
3708
+ pairingRevoked: z14.boolean(),
3709
+ revokedControllerPublicKeys: z14.array(z14.string())
3545
3710
  });
3546
3711
  function jitteredInterval(deps) {
3547
3712
  const interval = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
@@ -3891,6 +4056,45 @@ ${new TmuxNotFoundError().message}
3891
4056
  // src/sessions/session-manager.ts
3892
4057
  import { v4 as uuidv4 } from "uuid";
3893
4058
 
4059
+ // src/sessions/agent-reports/lifecycle.ts
4060
+ import { readdir, rm as rm4, stat as stat3 } from "fs/promises";
4061
+ import { basename, join as join9 } from "path";
4062
+ var SWEEP_MIN_AGE_MS = 6e4;
4063
+ async function removeHookPlanFiles(sessionId, plansDir = HOOK_PLANS_DIR) {
4064
+ await Promise.all(
4065
+ [PLAN_FILE_SUFFIX, TASKS_FILE_SUFFIX].map(
4066
+ (suffix) => rm4(join9(plansDir, `${basename(sessionId)}${suffix}`), { force: true })
4067
+ )
4068
+ );
4069
+ }
4070
+ function hookFileSessionId(name) {
4071
+ for (const suffix of [PLAN_FILE_SUFFIX, TASKS_FILE_SUFFIX]) {
4072
+ const sessionId = name.endsWith(suffix) ? name.slice(0, -suffix.length) : "";
4073
+ if (sessionId && !sessionId.includes(".")) return sessionId;
4074
+ }
4075
+ return null;
4076
+ }
4077
+ async function sweepOrphanedHookPlanFiles(liveSessionIds, plansDir = HOOK_PLANS_DIR) {
4078
+ let names;
4079
+ try {
4080
+ names = await readdir(plansDir);
4081
+ } catch (error2) {
4082
+ if (ABSENT_CODES.has(error2.code ?? "")) return;
4083
+ throw error2;
4084
+ }
4085
+ const now = Date.now();
4086
+ await Promise.all(
4087
+ names.map(async (name) => {
4088
+ const sessionId = hookFileSessionId(name);
4089
+ if (sessionId === null || liveSessionIds.has(sessionId)) return;
4090
+ const path = join9(plansDir, name);
4091
+ const modifiedMs = await stat3(path).then((info) => info.mtimeMs).catch(() => null);
4092
+ if (modifiedMs === null || now - modifiedMs < SWEEP_MIN_AGE_MS) return;
4093
+ await rm4(path, { force: true });
4094
+ })
4095
+ );
4096
+ }
4097
+
3894
4098
  // src/sessions/git/worktree.ts
3895
4099
  import { execFile as execFileCb3 } from "child_process";
3896
4100
  import { promisify as promisify3 } from "util";
@@ -3989,19 +4193,18 @@ function decodeSkippedFiles(raw) {
3989
4193
 
3990
4194
  // src/sessions/tmux/agent-state.ts
3991
4195
  var AGENT_STATE_OPTION = "@bondsmith-agent-state";
3992
- var HEADLINE_OPTION = "@bondsmith-headline";
3993
- var MAX_HEADLINE_CHARS = 200;
3994
- var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${SETUP_SKIPPED_OPTION}} #{${HEADLINE_OPTION}}`;
4196
+ var PULL_REQUESTS_OPTION = "@bondsmith-prs";
4197
+ var PULL_REQUEST_URL = /https?:\/\/[^\s/]+\/[^\s/]+\/[^\s/]+\/pull\/\d+/g;
4198
+ var LIVE_SESSION_REPORTS_FORMAT = `#{session_name} #{${AGENT_STATE_OPTION}} #{window_bell_flag} #{${SETUP_FAILED_OPTION}} #{${SETUP_SKIPPED_OPTION}} #{${PULL_REQUESTS_OPTION}}`;
3995
4199
  var AGENT_STATE_OUTPUT_GRACE_MS = 2e3;
3996
4200
  var FUTURE_REPORT_ALLOWANCE_MS = 2e3;
3997
4201
  var WORKING_SILENCE_MS = 12e4;
3998
4202
  var SETTLE_QUIET_MS = 8e3;
3999
4203
  var ACKNOWLEDGE_GRACE_MS = 3e3;
4000
4204
  var SETTLED_STATES = /* @__PURE__ */ new Set(["waiting", "done"]);
4001
- function cleanHeadline(raw) {
4002
- const line = raw.replace(/\s+/g, " ").trim();
4003
- if (!line) return null;
4004
- return line.length > MAX_HEADLINE_CHARS ? `${line.slice(0, MAX_HEADLINE_CHARS - 1)}\u2026` : line;
4205
+ function parsePullRequestUrls(raw) {
4206
+ const urls = [...new Set(raw.match(PULL_REQUEST_URL) ?? [])];
4207
+ return urls.slice(-MAX_SESSION_PULL_REQUESTS);
4005
4208
  }
4006
4209
  function isAgentState(value) {
4007
4210
  return AGENT_STATES.includes(value);
@@ -4015,7 +4218,7 @@ function parseLiveSessionReports(output) {
4015
4218
  bell = "",
4016
4219
  setupFailed = "",
4017
4220
  setupSkipped = "",
4018
- ...headline
4221
+ pullRequests = ""
4019
4222
  ] = line.split(" ");
4020
4223
  if (sessionName) {
4021
4224
  reports.set(sessionName, {
@@ -4023,17 +4226,22 @@ function parseLiveSessionReports(output) {
4023
4226
  bell: bell === "1",
4024
4227
  setupFailed: setupFailed === "1",
4025
4228
  setupSkipped: decodeSkippedFiles(setupSkipped),
4026
- headline: cleanHeadline(headline.join(" "))
4229
+ pullRequestUrls: parsePullRequestUrls(pullRequests)
4027
4230
  });
4028
4231
  }
4029
4232
  }
4030
4233
  return reports;
4031
4234
  }
4032
- function resolveAgentState(raw, lastActivityAt, nowMs = Date.now()) {
4235
+ function parseReport(raw) {
4033
4236
  if (!raw) return null;
4034
4237
  const [state, epoch] = raw.split(":");
4035
4238
  if (!state || !isAgentState(state) || !epoch || !/^\d+$/.test(epoch)) return null;
4036
- const reportedAtMs = Number(epoch) * 1e3;
4239
+ return { state, reportedAtMs: Number(epoch) * 1e3 };
4240
+ }
4241
+ function resolveAgentState(raw, lastActivityAt, nowMs = Date.now()) {
4242
+ const parsed = parseReport(raw);
4243
+ if (!parsed) return null;
4244
+ const { state, reportedAtMs } = parsed;
4037
4245
  if (reportedAtMs > nowMs + FUTURE_REPORT_ALLOWANCE_MS) return null;
4038
4246
  if (SETTLED_STATES.has(state) && lastActivityAt) {
4039
4247
  if (Date.parse(lastActivityAt) > reportedAtMs + AGENT_STATE_OUTPUT_GRACE_MS) return null;
@@ -4053,79 +4261,137 @@ function hasSettled(lastActivityAt, acknowledgedAtMs, nowMs = Date.now()) {
4053
4261
  }
4054
4262
  return nowMs - lastActivityMs >= SETTLE_QUIET_MS;
4055
4263
  }
4264
+ var REPORT_ROUNDING_MS = 1e3;
4265
+ function isDoneAcknowledged(raw, acknowledgedAtMs) {
4266
+ const reportedAtMs = parseReport(raw)?.reportedAtMs;
4267
+ if (acknowledgedAtMs === null || reportedAtMs === void 0) return false;
4268
+ return acknowledgedAtMs >= reportedAtMs + REPORT_ROUNDING_MS;
4269
+ }
4056
4270
  function resolveSessionAttention(report, lastActivityAt, { nowMs, acknowledgedAtMs = null } = {}) {
4057
4271
  if (report?.bell) return "waiting";
4058
4272
  const reported = resolveAgentState(report?.agentState, lastActivityAt, nowMs);
4273
+ if (reported === "done" && isDoneAcknowledged(report?.agentState, acknowledgedAtMs)) return null;
4059
4274
  if (reported) return reported;
4060
4275
  return hasSettled(lastActivityAt, acknowledgedAtMs, nowMs) ? "waiting" : null;
4061
4276
  }
4062
4277
 
4063
4278
  // src/sessions/tmux/create.ts
4064
4279
  import { access } from "fs/promises";
4065
- import { homedir as homedir7 } from "os";
4066
- import { join as join11 } from "path";
4067
4280
 
4068
- // src/sessions/agents/claude-hooks.ts
4281
+ // src/sessions/agents/adapter.ts
4282
+ var ENV_ASSIGNMENT_WORD = String.raw`\w+=\S*`;
4283
+ var ENV_ASSIGNMENT = new RegExp(`^${ENV_ASSIGNMENT_WORD}$`);
4284
+ function leadingBinary(command) {
4285
+ return command.split(/\s+/).filter(Boolean).find((word) => !ENV_ASSIGNMENT.test(word));
4286
+ }
4287
+ function plainAdapter(id) {
4288
+ return {
4289
+ id,
4290
+ matches: (command) => leadingBinary(command) === id,
4291
+ prepareCommand: async (command) => command
4292
+ };
4293
+ }
4294
+
4295
+ // src/sessions/agents/claude/hooks.ts
4069
4296
  import { execFile as execFile4 } from "child_process";
4070
4297
  import { randomUUID as randomUUID2 } from "crypto";
4071
- import { mkdir as mkdir4, rename as rename4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
4298
+ import { mkdir as mkdir4, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4072
4299
  import { homedir as homedir6 } from "os";
4073
- import { join as join9 } from "path";
4300
+ import { join as join10 } from "path";
4074
4301
  import { promisify as promisify4 } from "util";
4075
4302
 
4076
4303
  // src/sessions/session-env.ts
4077
4304
  var SESSION_ENV_VAR = "BONDSMITH_SESSION";
4078
4305
 
4079
- // src/sessions/agents/claude-hook-scripts.ts
4080
- var REPLY_ONLY_PROMPT = /^(y|yes|yep|yeah|no|nope|ok|okay|k|sure|go|go on|go ahead|continue|proceed|thanks|thank you|thx|done|great|nice|cool|lgtm)[.!\s]*$/i;
4081
- var MIN_HEADLINE_CHARS = 4;
4082
- function planHookScript(plansDir) {
4306
+ // src/sessions/agent-reports/hook-scripts.ts
4307
+ function storePlanScript(plansDir, planExpr) {
4083
4308
  return [
4084
4309
  `let d="";`,
4085
4310
  `process.stdin.on("data",c=>d+=c).on("end",()=>{try{`,
4086
- `const p=JSON.parse(d).tool_response?.plan;if(!p)return;`,
4311
+ `const j=JSON.parse(d),p=${planExpr};if(typeof p!=="string"||!p)return;`,
4087
4312
  `const fs=require("fs"),path=require("path"),dir=${JSON.stringify(plansDir)};`,
4088
4313
  // basename keeps an odd session value from naming a file outside the plans folder.
4089
- `const name=path.basename(process.env.${SESSION_ENV_VAR})+".md",tmp=path.join(dir,"."+name+"."+process.pid);`,
4314
+ `const name=path.basename(process.env.${SESSION_ENV_VAR})+${JSON.stringify(PLAN_FILE_SUFFIX)},tmp=path.join(dir,"."+name+"."+process.pid);`,
4090
4315
  // Written aside and renamed: a reader never sees half a plan, and a symlink in the way is replaced, not followed.
4091
4316
  `fs.mkdirSync(dir,{recursive:true,mode:0o700});`,
4092
4317
  `fs.writeFileSync(tmp,p,{mode:0o600});fs.renameSync(tmp,path.join(dir,name))`,
4093
4318
  `}catch{}})`
4094
4319
  ].join("");
4095
4320
  }
4096
- function headlineHookScript() {
4321
+ function appendTaskEventScript(plansDir, eventExpr) {
4322
+ return [
4323
+ `let d="";`,
4324
+ `process.stdin.on("data",c=>d+=c).on("end",()=>{try{`,
4325
+ `const j=JSON.parse(d),e=${eventExpr};if(!e)return;`,
4326
+ `const fs=require("fs"),path=require("path"),dir=${JSON.stringify(plansDir)},c=fs.constants;`,
4327
+ `fs.mkdirSync(dir,{recursive:true,mode:0o700});`,
4328
+ // O_NOFOLLOW so a symlink in the way is refused rather than written through.
4329
+ `const fd=fs.openSync(path.join(dir,path.basename(process.env.${SESSION_ENV_VAR})+${JSON.stringify(TASKS_FILE_SUFFIX)}),c.O_WRONLY|c.O_APPEND|c.O_CREAT|c.O_NOFOLLOW,0o600);`,
4330
+ `try{fs.writeSync(fd,JSON.stringify(e)+"\\n")}finally{fs.closeSync(fd)}`,
4331
+ `}catch{}})`
4332
+ ].join("");
4333
+ }
4334
+ var PR_CREATE_MENTION = "gh pr create";
4335
+ var PR_CREATE_COMMAND = /(?:^|[;&|\n(])\s*(?:\w+=\S*\s+)*gh\s+pr\s+create\b/;
4336
+ function recordPullRequestScript(commandExpr, outputExpr) {
4097
4337
  return [
4098
4338
  `let d="";`,
4099
4339
  `process.stdin.on("data",c=>d+=c).on("end",()=>{try{`,
4100
- `const p=JSON.parse(d).prompt;if(typeof p!=="string")return;`,
4101
- `const t=p.replace(/\\s+/g," ").trim();`,
4102
- `if(t.length<${MIN_HEADLINE_CHARS}||${REPLY_ONLY_PROMPT.toString()}.test(t))return;`,
4103
- `const h=t.length>${MAX_HEADLINE_CHARS}?t.slice(0,${MAX_HEADLINE_CHARS - 1})+"\\u2026":t;`,
4104
- // execFileSync takes the text as an argument, so the prompt is never parsed by a shell.
4105
- `require("child_process").execFileSync("tmux",["set-option","-t",process.env.TMUX_PANE,"-q","${HEADLINE_OPTION}",h])`,
4340
+ `const j=JSON.parse(d),c=${commandExpr},o=${outputExpr};`,
4341
+ `if(typeof c!=="string"||typeof o!=="string"||!${PR_CREATE_COMMAND.toString()}.test(c))return;`,
4342
+ `const m=o.match(${PULL_REQUEST_URL.toString()});if(!m)return;const u=m[m.length-1];`,
4343
+ `const cp=require("child_process"),p=process.env.TMUX_PANE;`,
4344
+ `const cur=cp.execFileSync("tmux",["show-options","-qv","-t",p,"${PULL_REQUESTS_OPTION}"],{encoding:"utf8"}).split(/\\s+/).filter(Boolean);`,
4345
+ `if(cur.includes(u))return;`,
4346
+ `cp.execFileSync("tmux",["set-option","-t",p,"-q","${PULL_REQUESTS_OPTION}",cur.concat(u).slice(-${MAX_SESSION_PULL_REQUESTS}).join(" ")])`,
4106
4347
  `}catch{}})`
4107
4348
  ].join("");
4108
4349
  }
4109
4350
 
4110
- // src/sessions/agents/claude-settings.ts
4351
+ // src/sessions/agent-reports/hook-commands.ts
4111
4352
  function shellQuote3(value) {
4112
4353
  return `'${value.replaceAll("'", `'\\''`)}'`;
4113
4354
  }
4114
4355
  function nodeHookCommand(script, nodePath) {
4115
4356
  return `[ -n "$${SESSION_ENV_VAR}" ] && ${shellQuote3(nodePath)} -e ${shellQuote3(script)}; exit 0`;
4116
4357
  }
4358
+ function recordPullRequestCommand(script, nodePath) {
4359
+ const run = `${shellQuote3(nodePath)} -e ${shellQuote3(script)}`;
4360
+ return `[ -n "$${SESSION_ENV_VAR}" ] && { input=$(cat); case "$input" in *"${PR_CREATE_MENTION}"*) printf '%s' "$input" | ${run};; esac; }; exit 0`;
4361
+ }
4362
+ function stateHookCommand(state) {
4363
+ return `[ -n "$${SESSION_ENV_VAR}" ] && tmux set-option -t "$TMUX_PANE" -q ${AGENT_STATE_OPTION} "${state}:$(date +%s)"; exit 0`;
4364
+ }
4365
+
4366
+ // src/sessions/agents/claude/scripts.ts
4367
+ var PLAN = `j.tool_response?.plan??j.tool_input?.plan`;
4368
+ var TASK_EVENT_BY_TOOL = {
4369
+ TaskCreate: `{op:"add",id:j.tool_response?.task?.id,subject:i.subject,activeForm:i.activeForm}`,
4370
+ TaskUpdate: `i.status==="deleted"?{op:"remove",id:i.taskId}:{op:"update",id:i.taskId,subject:i.subject,status:i.status,activeForm:i.activeForm}`,
4371
+ TodoWrite: `Array.isArray(i.todos)?{op:"replace",tasks:i.todos.map(t=>({subject:t?.content,status:t?.status,activeForm:t?.activeForm}))}:undefined`
4372
+ };
4373
+ var TASK_TOOLS = Object.keys(TASK_EVENT_BY_TOOL);
4374
+ var TASK_EVENT = `(()=>{const i=j.tool_input||{};switch(j.tool_name){${Object.entries(
4375
+ TASK_EVENT_BY_TOOL
4376
+ ).map(([tool, event]) => `case${JSON.stringify(tool)}:return ${event};`).join("")}}})()`;
4377
+ var planHookScript = (plansDir) => storePlanScript(plansDir, PLAN);
4378
+ var taskHookScript = (plansDir) => appendTaskEventScript(plansDir, TASK_EVENT);
4379
+ var pullRequestHookScript = () => recordPullRequestScript(`j.tool_input?.command`, `j.tool_response?.stdout`);
4380
+
4381
+ // src/sessions/agents/claude/settings.ts
4117
4382
  function planHookCommand(nodePath = process.execPath, plansDir = HOOK_PLANS_DIR) {
4118
4383
  return nodeHookCommand(planHookScript(plansDir), nodePath);
4119
4384
  }
4120
- function headlineHookCommand(nodePath = process.execPath) {
4121
- return nodeHookCommand(headlineHookScript(), nodePath);
4385
+ function taskHookCommand(nodePath = process.execPath, plansDir = HOOK_PLANS_DIR) {
4386
+ return nodeHookCommand(taskHookScript(plansDir), nodePath);
4122
4387
  }
4123
- function stateHookCommand(state) {
4124
- return `[ -n "$${SESSION_ENV_VAR}" ] && tmux set-option -t "$TMUX_PANE" -q ${AGENT_STATE_OPTION} "${state}:$(date +%s)"; exit 0`;
4388
+ function pullRequestHookCommand(nodePath = process.execPath) {
4389
+ return recordPullRequestCommand(pullRequestHookScript(), nodePath);
4125
4390
  }
4126
4391
  var STATE_HOOK_TIMEOUT_SECONDS = 5;
4127
4392
  var PLAN_HOOK_TIMEOUT_SECONDS = 10;
4128
- var HEADLINE_HOOK_TIMEOUT_SECONDS = 5;
4393
+ var TASK_HOOK_TIMEOUT_SECONDS = 10;
4394
+ var PULL_REQUEST_HOOK_TIMEOUT_SECONDS = 5;
4129
4395
  function claudeSettings() {
4130
4396
  const run = (command, timeout) => ({ type: "command", command, timeout });
4131
4397
  const report = (state) => run(stateHookCommand(state), STATE_HOOK_TIMEOUT_SECONDS);
@@ -4135,35 +4401,44 @@ function claudeSettings() {
4135
4401
  {
4136
4402
  matcher: "ExitPlanMode",
4137
4403
  hooks: [run(planHookCommand(), PLAN_HOOK_TIMEOUT_SECONDS)]
4138
- }
4139
- ],
4140
- // Working from the moment a prompt is sent, which is also the task the session is on; waiting whenever the user is next to act.
4141
- UserPromptSubmit: [
4404
+ },
4405
+ {
4406
+ matcher: TASK_TOOLS.join("|"),
4407
+ hooks: [run(taskHookCommand(), TASK_HOOK_TIMEOUT_SECONDS)]
4408
+ },
4142
4409
  {
4143
- hooks: [report("working"), run(headlineHookCommand(), HEADLINE_HOOK_TIMEOUT_SECONDS)]
4410
+ matcher: "Bash",
4411
+ hooks: [run(pullRequestHookCommand(), PULL_REQUEST_HOOK_TIMEOUT_SECONDS)]
4144
4412
  }
4145
4413
  ],
4146
- PermissionRequest: [{ matcher: "*", hooks: [report("waiting")] }],
4147
- Stop: [{ hooks: [report("waiting")] }],
4414
+ // Working from the moment a prompt is sent; waiting when it needs the user; done when a turn ends.
4415
+ UserPromptSubmit: [{ hooks: [report("working")] }],
4416
+ // The plan is stored while it waits for approval, so it shows before the user decides.
4417
+ PermissionRequest: [
4418
+ { matcher: "*", hooks: [report("waiting")] },
4419
+ { matcher: "ExitPlanMode", hooks: [run(planHookCommand(), PLAN_HOOK_TIMEOUT_SECONDS)] }
4420
+ ],
4421
+ Stop: [{ hooks: [report("done")] }],
4148
4422
  StopFailure: [{ hooks: [report("waiting")] }]
4149
4423
  }
4150
4424
  };
4151
4425
  }
4152
4426
 
4153
- // src/sessions/agents/claude-hooks.ts
4427
+ // src/sessions/agents/claude/hooks.ts
4154
4428
  var execFileAsync = promisify4(execFile4);
4155
- var CLAUDE_HOOKS_DIR = join9(homedir6(), ".bondsmith", "hooks");
4429
+ var CLAUDE_HOOKS_DIR = join10(homedir6(), ".bondsmith", "hooks");
4156
4430
  var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
4157
4431
  var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
4158
4432
  var CLAUDE_BINARY = "claude";
4159
- var LEADING_CLAUDE = new RegExp(`^(\\s*)${CLAUDE_BINARY}(?=\\s|$)`);
4433
+ var LEADING_CLAUDE = new RegExp(
4434
+ `^(\\s*(?:${ENV_ASSIGNMENT_WORD}\\s+)*)${CLAUDE_BINARY}(?=\\s|$)`
4435
+ );
4160
4436
  var SETTINGS_FLAG = "--settings";
4161
4437
  var SUPPORT_PROBE_TIMEOUT_MS = 1e4;
4162
4438
  var SUPPORT_RETRY_AFTER_MS = 6e4;
4163
4439
  function isPlainClaudeCommand(command) {
4164
- const words = command.trim().split(/\s+/);
4165
- if (words[0] !== CLAUDE_BINARY) return false;
4166
- return !words.some((word) => word === SETTINGS_FLAG || word.startsWith(`${SETTINGS_FLAG}=`));
4440
+ if (leadingBinary(command) !== CLAUDE_BINARY) return false;
4441
+ return !command.trim().split(/\s+/).some((word) => word === SETTINGS_FLAG || word.startsWith(`${SETTINGS_FLAG}=`));
4167
4442
  }
4168
4443
  async function probeSettingsSupport() {
4169
4444
  try {
@@ -4204,14 +4479,14 @@ async function prepareAgentCommand(command, {
4204
4479
  if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
4205
4480
  if (!isPlainClaudeCommand(command)) return command;
4206
4481
  if (!await supportsSettings()) return command;
4207
- const file = join9(hooksDir, "claude-settings.json");
4482
+ const file = join10(hooksDir, "claude-settings.json");
4208
4483
  const staging = `${file}.${randomUUID2()}.tmp`;
4209
4484
  try {
4210
4485
  await mkdir4(hooksDir, { recursive: true });
4211
4486
  await writeFile4(staging, JSON.stringify(claudeSettings(), null, " "));
4212
4487
  await rename4(staging, file);
4213
4488
  } catch (error2) {
4214
- await rm4(staging, { force: true }).catch(() => void 0);
4489
+ await rm5(staging, { force: true }).catch(() => void 0);
4215
4490
  console.error(
4216
4491
  "could not write the Claude Code hook settings; starting claude without them",
4217
4492
  error2
@@ -4224,6 +4499,29 @@ async function prepareAgentCommand(command, {
4224
4499
  );
4225
4500
  }
4226
4501
 
4502
+ // src/sessions/agents/claude/adapter.ts
4503
+ var claudeAdapter = {
4504
+ id: "claude",
4505
+ matches: (command) => leadingBinary(command) === "claude",
4506
+ prepareCommand: (command) => prepareAgentCommand(command)
4507
+ };
4508
+
4509
+ // src/sessions/agents/registry.ts
4510
+ var ADAPTERS = {
4511
+ claude: claudeAdapter,
4512
+ codex: plainAdapter("codex"),
4513
+ opencode: plainAdapter("opencode")
4514
+ };
4515
+ function adapterFor(command) {
4516
+ return Object.values(ADAPTERS).find((adapter) => adapter.matches(command));
4517
+ }
4518
+ function agentIdFor(command) {
4519
+ return adapterFor(command)?.id ?? null;
4520
+ }
4521
+ async function prepareCommand(command) {
4522
+ return await adapterFor(command)?.prepareCommand(command) ?? command;
4523
+ }
4524
+
4227
4525
  // src/sessions/git/start-point.ts
4228
4526
  var WorktreeBaseBranchNotFoundError = class extends Error {
4229
4527
  constructor(branch) {
@@ -4247,15 +4545,26 @@ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
4247
4545
  throw new WorktreeBaseBranchNotFoundError(baseBranch);
4248
4546
  }
4249
4547
 
4548
+ // src/sessions/worktree-root.ts
4549
+ import { lstat as lstat3, realpath as realpath2 } from "fs/promises";
4550
+ import { homedir as homedir7 } from "os";
4551
+ import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute3, join as join11, resolve as resolve2 } from "path";
4552
+ function worktreeRoot() {
4553
+ return join11(homedir7(), ".bondsmith", "worktrees");
4554
+ }
4555
+ function worktreePathFor(sessionId) {
4556
+ return join11(worktreeRoot(), sessionId);
4557
+ }
4558
+
4250
4559
  // src/sessions/worktree-setup/copy-files.ts
4251
4560
  import { constants as constants4 } from "fs";
4252
- import { copyFile, glob, lstat as lstat3, mkdir as mkdir5, realpath as realpath2 } from "fs/promises";
4253
- import { dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative, sep as sep2 } from "path";
4561
+ import { copyFile, glob, lstat as lstat4, mkdir as mkdir5, realpath as realpath3 } from "fs/promises";
4562
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join12, relative, sep as sep2 } from "path";
4254
4563
  var GIT_EXIT_NONE_IGNORED = 1;
4255
4564
  var TRACKED_CHECK_BATCH = 200;
4256
4565
  function isInside(root, path) {
4257
4566
  const rel = relative(root, path);
4258
- return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
4567
+ return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute4(rel);
4259
4568
  }
4260
4569
  async function ignoredPaths(worktree, paths) {
4261
4570
  if (paths.length === 0) return /* @__PURE__ */ new Set();
@@ -4284,13 +4593,13 @@ async function trackedPaths(worktree, paths) {
4284
4593
  }
4285
4594
  return tracked;
4286
4595
  }
4287
- async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4596
+ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot2, globs) {
4288
4597
  const result = { copied: [], failed: 0, skipped: [] };
4289
4598
  const patterns = globs.filter(isSafeCopyGlob);
4290
4599
  if (patterns.length === 0) return result;
4291
4600
  try {
4292
- const source = await realpath2(sourceRoot);
4293
- const worktree = await realpath2(worktreeRoot);
4601
+ const source = await realpath3(sourceRoot);
4602
+ const worktree = await realpath3(worktreeRoot2);
4294
4603
  const candidates = [];
4295
4604
  for await (const match of glob(patterns, { cwd: source })) {
4296
4605
  if (match.split(sep2).includes(".git")) continue;
@@ -4333,11 +4642,11 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot, globs) {
4333
4642
  return result;
4334
4643
  }
4335
4644
  async function isCopyCandidate(source, worktree, match) {
4336
- const from = join10(source, match);
4337
- if (!(await lstat3(from)).isFile()) return false;
4338
- if (!isInside(source, await realpath2(from))) return false;
4645
+ const from = join12(source, match);
4646
+ if (!(await lstat4(from)).isFile()) return false;
4647
+ if (!isInside(source, await realpath3(from))) return false;
4339
4648
  try {
4340
- await lstat3(join10(worktree, match));
4649
+ await lstat4(join12(worktree, match));
4341
4650
  return false;
4342
4651
  } catch (error2) {
4343
4652
  if (error2.code === "ENOENT") return true;
@@ -4345,11 +4654,11 @@ async function isCopyCandidate(source, worktree, match) {
4345
4654
  }
4346
4655
  }
4347
4656
  async function copyOne(source, worktree, match) {
4348
- const to = join10(worktree, match);
4349
- await mkdir5(dirname4(to), { recursive: true });
4350
- if (!isInside(worktree, await realpath2(dirname4(to)))) return false;
4657
+ const to = join12(worktree, match);
4658
+ await mkdir5(dirname5(to), { recursive: true });
4659
+ if (!isInside(worktree, await realpath3(dirname5(to)))) return false;
4351
4660
  try {
4352
- await copyFile(join10(source, match), to, constants4.COPYFILE_EXCL);
4661
+ await copyFile(join12(source, match), to, constants4.COPYFILE_EXCL);
4353
4662
  } catch (error2) {
4354
4663
  if (error2.code === "EEXIST") return false;
4355
4664
  throw error2;
@@ -4364,9 +4673,6 @@ var InvalidCwdError = class extends Error {
4364
4673
  this.name = "InvalidCwdError";
4365
4674
  }
4366
4675
  };
4367
- function worktreePathFor(sessionId) {
4368
- return join11(homedir7(), ".bondsmith", "worktrees", sessionId);
4369
- }
4370
4676
  async function createTmuxSession(input) {
4371
4677
  const sourceCwd = expandHome(input.cwd);
4372
4678
  let cwd = sourceCwd;
@@ -4397,7 +4703,7 @@ async function createTmuxSession(input) {
4397
4703
  const tmuxSessionName = tmuxSessionNameFor(input.id);
4398
4704
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
4399
4705
  const baseline = await captureBaseline(cwd, { freshCheckout: worktreeOf !== null });
4400
- const agentCommand = await prepareAgentCommand(input.command);
4706
+ const agentCommand = await prepareCommand(input.command);
4401
4707
  const setupCommand = worktreeOf ? input.worktreeSetup?.setupCommand?.trim() : void 0;
4402
4708
  const launchCommand = setupCommand ? wrapCommandWithSetup({ setupCommand, command: agentCommand, tmuxSessionName }) : agentCommand;
4403
4709
  await runTmux(
@@ -4446,6 +4752,7 @@ async function createTmuxSession(input) {
4446
4752
  id: input.id,
4447
4753
  title: input.title,
4448
4754
  command: input.command,
4755
+ agent: agentIdFor(input.command),
4449
4756
  cwd,
4450
4757
  tmuxSessionName,
4451
4758
  status: "running",
@@ -4482,6 +4789,7 @@ function parseTmuxSessions(output) {
4482
4789
  id,
4483
4790
  title,
4484
4791
  command,
4792
+ agent: agentIdFor(command),
4485
4793
  cwd,
4486
4794
  tmuxSessionName: sessionName,
4487
4795
  status: "running",
@@ -4565,6 +4873,9 @@ var SessionManager = class {
4565
4873
  for (const [id, baseline] of discovered.baselines) {
4566
4874
  this.baselines.set(id, baseline);
4567
4875
  }
4876
+ sweepOrphanedHookPlanFiles(new Set(this.sessions.keys())).catch(
4877
+ (error2) => console.error("Failed to sweep stored plans of ended sessions", error2)
4878
+ );
4568
4879
  }
4569
4880
  list() {
4570
4881
  return Array.from(this.sessions.values());
@@ -4660,7 +4971,7 @@ ${error2.message}
4660
4971
  agentState: resolveSessionAttention(report, enriched.lastActivityAt, {
4661
4972
  acknowledgedAtMs: this.acknowledgements.get(session.id) ?? null
4662
4973
  }),
4663
- headline: report?.headline ?? null,
4974
+ ...report?.pullRequestUrls?.length && { pullRequestUrls: report.pullRequestUrls },
4664
4975
  ...report?.setupFailed && { worktreeSetupFailed: true },
4665
4976
  ...report?.setupSkipped?.length && { worktreeSetupSkipped: report.setupSkipped }
4666
4977
  };
@@ -4682,12 +4993,15 @@ ${error2.message}
4682
4993
  this.sessions.delete(session.id);
4683
4994
  this.baselines.delete(session.id);
4684
4995
  this.acknowledgements.delete(session.id);
4996
+ removeHookPlanFiles(session.id).catch(
4997
+ (error2) => console.error("Failed to remove the session's stored plan", { sessionId: session.id, error: error2 })
4998
+ );
4685
4999
  this.emitUpdated(updated);
4686
5000
  return updated;
4687
5001
  }
4688
5002
  };
4689
5003
  function snapshotEquals(a, b) {
4690
- return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && (a.headline ?? null) === (b.headline ?? null) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false) && JSON.stringify(a.worktreeSetupSkipped ?? []) === JSON.stringify(b.worktreeSetupSkipped ?? []);
5004
+ return a.branch === b.branch && a.repositoryFullName === b.repositoryFullName && a.changedFileCount === b.changedFileCount && a.lastActivityAt === b.lastActivityAt && (a.agentState ?? null) === (b.agentState ?? null) && JSON.stringify(a.pullRequestUrls ?? []) === JSON.stringify(b.pullRequestUrls ?? []) && (a.worktreeSetupFailed ?? false) === (b.worktreeSetupFailed ?? false) && JSON.stringify(a.worktreeSetupSkipped ?? []) === JSON.stringify(b.worktreeSetupSkipped ?? []);
4691
5005
  }
4692
5006
 
4693
5007
  // src/ws/server.ts
@@ -4854,10 +5168,10 @@ if (isVersionRequest(process.argv.slice(2))) {
4854
5168
  process.exit(0);
4855
5169
  }
4856
5170
  function loadRepoRootEnv() {
4857
- const repoRoot = join12(dirname5(fileURLToPath(import.meta.url)), "..", "..", "..");
4858
- if (!existsSync(join12(repoRoot, "pnpm-workspace.yaml"))) return;
4859
- const envLocal = join12(repoRoot, ".env.local");
4860
- const envDefault = join12(repoRoot, ".env");
5171
+ const repoRoot = join13(dirname6(fileURLToPath(import.meta.url)), "..", "..", "..");
5172
+ if (!existsSync(join13(repoRoot, "pnpm-workspace.yaml"))) return;
5173
+ const envLocal = join13(repoRoot, ".env.local");
5174
+ const envDefault = join13(repoRoot, ".env");
4861
5175
  loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
4862
5176
  }
4863
5177
  loadRepoRootEnv();