@factiii/runner 0.11.3 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -963,7 +963,7 @@ var require_command = __commonJS({
963
963
  "node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path21 = require("node:path");
966
+ var path23 = require("node:path");
967
967
  var fs15 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -1896,9 +1896,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path21.resolve(baseDir, baseName);
1899
+ const localBin = path23.resolve(baseDir, baseName);
1900
1900
  if (fs15.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path21.extname(baseName))) return void 0;
1901
+ if (sourceExt.includes(path23.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
1903
  (ext) => fs15.existsSync(`${localBin}${ext}`)
1904
1904
  );
@@ -1916,17 +1916,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path21.resolve(
1920
- path21.dirname(resolvedScriptPath),
1919
+ executableDir = path23.resolve(
1920
+ path23.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path21.basename(
1927
+ const legacyName = path23.basename(
1928
1928
  this._scriptPath,
1929
- path21.extname(this._scriptPath)
1929
+ path23.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path21.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path23.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process2.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path21.basename(filename, path21.extname(filename));
2780
+ this._name = path23.basename(filename, path23.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path22) {
2795
- if (path22 === void 0) return this._executableDir;
2796
- this._executableDir = path22;
2794
+ executableDir(path24) {
2795
+ if (path24 === void 0) return this._executableDir;
2796
+ this._executableDir = path24;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -6499,7 +6499,7 @@ var require_websocket = __commonJS({
6499
6499
  var http = require("http");
6500
6500
  var net3 = require("net");
6501
6501
  var tls = require("tls");
6502
- var { randomBytes, createHash: createHash2 } = require("crypto");
6502
+ var { randomBytes: randomBytes2, createHash: createHash2 } = require("crypto");
6503
6503
  var { Duplex, Readable } = require("stream");
6504
6504
  var { URL: URL2 } = require("url");
6505
6505
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -7037,7 +7037,7 @@ var require_websocket = __commonJS({
7037
7037
  }
7038
7038
  }
7039
7039
  const defaultPort = isSecure ? 443 : 80;
7040
- const key = randomBytes(16).toString("base64");
7040
+ const key = randomBytes2(16).toString("base64");
7041
7041
  const request = isSecure ? https.request : http.request;
7042
7042
  const protocolSet = /* @__PURE__ */ new Set();
7043
7043
  let perMessageDeflate;
@@ -8428,8 +8428,8 @@ function getErrorMap() {
8428
8428
 
8429
8429
  // ../../node_modules/zod/v3/helpers/parseUtil.js
8430
8430
  var makeIssue = (params) => {
8431
- const { data, path: path21, errorMaps, issueData } = params;
8432
- const fullPath = [...path21, ...issueData.path || []];
8431
+ const { data, path: path23, errorMaps, issueData } = params;
8432
+ const fullPath = [...path23, ...issueData.path || []];
8433
8433
  const fullIssue = {
8434
8434
  ...issueData,
8435
8435
  path: fullPath
@@ -8545,11 +8545,11 @@ var errorUtil;
8545
8545
 
8546
8546
  // ../../node_modules/zod/v3/types.js
8547
8547
  var ParseInputLazyPath = class {
8548
- constructor(parent, value2, path21, key) {
8548
+ constructor(parent, value2, path23, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path21;
8552
+ this._path = path23;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12910,7 +12910,7 @@ ${diff.slice(0, 8e3)}`,
12910
12910
  }
12911
12911
 
12912
12912
  // ../../shared/all/helpers/board-agent-core/engine.ts
12913
- var import_path13 = require("path");
12913
+ var import_path15 = require("path");
12914
12914
 
12915
12915
  // ../../shared/all/domains/electron.ts
12916
12916
  var SETUP_LOG_PREFIX = "log ";
@@ -13019,7 +13019,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13019
13019
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13020
13020
  }
13021
13021
  async function addWorktree(target, workName, baseBranch, kind) {
13022
- const path21 = worktreeFor(target.paths, workName);
13022
+ const path23 = worktreeFor(target.paths, workName);
13023
13023
  const branch = workBranch(workName, kind);
13024
13024
  await gitInDir(
13025
13025
  target,
@@ -13029,7 +13029,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13029
13029
  baseBranch
13030
13030
  ).catch(() => {
13031
13031
  });
13032
- await removeWorktree(target, path21).catch(() => {
13032
+ await removeWorktree(target, path23).catch(() => {
13033
13033
  });
13034
13034
  await gitInDir(
13035
13035
  target,
@@ -13038,30 +13038,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13038
13038
  "add",
13039
13039
  "-B",
13040
13040
  branch,
13041
- path21,
13041
+ path23,
13042
13042
  `origin/${baseBranch}`
13043
13043
  );
13044
- return { path: path21, branch };
13044
+ return { path: path23, branch };
13045
13045
  }
13046
- async function removeWorktree(target, path21) {
13046
+ async function removeWorktree(target, path23) {
13047
13047
  await gitInDir(
13048
13048
  target,
13049
13049
  target.paths.repo,
13050
13050
  "worktree",
13051
13051
  "remove",
13052
13052
  "--force",
13053
- path21
13053
+ path23
13054
13054
  );
13055
13055
  }
13056
- async function removeWorktreeAndBranch(target, path21) {
13056
+ async function removeWorktreeAndBranch(target, path23) {
13057
13057
  const branch = await gitInDir(
13058
13058
  target,
13059
- path21,
13059
+ path23,
13060
13060
  "rev-parse",
13061
13061
  "--abbrev-ref",
13062
13062
  "HEAD"
13063
13063
  ).then((out) => out.trim()).catch(() => "");
13064
- await removeWorktree(target, path21);
13064
+ await removeWorktree(target, path23);
13065
13065
  if (branch && branch !== "HEAD") {
13066
13066
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13067
13067
  () => {
@@ -13075,8 +13075,8 @@ async function pruneWorktrees(target) {
13075
13075
  }
13076
13076
  );
13077
13077
  }
13078
- function isWorktreePath(target, path21) {
13079
- return path21.startsWith(`${target.paths.worktrees}/`);
13078
+ function isWorktreePath(target, path23) {
13079
+ return path23.startsWith(`${target.paths.worktrees}/`);
13080
13080
  }
13081
13081
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13082
13082
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13102,13 +13102,13 @@ function parseStatus(raw) {
13102
13102
  const y = line[1];
13103
13103
  const rest = line.slice(3);
13104
13104
  const arrow = rest.indexOf(" -> ");
13105
- const path21 = arrow === -1 ? rest : rest.slice(arrow + 4);
13105
+ const path23 = arrow === -1 ? rest : rest.slice(arrow + 4);
13106
13106
  if (x === "?" || y === "?") {
13107
- unstaged.push({ path: path21, code: "A" });
13107
+ unstaged.push({ path: path23, code: "A" });
13108
13108
  continue;
13109
13109
  }
13110
- if (x !== " ") staged.push({ path: path21, code: x });
13111
- if (y !== " ") unstaged.push({ path: path21, code: y });
13110
+ if (x !== " ") staged.push({ path: path23, code: x });
13111
+ if (y !== " ") unstaged.push({ path: path23, code: y });
13112
13112
  }
13113
13113
  return { staged, unstaged };
13114
13114
  }
@@ -13337,6 +13337,13 @@ async function ensureRedis(target) {
13337
13337
  }
13338
13338
  throw new Error("redis-server did not come up.");
13339
13339
  }
13340
+ var TERMINAL_ACK_KEY = "terminal:ack";
13341
+ async function getTerminalAck(target) {
13342
+ return await redisCli(target, `GET ${TERMINAL_ACK_KEY}`).catch(() => "");
13343
+ }
13344
+ async function setTerminalAck(target, digest) {
13345
+ await redisCliStdin(target, ["SET", TERMINAL_ACK_KEY], digest);
13346
+ }
13340
13347
  async function putCard(target, record) {
13341
13348
  const id = safeId(record.postId);
13342
13349
  await redisCliStdin(target, ["SET", cardKey(id)], JSON.stringify(record));
@@ -13372,49 +13379,10 @@ async function deleteCard(target, postId) {
13372
13379
  await redisCli(target, `SREM ${INDEX_KEY} ${id}`);
13373
13380
  }
13374
13381
 
13375
- // ../../shared/all/helpers/card-ai/card-session.ts
13376
- function isWorktreeSession(s, target) {
13377
- return isWorktreePath(target, s.workspacePath);
13378
- }
13379
- function sessionToRecord(s, prompt2 = "") {
13380
- return {
13381
- postId: s.postId,
13382
- taskId: s.taskId,
13383
- mode: s.mode,
13384
- phase: s.phase,
13385
- provider: s.provider,
13386
- worktreePath: s.workspacePath,
13387
- agentSessionId: s.agentSessionId,
13388
- taskTitle: s.taskTitle,
13389
- targetBranch: s.targetBranch,
13390
- startedAt: s.startedAt,
13391
- prompt: prompt2,
13392
- pendingQuestion: s.pendingQuestion
13393
- };
13394
- }
13395
- async function putSessionCard(s, target, prompt2 = "") {
13396
- try {
13397
- await putCard(target, sessionToRecord(s, prompt2));
13398
- } catch {
13399
- }
13400
- }
13401
- async function teardownSession(s, target) {
13402
- if (isWorktreeSession(s, target)) {
13403
- const drop = s.mode === "bare" ? removeWorktreeAndBranch : removeWorktree;
13404
- await drop(target, s.workspacePath).catch(() => {
13405
- });
13406
- }
13407
- await deleteCard(target, s.postId).catch(() => {
13408
- });
13409
- }
13410
-
13411
- // ../../shared/all/helpers/board-agent-core/exec.ts
13412
- var import_child_process2 = require("child_process");
13382
+ // ../../shared/all/helpers/board-agent-core/tmux.ts
13413
13383
  var import_fs = require("fs");
13414
- var import_promises = __toESM(require("fs/promises"));
13415
- var import_net = __toESM(require("net"));
13384
+ var import_promises = require("fs/promises");
13416
13385
  var import_path3 = __toESM(require("path"));
13417
- var import_util4 = require("util");
13418
13386
 
13419
13387
  // ../../shared/all/helpers/board-agent-core/host-env.ts
13420
13388
  var import_child_process = require("child_process");
@@ -13541,13 +13509,314 @@ async function hostEnv() {
13541
13509
  return { ...process.env, PATH: await freshPath() };
13542
13510
  }
13543
13511
 
13512
+ // ../../shared/all/helpers/board-agent-core/tmux.ts
13513
+ var TMUX_CONF = [
13514
+ // Wheel scrolls, drag selects in copy mode.
13515
+ "set -g mouse on",
13516
+ "set -g history-limit 50000",
13517
+ // These three are copy-on-select: tmux emits OSC 52, the client clips it.
13518
+ "set -g set-clipboard on",
13519
+ "set -ga terminal-features ,*:clipboard",
13520
+ "bind -T copy-mode MouseDragEnd1Pane send -X copy-selection-and-cancel",
13521
+ "bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel",
13522
+ // A pane whose process exits stays as a dead pane instead of taking the
13523
+ // session — and the tab — with it. `respawnTmuxPane` brings it back. Without
13524
+ // this a stray `exit` or Ctrl-D destroys the tab the user was working in.
13525
+ "set -g remain-on-exit on",
13526
+ ""
13527
+ ].join("\n");
13528
+ var DEFAULT_SIZE = { cols: 80, rows: 24 };
13529
+ var SOCKET = "factiii";
13530
+ function safeTmuxName(raw) {
13531
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
13532
+ }
13533
+ function tmuxConfPath(target) {
13534
+ return `${target.paths.root}/tmux.conf`;
13535
+ }
13536
+ function tmuxCli(target) {
13537
+ return `tmux -L ${SOCKET} -f ${tmuxConfPath(target)}`;
13538
+ }
13539
+ function shellQuote(argv) {
13540
+ return argv.map((arg) => `'${arg.replace(/'/g, `'\\''`)}'`).join(" ");
13541
+ }
13542
+ function ptyArgv(argv, size) {
13543
+ const sized = [
13544
+ "sh",
13545
+ "-c",
13546
+ `stty rows ${size.rows} cols ${size.cols} 2>/dev/null; exec ${shellQuote(argv)}`
13547
+ ];
13548
+ const script = process.platform === "darwin" ? ["script", "-q", "/dev/null", ...sized] : ["script", "-qec", shellQuote(sized), "/dev/null"];
13549
+ return ["bash", "-c", 'exec "$@" < <(cat)', "bash", ...script];
13550
+ }
13551
+ async function respawnTmuxPane(opts) {
13552
+ const { target, session, argv } = opts;
13553
+ const cmd = ["respawn-pane", "-k"];
13554
+ if (opts.cwd) cmd.push("-c", opts.cwd);
13555
+ for (const [k, v] of Object.entries(opts.env ?? {})) {
13556
+ cmd.push("-e", `${k}=${v}`);
13557
+ }
13558
+ cmd.push("-t", session, "--", ...argv);
13559
+ await target.sh(`${tmuxCli(target)} ${shellQuote(cmd)}`);
13560
+ }
13561
+ async function tmuxSessionExists(target, session) {
13562
+ return await target.sh(
13563
+ `${tmuxCli(target)} has-session -t ${shellQuote([session])} 2>/dev/null`
13564
+ ).then(() => true).catch(() => false);
13565
+ }
13566
+ async function setTmuxSessionOption(target, session, name, value2) {
13567
+ await target.sh(
13568
+ `${tmuxCli(target)} set-option -t ${shellQuote([session])} ${shellQuote([name, value2])}`
13569
+ ).catch(() => {
13570
+ });
13571
+ }
13572
+ async function sourceTmuxConf(target) {
13573
+ const conf = tmuxConfPath(target);
13574
+ await target.writeFile(conf, TMUX_CONF);
13575
+ await target.sh(`${tmuxCli(target)} source-file ${shellQuote([conf])}`).catch(() => {
13576
+ });
13577
+ }
13578
+ function resurrectSource() {
13579
+ const rel = ["vendor", "tmux-resurrect"];
13580
+ const candidates2 = [
13581
+ import_path3.default.join(__dirname, "..", ...rel),
13582
+ import_path3.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", ...rel),
13583
+ import_path3.default.join(process.cwd(), "apps", "runner", ...rel)
13584
+ ];
13585
+ return candidates2.find(
13586
+ (dir) => (0, import_fs.existsSync)(import_path3.default.join(dir, "scripts", "save.sh"))
13587
+ ) ?? null;
13588
+ }
13589
+ function hostRoot(target) {
13590
+ return import_path3.default.dirname(target.paths.root);
13591
+ }
13592
+ function resurrectRoot(target) {
13593
+ return `${hostRoot(target)}/tmux-plugins/tmux-resurrect`;
13594
+ }
13595
+ function resurrectStateDir(target) {
13596
+ return `${hostRoot(target)}/tmux-resurrect-state`;
13597
+ }
13598
+ async function ensureTmuxResurrect(target) {
13599
+ const source = resurrectSource();
13600
+ if (!source) return;
13601
+ const root = resurrectRoot(target);
13602
+ try {
13603
+ const want = await (0, import_promises.readFile)(import_path3.default.join(source, "VERSION"), "utf-8").then((v) => v.trim()).catch(() => "");
13604
+ const have = await target.sh(`cat ${shellQuote([`${root}/VERSION`])} 2>/dev/null || true`).catch(() => "");
13605
+ if (want && have.trim() === want) return;
13606
+ const files = (await resurrectFiles(source)).filter((f) => f !== "VERSION");
13607
+ for (const rel of files) {
13608
+ await target.writeFile(
13609
+ `${root}/${rel}`,
13610
+ await (0, import_promises.readFile)(import_path3.default.join(source, rel), "utf-8")
13611
+ );
13612
+ }
13613
+ await target.sh(
13614
+ `chmod +x ${shellQuote([`${root}/scripts`])}/*.sh ${shellQuote([`${root}/save_command_strategies`])}/*.sh`
13615
+ );
13616
+ await target.writeFile(`${root}/VERSION`, want);
13617
+ } catch {
13618
+ }
13619
+ }
13620
+ async function resurrectFiles(source) {
13621
+ const out = [];
13622
+ const walk = async (rel) => {
13623
+ const entries = await (0, import_promises.readdir)(import_path3.default.join(source, rel), {
13624
+ withFileTypes: true
13625
+ });
13626
+ for (const entry of entries) {
13627
+ const next = rel ? `${rel}/${entry.name}` : entry.name;
13628
+ if (entry.isDirectory()) await walk(next);
13629
+ else out.push(next);
13630
+ }
13631
+ };
13632
+ await walk("");
13633
+ return out;
13634
+ }
13635
+ async function setResurrectOptions(target) {
13636
+ const opts = [
13637
+ ["@resurrect-processes", "false"],
13638
+ ["@resurrect-capture-pane-contents", "on"],
13639
+ ["@resurrect-dir", resurrectStateDir(target)]
13640
+ ];
13641
+ for (const [name, value2] of opts) {
13642
+ await target.sh(`${tmuxCli(target)} set-option -g ${shellQuote([name, value2])}`).catch(() => {
13643
+ });
13644
+ }
13645
+ }
13646
+ async function tmuxServerRunning(target) {
13647
+ return await target.sh(`${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null`).then(() => true).catch(() => false);
13648
+ }
13649
+ async function resurrectSave(target) {
13650
+ if (!await tmuxServerRunning(target)) return;
13651
+ await setResurrectOptions(target);
13652
+ await target.sh(
13653
+ `${tmuxCli(target)} run-shell ${shellQuote([`${resurrectRoot(target)}/scripts/save.sh`])}`
13654
+ ).catch(() => {
13655
+ });
13656
+ }
13657
+ async function resurrectRestore(target) {
13658
+ const scripts = resurrectRoot(target);
13659
+ const hasSave = await target.sh(`test -f ${shellQuote([`${resurrectStateDir(target)}/last`])}`).then(() => true).catch(() => false);
13660
+ if (!hasSave) return [];
13661
+ const placeholder = "factiii-restore";
13662
+ try {
13663
+ await target.sh(
13664
+ `${tmuxCli(target)} new-session -d -s ${placeholder} 'sleep 60' 2>/dev/null || true`
13665
+ );
13666
+ await setResurrectOptions(target);
13667
+ await target.sh(
13668
+ `${tmuxCli(target)} run-shell ${shellQuote([`${scripts}/scripts/restore.sh`])}`
13669
+ );
13670
+ const stdout = await target.sh(
13671
+ `${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null || true`
13672
+ );
13673
+ return stdout.split("\n").map((line) => line.trim()).filter((name) => name.startsWith("terminal-"));
13674
+ } catch {
13675
+ return [];
13676
+ } finally {
13677
+ await target.sh(
13678
+ `${tmuxCli(target)} kill-session -t ${placeholder} 2>/dev/null || true`
13679
+ ).catch(() => {
13680
+ });
13681
+ }
13682
+ }
13683
+ async function tmuxPanePaths(target) {
13684
+ const stdout = await target.sh(
13685
+ `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_current_path}' 2>/dev/null || true`
13686
+ ).catch(() => "");
13687
+ const byName = /* @__PURE__ */ new Map();
13688
+ for (const raw of stdout.split("\n")) {
13689
+ const [name, dir] = raw.trim().split("|");
13690
+ if (name && dir && !byName.has(name)) byName.set(name, dir);
13691
+ }
13692
+ return byName;
13693
+ }
13694
+ async function terminalSessionNames(target, postId) {
13695
+ const prefix = `terminal-${safeTmuxName(postId)}-`;
13696
+ const stdout = await target.sh(
13697
+ `${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null || true`
13698
+ ).catch(() => "");
13699
+ return stdout.split("\n").map((line) => line.trim()).filter((name) => name.startsWith(prefix));
13700
+ }
13701
+ async function killTerminalSessions(target, postId) {
13702
+ const names = await terminalSessionNames(target, postId);
13703
+ if (names.length === 0) return;
13704
+ const cli = tmuxCli(target);
13705
+ const script = names.map(
13706
+ (name) => (
13707
+ // -P walks children; the pane pid itself dies with the session below.
13708
+ `for p in $(${cli} list-panes -t ${name} -F '#{pane_pid}' 2>/dev/null); do pkill -TERM -P "$p" 2>/dev/null || true; done; ${cli} kill-session -t ${name} 2>/dev/null || true`
13709
+ )
13710
+ ).join("; ");
13711
+ await target.sh(script).catch(() => {
13712
+ });
13713
+ }
13714
+ async function startTmuxSession(opts) {
13715
+ const { target, session } = opts;
13716
+ const conf = tmuxConfPath(target);
13717
+ await target.writeFile(conf, TMUX_CONF);
13718
+ let size = opts.size ?? DEFAULT_SIZE;
13719
+ const dataCbs = [];
13720
+ const closeCbs = [];
13721
+ const errorCbs = [];
13722
+ let proc;
13723
+ const attach = async () => {
13724
+ const argv = ["tmux", "-L", SOCKET, "-u", "-f", conf, "new-session"];
13725
+ if (opts.attachExisting) argv.push("-A");
13726
+ argv.push("-s", session, "-x", String(size.cols), "-y", String(size.rows));
13727
+ for (const [k, v] of Object.entries(opts.env ?? {})) {
13728
+ argv.push("-e", `${k}=${v}`);
13729
+ }
13730
+ argv.push("--", ...opts.argv);
13731
+ const mine = await target.spawn(ptyArgv(argv, size), {
13732
+ env: { TERM: "xterm-256color" }
13733
+ });
13734
+ proc = mine;
13735
+ mine.stdin?.on("error", () => {
13736
+ });
13737
+ const live = () => proc === mine;
13738
+ const emit = (chunk) => {
13739
+ if (live()) for (const cb of dataCbs) cb(chunk);
13740
+ };
13741
+ mine.stdout?.on("data", emit);
13742
+ mine.stderr?.on("data", emit);
13743
+ mine.on("error", (err) => {
13744
+ if (live()) for (const cb of errorCbs) cb(err);
13745
+ });
13746
+ mine.on("close", (code) => {
13747
+ if (live()) for (const cb of closeCbs) cb(code ?? null);
13748
+ });
13749
+ };
13750
+ await attach();
13751
+ return {
13752
+ session,
13753
+ onData: (cb) => dataCbs.push(cb),
13754
+ onClose: (cb) => closeCbs.push(cb),
13755
+ onError: (cb) => errorCbs.push(cb),
13756
+ write: (data) => proc.stdin?.write(data),
13757
+ resize: async (cols, rows) => {
13758
+ if (cols === size.cols && rows === size.rows) return;
13759
+ size = { cols, rows };
13760
+ const old = proc;
13761
+ await attach();
13762
+ killTree(old);
13763
+ },
13764
+ detach: () => killTree(proc)
13765
+ };
13766
+ }
13767
+
13768
+ // ../../shared/all/helpers/card-ai/card-session.ts
13769
+ function isWorktreeSession(s, target) {
13770
+ return isWorktreePath(target, s.workspacePath);
13771
+ }
13772
+ function sessionToRecord(s, prompt2 = "") {
13773
+ return {
13774
+ postId: s.postId,
13775
+ taskId: s.taskId,
13776
+ mode: s.mode,
13777
+ phase: s.phase,
13778
+ provider: s.provider,
13779
+ worktreePath: s.workspacePath,
13780
+ agentSessionId: s.agentSessionId,
13781
+ taskTitle: s.taskTitle,
13782
+ targetBranch: s.targetBranch,
13783
+ startedAt: s.startedAt,
13784
+ prompt: prompt2,
13785
+ pendingQuestion: s.pendingQuestion
13786
+ };
13787
+ }
13788
+ async function putSessionCard(s, target, prompt2 = "") {
13789
+ try {
13790
+ await putCard(target, sessionToRecord(s, prompt2));
13791
+ } catch {
13792
+ }
13793
+ }
13794
+ async function teardownSession(s, target) {
13795
+ await killTerminalSessions(target, s.postId).catch(() => {
13796
+ });
13797
+ if (isWorktreeSession(s, target)) {
13798
+ const drop = s.mode === "bare" ? removeWorktreeAndBranch : removeWorktree;
13799
+ await drop(target, s.workspacePath).catch(() => {
13800
+ });
13801
+ }
13802
+ await deleteCard(target, s.postId).catch(() => {
13803
+ });
13804
+ }
13805
+
13544
13806
  // ../../shared/all/helpers/board-agent-core/exec.ts
13807
+ var import_child_process2 = require("child_process");
13808
+ var import_crypto = require("crypto");
13809
+ var import_fs2 = require("fs");
13810
+ var import_promises2 = __toESM(require("fs/promises"));
13811
+ var import_net = __toESM(require("net"));
13812
+ var import_path4 = __toESM(require("path"));
13813
+ var import_util4 = require("util");
13545
13814
  var execFileAsync = (0, import_util4.promisify)(import_child_process2.execFile);
13546
13815
  var MAX_BUFFER = 10 * 1024 * 1024;
13547
13816
  var PORTS_FILE = ".ports";
13548
13817
  var NUM_PORTS = 6;
13549
13818
  function portsFilePath(spaceDir) {
13550
- return import_path3.default.join(spaceDir, PORTS_FILE);
13819
+ return import_path4.default.join(spaceDir, PORTS_FILE);
13551
13820
  }
13552
13821
  async function allocateSpacePorts(spaceDir) {
13553
13822
  const existing = readSpacePorts(spaceDir);
@@ -13573,19 +13842,35 @@ async function allocateSpacePorts(spaceDir) {
13573
13842
  seen.add(port);
13574
13843
  ports.push(port);
13575
13844
  }
13576
- await import_promises.default.mkdir(spaceDir, { recursive: true });
13577
- await import_promises.default.writeFile(portsFilePath(spaceDir), ports.join(","), "utf-8");
13845
+ await import_promises2.default.mkdir(spaceDir, { recursive: true });
13846
+ await import_promises2.default.writeFile(portsFilePath(spaceDir), ports.join(","), "utf-8");
13578
13847
  return ports;
13579
13848
  }
13849
+ var SECRET_FILE = ".secret";
13850
+ function spaceSecret(spaceDir) {
13851
+ const file = import_path4.default.join(spaceDir, SECRET_FILE);
13852
+ try {
13853
+ const existing = (0, import_fs2.readFileSync)(file, "utf-8").trim();
13854
+ if (existing) return existing;
13855
+ } catch {
13856
+ }
13857
+ const secret = (0, import_crypto.randomBytes)(24).toString("hex");
13858
+ try {
13859
+ (0, import_fs2.mkdirSync)(spaceDir, { recursive: true });
13860
+ (0, import_fs2.writeFileSync)(file, secret, { encoding: "utf-8", mode: 384 });
13861
+ } catch {
13862
+ }
13863
+ return secret;
13864
+ }
13580
13865
  function readSpacePorts(spaceDir) {
13581
13866
  try {
13582
- return (0, import_fs.readFileSync)(portsFilePath(spaceDir), "utf-8").split(",").map((p) => Number(p.trim())).filter((p) => Number.isInteger(p) && p > 0);
13867
+ return (0, import_fs2.readFileSync)(portsFilePath(spaceDir), "utf-8").split(",").map((p) => Number(p.trim())).filter((p) => Number.isInteger(p) && p > 0);
13583
13868
  } catch {
13584
13869
  return [];
13585
13870
  }
13586
13871
  }
13587
13872
  function usableCwd(dir) {
13588
- return dir && (0, import_fs.existsSync)(dir) ? dir : void 0;
13873
+ return dir && (0, import_fs2.existsSync)(dir) ? dir : void 0;
13589
13874
  }
13590
13875
  function hostTarget(spaceDir, extraEnv) {
13591
13876
  const paths = hostSpacePaths(spaceDir);
@@ -13651,8 +13936,8 @@ function hostTarget(spaceDir, extraEnv) {
13651
13936
  });
13652
13937
  },
13653
13938
  async writeFile(filePath, content) {
13654
- await import_promises.default.mkdir(import_path3.default.dirname(filePath), { recursive: true });
13655
- await import_promises.default.writeFile(filePath, content, "utf-8");
13939
+ await import_promises2.default.mkdir(import_path4.default.dirname(filePath), { recursive: true });
13940
+ await import_promises2.default.writeFile(filePath, content, "utf-8");
13656
13941
  },
13657
13942
  async step(cmd, opts) {
13658
13943
  const res = await execLogin(cmd, {
@@ -13867,14 +14152,14 @@ function createEventSink(send, spaceSlug, notify) {
13867
14152
  }
13868
14153
 
13869
14154
  // ../../shared/all/helpers/card-ai/frpc.ts
13870
- var import_fs2 = require("fs");
13871
- var import_promises5 = __toESM(require("fs/promises"));
13872
- var import_path7 = __toESM(require("path"));
14155
+ var import_fs3 = require("fs");
14156
+ var import_promises6 = __toESM(require("fs/promises"));
14157
+ var import_path8 = __toESM(require("path"));
13873
14158
 
13874
14159
  // ../../shared/all/helpers/board-agent-core/host-skills.ts
13875
- var import_promises4 = __toESM(require("fs/promises"));
14160
+ var import_promises5 = __toESM(require("fs/promises"));
13876
14161
  var import_os2 = __toESM(require("os"));
13877
- var import_path6 = __toESM(require("path"));
14162
+ var import_path7 = __toESM(require("path"));
13878
14163
 
13879
14164
  // ../../shared/all/domains/posts.ts
13880
14165
  var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
@@ -14384,17 +14669,17 @@ var skillScopeSchema = external_exports.enum(["global", "repo"]);
14384
14669
 
14385
14670
  // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14386
14671
  var import_child_process3 = require("child_process");
14387
- var import_promises3 = __toESM(require("fs/promises"));
14388
- var import_path5 = __toESM(require("path"));
14672
+ var import_promises4 = __toESM(require("fs/promises"));
14673
+ var import_path6 = __toESM(require("path"));
14389
14674
  var import_util5 = require("util");
14390
14675
 
14391
14676
  // ../../shared/all/helpers/board-agent-core/secrets-bridge.ts
14392
- var import_promises2 = __toESM(require("fs/promises"));
14677
+ var import_promises3 = __toESM(require("fs/promises"));
14393
14678
  var import_net2 = __toESM(require("net"));
14394
- var import_path4 = __toESM(require("path"));
14679
+ var import_path5 = __toESM(require("path"));
14395
14680
  var SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14396
14681
  function secretsSocketPath(spaceDir) {
14397
- return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14682
+ return import_path5.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14398
14683
  }
14399
14684
  function redactor(values) {
14400
14685
  const significant = values.filter((v) => v.length >= 4);
@@ -14448,8 +14733,8 @@ async function handleRun(request, handlers, reply) {
14448
14733
  }
14449
14734
  async function startSecretsBridge(opts) {
14450
14735
  const socketPath = secretsSocketPath(opts.spaceDir);
14451
- await import_promises2.default.mkdir(import_path4.default.dirname(socketPath), { recursive: true });
14452
- await import_promises2.default.rm(socketPath, { force: true });
14736
+ await import_promises3.default.mkdir(import_path5.default.dirname(socketPath), { recursive: true });
14737
+ await import_promises3.default.rm(socketPath, { force: true });
14453
14738
  const { handlers } = opts;
14454
14739
  let queue = Promise.resolve();
14455
14740
  let queueDepth = 0;
@@ -14514,13 +14799,13 @@ async function startSecretsBridge(opts) {
14514
14799
  resolve2();
14515
14800
  });
14516
14801
  });
14517
- await import_promises2.default.chmod(socketPath, 384).catch(() => void 0);
14802
+ await import_promises3.default.chmod(socketPath, 384).catch(() => void 0);
14518
14803
  return {
14519
14804
  close: async () => {
14520
14805
  for (const conn of live) conn.destroy();
14521
14806
  live.clear();
14522
14807
  await new Promise((resolve2) => server.close(() => resolve2()));
14523
- await import_promises2.default.rm(socketPath, { force: true });
14808
+ await import_promises3.default.rm(socketPath, { force: true });
14524
14809
  }
14525
14810
  };
14526
14811
  }
@@ -14537,14 +14822,14 @@ var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
14537
14822
  var CLAUDE_SKILLS_DIR = ".claude/skills";
14538
14823
  var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
14539
14824
  async function ensureClaudeSkillLink(workspace) {
14540
- const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
14825
+ const linkPath = import_path6.default.join(workspace, CLAUDE_LINK);
14541
14826
  try {
14542
- await import_promises3.default.lstat(linkPath);
14827
+ await import_promises4.default.lstat(linkPath);
14543
14828
  return;
14544
14829
  } catch {
14545
14830
  }
14546
- await import_promises3.default.mkdir(import_path5.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14547
- await import_promises3.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14831
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14832
+ await import_promises4.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14548
14833
  }
14549
14834
  var GIT_NO_HELPER = ["-c", "credential.helper="];
14550
14835
  var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
@@ -14589,7 +14874,7 @@ async function gitCatFileBatch(cwd, shas) {
14589
14874
  return out;
14590
14875
  }
14591
14876
  async function ensureWorkspaceClone(opts) {
14592
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14877
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14593
14878
  const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
14594
14879
  const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
14595
14880
  let cloned = false;
@@ -14597,14 +14882,14 @@ async function ensureWorkspaceClone(opts) {
14597
14882
  await hostGit(workspace, "rev-parse", "--git-dir");
14598
14883
  await hostGit(workspace, "remote", "set-url", "origin", url2);
14599
14884
  } catch {
14600
- const leftovers = await import_promises3.default.readdir(workspace).catch(() => []);
14885
+ const leftovers = await import_promises4.default.readdir(workspace).catch(() => []);
14601
14886
  if (leftovers.includes(".git")) {
14602
14887
  throw new Error(
14603
14888
  `${workspace} holds a git repository that cannot be read. Remove it and retry.`
14604
14889
  );
14605
14890
  }
14606
14891
  for (const name of leftovers) {
14607
- await import_promises3.default.rm(import_path5.default.join(workspace, name), { recursive: true, force: true });
14892
+ await import_promises4.default.rm(import_path6.default.join(workspace, name), { recursive: true, force: true });
14608
14893
  }
14609
14894
  await execFileAsync2(
14610
14895
  "git",
@@ -14756,15 +15041,15 @@ function spaceEnv(spaceDir) {
14756
15041
  };
14757
15042
  }
14758
15043
  async function readContract(workspace) {
14759
- const skill = await import_promises3.default.readFile(import_path5.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
15044
+ const skill = await import_promises4.default.readFile(import_path6.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14760
15045
  throw new Error("The creator wrote no SKILL.md.");
14761
15046
  });
14762
- const checker = await import_promises3.default.readFile(import_path5.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
15047
+ const checker = await import_promises4.default.readFile(import_path6.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14763
15048
  throw new Error("The creator wrote no check-environment.sh.");
14764
15049
  });
14765
15050
  try {
14766
15051
  const raw = JSON.parse(
14767
- await import_promises3.default.readFile(import_path5.default.join(workspace, VARIABLES_PATH), "utf-8")
15052
+ await import_promises4.default.readFile(import_path6.default.join(workspace, VARIABLES_PATH), "utf-8")
14768
15053
  );
14769
15054
  return {
14770
15055
  skill,
@@ -14781,7 +15066,7 @@ async function generateDeploySkill(opts) {
14781
15066
  throw new Error("Describe your deployment process first.");
14782
15067
  }
14783
15068
  const status = await checkDeployStatus(opts);
14784
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15069
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14785
15070
  sendLog({
14786
15071
  type: "system",
14787
15072
  content: `Resetting workspace to origin/${status.branch}...`
@@ -14824,7 +15109,7 @@ async function generateDeploySkill(opts) {
14824
15109
  async function reviseDeploySkill(opts) {
14825
15110
  const { feedback, sendLog } = opts;
14826
15111
  if (!feedback.trim()) throw new Error("Corrections are empty.");
14827
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15112
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14828
15113
  sendLog({ type: "system", content: "Applying your corrections..." });
14829
15114
  const { result } = await spawnAgent(
14830
15115
  opts.provider,
@@ -14852,7 +15137,7 @@ async function loadSkillDraft(opts) {
14852
15137
  throw new Error("Not a deploy-skill branch.");
14853
15138
  }
14854
15139
  await checkDeployStatus(opts);
14855
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15140
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14856
15141
  await hostGit(
14857
15142
  workspace,
14858
15143
  "fetch",
@@ -14864,7 +15149,7 @@ async function loadSkillDraft(opts) {
14864
15149
  return readContract(workspace);
14865
15150
  }
14866
15151
  async function approveDeploySkill(opts) {
14867
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15152
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14868
15153
  await readContract(workspace);
14869
15154
  await ensureClaudeSkillLink(workspace);
14870
15155
  if (opts.gitName)
@@ -14907,19 +15192,19 @@ async function runEnvironmentCheck(opts) {
14907
15192
  "No environment checker on the default branch. Generate the deploy skill first."
14908
15193
  );
14909
15194
  }
14910
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15195
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14911
15196
  const script = await hostGit(
14912
15197
  workspace,
14913
15198
  "cat-file",
14914
15199
  "-p",
14915
15200
  `origin/${status.branch}:${CHECKER_PATH}`
14916
15201
  );
14917
- const tmp = import_path5.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14918
- await import_promises3.default.writeFile(tmp, script, { mode: 448 });
15202
+ const tmp = import_path6.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
15203
+ await import_promises4.default.writeFile(tmp, script, { mode: 448 });
14919
15204
  let ok = true;
14920
15205
  let output = "";
14921
15206
  try {
14922
- const scriptArg = tmp.split(import_path5.default.sep).join("/");
15207
+ const scriptArg = tmp.split(import_path6.default.sep).join("/");
14923
15208
  const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
14924
15209
  cwd: workspace,
14925
15210
  env: { ...process.env, ...spaceEnv(opts.spaceDir) },
@@ -14928,7 +15213,7 @@ async function runEnvironmentCheck(opts) {
14928
15213
  output = res.output;
14929
15214
  if (res.code !== 0) ok = false;
14930
15215
  } finally {
14931
- await import_promises3.default.rm(tmp, { force: true });
15216
+ await import_promises4.default.rm(tmp, { force: true });
14932
15217
  }
14933
15218
  const checks = [];
14934
15219
  for (const line of output.split("\n")) {
@@ -14950,7 +15235,7 @@ async function fixEnvironment(opts) {
14950
15235
  const before = await runEnvironmentCheck(opts);
14951
15236
  if (before.ok) return before;
14952
15237
  const status = await checkDeployStatus(opts);
14953
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15238
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14954
15239
  const script = await hostGit(
14955
15240
  workspace,
14956
15241
  "cat-file",
@@ -14985,7 +15270,7 @@ async function fixEnvironment(opts) {
14985
15270
  return runEnvironmentCheck(opts);
14986
15271
  }
14987
15272
  async function discardDeploySkill(opts) {
14988
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15273
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14989
15274
  await hostGit(workspace, "checkout", "--", ".").catch(() => null);
14990
15275
  await hostGit(workspace, "clean", "-fd");
14991
15276
  return { success: true };
@@ -15104,21 +15389,22 @@ var AGENTS_ROOT = ".agents/skills";
15104
15389
  var CLAUDE_ROOT = ".claude/skills";
15105
15390
  var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
15106
15391
  function globalAgentsRoot() {
15107
- return import_path6.default.join(import_os2.default.homedir(), ".agents", "skills");
15392
+ return import_path7.default.join(import_os2.default.homedir(), ".agents", "skills");
15108
15393
  }
15109
15394
  var AGENTS_LINK_TARGET = "../.agents/skills";
15110
15395
  var BRANCH_PREFIX = "skill/";
15396
+ var MIGRATE_BRANCH = `${BRANCH_PREFIX}migrate`;
15111
15397
  var SYMLINK_MODE = "120000";
15112
15398
  function summarize(raw, base) {
15113
15399
  const { frontmatter, errors } = parseSkill(raw);
15114
15400
  return { ...base, description: frontmatter.description, errors };
15115
15401
  }
15116
15402
  async function rootLinkState(root) {
15117
- const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15118
- const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15403
+ const linkPath = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15404
+ const stat = await import_promises5.default.lstat(linkPath).catch(() => null);
15119
15405
  if (!stat) return "missing";
15120
15406
  if (!stat.isSymbolicLink()) return "blocked";
15121
- const target = await import_promises4.default.readlink(linkPath).catch(() => "");
15407
+ const target = await import_promises5.default.readlink(linkPath).catch(() => "");
15122
15408
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
15123
15409
  }
15124
15410
  async function globalLinkState() {
@@ -15128,12 +15414,12 @@ async function globalLinkState() {
15128
15414
  }
15129
15415
  async function listGlobal() {
15130
15416
  const root = globalAgentsRoot();
15131
- const entries = await import_promises4.default.readdir(root, { withFileTypes: true }).catch(() => []);
15417
+ const entries = await import_promises5.default.readdir(root, { withFileTypes: true }).catch(() => []);
15132
15418
  const out = [];
15133
15419
  for (const entry of entries) {
15134
15420
  if (entry.isFile()) continue;
15135
- const dir = import_path6.default.join(root, entry.name);
15136
- const raw = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
15421
+ const dir = import_path7.default.join(root, entry.name);
15422
+ const raw = await import_promises5.default.readFile(import_path7.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
15137
15423
  if (!raw) continue;
15138
15424
  out.push(
15139
15425
  summarize(raw, {
@@ -15302,7 +15588,7 @@ async function ensureSkillsWorktree(spaceDir, branch) {
15302
15588
  } catch {
15303
15589
  await hostGit(repo, "worktree", "prune").catch(() => {
15304
15590
  });
15305
- await import_promises4.default.rm(worktree, { recursive: true, force: true });
15591
+ await import_promises5.default.rm(worktree, { recursive: true, force: true });
15306
15592
  await hostGit(
15307
15593
  repo,
15308
15594
  "worktree",
@@ -15367,29 +15653,34 @@ async function listSkillsImpl(opts) {
15367
15653
  globalLink,
15368
15654
  repoLink: "linked",
15369
15655
  repoBranch: "",
15370
- repoAvailable: false
15656
+ repoAvailable: false,
15657
+ repoMigrationBranch: "",
15658
+ repoMigrationMissing: []
15371
15659
  };
15372
15660
  }
15373
15661
  const ctx = await repoContext(opts);
15374
15662
  const repo = await repoSkills(ctx);
15663
+ const migration = openMigration(ctx.liveBlobs, ctx.pending);
15375
15664
  return {
15376
15665
  global: global2,
15377
15666
  repo,
15378
15667
  globalLink,
15379
15668
  repoLink: repoLinkState(ctx),
15380
15669
  repoBranch: ctx.branch,
15381
- repoAvailable: true
15670
+ repoAvailable: true,
15671
+ repoMigrationBranch: migration.branch,
15672
+ repoMigrationMissing: migration.missing
15382
15673
  };
15383
15674
  }
15384
15675
  async function bundleFilesOnDisk(dir) {
15385
15676
  const out = [];
15386
15677
  const walk = async (current, prefix, depth) => {
15387
15678
  if (depth > 3 || out.length >= 200) return;
15388
- const entries = await import_promises4.default.readdir(current, { withFileTypes: true }).catch(() => []);
15679
+ const entries = await import_promises5.default.readdir(current, { withFileTypes: true }).catch(() => []);
15389
15680
  for (const entry of entries) {
15390
15681
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
15391
15682
  if (entry.isDirectory()) {
15392
- await walk(import_path6.default.join(current, entry.name), rel, depth + 1);
15683
+ await walk(import_path7.default.join(current, entry.name), rel, depth + 1);
15393
15684
  } else if (rel !== "SKILL.md") {
15394
15685
  out.push(rel);
15395
15686
  }
@@ -15401,7 +15692,7 @@ async function bundleFilesOnDisk(dir) {
15401
15692
  async function readSkillImpl(opts) {
15402
15693
  if (opts.scope === "global") {
15403
15694
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15404
- const content2 = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8");
15695
+ const content2 = await import_promises5.default.readFile(import_path7.default.join(dir, "SKILL.md"), "utf-8");
15405
15696
  const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
15406
15697
  return {
15407
15698
  name: opts.name,
@@ -15436,71 +15727,102 @@ async function readSkillImpl(opts) {
15436
15727
  };
15437
15728
  }
15438
15729
  async function moveIntoAgents(from, to) {
15439
- if (await import_promises4.default.access(to).then(
15730
+ if (await import_promises5.default.access(to).then(
15440
15731
  () => true,
15441
15732
  () => false
15442
15733
  )) {
15443
15734
  throw new Error(
15444
- `${import_path6.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15735
+ `${import_path7.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15445
15736
  );
15446
15737
  }
15447
- await import_promises4.default.mkdir(import_path6.default.dirname(to), { recursive: true });
15738
+ await import_promises5.default.mkdir(import_path7.default.dirname(to), { recursive: true });
15448
15739
  try {
15449
- await import_promises4.default.rename(from, to);
15740
+ await import_promises5.default.rename(from, to);
15450
15741
  } catch {
15451
- await import_promises4.default.cp(from, to, { recursive: true });
15452
- await import_promises4.default.rm(from, { recursive: true, force: true });
15742
+ await import_promises5.default.cp(from, to, { recursive: true });
15743
+ await import_promises5.default.rm(from, { recursive: true, force: true });
15453
15744
  }
15454
15745
  }
15455
15746
  async function migrateGlobal() {
15456
15747
  const moved = [];
15457
15748
  for (const root of CLI_ROOTS) {
15458
- const dir = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15459
- const stat = await import_promises4.default.lstat(dir).catch(() => null);
15749
+ const dir = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15750
+ const stat = await import_promises5.default.lstat(dir).catch(() => null);
15460
15751
  if (!stat || stat.isSymbolicLink()) continue;
15461
- const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15752
+ const entries = await import_promises5.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15462
15753
  for (const entry of entries) {
15463
15754
  if (entry.isFile() || entry.isSymbolicLink()) continue;
15464
- const src = import_path6.default.join(dir, entry.name);
15465
- if (!await import_promises4.default.access(import_path6.default.join(src, "SKILL.md")).then(
15755
+ const src = import_path7.default.join(dir, entry.name);
15756
+ if (!await import_promises5.default.access(import_path7.default.join(src, "SKILL.md")).then(
15466
15757
  () => true,
15467
15758
  () => false
15468
15759
  )) {
15469
15760
  continue;
15470
15761
  }
15471
- await moveIntoAgents(src, import_path6.default.join(globalAgentsRoot(), entry.name));
15762
+ await moveIntoAgents(src, import_path7.default.join(globalAgentsRoot(), entry.name));
15472
15763
  moved.push(entry.name);
15473
15764
  }
15474
- await import_promises4.default.rmdir(dir).catch(() => {
15765
+ await import_promises5.default.rmdir(dir).catch(() => {
15475
15766
  });
15476
15767
  }
15477
15768
  for (const root of CLI_ROOTS) {
15478
- const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15479
- const after = await import_promises4.default.lstat(linkPath).catch(() => null);
15480
- if (after?.isSymbolicLink()) await import_promises4.default.rm(linkPath, { force: true });
15769
+ const linkPath = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15770
+ const after = await import_promises5.default.lstat(linkPath).catch(() => null);
15771
+ if (after?.isSymbolicLink()) await import_promises5.default.rm(linkPath, { force: true });
15481
15772
  else if (after) {
15482
15773
  throw new Error(
15483
15774
  `~/${root} still has files that are not skills. Empty it, then migrate.`
15484
15775
  );
15485
15776
  }
15486
- await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15487
- await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15777
+ await import_promises5.default.mkdir(import_path7.default.dirname(linkPath), { recursive: true });
15778
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15488
15779
  }
15489
15780
  return moved;
15490
15781
  }
15782
+ var MIGRATE_SUBJECT = /^Migrate \d+ skill\(s\) to /;
15783
+ function migrationCommitsOnly(log) {
15784
+ return log.split("\n").map((line) => line.trim()).filter(Boolean).every((subject) => MIGRATE_SUBJECT.test(subject));
15785
+ }
15786
+ function openMigration(liveBlobs, pending) {
15787
+ const onBranch = pending.find(
15788
+ (entry) => entry.branch === MIGRATE_BRANCH
15789
+ )?.blobs;
15790
+ if (!onBranch) return { branch: "", missing: [] };
15791
+ const missing = /* @__PURE__ */ new Set();
15792
+ for (const stray of strayRepoSkills(liveBlobs)) {
15793
+ const name = stray.slice(stray.lastIndexOf("/") + 1);
15794
+ if (!onBranch.has(`${AGENTS_ROOT}/${name}`)) missing.add(name);
15795
+ }
15796
+ return { branch: MIGRATE_BRANCH, missing: [...missing].sort() };
15797
+ }
15491
15798
  async function migrateSkillsImpl(opts) {
15492
15799
  if (opts.scope === "global") {
15493
- return { branch: "", moved: await migrateGlobal() };
15800
+ return { branch: "", moved: await migrateGlobal(), pushed: false };
15494
15801
  }
15495
15802
  const ctx = await repoContext(opts);
15496
15803
  const strays = strayRepoSkills(ctx.liveBlobs);
15497
15804
  const linked = ctx.links.get(CLAUDE_ROOT);
15498
15805
  if (!strays.length && linked === AGENTS_LINK_TARGET) {
15499
- return { branch: "", moved: [] };
15806
+ return { branch: "", moved: [], pushed: false };
15500
15807
  }
15501
15808
  const { branch } = ctx;
15502
15809
  const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
15503
- const target = `${BRANCH_PREFIX}migrate`;
15810
+ const target = MIGRATE_BRANCH;
15811
+ const existing = await hostGit(
15812
+ workspace,
15813
+ "ls-remote",
15814
+ "--heads",
15815
+ "origin",
15816
+ target
15817
+ ).catch(() => "");
15818
+ if (existing.trim()) {
15819
+ await hostGit(
15820
+ workspace,
15821
+ "fetch",
15822
+ "origin",
15823
+ `+refs/heads/${target}:refs/remotes/origin/${target}`
15824
+ );
15825
+ }
15504
15826
  await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
15505
15827
  await hostGit(workspace, "clean", "-fd");
15506
15828
  if (opts.gitName) {
@@ -15518,24 +15840,46 @@ async function migrateSkillsImpl(opts) {
15518
15840
  `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
15519
15841
  );
15520
15842
  }
15521
- await import_promises4.default.mkdir(import_path6.default.join(workspace, AGENTS_ROOT), { recursive: true });
15843
+ await import_promises5.default.mkdir(import_path7.default.join(workspace, AGENTS_ROOT), { recursive: true });
15522
15844
  await hostGit(workspace, "mv", stray, dest);
15523
15845
  moved.push(name);
15524
15846
  }
15525
- const linkPath = import_path6.default.join(workspace, CLAUDE_ROOT);
15526
- await import_promises4.default.rm(linkPath, { recursive: true, force: true });
15527
- await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15847
+ const linkPath = import_path7.default.join(workspace, CLAUDE_ROOT);
15848
+ await import_promises5.default.rm(linkPath, { recursive: true, force: true });
15849
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15528
15850
  await hostGit(workspace, "add", "-A");
15529
15851
  const dirty = await hostGit(workspace, "status", "--porcelain");
15530
- if (!dirty.trim()) return { branch: "", moved: [] };
15852
+ if (!dirty.trim()) return { branch: "", moved: [], pushed: false };
15531
15853
  await hostGit(
15532
15854
  workspace,
15533
15855
  "commit",
15534
15856
  "-m",
15535
15857
  `Migrate ${moved.length} skill(s) to ${AGENTS_ROOT} and link ${CLAUDE_ROOT}`
15536
15858
  );
15537
- await hostGit(workspace, "push", "origin", target);
15538
- return { branch: target, moved };
15859
+ if (!existing.trim()) {
15860
+ await hostGit(workspace, "push", "origin", target);
15861
+ return { branch: target, moved, pushed: true };
15862
+ }
15863
+ const [rebuilt, onBranch] = await Promise.all([
15864
+ hostGit(workspace, "rev-parse", "HEAD^{tree}"),
15865
+ hostGit(workspace, "rev-parse", `origin/${target}^{tree}`)
15866
+ ]);
15867
+ if (rebuilt.trim() === onBranch.trim()) {
15868
+ return { branch: target, moved, pushed: false };
15869
+ }
15870
+ const log = await hostGit(
15871
+ workspace,
15872
+ "log",
15873
+ "--format=%s",
15874
+ `origin/${branch}..origin/${target}`
15875
+ );
15876
+ if (!migrationCommitsOnly(log)) {
15877
+ throw new Error(
15878
+ `${target} carries commits that are not this migration. Merge or delete that branch, then migrate.`
15879
+ );
15880
+ }
15881
+ await hostGit(workspace, "push", "--force-with-lease", "origin", target);
15882
+ return { branch: target, moved, pushed: true };
15539
15883
  }
15540
15884
  function assertWritable(name, content) {
15541
15885
  const parsed = parseSkill(content);
@@ -15550,16 +15894,16 @@ function assertWritable(name, content) {
15550
15894
  }
15551
15895
  async function writeSkillFile(opts) {
15552
15896
  const dir = resolveSkillDir(opts.root, opts.name);
15553
- const file = import_path6.default.join(dir, "SKILL.md");
15897
+ const file = import_path7.default.join(dir, "SKILL.md");
15554
15898
  if (opts.keepExisting) {
15555
- const exists = await import_promises4.default.access(file).then(
15899
+ const exists = await import_promises5.default.access(file).then(
15556
15900
  () => true,
15557
15901
  () => false
15558
15902
  );
15559
15903
  if (exists) return;
15560
15904
  }
15561
- await import_promises4.default.mkdir(dir, { recursive: true });
15562
- await import_promises4.default.writeFile(file, opts.content, "utf-8");
15905
+ await import_promises5.default.mkdir(dir, { recursive: true });
15906
+ await import_promises5.default.writeFile(file, opts.content, "utf-8");
15563
15907
  }
15564
15908
  async function writeGlobalSkill(opts) {
15565
15909
  assertWritable(opts.name, opts.content);
@@ -15574,14 +15918,14 @@ async function installBundledSkill(opts) {
15574
15918
  content: opts.content,
15575
15919
  keepExisting: true
15576
15920
  });
15577
- await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15921
+ await write(import_path7.default.join(home, ...AGENTS_ROOT.split("/")));
15578
15922
  for (const root of CLI_ROOTS) {
15579
- const linkPath = import_path6.default.join(home, ...root.split("/"));
15580
- const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15923
+ const linkPath = import_path7.default.join(home, ...root.split("/"));
15924
+ const stat = await import_promises5.default.lstat(linkPath).catch(() => null);
15581
15925
  if (stat?.isSymbolicLink()) continue;
15582
15926
  if (!stat) {
15583
- await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15584
- await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15927
+ await import_promises5.default.mkdir(import_path7.default.dirname(linkPath), { recursive: true });
15928
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15585
15929
  continue;
15586
15930
  }
15587
15931
  await write(linkPath);
@@ -15589,7 +15933,7 @@ async function installBundledSkill(opts) {
15589
15933
  }
15590
15934
  async function deleteGlobalSkill(opts) {
15591
15935
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15592
- await import_promises4.default.rm(dir, { recursive: true, force: true });
15936
+ await import_promises5.default.rm(dir, { recursive: true, force: true });
15593
15937
  }
15594
15938
  async function proposeRepoSkillImpl(opts) {
15595
15939
  assertWritable(opts.name, opts.content);
@@ -15627,9 +15971,9 @@ async function proposeRepoSkillImpl(opts) {
15627
15971
  if (opts.gitEmail) {
15628
15972
  await hostGit(workspace, "config", "user.email", opts.gitEmail);
15629
15973
  }
15630
- await import_promises4.default.mkdir(import_path6.default.join(workspace, dir), { recursive: true });
15631
- await import_promises4.default.writeFile(
15632
- import_path6.default.join(workspace, dir, "SKILL.md"),
15974
+ await import_promises5.default.mkdir(import_path7.default.join(workspace, dir), { recursive: true });
15975
+ await import_promises5.default.writeFile(
15976
+ import_path7.default.join(workspace, dir, "SKILL.md"),
15633
15977
  opts.content,
15634
15978
  "utf-8"
15635
15979
  );
@@ -15736,18 +16080,18 @@ function vendoredBinaryName() {
15736
16080
  async function installFrpcBinary(target) {
15737
16081
  const p = frpcPaths(target);
15738
16082
  const binaryName = vendoredBinaryName();
15739
- const src = import_path7.default.join(__dirname, "..", "vendor", binaryName);
15740
- if (!(0, import_fs2.existsSync)(src)) {
16083
+ const src = import_path8.default.join(__dirname, "..", "vendor", binaryName);
16084
+ if (!(0, import_fs3.existsSync)(src)) {
15741
16085
  throw new Error(
15742
16086
  `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
15743
16087
  );
15744
16088
  }
15745
- const staged = await import_promises5.default.stat(p.binary).catch(() => null);
15746
- const source = await import_promises5.default.stat(src);
16089
+ const staged = await import_promises6.default.stat(p.binary).catch(() => null);
16090
+ const source = await import_promises6.default.stat(src);
15747
16091
  if (staged?.size === source.size) return;
15748
- await import_promises5.default.mkdir(p.binDir, { recursive: true });
15749
- await import_promises5.default.copyFile(src, p.binary);
15750
- await import_promises5.default.chmod(p.binary, 493);
16092
+ await import_promises6.default.mkdir(p.binDir, { recursive: true });
16093
+ await import_promises6.default.copyFile(src, p.binary);
16094
+ await import_promises6.default.chmod(p.binary, 493);
15751
16095
  }
15752
16096
  var EXPOSE_SKILL_NAME = "share-a-port";
15753
16097
  var FENCE = "```";
@@ -16137,18 +16481,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16137
16481
  await this.core.ensureSpaceWorkspace();
16138
16482
  await assertAgentSignedIn(session.provider, this.core.target());
16139
16483
  if (hasGit) {
16140
- const { path: path21 } = await addWorktree(
16484
+ const { path: path23 } = await addWorktree(
16141
16485
  this.core.target(),
16142
16486
  session.taskId,
16143
16487
  baseBranch || config.mainBranch || "main",
16144
16488
  "card"
16145
16489
  );
16146
- session.workspacePath = path21;
16490
+ session.workspacePath = path23;
16147
16491
  await setGitIdentity(
16148
16492
  this.core.target(),
16149
16493
  config.gitName,
16150
16494
  config.gitEmail,
16151
- path21
16495
+ path23
16152
16496
  );
16153
16497
  this.emitter.addLog(session, "system", "Worktree ready.");
16154
16498
  } else {
@@ -16370,18 +16714,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16370
16714
  this.emitter.addLog(session, "system", "Preparing environment...");
16371
16715
  await this.core.ensureSpaceWorkspace();
16372
16716
  await assertAgentSignedIn(session.provider, this.core.target());
16373
- const { path: path21 } = await addWorktree(
16717
+ const { path: path23 } = await addWorktree(
16374
16718
  this.core.target(),
16375
16719
  session.taskId,
16376
16720
  config.mainBranch || "main",
16377
16721
  "card"
16378
16722
  );
16379
- session.workspacePath = path21;
16723
+ session.workspacePath = path23;
16380
16724
  await setGitIdentity(
16381
16725
  this.core.target(),
16382
16726
  config.gitName,
16383
16727
  config.gitEmail,
16384
- path21
16728
+ path23
16385
16729
  );
16386
16730
  await this.putCard(session);
16387
16731
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16576,7 +16920,7 @@ ${buildPrompt(prompts_default.implement)}`;
16576
16920
  };
16577
16921
 
16578
16922
  // ../../shared/all/helpers/terminal-ai/terminal-mode.ts
16579
- var import_crypto = require("crypto");
16923
+ var import_crypto4 = require("crypto");
16580
16924
 
16581
16925
  // ../../shared/all/helpers/board-agent-core/boardTerminal.ts
16582
16926
  function boardTerminalKey(spaceSlug) {
@@ -16587,95 +16931,6 @@ function isCardPostId(id) {
16587
16931
  return UUID_RE.test(id);
16588
16932
  }
16589
16933
 
16590
- // ../../shared/all/helpers/board-agent-core/tmux.ts
16591
- var TMUX_CONF = [
16592
- // Wheel scrolls, drag selects in copy mode.
16593
- "set -g mouse on",
16594
- "set -g history-limit 50000",
16595
- // These three are copy-on-select: tmux emits OSC 52, the client clips it.
16596
- "set -g set-clipboard on",
16597
- "set -ga terminal-features ,*:clipboard",
16598
- "bind -T copy-mode MouseDragEnd1Pane send -X copy-selection-and-cancel",
16599
- "bind -T copy-mode-vi MouseDragEnd1Pane send -X copy-selection-and-cancel",
16600
- ""
16601
- ].join("\n");
16602
- var DEFAULT_SIZE = { cols: 80, rows: 24 };
16603
- var SOCKET = "factiii";
16604
- function safeTmuxName(raw) {
16605
- return raw.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
16606
- }
16607
- function tmuxConfPath(target) {
16608
- return `${target.paths.root}/tmux.conf`;
16609
- }
16610
- function tmuxCli(target) {
16611
- return `tmux -L ${SOCKET} -f ${tmuxConfPath(target)}`;
16612
- }
16613
- function shellQuote(argv) {
16614
- return argv.map((arg) => `'${arg.replace(/'/g, `'\\''`)}'`).join(" ");
16615
- }
16616
- function ptyArgv(argv, size) {
16617
- const sized = [
16618
- "sh",
16619
- "-c",
16620
- `stty rows ${size.rows} cols ${size.cols} 2>/dev/null; exec ${shellQuote(argv)}`
16621
- ];
16622
- const script = process.platform === "darwin" ? ["script", "-q", "/dev/null", ...sized] : ["script", "-qec", shellQuote(sized), "/dev/null"];
16623
- return ["bash", "-c", 'exec "$@" < <(cat)', "bash", ...script];
16624
- }
16625
- async function startTmuxSession(opts) {
16626
- const { target, session } = opts;
16627
- const conf = tmuxConfPath(target);
16628
- await target.writeFile(conf, TMUX_CONF);
16629
- let size = opts.size ?? DEFAULT_SIZE;
16630
- const dataCbs = [];
16631
- const closeCbs = [];
16632
- const errorCbs = [];
16633
- let proc;
16634
- const attach = async () => {
16635
- const argv = ["tmux", "-L", SOCKET, "-u", "-f", conf, "new-session"];
16636
- if (opts.attachExisting) argv.push("-A");
16637
- argv.push("-s", session, "-x", String(size.cols), "-y", String(size.rows));
16638
- for (const [k, v] of Object.entries(opts.env ?? {})) {
16639
- argv.push("-e", `${k}=${v}`);
16640
- }
16641
- argv.push("--", ...opts.argv);
16642
- const mine = await target.spawn(ptyArgv(argv, size), {
16643
- env: { TERM: "xterm-256color" }
16644
- });
16645
- proc = mine;
16646
- mine.stdin?.on("error", () => {
16647
- });
16648
- const live = () => proc === mine;
16649
- const emit = (chunk) => {
16650
- if (live()) for (const cb of dataCbs) cb(chunk);
16651
- };
16652
- mine.stdout?.on("data", emit);
16653
- mine.stderr?.on("data", emit);
16654
- mine.on("error", (err) => {
16655
- if (live()) for (const cb of errorCbs) cb(err);
16656
- });
16657
- mine.on("close", (code) => {
16658
- if (live()) for (const cb of closeCbs) cb(code ?? null);
16659
- });
16660
- };
16661
- await attach();
16662
- return {
16663
- session,
16664
- onData: (cb) => dataCbs.push(cb),
16665
- onClose: (cb) => closeCbs.push(cb),
16666
- onError: (cb) => errorCbs.push(cb),
16667
- write: (data) => proc.stdin?.write(data),
16668
- resize: async (cols, rows) => {
16669
- if (cols === size.cols && rows === size.rows) return;
16670
- size = { cols, rows };
16671
- const old = proc;
16672
- await attach();
16673
- killTree(old);
16674
- },
16675
- detach: () => killTree(proc)
16676
- };
16677
- }
16678
-
16679
16934
  // ../../shared/all/helpers/terminal-ai/agentState.ts
16680
16935
  var STATES = /* @__PURE__ */ new Set(["working", "idle", "blocked"]);
16681
16936
  var AGENT_STATE_CHANNEL = "agent-state";
@@ -16720,6 +16975,189 @@ function drainStateMessages(buffer) {
16720
16975
  return { states, rest: [...lines.slice(consumed), partial].join("\n") };
16721
16976
  }
16722
16977
 
16978
+ // ../../shared/all/helpers/terminal-ai/digest.ts
16979
+ var import_crypto3 = __toESM(require("crypto"));
16980
+ var import_promises8 = require("fs/promises");
16981
+ var import_path10 = __toESM(require("path"));
16982
+
16983
+ // ../../shared/all/helpers/board-agent-core/plugin.ts
16984
+ var import_crypto2 = __toESM(require("crypto"));
16985
+ var import_fs4 = require("fs");
16986
+ var import_promises7 = require("fs/promises");
16987
+ var import_os3 = __toESM(require("os"));
16988
+ var import_path9 = __toESM(require("path"));
16989
+ var MARKETPLACE_NAME = "factiii-runner";
16990
+ var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
16991
+ var SECRETS_BIN = "factiii-secrets";
16992
+ function candidates() {
16993
+ return [
16994
+ import_path9.default.join(__dirname, "..", "plugin"),
16995
+ import_path9.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
16996
+ import_path9.default.join(process.cwd(), "apps", "runner", "plugin")
16997
+ ];
16998
+ }
16999
+ function pluginRoot() {
17000
+ for (const dir of candidates()) {
17001
+ if ((0, import_fs4.existsSync)(import_path9.default.join(dir, ".claude-plugin", "marketplace.json"))) {
17002
+ return dir;
17003
+ }
17004
+ }
17005
+ return null;
17006
+ }
17007
+ function renderedPluginRoot() {
17008
+ return import_path9.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
17009
+ }
17010
+ async function runnerVersion(source) {
17011
+ const pkg = import_path9.default.join(source, "..", "package.json");
17012
+ const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
17013
+ if (!raw) return FALLBACK_VERSION;
17014
+ try {
17015
+ const { version } = JSON.parse(raw);
17016
+ return version || FALLBACK_VERSION;
17017
+ } catch {
17018
+ return FALLBACK_VERSION;
17019
+ }
17020
+ }
17021
+ var FALLBACK_VERSION = "0.0.0-unknown";
17022
+ async function treeFiles(dir) {
17023
+ const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
17024
+ const files = [];
17025
+ for (const entry of entries) {
17026
+ const full = import_path9.default.join(dir, entry.name);
17027
+ if (entry.isDirectory()) files.push(...await treeFiles(full));
17028
+ else if (entry.isFile()) files.push(full);
17029
+ }
17030
+ return files.sort();
17031
+ }
17032
+ async function fingerprint(source, url2, version = "") {
17033
+ const hash = import_crypto2.default.createHash("sha256").update(url2).update(version);
17034
+ for (const file of await treeFiles(source)) {
17035
+ hash.update(import_path9.default.relative(source, file));
17036
+ hash.update(await (0, import_promises7.readFile)(file));
17037
+ }
17038
+ return hash.digest("hex");
17039
+ }
17040
+ function stampPath(dest) {
17041
+ return `${dest}.stamp`;
17042
+ }
17043
+ var inFlight = /* @__PURE__ */ new Map();
17044
+ function renderPlugin(boardMcpUrl) {
17045
+ const running = inFlight.get(boardMcpUrl);
17046
+ if (running) return running;
17047
+ const started = renderPluginImpl(boardMcpUrl).finally(
17048
+ () => inFlight.delete(boardMcpUrl)
17049
+ );
17050
+ inFlight.set(boardMcpUrl, started);
17051
+ return started;
17052
+ }
17053
+ async function renderPluginImpl(boardMcpUrl) {
17054
+ const source = pluginRoot();
17055
+ if (!source) return null;
17056
+ const dest = renderedPluginRoot();
17057
+ const version = await runnerVersion(source);
17058
+ const want = await fingerprint(source, boardMcpUrl, version);
17059
+ const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
17060
+ if (have === want) return dest;
17061
+ await (0, import_promises7.mkdir)(import_path9.default.dirname(dest), { recursive: true });
17062
+ await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
17063
+ await (0, import_promises7.writeFile)(
17064
+ import_path9.default.join(dest, "factiii", ".mcp.json"),
17065
+ `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
17066
+ `
17067
+ );
17068
+ await stampVersion(dest, version);
17069
+ await (0, import_promises7.writeFile)(stampPath(dest), want);
17070
+ return dest;
17071
+ }
17072
+ async function stampVersion(dest, version) {
17073
+ const manifest = import_path9.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
17074
+ const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
17075
+ if (!raw) return;
17076
+ const parsed = JSON.parse(raw);
17077
+ await (0, import_promises7.writeFile)(
17078
+ manifest,
17079
+ `${JSON.stringify({ ...parsed, version }, null, 2)}
17080
+ `
17081
+ );
17082
+ }
17083
+ function boardMcpConfig(url2) {
17084
+ return {
17085
+ mcpServers: {
17086
+ "factiii-board": {
17087
+ type: "http",
17088
+ url: url2,
17089
+ bearer_token_env_var: BOARD_MCP_TOKEN_ENV,
17090
+ // Written literally, for claude to expand at connect time. Not a
17091
+ // template string: the `${…}` has to survive into the file.
17092
+ headers: { Authorization: "Bearer ${" + BOARD_MCP_TOKEN_ENV + "}" }
17093
+ }
17094
+ }
17095
+ };
17096
+ }
17097
+
17098
+ // ../../shared/all/helpers/terminal-ai/initScript.ts
17099
+ function terminalPrelude(cwd, binDir) {
17100
+ return [
17101
+ // cd in-script, falling back to $HOME. Must match the agent's worktree:
17102
+ // the CLIs key transcripts by cwd, so --resume needs it.
17103
+ `cd ${cwd} 2>/dev/null || cd "$HOME" 2>/dev/null || cd /`,
17104
+ // Prepended here rather than via the session env, so the login shell's
17105
+ // own PATH survives.
17106
+ `export PATH="${binDir}:$PATH"`,
17107
+ `export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`
17108
+ ].join("; ");
17109
+ }
17110
+ var SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;
17111
+ function cardBriefPrompt(postId) {
17112
+ return `You are working on a Factiii board card. Its postId is ${postId}. Pass that postId to the factiii-board MCP: get_card to read the card, then card_comments for its discussion. Then start working on it.`;
17113
+ }
17114
+ var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
17115
+ var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
17116
+ function terminalInitScript(cwd, binDir, resumeAgentSessionId, provider, cardPostId) {
17117
+ const prelude = terminalPrelude(cwd, binDir);
17118
+ const shell = `exec bash --norc -i`;
17119
+ if (!resumeAgentSessionId || !SAFE_SESSION_ID.test(resumeAgentSessionId)) {
17120
+ if (!cardPostId) return `${prelude}; ${shell}`;
17121
+ const prompt2 = cardBriefPrompt(cardPostId);
17122
+ const start = provider === "codex" ? `codex '${prompt2}'` : `claude ${CLAUDE_PERMISSION_MODE} '${prompt2}'`;
17123
+ return `${prelude}; ${start}; ${shell}`;
17124
+ }
17125
+ const resume = provider === "codex" ? `codex resume ${resumeAgentSessionId} '${RESUME_SEED}'` : `claude ${CLAUDE_PERMISSION_MODE} --resume ${resumeAgentSessionId} '${RESUME_SEED}'`;
17126
+ return `${prelude}; ${resume}; ${shell}`;
17127
+ }
17128
+
17129
+ // ../../shared/all/helpers/terminal-ai/digest.ts
17130
+ var TERMINAL_FP_OPTION = "@factiii-fp";
17131
+ var CWD_PLACEHOLDER = "/__cwd__";
17132
+ var VOLATILE_ENV_KEYS = /* @__PURE__ */ new Set(["FACTIII_PORTS"]);
17133
+ var PLUGIN_CACHE_MS = 3e4;
17134
+ var pluginCache = null;
17135
+ async function pluginContribution() {
17136
+ const now = Date.now();
17137
+ if (pluginCache && now - pluginCache.at < PLUGIN_CACHE_MS) {
17138
+ return pluginCache.value;
17139
+ }
17140
+ const source = pluginRoot();
17141
+ const shipped = source ? await fingerprint(source, "", "").catch(() => "") : "";
17142
+ const mcp = await (0, import_promises8.readFile)(
17143
+ import_path10.default.join(renderedPluginRoot(), "factiii", ".mcp.json"),
17144
+ "utf-8"
17145
+ ).catch(() => "");
17146
+ const value2 = `${shipped}|${mcp}`;
17147
+ pluginCache = { at: now, value: value2 };
17148
+ return value2;
17149
+ }
17150
+ async function terminalDigest(target) {
17151
+ const hash = import_crypto3.default.createHash("sha256").update(TMUX_CONF).update(terminalPrelude(CWD_PLACEHOLDER, target.paths.bin));
17152
+ const env = target.stateEnv();
17153
+ for (const key of Object.keys(env).sort()) {
17154
+ if (VOLATILE_ENV_KEYS.has(key)) continue;
17155
+ hash.update(`${key}=${env[key]}`);
17156
+ }
17157
+ hash.update(await pluginContribution());
17158
+ return hash.digest("hex");
17159
+ }
17160
+
16723
17161
  // ../../shared/all/helpers/terminal-ai/processes.ts
16724
17162
  var PS_FORMAT = "pid=,ppid=,pcpu=,rss=,etime=,args=";
16725
17163
  var PS_FIELDS = 5;
@@ -16889,19 +17327,306 @@ function parseSsListen(raw) {
16889
17327
  use.set(pid, found);
16890
17328
  }
16891
17329
  }
16892
- return use;
16893
- }
16894
- async function socketsFor(target, pids) {
16895
- if (pids.length === 0) return /* @__PURE__ */ new Map();
16896
- const lsof = await target.run(["lsof", "-a", "-p", pids.join(","), "-nP", "-iTCP", "-F", "pnT"]).catch((err) => String(err.stdout ?? ""));
16897
- if (lsof.trim()) return parseLsofSockets(lsof);
16898
- const ss = await target.run(["ss", "-ltnpH"]).catch(() => "");
16899
- if (!ss.trim()) return /* @__PURE__ */ new Map();
16900
- const all = parseSsListen(ss);
16901
- const wanted = new Set(pids);
16902
- for (const pid of all.keys()) if (!wanted.has(pid)) all.delete(pid);
16903
- return all;
16904
- }
17330
+ return use;
17331
+ }
17332
+ async function socketsFor(target, pids) {
17333
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
17334
+ const lsof = await target.run(["lsof", "-a", "-p", pids.join(","), "-nP", "-iTCP", "-F", "pnT"]).catch((err) => String(err.stdout ?? ""));
17335
+ if (lsof.trim()) return parseLsofSockets(lsof);
17336
+ const ss = await target.run(["ss", "-ltnpH"]).catch(() => "");
17337
+ if (!ss.trim()) return /* @__PURE__ */ new Map();
17338
+ const all = parseSsListen(ss);
17339
+ const wanted = new Set(pids);
17340
+ for (const pid of all.keys()) if (!wanted.has(pid)) all.delete(pid);
17341
+ return all;
17342
+ }
17343
+
17344
+ // ../../shared/all/helpers/terminal-ai/tabNames.ts
17345
+ var TAB_LIST_FORMAT = "#{session_name}|#{pane_current_command}|#{host}|#{@factiii-fp}|#{pane_dead}|#{pane_title}";
17346
+ var SHELL_COMMANDS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish"]);
17347
+ var tabName = (cmd, host, title) => {
17348
+ const command = cmd.replace(/\.exe$/, "") || "bash";
17349
+ if (!title || title === host || SHELL_COMMANDS.has(command)) return command;
17350
+ return title;
17351
+ };
17352
+ var parseTerminalTabs = (stdout, prefix) => {
17353
+ const byId = /* @__PURE__ */ new Map();
17354
+ for (const raw of stdout.split("\n")) {
17355
+ const line = raw.trim();
17356
+ if (!line.startsWith(prefix)) continue;
17357
+ const parts2 = line.slice(prefix.length).split("|");
17358
+ if (parts2.length < 6) continue;
17359
+ const id = parts2[0].trim();
17360
+ if (!id || byId.has(id)) continue;
17361
+ byId.set(id, {
17362
+ id,
17363
+ name: tabName(
17364
+ parts2[1].trim(),
17365
+ parts2[2].trim(),
17366
+ parts2.slice(5).join("|").trim()
17367
+ ),
17368
+ fp: parts2[3].trim(),
17369
+ dead: parts2[4].trim() === "1"
17370
+ });
17371
+ }
17372
+ return Array.from(byId.values()).sort(
17373
+ (a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
17374
+ );
17375
+ };
17376
+
17377
+ // ../../shared/all/helpers/wireChunk.ts
17378
+ var MAX_INLINE_BYTES = 14e3;
17379
+ var chunkCounter = 0;
17380
+ function nextChunkId() {
17381
+ return `c-${++chunkCounter}-${Date.now()}`;
17382
+ }
17383
+ function chunkWire(wire) {
17384
+ if (wire.length <= MAX_INLINE_BYTES) return [wire];
17385
+ const id = nextChunkId();
17386
+ const total = Math.ceil(wire.length / MAX_INLINE_BYTES);
17387
+ const frames = [];
17388
+ for (let i = 0; i < total; i++) {
17389
+ const start = i * MAX_INLINE_BYTES;
17390
+ const slice = wire.slice(start, start + MAX_INLINE_BYTES);
17391
+ const frame = { __c: id, __i: i, __n: total, d: slice };
17392
+ frames.push(JSON.stringify(frame));
17393
+ }
17394
+ return frames;
17395
+ }
17396
+ function makeChunkReassembler() {
17397
+ const buffers = /* @__PURE__ */ new Map();
17398
+ return {
17399
+ feed(raw) {
17400
+ if (!raw.startsWith('{"__c"')) return raw;
17401
+ let frame;
17402
+ try {
17403
+ frame = JSON.parse(raw);
17404
+ } catch {
17405
+ return raw;
17406
+ }
17407
+ if (typeof frame.__c !== "string" || typeof frame.__i !== "number" || typeof frame.__n !== "number" || typeof frame.d !== "string") {
17408
+ return raw;
17409
+ }
17410
+ let buf = buffers.get(frame.__c);
17411
+ if (!buf) {
17412
+ buf = { received: new Array(frame.__n), total: frame.__n };
17413
+ buffers.set(frame.__c, buf);
17414
+ }
17415
+ buf.received[frame.__i] = frame.d;
17416
+ for (let i = 0; i < buf.total; i++) {
17417
+ if (buf.received[i] === void 0) return null;
17418
+ }
17419
+ buffers.delete(frame.__c);
17420
+ return buf.received.join("");
17421
+ }
17422
+ };
17423
+ }
17424
+
17425
+ // ../../shared/all/helpers/terminal-ai/terminal.ts
17426
+ function tmuxName(terminalKey) {
17427
+ return `terminal-${safeTmuxName(terminalKey)}`;
17428
+ }
17429
+ async function restartTerminalSession(opts) {
17430
+ const { target, terminalKey, cwd } = opts;
17431
+ await respawnTmuxPane({
17432
+ target,
17433
+ session: tmuxName(terminalKey),
17434
+ argv: ["bash", "-c", terminalInitScript(cwd, target.paths.bin)],
17435
+ cwd,
17436
+ // Rebuilt rather than inherited: picking up an env the session was created
17437
+ // too early to see is half the reason to restart at all.
17438
+ env: target.stateEnv()
17439
+ }).catch(() => {
17440
+ });
17441
+ await terminalDigest(target).then(
17442
+ (digest) => setTmuxSessionOption(
17443
+ target,
17444
+ tmuxName(terminalKey),
17445
+ TERMINAL_FP_OPTION,
17446
+ digest
17447
+ )
17448
+ ).catch(() => {
17449
+ });
17450
+ }
17451
+ var TerminalManager = class {
17452
+ constructor(send, getCardTarget, getCardWorkdir) {
17453
+ this.send = send;
17454
+ this.getCardTarget = getCardTarget;
17455
+ this.getCardWorkdir = getCardWorkdir;
17456
+ this.sessions = /* @__PURE__ */ new Map();
17457
+ }
17458
+ handle(msg) {
17459
+ switch (msg.op) {
17460
+ case "open":
17461
+ void this.open(msg);
17462
+ return;
17463
+ case "stdin":
17464
+ this.stdin(msg.sessionId, msg.data);
17465
+ return;
17466
+ case "resize":
17467
+ void this.resize(msg.sessionId, msg.cols, msg.rows);
17468
+ return;
17469
+ case "close":
17470
+ this.close(msg.sessionId);
17471
+ return;
17472
+ case "kill":
17473
+ void this.killTmuxSession(msg.postId, msg.terminalKey);
17474
+ return;
17475
+ case "restart":
17476
+ void this.restart(msg.postId, msg.terminalKey);
17477
+ return;
17478
+ }
17479
+ }
17480
+ async open(msg) {
17481
+ if (this.sessions.has(msg.sessionId)) {
17482
+ this.emitError(msg.sessionId, "Session already open.");
17483
+ return;
17484
+ }
17485
+ const target = this.getCardTarget(msg.postId);
17486
+ if (!target) {
17487
+ this.emitError(
17488
+ msg.sessionId,
17489
+ `No active card session for postId=${msg.postId}.`
17490
+ );
17491
+ return;
17492
+ }
17493
+ const tmuxSession = tmuxName(msg.terminalKey);
17494
+ const cardPostId = msg.briefCard && !msg.resumeAgentSessionId && isCardPostId(msg.postId) ? msg.postId : void 0;
17495
+ const init = this.buildInit(target, msg.postId, {
17496
+ resumeAgentSessionId: msg.resumeAgentSessionId,
17497
+ provider: msg.resumeProvider,
17498
+ cardPostId
17499
+ });
17500
+ const creating = !await tmuxSessionExists(target, tmuxSession);
17501
+ try {
17502
+ const session = await startTmuxSession({
17503
+ target,
17504
+ session: tmuxSession,
17505
+ // Attach if it exists, else create. This is what makes the shell
17506
+ // survive modal close/reopen: the server holds it, we just view.
17507
+ attachExisting: true,
17508
+ size: { cols: msg.cols, rows: msg.rows },
17509
+ // Without these, `claude` in the shell reads the default ~/.claude and
17510
+ // asks to sign in, while the agent spawns (which pass agentEnv) don't.
17511
+ env: target.stateEnv(),
17512
+ argv: init.argv
17513
+ });
17514
+ this.sessions.set(msg.sessionId, session);
17515
+ if (creating) {
17516
+ void resurrectSave(target);
17517
+ void terminalDigest(target).then(
17518
+ (digest) => setTmuxSessionOption(
17519
+ target,
17520
+ tmuxSession,
17521
+ TERMINAL_FP_OPTION,
17522
+ digest
17523
+ )
17524
+ ).catch(() => {
17525
+ });
17526
+ }
17527
+ session.onData(
17528
+ (data) => this.emit({
17529
+ type: "terminal",
17530
+ op: "stdout",
17531
+ sessionId: msg.sessionId,
17532
+ data: data.toString("base64")
17533
+ })
17534
+ );
17535
+ session.onError((err) => {
17536
+ this.cleanup(msg.sessionId);
17537
+ this.emitError(msg.sessionId, err.message);
17538
+ });
17539
+ session.onClose((code) => {
17540
+ if (!this.sessions.has(msg.sessionId)) return;
17541
+ this.cleanup(msg.sessionId);
17542
+ this.emit({
17543
+ type: "terminal",
17544
+ op: "exit",
17545
+ sessionId: msg.sessionId,
17546
+ code
17547
+ });
17548
+ });
17549
+ this.emit({ type: "terminal", op: "opened", sessionId: msg.sessionId });
17550
+ } catch (err) {
17551
+ this.emitError(
17552
+ msg.sessionId,
17553
+ err instanceof Error ? err.message : String(err)
17554
+ );
17555
+ }
17556
+ }
17557
+ /** What tmux runs for a tab, and where. One builder so `open` and `restart`
17558
+ * cannot drift. */
17559
+ buildInit(target, postId, agent) {
17560
+ const cwd = this.getCardWorkdir(postId) || target.paths.workspace;
17561
+ return {
17562
+ cwd,
17563
+ argv: [
17564
+ "bash",
17565
+ "-c",
17566
+ terminalInitScript(
17567
+ cwd,
17568
+ target.paths.bin,
17569
+ agent?.resumeAgentSessionId,
17570
+ agent?.provider,
17571
+ agent?.cardPostId
17572
+ )
17573
+ ]
17574
+ };
17575
+ }
17576
+ /** Restart the tab's process while keeping the tab. */
17577
+ async restart(postId, terminalKey) {
17578
+ const target = this.getCardTarget(postId);
17579
+ if (!target) return;
17580
+ await restartTerminalSession({
17581
+ target,
17582
+ terminalKey,
17583
+ cwd: this.getCardWorkdir(postId) || target.paths.workspace
17584
+ });
17585
+ }
17586
+ stdin(sessionId, b64) {
17587
+ this.sessions.get(sessionId)?.write(Buffer.from(b64, "base64"));
17588
+ }
17589
+ async resize(sessionId, cols, rows) {
17590
+ const session = this.sessions.get(sessionId);
17591
+ if (!session) return;
17592
+ if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
17593
+ if (cols < 1 || rows < 1) return;
17594
+ await session.resize(cols, rows).catch(() => {
17595
+ });
17596
+ }
17597
+ close(sessionId) {
17598
+ const session = this.sessions.get(sessionId);
17599
+ if (!session) return;
17600
+ this.sessions.delete(sessionId);
17601
+ session.detach();
17602
+ }
17603
+ /** Tear down the tmux session for a tab. Used when the user explicitly
17604
+ * removes a tab (the X button), not on transient detach. */
17605
+ async killTmuxSession(postId, terminalKey) {
17606
+ const target = this.getCardTarget(postId);
17607
+ if (!target) return;
17608
+ await target.sh(
17609
+ `${tmuxCli(target)} kill-session -t ${tmuxName(terminalKey)} 2>/dev/null || true`
17610
+ ).catch(() => {
17611
+ });
17612
+ await resurrectSave(target);
17613
+ }
17614
+ destroyAll() {
17615
+ for (const sessionId of [...this.sessions.keys()]) {
17616
+ this.close(sessionId);
17617
+ }
17618
+ }
17619
+ cleanup(sessionId) {
17620
+ this.sessions.delete(sessionId);
17621
+ }
17622
+ emitError(sessionId, message) {
17623
+ this.emit({ type: "terminal", op: "error", sessionId, message });
17624
+ }
17625
+ emit(msg) {
17626
+ const wire = JSON.stringify(msg);
17627
+ for (const frame of chunkWire(wire)) this.send(frame);
17628
+ }
17629
+ };
16905
17630
 
16906
17631
  // ../../shared/all/helpers/terminal-ai/watcher.ts
16907
17632
  var PRUNED_DIRS = [
@@ -16928,12 +17653,12 @@ var FLUSH_MS = 120;
16928
17653
  var SELF_WRITE_MS = 2e3;
16929
17654
  var dynamicImport = new Function("s", "return import(s)");
16930
17655
  var importFs = new Function("s", "return import(s)");
16931
- async function resolveReal(path21) {
17656
+ async function resolveReal(path23) {
16932
17657
  try {
16933
17658
  const { realpath } = await importFs("fs/promises");
16934
- return await realpath(path21);
17659
+ return await realpath(path23);
16935
17660
  } catch {
16936
- return path21;
17661
+ return path23;
16937
17662
  }
16938
17663
  }
16939
17664
  var loadSubscribe = null;
@@ -17047,19 +17772,19 @@ var WorkspaceWatcher = class {
17047
17772
  }
17048
17773
  record(kind, relPath) {
17049
17774
  if (this.closed) return;
17050
- const path21 = "/" + relPath;
17051
- const expiry = this.selfWrites.get(path21);
17775
+ const path23 = "/" + relPath;
17776
+ const expiry = this.selfWrites.get(path23);
17052
17777
  if (expiry !== void 0) {
17053
17778
  if (expiry > Date.now()) return;
17054
- this.selfWrites.delete(path21);
17779
+ this.selfWrites.delete(path23);
17055
17780
  }
17056
17781
  const { added, changed, removed } = this.pending;
17057
- added.delete(path21);
17058
- changed.delete(path21);
17059
- removed.delete(path21);
17060
- if (kind === "added") added.add(path21);
17061
- else if (kind === "changed") changed.add(path21);
17062
- else removed.add(path21);
17782
+ added.delete(path23);
17783
+ changed.delete(path23);
17784
+ removed.delete(path23);
17785
+ if (kind === "added") added.add(path23);
17786
+ else if (kind === "changed") changed.add(path23);
17787
+ else removed.add(path23);
17063
17788
  if (added.size + changed.size + removed.size > MAX_BATCH) {
17064
17789
  this.pending = { ...emptyPending(), resync: true };
17065
17790
  }
@@ -17078,8 +17803,8 @@ var WorkspaceWatcher = class {
17078
17803
  this.pending = emptyPending();
17079
17804
  if (!resync && !added.size && !changed.size && !removed.size) return;
17080
17805
  const now = Date.now();
17081
- for (const [path21, expiry] of this.selfWrites) {
17082
- if (expiry <= now) this.selfWrites.delete(path21);
17806
+ for (const [path23, expiry] of this.selfWrites) {
17807
+ if (expiry <= now) this.selfWrites.delete(path23);
17083
17808
  }
17084
17809
  this.emit({
17085
17810
  added: resync ? [] : [...added],
@@ -17283,8 +18008,8 @@ var TerminalModeEngine = class {
17283
18008
  /** Suppress the change event our own write is about to produce, so a save
17284
18009
  * never comes back to the editor as an outside edit. Normalized to the form
17285
18010
  * the watcher emits, since callers spell the path either way. */
17286
- expectWrite(postId, path21) {
17287
- this.watchers.get(postId)?.expectWrite(posixNormalize(path21));
18011
+ expectWrite(postId, path23) {
18012
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path23));
17288
18013
  }
17289
18014
  // ── Lifecycle / ModeEngine ──
17290
18015
  start(base, args) {
@@ -17344,7 +18069,7 @@ var TerminalModeEngine = class {
17344
18069
  }
17345
18070
  if (!session) return;
17346
18071
  this.stopWatching(session.postId);
17347
- await this.teardown(session);
18072
+ await teardownSession(session, this.core.target());
17348
18073
  const removedPostId = session.postId;
17349
18074
  this.reservedNames.delete(removedPostId);
17350
18075
  this.sessions.delete(removedPostId);
@@ -17357,33 +18082,6 @@ var TerminalModeEngine = class {
17357
18082
  this.reservedNames.clear();
17358
18083
  this.sessions.clear();
17359
18084
  }
17360
- /** Every tmux session this post owns, by the `terminal-<postId>-<tab>` name
17361
- * tmuxName() builds. */
17362
- async terminalSessionNames(target, postId) {
17363
- const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
17364
- const stdout = await target.sh(
17365
- `${tmuxCli(target)} list-sessions -F '#{session_name}' 2>/dev/null || true`
17366
- ).catch(() => "");
17367
- return stdout.split("\n").map((line) => line.trim()).filter((name) => name.startsWith(prefix));
17368
- }
17369
- /** Kill the post's terminals and everything running inside them. Killing the
17370
- * tmux session alone leaves a process that reparented away from the shell,
17371
- * so each pane's process tree is killed first. */
17372
- async killTerminals(postId) {
17373
- const target = this.core.target();
17374
- const names = await this.terminalSessionNames(target, postId);
17375
- for (const name of names) {
17376
- await target.sh(
17377
- `for p in $(${tmuxCli(target)} list-panes -t ${name} -F '#{pane_pid}' 2>/dev/null); do pkill -TERM -P "$p" 2>/dev/null || true; done; ${tmuxCli(target)} kill-session -t ${name} 2>/dev/null || true`
17378
- ).catch(() => {
17379
- });
17380
- }
17381
- }
17382
- async teardown(session) {
17383
- await this.killTerminals(session.postId).catch(() => {
17384
- });
17385
- await teardownSession(session, this.core.target());
17386
- }
17387
18085
  putCard(s) {
17388
18086
  return putSessionCard(s, this.core.target());
17389
18087
  }
@@ -17481,18 +18179,18 @@ var TerminalModeEngine = class {
17481
18179
  this.reservedNames.values()
17482
18180
  );
17483
18181
  this.reservedNames.set(session.postId, workName);
17484
- const { path: path21 } = await addWorktree(
18182
+ const { path: path23 } = await addWorktree(
17485
18183
  this.core.target(),
17486
18184
  workName,
17487
18185
  branch,
17488
18186
  "terminal"
17489
18187
  );
17490
- session.workspacePath = path21;
18188
+ session.workspacePath = path23;
17491
18189
  await setGitIdentity(
17492
18190
  this.core.target(),
17493
18191
  config.gitName,
17494
18192
  config.gitEmail,
17495
- path21
18193
+ path23
17496
18194
  );
17497
18195
  this.emitter.addLog(session, "system", "Worktree ready.");
17498
18196
  } else {
@@ -17554,11 +18252,11 @@ var TerminalModeEngine = class {
17554
18252
  }
17555
18253
  async bareFsList({
17556
18254
  postId,
17557
- path: path21
18255
+ path: path23
17558
18256
  }) {
17559
18257
  const target = this.getBareTarget(postId);
17560
18258
  const root = this.getBareRoot(postId);
17561
- const abs = this.resolveBarePath(path21, root);
18259
+ const abs = this.resolveBarePath(path23, root);
17562
18260
  const relRoot = abs.slice(root.length) || "/";
17563
18261
  const out = await target.run([
17564
18262
  "sh",
@@ -17608,30 +18306,30 @@ var TerminalModeEngine = class {
17608
18306
  }
17609
18307
  async bareFsRead({
17610
18308
  postId,
17611
- path: path21
18309
+ path: path23
17612
18310
  }) {
17613
18311
  const target = this.getBareTarget(postId);
17614
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18312
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17615
18313
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17616
18314
  '`)).trim();
17617
18315
  return { content: b64, encoding: "base64" };
17618
18316
  }
17619
18317
  async bareFsWrite({
17620
18318
  postId,
17621
- path: path21,
18319
+ path: path23,
17622
18320
  content,
17623
18321
  encoding
17624
18322
  }) {
17625
18323
  const target = this.getBareTarget(postId);
17626
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18324
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17627
18325
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17628
18326
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17629
18327
  throw new Error("Invalid base64 payload.");
17630
18328
  }
17631
- this.expectWrite(String(postId), path21);
18329
+ this.expectWrite(String(postId), path23);
17632
18330
  await writeBase64File(target, abs, b64);
17633
18331
  const session = this.sessions.get(String(postId));
17634
- if (session) delete session.buffers[path21];
18332
+ if (session) delete session.buffers[path23];
17635
18333
  }
17636
18334
  /** Append one chunk of a dropped image to the per-space drops dir
17637
18335
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17649,7 +18347,7 @@ var TerminalModeEngine = class {
17649
18347
  }
17650
18348
  const safeExt = (ext || "png").toLowerCase().replace(/[^a-z0-9]/g, "") || "png";
17651
18349
  const oneShot = !dropId;
17652
- const safeId2 = oneShot ? (0, import_crypto.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
18350
+ const safeId2 = oneShot ? (0, import_crypto4.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
17653
18351
  if (!safeId2) throw new Error("Invalid drop id.");
17654
18352
  const dropsDir = `${target.paths.root}/drops`;
17655
18353
  const abs = `${dropsDir}/${safeId2}.${safeExt}`;
@@ -17681,39 +18379,39 @@ var TerminalModeEngine = class {
17681
18379
  }
17682
18380
  bareOpenFile({
17683
18381
  postId: rawPostId,
17684
- path: path21
18382
+ path: path23
17685
18383
  }) {
17686
18384
  const session = this.sessions.get(String(rawPostId));
17687
18385
  if (!session || session.mode !== "bare") return;
17688
- if (!session.openedFiles.includes(path21)) {
17689
- session.openedFiles.push(path21);
18386
+ if (!session.openedFiles.includes(path23)) {
18387
+ session.openedFiles.push(path23);
17690
18388
  }
17691
18389
  }
17692
18390
  bareCloseFile({
17693
18391
  postId: rawPostId,
17694
- path: path21
18392
+ path: path23
17695
18393
  }) {
17696
18394
  const session = this.sessions.get(String(rawPostId));
17697
18395
  if (!session || session.mode !== "bare") return;
17698
- session.openedFiles = session.openedFiles.filter((p) => p !== path21);
17699
- delete session.buffers[path21];
18396
+ session.openedFiles = session.openedFiles.filter((p) => p !== path23);
18397
+ delete session.buffers[path23];
17700
18398
  }
17701
18399
  bareBufferSet({
17702
18400
  postId: rawPostId,
17703
- path: path21,
18401
+ path: path23,
17704
18402
  content
17705
18403
  }) {
17706
18404
  const session = this.sessions.get(String(rawPostId));
17707
18405
  if (!session || session.mode !== "bare") return;
17708
- session.buffers[path21] = content;
18406
+ session.buffers[path23] = content;
17709
18407
  }
17710
18408
  async bareFsDelete({
17711
18409
  postId,
17712
- path: path21
18410
+ path: path23
17713
18411
  }) {
17714
18412
  const target = this.getBareTarget(postId);
17715
18413
  const root = this.getBareRoot(postId);
17716
- const abs = this.resolveBarePath(path21, root);
18414
+ const abs = this.resolveBarePath(path23, root);
17717
18415
  if (abs === root) {
17718
18416
  throw new Error("Refusing to delete the repo root.");
17719
18417
  }
@@ -17721,10 +18419,10 @@ var TerminalModeEngine = class {
17721
18419
  }
17722
18420
  async bareFsMkdir({
17723
18421
  postId,
17724
- path: path21
18422
+ path: path23
17725
18423
  }) {
17726
18424
  const target = this.getBareTarget(postId);
17727
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18425
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17728
18426
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17729
18427
  }
17730
18428
  async bareReview({
@@ -17847,26 +18545,68 @@ var TerminalModeEngine = class {
17847
18545
  const prefix = `terminal-${sanitizedPostId}-`;
17848
18546
  try {
17849
18547
  const stdout = await target.sh(
17850
- `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_current_command}' 2>/dev/null || true`
17851
- );
17852
- const byId = /* @__PURE__ */ new Map();
17853
- for (const raw of stdout.split("\n")) {
17854
- const line = raw.trim();
17855
- if (!line.startsWith(prefix)) continue;
17856
- const rest = line.slice(prefix.length);
17857
- const sep = rest.indexOf("|");
17858
- if (sep === -1) continue;
17859
- const id = rest.slice(0, sep);
17860
- const cmd = rest.slice(sep + 1).trim() || "bash";
17861
- if (!byId.has(id)) byId.set(id, cmd);
17862
- }
17863
- return Array.from(byId, ([id, name]) => ({ id, name })).sort(
17864
- (a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
18548
+ `${tmuxCli(target)} list-panes -a -F '${TAB_LIST_FORMAT}' 2>/dev/null || true`
17865
18549
  );
18550
+ const rows = parseTerminalTabs(stdout, prefix);
18551
+ return rows.map(({ id, name, dead }) => ({ id, name, dead }));
17866
18552
  } catch {
17867
18553
  return [];
17868
18554
  }
17869
18555
  }
18556
+ /** Terminal tabs across several posts in one sweep, so the sessions view can
18557
+ * ask about a whole board without a round trip per card. */
18558
+ async staleTabsFor(target, postIds, current) {
18559
+ if (!current) return [];
18560
+ const stdout = await target.sh(
18561
+ `${tmuxCli(target)} list-panes -a -F '${TAB_LIST_FORMAT}' 2>/dev/null || true`
18562
+ ).catch(() => "");
18563
+ const stale = [];
18564
+ for (const postId of postIds) {
18565
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
18566
+ for (const row of parseTerminalTabs(stdout, prefix)) {
18567
+ if (row.fp !== current) stale.push({ postId, tabId: row.id });
18568
+ }
18569
+ }
18570
+ return stale;
18571
+ }
18572
+ async bareTerminalHealth({
18573
+ postIds
18574
+ }) {
18575
+ const target = this.core.target();
18576
+ const digest = await terminalDigest(target).catch(() => "");
18577
+ const acked = await getTerminalAck(target);
18578
+ return {
18579
+ digest,
18580
+ acked: !!digest && acked === digest,
18581
+ staleTabs: await this.staleTabsFor(target, postIds, digest)
18582
+ };
18583
+ }
18584
+ async bareAckTerminalDigest() {
18585
+ const target = this.core.target();
18586
+ const digest = await terminalDigest(target).catch(() => "");
18587
+ if (!digest) return;
18588
+ await setTerminalAck(target, digest);
18589
+ }
18590
+ async bareRestartStaleTerminals({
18591
+ postIds
18592
+ }) {
18593
+ const target = this.core.target();
18594
+ const digest = await terminalDigest(target).catch(() => "");
18595
+ const stale = await this.staleTabsFor(target, postIds, digest);
18596
+ for (const { postId, tabId } of stale) {
18597
+ let cwd = target.paths.workspace;
18598
+ try {
18599
+ cwd = this.gitWorkdir(postId) || cwd;
18600
+ } catch {
18601
+ }
18602
+ await restartTerminalSession({
18603
+ target,
18604
+ terminalKey: `${postId}-${tabId}`,
18605
+ cwd
18606
+ });
18607
+ }
18608
+ return { restarted: stale.length };
18609
+ }
17870
18610
  /** This card's panes: the pid its tree hangs off, and the pane id its
17871
18611
  * processes carry in their environment. */
17872
18612
  async panePids(target, postId) {
@@ -18406,8 +19146,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
18406
19146
  };
18407
19147
  }
18408
19148
  }
18409
- async function githubApi(token, path21) {
18410
- const res = await fetch(`${API_BASE}${path21}`, {
19149
+ async function githubApi(token, path23) {
19150
+ const res = await fetch(`${API_BASE}${path23}`, {
18411
19151
  headers: {
18412
19152
  Authorization: `Bearer ${token}`,
18413
19153
  Accept: "application/vnd.github+json",
@@ -18416,7 +19156,7 @@ async function githubApi(token, path21) {
18416
19156
  });
18417
19157
  if (!res.ok) {
18418
19158
  throw new Error(
18419
- `GitHub ${path21} failed (${res.status}): ${await res.text()}`
19159
+ `GitHub ${path23} failed (${res.status}): ${await res.text()}`
18420
19160
  );
18421
19161
  }
18422
19162
  return await res.json();
@@ -18461,10 +19201,10 @@ async function listGithubBranches(token, fullName) {
18461
19201
  }
18462
19202
 
18463
19203
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18464
- var import_path10 = __toESM(require("path"));
19204
+ var import_path13 = __toESM(require("path"));
18465
19205
 
18466
19206
  // ../../shared/all/helpers/board-agent-core/secrets-gate.ts
18467
- var import_crypto2 = __toESM(require("crypto"));
19207
+ var import_crypto5 = __toESM(require("crypto"));
18468
19208
  var DEPLOY_ORIGIN = "Deploy run";
18469
19209
  var UNATTRIBUTED = "This runner did not start the session that asked, so the request was refused. `factiii-secrets` only works inside a session the runner spawned.";
18470
19210
  var SecretsGate = class {
@@ -18489,7 +19229,14 @@ var SecretsGate = class {
18489
19229
  /** Mint a token for one session, labelled with what the owner should see on
18490
19230
  * the prompt - the label is most of the decision, so name the session. */
18491
19231
  issueToken(origin) {
18492
- const token = import_crypto2.default.randomBytes(24).toString("hex");
19232
+ const token = import_crypto5.default.randomBytes(24).toString("hex");
19233
+ this.tokens.set(token, origin);
19234
+ return token;
19235
+ }
19236
+ /** Register a durable token the caller already holds, instead of minting a
19237
+ * fresh one. For the space token: terminals outlive this process, so a
19238
+ * minted one leaves them unattributed after a restart. Idempotent. */
19239
+ adoptToken(token, origin) {
18493
19240
  this.tokens.set(token, origin);
18494
19241
  return token;
18495
19242
  }
@@ -18513,7 +19260,7 @@ var SecretsGate = class {
18513
19260
  return new Promise((resolve2) => {
18514
19261
  this.waiting = {
18515
19262
  request: {
18516
- id: import_crypto2.default.randomUUID(),
19263
+ id: import_crypto5.default.randomUUID(),
18517
19264
  command: opts.command,
18518
19265
  keys: opts.keys,
18519
19266
  origin: opts.origin,
@@ -18553,13 +19300,13 @@ var SecretsGate = class {
18553
19300
  };
18554
19301
 
18555
19302
  // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18556
- var import_path9 = __toESM(require("path"));
19303
+ var import_path12 = __toESM(require("path"));
18557
19304
 
18558
19305
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18559
19306
  var import_child_process4 = require("child_process");
18560
- var import_crypto3 = __toESM(require("crypto"));
18561
- var import_promises6 = __toESM(require("fs/promises"));
18562
- var import_path8 = __toESM(require("path"));
19307
+ var import_crypto6 = __toESM(require("crypto"));
19308
+ var import_promises9 = __toESM(require("fs/promises"));
19309
+ var import_path11 = __toESM(require("path"));
18563
19310
  var import_util6 = require("util");
18564
19311
  var execFileAsync3 = (0, import_util6.promisify)(import_child_process4.execFile);
18565
19312
  var IDENTITY_FILE = "age-identity.enc";
@@ -18585,14 +19332,14 @@ async function assertBinaries() {
18585
19332
  }
18586
19333
  }
18587
19334
  function deriveKey(password, salt) {
18588
- return import_crypto3.default.scryptSync(password, salt, 32, {
19335
+ return import_crypto6.default.scryptSync(password, salt, 32, {
18589
19336
  N: SCRYPT_N,
18590
19337
  maxmem: 64 * 1024 * 1024
18591
19338
  });
18592
19339
  }
18593
19340
  async function secretsInitialized(configDir) {
18594
19341
  try {
18595
- await import_promises6.default.access(import_path8.default.join(configDir, IDENTITY_FILE));
19342
+ await import_promises9.default.access(import_path11.default.join(configDir, IDENTITY_FILE));
18596
19343
  return true;
18597
19344
  } catch {
18598
19345
  return false;
@@ -18614,9 +19361,9 @@ async function initSecrets(configDir, password) {
18614
19361
  if (!identity || !recipient) {
18615
19362
  throw new Error("age-keygen returned no usable keypair.");
18616
19363
  }
18617
- const salt = import_crypto3.default.randomBytes(16);
18618
- const iv = import_crypto3.default.randomBytes(12);
18619
- const cipher = import_crypto3.default.createCipheriv(
19364
+ const salt = import_crypto6.default.randomBytes(16);
19365
+ const iv = import_crypto6.default.randomBytes(12);
19366
+ const cipher = import_crypto6.default.createCipheriv(
18620
19367
  "aes-256-gcm",
18621
19368
  deriveKey(password, salt),
18622
19369
  iv
@@ -18631,17 +19378,17 @@ async function initSecrets(configDir, password) {
18631
19378
  tag: cipher.getAuthTag().toString("base64"),
18632
19379
  data: data.toString("base64")
18633
19380
  };
18634
- await import_promises6.default.writeFile(import_path8.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
19381
+ await import_promises9.default.writeFile(import_path11.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18635
19382
  mode: 384
18636
19383
  });
18637
- await import_promises6.default.writeFile(import_path8.default.join(configDir, RECIPIENT_FILE), `${recipient}
19384
+ await import_promises9.default.writeFile(import_path11.default.join(configDir, RECIPIENT_FILE), `${recipient}
18638
19385
  `, {
18639
19386
  mode: 384
18640
19387
  });
18641
19388
  return { success: true };
18642
19389
  }
18643
19390
  function decryptIdentity(box, password) {
18644
- const decipher = import_crypto3.default.createDecipheriv(
19391
+ const decipher = import_crypto6.default.createDecipheriv(
18645
19392
  "aes-256-gcm",
18646
19393
  deriveKey(password, Buffer.from(box.salt, "base64")),
18647
19394
  Buffer.from(box.iv, "base64")
@@ -18657,17 +19404,17 @@ function decryptIdentity(box, password) {
18657
19404
  }
18658
19405
  }
18659
19406
  function envPath(spaceDir) {
18660
- return import_path8.default.join(spaceDir, "secrets", ENV_FILE);
19407
+ return import_path11.default.join(spaceDir, "secrets", ENV_FILE);
18661
19408
  }
18662
19409
  async function identityFor(configDir, password) {
18663
19410
  const box = JSON.parse(
18664
- await import_promises6.default.readFile(import_path8.default.join(configDir, IDENTITY_FILE), "utf-8")
19411
+ await import_promises9.default.readFile(import_path11.default.join(configDir, IDENTITY_FILE), "utf-8")
18665
19412
  );
18666
19413
  return decryptIdentity(box, password);
18667
19414
  }
18668
19415
  async function readEnv(spaceDir, identity) {
18669
19416
  try {
18670
- await import_promises6.default.access(envPath(spaceDir));
19417
+ await import_promises9.default.access(envPath(spaceDir));
18671
19418
  } catch {
18672
19419
  return {};
18673
19420
  }
@@ -18696,14 +19443,14 @@ async function readEnv(spaceDir, identity) {
18696
19443
  return values;
18697
19444
  }
18698
19445
  async function writeEnv(configDir, spaceDir, identity, values) {
18699
- const recipient = (await import_promises6.default.readFile(import_path8.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
19446
+ const recipient = (await import_promises9.default.readFile(import_path11.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18700
19447
  const plain = `${Object.entries(values).map(([k, v]) => `${k}=${v}`).join("\n")}
18701
19448
  `;
18702
19449
  const tmp = envPath(spaceDir).replace(
18703
19450
  /\.enc\.env$/,
18704
19451
  `.${process.pid}.tmp.env`
18705
19452
  );
18706
- await import_promises6.default.writeFile(tmp, plain, { mode: 384 });
19453
+ await import_promises9.default.writeFile(tmp, plain, { mode: 384 });
18707
19454
  try {
18708
19455
  const { stdout } = await execFileAsync3(
18709
19456
  "sops",
@@ -18722,13 +19469,13 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18722
19469
  maxBuffer: 4 * 1024 * 1024
18723
19470
  }
18724
19471
  );
18725
- await import_promises6.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
19472
+ await import_promises9.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18726
19473
  } finally {
18727
- await import_promises6.default.rm(tmp, { force: true });
19474
+ await import_promises9.default.rm(tmp, { force: true });
18728
19475
  }
18729
19476
  }
18730
19477
  async function storedSecretKeys(spaceDir) {
18731
- const raw = await import_promises6.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
19478
+ const raw = await import_promises9.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
18732
19479
  const keys = [];
18733
19480
  for (const line of raw.split("\n")) {
18734
19481
  const eq = line.indexOf("=");
@@ -18772,9 +19519,9 @@ var COMMAND_TIMEOUT_MS = 6 * 60 * 6e4;
18772
19519
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18773
19520
  function containedCwd(cwd, spaceDir) {
18774
19521
  if (!cwd) return null;
18775
- const resolved = import_path9.default.resolve(cwd);
18776
- const root = import_path9.default.resolve(spaceDir);
18777
- return resolved === root || resolved.startsWith(root + import_path9.default.sep) ? resolved : null;
19522
+ const resolved = import_path12.default.resolve(cwd);
19523
+ const root = import_path12.default.resolve(spaceDir);
19524
+ return resolved === root || resolved.startsWith(root + import_path12.default.sep) ? resolved : null;
18778
19525
  }
18779
19526
  async function authorizeAndRun(opts) {
18780
19527
  const { command, gate } = opts;
@@ -18859,8 +19606,8 @@ function tailMarker(text) {
18859
19606
  }
18860
19607
  async function startDeployRun(opts) {
18861
19608
  const { hooks } = opts;
18862
- const workspace = import_path10.default.join(opts.spaceDir, "workspace");
18863
- const configDir = import_path10.default.dirname(opts.spaceDir);
19609
+ const workspace = import_path13.default.join(opts.spaceDir, "workspace");
19610
+ const configDir = import_path13.default.dirname(opts.spaceDir);
18864
19611
  const status = await checkDeployStatus(opts);
18865
19612
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18866
19613
  throw new Error(
@@ -19079,123 +19826,8 @@ async function getDriveRoot(accessToken) {
19079
19826
  }
19080
19827
 
19081
19828
  // ../../shared/all/helpers/board-agent-core/space-core.ts
19082
- var import_promises8 = require("fs/promises");
19083
- var import_path12 = __toESM(require("path"));
19084
-
19085
- // ../../shared/all/helpers/board-agent-core/plugin.ts
19086
- var import_crypto4 = __toESM(require("crypto"));
19087
- var import_fs3 = require("fs");
19088
- var import_promises7 = require("fs/promises");
19089
- var import_os3 = __toESM(require("os"));
19090
- var import_path11 = __toESM(require("path"));
19091
- var MARKETPLACE_NAME = "factiii-runner";
19092
- var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
19093
- var SECRETS_BIN = "factiii-secrets";
19094
- function candidates() {
19095
- return [
19096
- import_path11.default.join(__dirname, "..", "plugin"),
19097
- import_path11.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
19098
- import_path11.default.join(process.cwd(), "apps", "runner", "plugin")
19099
- ];
19100
- }
19101
- function pluginRoot() {
19102
- for (const dir of candidates()) {
19103
- if ((0, import_fs3.existsSync)(import_path11.default.join(dir, ".claude-plugin", "marketplace.json"))) {
19104
- return dir;
19105
- }
19106
- }
19107
- return null;
19108
- }
19109
- function renderedPluginRoot() {
19110
- return import_path11.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
19111
- }
19112
- async function runnerVersion(source) {
19113
- const pkg = import_path11.default.join(source, "..", "package.json");
19114
- const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
19115
- if (!raw) return FALLBACK_VERSION;
19116
- try {
19117
- const { version } = JSON.parse(raw);
19118
- return version || FALLBACK_VERSION;
19119
- } catch {
19120
- return FALLBACK_VERSION;
19121
- }
19122
- }
19123
- var FALLBACK_VERSION = "0.0.0-unknown";
19124
- async function treeFiles(dir) {
19125
- const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
19126
- const files = [];
19127
- for (const entry of entries) {
19128
- const full = import_path11.default.join(dir, entry.name);
19129
- if (entry.isDirectory()) files.push(...await treeFiles(full));
19130
- else if (entry.isFile()) files.push(full);
19131
- }
19132
- return files.sort();
19133
- }
19134
- async function fingerprint(source, url2, version = "") {
19135
- const hash = import_crypto4.default.createHash("sha256").update(url2).update(version);
19136
- for (const file of await treeFiles(source)) {
19137
- hash.update(import_path11.default.relative(source, file));
19138
- hash.update(await (0, import_promises7.readFile)(file));
19139
- }
19140
- return hash.digest("hex");
19141
- }
19142
- function stampPath(dest) {
19143
- return `${dest}.stamp`;
19144
- }
19145
- var inFlight = /* @__PURE__ */ new Map();
19146
- function renderPlugin(boardMcpUrl) {
19147
- const running = inFlight.get(boardMcpUrl);
19148
- if (running) return running;
19149
- const started = renderPluginImpl(boardMcpUrl).finally(
19150
- () => inFlight.delete(boardMcpUrl)
19151
- );
19152
- inFlight.set(boardMcpUrl, started);
19153
- return started;
19154
- }
19155
- async function renderPluginImpl(boardMcpUrl) {
19156
- const source = pluginRoot();
19157
- if (!source) return null;
19158
- const dest = renderedPluginRoot();
19159
- const version = await runnerVersion(source);
19160
- const want = await fingerprint(source, boardMcpUrl, version);
19161
- const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
19162
- if (have === want) return dest;
19163
- await (0, import_promises7.mkdir)(import_path11.default.dirname(dest), { recursive: true });
19164
- await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
19165
- await (0, import_promises7.writeFile)(
19166
- import_path11.default.join(dest, "factiii", ".mcp.json"),
19167
- `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
19168
- `
19169
- );
19170
- await stampVersion(dest, version);
19171
- await (0, import_promises7.writeFile)(stampPath(dest), want);
19172
- return dest;
19173
- }
19174
- async function stampVersion(dest, version) {
19175
- const manifest = import_path11.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
19176
- const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
19177
- if (!raw) return;
19178
- const parsed = JSON.parse(raw);
19179
- await (0, import_promises7.writeFile)(
19180
- manifest,
19181
- `${JSON.stringify({ ...parsed, version }, null, 2)}
19182
- `
19183
- );
19184
- }
19185
- function boardMcpConfig(url2) {
19186
- return {
19187
- mcpServers: {
19188
- "factiii-board": {
19189
- type: "http",
19190
- url: url2,
19191
- bearer_token_env_var: BOARD_MCP_TOKEN_ENV,
19192
- // Written literally, for claude to expand at connect time. Not a
19193
- // template string: the `${…}` has to survive into the file.
19194
- headers: { Authorization: "Bearer ${" + BOARD_MCP_TOKEN_ENV + "}" }
19195
- }
19196
- }
19197
- };
19198
- }
19829
+ var import_promises10 = require("fs/promises");
19830
+ var import_path14 = __toESM(require("path"));
19199
19831
 
19200
19832
  // ../../shared/all/helpers/board-agent-core/toolchain.ts
19201
19833
  var TOOLS = [
@@ -19421,6 +20053,7 @@ var SpaceCore = class {
19421
20053
  this.runnerToken = options.runnerToken;
19422
20054
  this.serverUrl = options.serverUrl;
19423
20055
  this.mcp = options.mcp;
20056
+ this.mcpToken = options.mcpToken;
19424
20057
  this.requestSudo = options.requestSudo;
19425
20058
  this.activity = new ActivityTracker(this.emitter);
19426
20059
  this.gate.subscribe(() => {
@@ -19464,7 +20097,7 @@ var SpaceCore = class {
19464
20097
  return { code: 3, output: "" };
19465
20098
  }
19466
20099
  return authorizeAndRun({
19467
- configDir: import_path12.default.dirname(spaceDir),
20100
+ configDir: import_path14.default.dirname(spaceDir),
19468
20101
  spaceDir,
19469
20102
  // A step legitimately cds into subdirectories, so the caller's own
19470
20103
  // cwd is right - but only inside the space. Anywhere else is a
@@ -19515,7 +20148,11 @@ var SpaceCore = class {
19515
20148
  * that DOES know itself - the deploy run - mints its own and overrides this.
19516
20149
  */
19517
20150
  secretsToken() {
19518
- this.spaceSecretsToken ||= this.gate.issueToken("A session on this board");
20151
+ if (this.spaceSecretsToken) return this.spaceSecretsToken;
20152
+ this.spaceSecretsToken = this.gate.adoptToken(
20153
+ spaceSecret(this.spaceDir()),
20154
+ "A session on this board"
20155
+ );
19519
20156
  return this.spaceSecretsToken;
19520
20157
  }
19521
20158
  readConfig() {
@@ -19526,7 +20163,8 @@ var SpaceCore = class {
19526
20163
  target() {
19527
20164
  this.execTarget ??= hostTarget(this.spaceDir(), () => {
19528
20165
  const env = {};
19529
- for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
20166
+ const onedrive = this.mcpToken?.();
20167
+ if (onedrive) env[MCP_TOKEN_ENV] = onedrive;
19530
20168
  const board = this.boardMcp();
19531
20169
  if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
19532
20170
  env.FACTIII_SECRETS_TOKEN = this.secretsToken();
@@ -19717,6 +20355,8 @@ var SpaceCore = class {
19717
20355
  markOnboardingComplete(target).catch(() => {
19718
20356
  })
19719
20357
  ]);
20358
+ await this.restoreTerminals();
20359
+ await this.applyTerminalConfig();
19720
20360
  status("");
19721
20361
  this.ready = true;
19722
20362
  } catch (err) {
@@ -19802,17 +20442,17 @@ var SpaceCore = class {
19802
20442
  const source = pluginRoot();
19803
20443
  if (!source) return;
19804
20444
  const { bin } = this.target().paths;
19805
- const from = import_path12.default.join(source, "factiii", "bin", SECRETS_BIN);
19806
- const to = import_path12.default.join(bin, SECRETS_BIN);
20445
+ const from = import_path14.default.join(source, "factiii", "bin", SECRETS_BIN);
20446
+ const to = import_path14.default.join(bin, SECRETS_BIN);
19807
20447
  try {
19808
20448
  const [src, staged] = await Promise.all([
19809
- (0, import_promises8.readFile)(from),
19810
- (0, import_promises8.readFile)(to).catch(() => null)
20449
+ (0, import_promises10.readFile)(from),
20450
+ (0, import_promises10.readFile)(to).catch(() => null)
19811
20451
  ]);
19812
20452
  if (staged?.equals(src)) return;
19813
- await (0, import_promises8.mkdir)(bin, { recursive: true });
19814
- await (0, import_promises8.copyFile)(from, to);
19815
- await (0, import_promises8.chmod)(to, 493);
20453
+ await (0, import_promises10.mkdir)(bin, { recursive: true });
20454
+ await (0, import_promises10.copyFile)(from, to);
20455
+ await (0, import_promises10.chmod)(to, 493);
19816
20456
  } catch {
19817
20457
  }
19818
20458
  }
@@ -19875,9 +20515,79 @@ var SpaceCore = class {
19875
20515
  });
19876
20516
  await Promise.all([this.provisionPluginLane(), this.provisionBinLane()]);
19877
20517
  await this.provisionMcpLane();
20518
+ await this.restoreTerminals();
20519
+ await this.applyTerminalConfig();
19878
20520
  this.ready = true;
19879
20521
  return true;
19880
20522
  }
20523
+ /** Push a changed tmux config into the server that is already running.
20524
+ *
20525
+ * tmux reads `-f` only when it STARTS, so a config a runner update changed
20526
+ * would otherwise sit unread until every session on the host died. Sourcing
20527
+ * it reaches the live sessions and costs no process its life, which is why
20528
+ * this half never has to bother the user - unlike the env half, where a
20529
+ * restart is the only way in.
20530
+ *
20531
+ * Runs AFTER the plugin and helper lanes above, deliberately: those are two
20532
+ * of the inputs the digest covers, and recording one before they finish
20533
+ * would stamp a state the runner is about to invalidate. */
20534
+ /** Bring this space's tabs back after the tmux server died (a reboot, or a
20535
+ * kill). A runner restart needs none of this — tmux outlives us.
20536
+ *
20537
+ * Structure only: every tab returns as a fresh shell.
20538
+ *
20539
+ * Each restored pane is respawned, because resurrect rebuilds panes on the
20540
+ * DEFAULT shell with no bin dir on PATH, and stamped, because it saves no
20541
+ * user options and an unstamped session would read as stale on every boot. */
20542
+ async restoreTerminals() {
20543
+ try {
20544
+ const target = this.target();
20545
+ await ensureTmuxResurrect(target);
20546
+ const root = target.paths.root;
20547
+ const before = await tmuxPanePaths(target);
20548
+ const mineUp = [...before].some(
20549
+ ([name, dir]) => name.startsWith("terminal-") && dir.startsWith(root)
20550
+ );
20551
+ if (mineUp) return;
20552
+ const restored = await resurrectRestore(target);
20553
+ const paths = await tmuxPanePaths(target);
20554
+ const revived = restored.filter(
20555
+ (name) => !before.has(name) && (paths.get(name) ?? "").startsWith(root)
20556
+ );
20557
+ if (revived.length === 0) return;
20558
+ const digest = await terminalDigest(target).catch(() => "");
20559
+ for (const session of revived) {
20560
+ await respawnTmuxPane({
20561
+ target,
20562
+ session,
20563
+ argv: [
20564
+ "bash",
20565
+ "-c",
20566
+ terminalInitScript(
20567
+ paths.get(session) || target.paths.workspace,
20568
+ target.paths.bin
20569
+ )
20570
+ ],
20571
+ cwd: paths.get(session) || target.paths.workspace,
20572
+ env: target.stateEnv()
20573
+ }).catch(() => {
20574
+ });
20575
+ if (digest) {
20576
+ await setTmuxSessionOption(
20577
+ target,
20578
+ session,
20579
+ TERMINAL_FP_OPTION,
20580
+ digest
20581
+ );
20582
+ }
20583
+ }
20584
+ } catch {
20585
+ }
20586
+ }
20587
+ async applyTerminalConfig() {
20588
+ await sourceTmuxConf(this.target()).catch(() => {
20589
+ });
20590
+ }
19881
20591
  async ensureSetup() {
19882
20592
  await this.ensureSpaceWorkspace();
19883
20593
  return { ready: true };
@@ -20528,7 +21238,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20528
21238
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
20529
21239
  secretsDirs() {
20530
21240
  const spaceDir = this.core.spaceDir();
20531
- return { configDir: (0, import_path13.dirname)(spaceDir), spaceDir };
21241
+ return { configDir: (0, import_path15.dirname)(spaceDir), spaceDir };
20532
21242
  }
20533
21243
  async deploySecretsStatus() {
20534
21244
  return {
@@ -20919,6 +21629,15 @@ var BoardAgentEngine = class _BoardAgentEngine {
20919
21629
  this.getCardTarget(String(p.postId)) ?? void 0
20920
21630
  );
20921
21631
  }
21632
+ bareTerminalHealth(p) {
21633
+ return this.terminal.bareTerminalHealth(p);
21634
+ }
21635
+ bareAckTerminalDigest() {
21636
+ return this.terminal.bareAckTerminalDigest();
21637
+ }
21638
+ bareRestartStaleTerminals(p) {
21639
+ return this.terminal.bareRestartStaleTerminals(p);
21640
+ }
20922
21641
  bareEditorState(p) {
20923
21642
  return this.terminal.bareEditorState(p);
20924
21643
  }
@@ -21556,6 +22275,17 @@ ${c.content.trim() || "(no description)"}`
21556
22275
  return await this.bareListTerminals(
21557
22276
  payload
21558
22277
  );
22278
+ case "bareTerminalHealth":
22279
+ return await this.bareTerminalHealth(
22280
+ payload
22281
+ );
22282
+ case "bareAckTerminalDigest":
22283
+ await this.bareAckTerminalDigest();
22284
+ return void 0;
22285
+ case "bareRestartStaleTerminals":
22286
+ return await this.bareRestartStaleTerminals(
22287
+ payload
22288
+ );
21559
22289
  case "bareEditorState":
21560
22290
  return this.bareEditorState(
21561
22291
  payload
@@ -21598,11 +22328,11 @@ ${c.content.trim() || "(no description)"}`
21598
22328
 
21599
22329
  // ../../shared/all/helpers/board-agent-core/index-service.ts
21600
22330
  var import_child_process5 = require("child_process");
21601
- var import_crypto5 = require("crypto");
21602
- var import_fs4 = require("fs");
21603
- var import_promises9 = __toESM(require("fs/promises"));
22331
+ var import_crypto7 = require("crypto");
22332
+ var import_fs5 = require("fs");
22333
+ var import_promises11 = __toESM(require("fs/promises"));
21604
22334
  var import_net3 = require("net");
21605
- var import_path14 = require("path");
22335
+ var import_path16 = require("path");
21606
22336
  var import_util7 = require("util");
21607
22337
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
21608
22338
  var IndexService = class {
@@ -21616,7 +22346,7 @@ var IndexService = class {
21616
22346
  /** Per-space bearer for MCP + file, derived from the control token (the
21617
22347
  * service computes the same). */
21618
22348
  bearerToken(spaceSlug) {
21619
- return (0, import_crypto5.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
22349
+ return (0, import_crypto7.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21620
22350
  }
21621
22351
  /** MCP bridge for a space. Null until the service is up (which it kicks
21622
22352
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -21661,7 +22391,7 @@ var IndexService = class {
21661
22391
  const log = await this.openLog();
21662
22392
  this.proc = (0, import_child_process5.spawn)(
21663
22393
  process.execPath,
21664
- [(0, import_path14.join)(this.opts.indexDir, "index.js")],
22394
+ [(0, import_path16.join)(this.opts.indexDir, "index.js")],
21665
22395
  {
21666
22396
  cwd: this.opts.indexDir,
21667
22397
  env: {
@@ -21689,8 +22419,8 @@ var IndexService = class {
21689
22419
  * opened (a missing log must never stop the service from starting). */
21690
22420
  async openLog() {
21691
22421
  try {
21692
- await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true });
21693
- return (0, import_fs4.openSync)((0, import_path14.join)(this.opts.dataDir, "index.log"), "a");
22422
+ await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true });
22423
+ return (0, import_fs5.openSync)((0, import_path16.join)(this.opts.dataDir, "index.log"), "a");
21694
22424
  } catch {
21695
22425
  return null;
21696
22426
  }
@@ -21699,10 +22429,10 @@ var IndexService = class {
21699
22429
  * platform needing node-gyp fails here rather than at query time. */
21700
22430
  async installDeps() {
21701
22431
  const dir = this.opts.indexDir;
21702
- if ((0, import_fs4.existsSync)((0, import_path14.join)(dir, "node_modules"))) return;
21703
- await import_promises9.default.copyFile(
21704
- (0, import_path14.join)(dir, "image-package.json"),
21705
- (0, import_path14.join)(dir, "package.json")
22432
+ if ((0, import_fs5.existsSync)((0, import_path16.join)(dir, "node_modules"))) return;
22433
+ await import_promises11.default.copyFile(
22434
+ (0, import_path16.join)(dir, "image-package.json"),
22435
+ (0, import_path16.join)(dir, "package.json")
21706
22436
  );
21707
22437
  await execFileAsync4(
21708
22438
  "npm",
@@ -21714,15 +22444,15 @@ var IndexService = class {
21714
22444
  * CLIs' persistent MCP config, so a fresh port each start would leave every
21715
22445
  * registration pointing at a dead one. */
21716
22446
  async stablePort() {
21717
- const file = (0, import_path14.join)(this.opts.dataDir, ".port");
21718
- const saved = Number(await import_promises9.default.readFile(file, "utf-8").catch(() => ""));
22447
+ const file = (0, import_path16.join)(this.opts.dataDir, ".port");
22448
+ const saved = Number(await import_promises11.default.readFile(file, "utf-8").catch(() => ""));
21719
22449
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
21720
22450
  return saved;
21721
22451
  }
21722
22452
  const port = await freePort();
21723
- await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
22453
+ await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21724
22454
  });
21725
- await import_promises9.default.writeFile(file, String(port), "utf-8").catch(() => {
22455
+ await import_promises11.default.writeFile(file, String(port), "utf-8").catch(() => {
21726
22456
  });
21727
22457
  return port;
21728
22458
  }
@@ -21889,24 +22619,24 @@ async function pairWithBrowser(serverUrl) {
21889
22619
  }
21890
22620
 
21891
22621
  // src/config.ts
21892
- var import_fs8 = __toESM(require("fs"));
22622
+ var import_fs9 = __toESM(require("fs"));
21893
22623
  var import_os6 = __toESM(require("os"));
21894
- var import_path19 = __toESM(require("path"));
22624
+ var import_path21 = __toESM(require("path"));
21895
22625
 
21896
22626
  // src/secureFile.ts
21897
- var import_crypto8 = __toESM(require("crypto"));
21898
- var import_fs7 = __toESM(require("fs"));
21899
- var import_path18 = __toESM(require("path"));
22627
+ var import_crypto10 = __toESM(require("crypto"));
22628
+ var import_fs8 = __toESM(require("fs"));
22629
+ var import_path20 = __toESM(require("path"));
21900
22630
 
21901
22631
  // src/keychain.ts
21902
22632
  var import_child_process7 = require("child_process");
21903
- var import_crypto6 = __toESM(require("crypto"));
21904
- var import_fs5 = __toESM(require("fs"));
22633
+ var import_crypto8 = __toESM(require("crypto"));
22634
+ var import_fs6 = __toESM(require("fs"));
21905
22635
  var import_os4 = __toESM(require("os"));
21906
- var import_path15 = __toESM(require("path"));
22636
+ var import_path17 = __toESM(require("path"));
21907
22637
  var SERVICE = "factiii-runner";
21908
22638
  var ACCOUNT = "config-encryption-key";
21909
- var DPAPI_KEY_FILE = import_path15.default.join(
22639
+ var DPAPI_KEY_FILE = import_path17.default.join(
21910
22640
  import_os4.default.homedir(),
21911
22641
  ".factiii-runner",
21912
22642
  "config-key.dpapi"
@@ -21987,7 +22717,7 @@ function linuxWrite(key) {
21987
22717
  function winRead() {
21988
22718
  let wrapped;
21989
22719
  try {
21990
- wrapped = import_fs5.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
22720
+ wrapped = import_fs6.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21991
22721
  } catch {
21992
22722
  return { state: "missing" };
21993
22723
  }
@@ -22012,8 +22742,8 @@ function winWrite(key) {
22012
22742
  "-Command",
22013
22743
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
22014
22744
  ]);
22015
- import_fs5.default.mkdirSync(import_path15.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22016
- import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22745
+ import_fs6.default.mkdirSync(import_path17.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22746
+ import_fs6.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22017
22747
  `, { mode: 384 });
22018
22748
  }
22019
22749
  function keychainBackend() {
@@ -22152,7 +22882,7 @@ function resolve() {
22152
22882
  cached = { key: null, status: diagnose() };
22153
22883
  return cached;
22154
22884
  }
22155
- const fresh = import_crypto6.default.randomBytes(32);
22885
+ const fresh = import_crypto8.default.randomBytes(32);
22156
22886
  writeKey(fresh);
22157
22887
  const verify = readKey();
22158
22888
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -22174,23 +22904,23 @@ function keychainStatus() {
22174
22904
  }
22175
22905
 
22176
22906
  // src/secureStore.ts
22177
- var import_crypto7 = __toESM(require("crypto"));
22178
- var import_fs6 = __toESM(require("fs"));
22179
- var import_path17 = __toESM(require("path"));
22907
+ var import_crypto9 = __toESM(require("crypto"));
22908
+ var import_fs7 = __toESM(require("fs"));
22909
+ var import_path19 = __toESM(require("path"));
22180
22910
 
22181
22911
  // src/paths.ts
22182
22912
  var import_os5 = __toESM(require("os"));
22183
- var import_path16 = __toESM(require("path"));
22184
- var CONFIG_DIR = import_path16.default.join(import_os5.default.homedir(), ".factiii-runner");
22913
+ var import_path18 = __toESM(require("path"));
22914
+ var CONFIG_DIR = import_path18.default.join(import_os5.default.homedir(), ".factiii-runner");
22185
22915
  function safeSlug(spaceSlug) {
22186
22916
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
22187
22917
  }
22188
22918
  function spaceDirPath(spaceSlug) {
22189
- return import_path16.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22919
+ return import_path18.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22190
22920
  }
22191
22921
 
22192
22922
  // src/secureStore.ts
22193
- var VAULT_KEY_FILE = import_path17.default.join(CONFIG_DIR, "vault-key.json");
22923
+ var VAULT_KEY_FILE = import_path19.default.join(CONFIG_DIR, "vault-key.json");
22194
22924
  var SCRYPT_N2 = 1 << 15;
22195
22925
  var MIN_PASSWORD_LENGTH = 8;
22196
22926
  var SecureStoreError = class extends Error {
@@ -22217,7 +22947,7 @@ function requireKeychainKey() {
22217
22947
  );
22218
22948
  }
22219
22949
  function passwordKey(password, salt) {
22220
- return import_crypto7.default.scryptSync(password, salt, 32, {
22950
+ return import_crypto9.default.scryptSync(password, salt, 32, {
22221
22951
  N: SCRYPT_N2,
22222
22952
  maxmem: 64 * 1024 * 1024
22223
22953
  });
@@ -22225,7 +22955,7 @@ function passwordKey(password, salt) {
22225
22955
  function wrapKey(machineKey, pwKey) {
22226
22956
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
22227
22957
  return Buffer.from(
22228
- import_crypto7.default.hkdfSync(
22958
+ import_crypto9.default.hkdfSync(
22229
22959
  "sha256",
22230
22960
  ikm,
22231
22961
  Buffer.alloc(0),
@@ -22235,8 +22965,8 @@ function wrapKey(machineKey, pwKey) {
22235
22965
  );
22236
22966
  }
22237
22967
  function seal(plaintext, key) {
22238
- const iv = import_crypto7.default.randomBytes(12);
22239
- const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
22968
+ const iv = import_crypto9.default.randomBytes(12);
22969
+ const cipher = import_crypto9.default.createCipheriv("aes-256-gcm", key, iv);
22240
22970
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
22241
22971
  return {
22242
22972
  iv: iv.toString("base64"),
@@ -22246,7 +22976,7 @@ function seal(plaintext, key) {
22246
22976
  }
22247
22977
  function unseal(box, key) {
22248
22978
  try {
22249
- const decipher = import_crypto7.default.createDecipheriv(
22979
+ const decipher = import_crypto9.default.createDecipheriv(
22250
22980
  "aes-256-gcm",
22251
22981
  key,
22252
22982
  Buffer.from(box.iv, "base64")
@@ -22263,7 +22993,7 @@ function unseal(box, key) {
22263
22993
  function readVaultFile() {
22264
22994
  try {
22265
22995
  const parsed = JSON.parse(
22266
- import_fs6.default.readFileSync(VAULT_KEY_FILE, "utf-8")
22996
+ import_fs7.default.readFileSync(VAULT_KEY_FILE, "utf-8")
22267
22997
  );
22268
22998
  return parsed.v === 1 ? parsed : null;
22269
22999
  } catch {
@@ -22271,15 +23001,15 @@ function readVaultFile() {
22271
23001
  }
22272
23002
  }
22273
23003
  function writeVaultFile(file) {
22274
- import_fs6.default.mkdirSync(CONFIG_DIR, { recursive: true });
23004
+ import_fs7.default.mkdirSync(CONFIG_DIR, { recursive: true });
22275
23005
  const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
22276
- import_fs6.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
23006
+ import_fs7.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
22277
23007
  `, { mode: 384 });
22278
- import_fs6.default.renameSync(tmp, VAULT_KEY_FILE);
23008
+ import_fs7.default.renameSync(tmp, VAULT_KEY_FILE);
22279
23009
  }
22280
23010
  var unlockedDek = null;
22281
23011
  function createVault(machineKey) {
22282
- const dek = import_crypto7.default.randomBytes(32);
23012
+ const dek = import_crypto9.default.randomBytes(32);
22283
23013
  writeVaultFile({
22284
23014
  v: 1,
22285
23015
  passwordEnabled: false,
@@ -22374,7 +23104,7 @@ function enablePassword(password) {
22374
23104
  );
22375
23105
  }
22376
23106
  const dek = dataKey();
22377
- const salt = import_crypto7.default.randomBytes(16);
23107
+ const salt = import_crypto9.default.randomBytes(16);
22378
23108
  writeVaultFile({
22379
23109
  v: 1,
22380
23110
  passwordEnabled: true,
@@ -22402,7 +23132,7 @@ function changePassword(current, next) {
22402
23132
  "The current password is not correct."
22403
23133
  );
22404
23134
  }
22405
- const salt = import_crypto7.default.randomBytes(16);
23135
+ const salt = import_crypto9.default.randomBytes(16);
22406
23136
  writeVaultFile({
22407
23137
  v: 1,
22408
23138
  passwordEnabled: true,
@@ -22479,8 +23209,8 @@ function storeStatus() {
22479
23209
  // src/secureFile.ts
22480
23210
  var MAGIC = "FACTIII1";
22481
23211
  function encrypt(plain, key, version) {
22482
- const iv = import_crypto8.default.randomBytes(12);
22483
- const cipher = import_crypto8.default.createCipheriv("aes-256-gcm", key, iv);
23212
+ const iv = import_crypto10.default.randomBytes(12);
23213
+ const cipher = import_crypto10.default.createCipheriv("aes-256-gcm", key, iv);
22484
23214
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
22485
23215
  const envelope = {
22486
23216
  v: version,
@@ -22491,7 +23221,7 @@ function encrypt(plain, key, version) {
22491
23221
  return `${MAGIC}${JSON.stringify(envelope)}`;
22492
23222
  }
22493
23223
  function decrypt(envelope, key) {
22494
- const decipher = import_crypto8.default.createDecipheriv(
23224
+ const decipher = import_crypto10.default.createDecipheriv(
22495
23225
  "aes-256-gcm",
22496
23226
  key,
22497
23227
  Buffer.from(envelope.iv, "base64")
@@ -22512,15 +23242,15 @@ function parseEnvelope(raw) {
22512
23242
  }
22513
23243
  }
22514
23244
  function atomicWrite(filePath, body) {
22515
- import_fs7.default.mkdirSync(import_path18.default.dirname(filePath), { recursive: true });
23245
+ import_fs8.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
22516
23246
  const tmp = `${filePath}.${process.pid}.tmp`;
22517
- import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
22518
- import_fs7.default.renameSync(tmp, filePath);
23247
+ import_fs8.default.writeFileSync(tmp, body, { mode: 384 });
23248
+ import_fs8.default.renameSync(tmp, filePath);
22519
23249
  }
22520
23250
  function readSecureBuffer(filePath) {
22521
23251
  let raw;
22522
23252
  try {
22523
- raw = import_fs7.default.readFileSync(filePath, "utf-8");
23253
+ raw = import_fs8.default.readFileSync(filePath, "utf-8");
22524
23254
  } catch {
22525
23255
  return null;
22526
23256
  }
@@ -22577,7 +23307,7 @@ function writeSecureJson(filePath, value2) {
22577
23307
  function readBootJson(filePath) {
22578
23308
  let raw;
22579
23309
  try {
22580
- raw = import_fs7.default.readFileSync(filePath, "utf-8");
23310
+ raw = import_fs8.default.readFileSync(filePath, "utf-8");
22581
23311
  } catch {
22582
23312
  return null;
22583
23313
  }
@@ -22627,8 +23357,8 @@ function writeBootJson(filePath, value2) {
22627
23357
  }
22628
23358
 
22629
23359
  // src/config.ts
22630
- var CONFIG_DIR2 = import_path19.default.join(import_os6.default.homedir(), ".factiii-runner");
22631
- var CONFIG_FILE = import_path19.default.join(CONFIG_DIR2, "config.json");
23360
+ var CONFIG_DIR2 = import_path21.default.join(import_os6.default.homedir(), ".factiii-runner");
23361
+ var CONFIG_FILE = import_path21.default.join(CONFIG_DIR2, "config.json");
22632
23362
  function readRunnerConfig() {
22633
23363
  return readBootJson(CONFIG_FILE);
22634
23364
  }
@@ -22636,7 +23366,7 @@ function writeRunnerConfig(config) {
22636
23366
  writeBootJson(CONFIG_FILE, config);
22637
23367
  }
22638
23368
  function configExists() {
22639
- return import_fs8.default.existsSync(CONFIG_FILE);
23369
+ return import_fs9.default.existsSync(CONFIG_FILE);
22640
23370
  }
22641
23371
 
22642
23372
  // src/promptPassword.ts
@@ -22735,72 +23465,24 @@ function installLatest() {
22735
23465
  stdio: "inherit",
22736
23466
  // On Windows npm is npm.cmd, which spawnSync can't exec without a shell.
22737
23467
  shell: process.platform === "win32"
22738
- });
22739
- if (res.status !== 0) {
22740
- console.error(
22741
- `[update] npm install failed (exit ${res.status ?? "signal"}). Staying on ${currentVersion()}.`
22742
- );
22743
- return false;
22744
- }
22745
- return true;
22746
- }
22747
-
22748
- // src/supervisor.ts
22749
- var import_node_child_process2 = require("node:child_process");
22750
-
22751
- // ../../shared/all/helpers/wireChunk.ts
22752
- var MAX_INLINE_BYTES = 14e3;
22753
- var chunkCounter = 0;
22754
- function nextChunkId() {
22755
- return `c-${++chunkCounter}-${Date.now()}`;
22756
- }
22757
- function chunkWire(wire) {
22758
- if (wire.length <= MAX_INLINE_BYTES) return [wire];
22759
- const id = nextChunkId();
22760
- const total = Math.ceil(wire.length / MAX_INLINE_BYTES);
22761
- const frames = [];
22762
- for (let i = 0; i < total; i++) {
22763
- const start = i * MAX_INLINE_BYTES;
22764
- const slice = wire.slice(start, start + MAX_INLINE_BYTES);
22765
- const frame = { __c: id, __i: i, __n: total, d: slice };
22766
- frames.push(JSON.stringify(frame));
23468
+ });
23469
+ if (res.status !== 0) {
23470
+ console.error(
23471
+ `[update] npm install failed (exit ${res.status ?? "signal"}). Staying on ${currentVersion()}.`
23472
+ );
23473
+ return false;
22767
23474
  }
22768
- return frames;
22769
- }
22770
- function makeChunkReassembler() {
22771
- const buffers = /* @__PURE__ */ new Map();
22772
- return {
22773
- feed(raw) {
22774
- if (!raw.startsWith('{"__c"')) return raw;
22775
- let frame;
22776
- try {
22777
- frame = JSON.parse(raw);
22778
- } catch {
22779
- return raw;
22780
- }
22781
- if (typeof frame.__c !== "string" || typeof frame.__i !== "number" || typeof frame.__n !== "number" || typeof frame.d !== "string") {
22782
- return raw;
22783
- }
22784
- let buf = buffers.get(frame.__c);
22785
- if (!buf) {
22786
- buf = { received: new Array(frame.__n), total: frame.__n };
22787
- buffers.set(frame.__c, buf);
22788
- }
22789
- buf.received[frame.__i] = frame.d;
22790
- for (let i = 0; i < buf.total; i++) {
22791
- if (buf.received[i] === void 0) return null;
22792
- }
22793
- buffers.delete(frame.__c);
22794
- return buf.received.join("");
22795
- }
22796
- };
23475
+ return true;
22797
23476
  }
22798
23477
 
23478
+ // src/supervisor.ts
23479
+ var import_node_child_process2 = require("node:child_process");
23480
+
22799
23481
  // src/daemon.ts
22800
- var import_crypto9 = require("crypto");
22801
- var import_fs10 = __toESM(require("fs"));
23482
+ var import_crypto11 = require("crypto");
23483
+ var import_fs11 = __toESM(require("fs"));
22802
23484
  var import_node_datachannel = require("node-datachannel");
22803
- var import_path21 = __toESM(require("path"));
23485
+ var import_path23 = __toESM(require("path"));
22804
23486
 
22805
23487
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
22806
23488
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23980,12 +24662,12 @@ function parse2(str) {
23980
24662
  uri.queryKey = queryKey(uri, uri["query"]);
23981
24663
  return uri;
23982
24664
  }
23983
- function pathNames(obj, path21) {
23984
- const regx = /\/{2,9}/g, names = path21.replace(regx, "/").split("/");
23985
- if (path21.slice(0, 1) == "/" || path21.length === 0) {
24665
+ function pathNames(obj, path23) {
24666
+ const regx = /\/{2,9}/g, names = path23.replace(regx, "/").split("/");
24667
+ if (path23.slice(0, 1) == "/" || path23.length === 0) {
23986
24668
  names.splice(0, 1);
23987
24669
  }
23988
- if (path21.slice(-1) == "/") {
24670
+ if (path23.slice(-1) == "/") {
23989
24671
  names.splice(names.length - 1, 1);
23990
24672
  }
23991
24673
  return names;
@@ -24604,7 +25286,7 @@ var protocol2 = Socket.protocol;
24604
25286
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
24605
25287
  var import_debug7 = __toESM(require_src(), 1);
24606
25288
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
24607
- function url(uri, path21 = "", loc) {
25289
+ function url(uri, path23 = "", loc) {
24608
25290
  let obj = uri;
24609
25291
  loc = loc || typeof location !== "undefined" && location;
24610
25292
  if (null == uri)
@@ -24638,7 +25320,7 @@ function url(uri, path21 = "", loc) {
24638
25320
  obj.path = obj.path || "/";
24639
25321
  const ipv6 = obj.host.indexOf(":") !== -1;
24640
25322
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
24641
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path21;
25323
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path23;
24642
25324
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
24643
25325
  return obj;
24644
25326
  }
@@ -26272,8 +26954,8 @@ function lookup(uri, opts) {
26272
26954
  const parsed = url(uri, opts.path || "/socket.io");
26273
26955
  const source = parsed.source;
26274
26956
  const id = parsed.id;
26275
- const path21 = parsed.path;
26276
- const sameNamespace = cache[id] && path21 in cache[id]["nsps"];
26957
+ const path23 = parsed.path;
26958
+ const sameNamespace = cache[id] && path23 in cache[id]["nsps"];
26277
26959
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
26278
26960
  let io;
26279
26961
  if (newConnection) {
@@ -26298,205 +26980,22 @@ Object.assign(lookup, {
26298
26980
  connect: lookup
26299
26981
  });
26300
26982
 
26301
- // ../../shared/all/helpers/terminal-ai/terminal.ts
26302
- function terminalPrelude(cwd, binDir) {
26303
- return [
26304
- // cd in-script, falling back to $HOME. Must match the agent's worktree:
26305
- // the CLIs key transcripts by cwd, so --resume needs it.
26306
- `cd ${cwd} 2>/dev/null || cd "$HOME" 2>/dev/null || cd /`,
26307
- // Prepended here rather than via the session env, so the login shell's
26308
- // own PATH survives.
26309
- `export PATH="${binDir}:$PATH"`,
26310
- `export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`
26311
- ].join("; ");
26312
- }
26313
- var SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;
26314
- function cardContextPath(target, postId) {
26315
- const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
26316
- return `${target.paths.root}/.cards/${safe}.md`;
26317
- }
26318
- function cardContextPrompt(path21) {
26319
- return `Read ${path21} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
26320
- }
26321
- var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
26322
- var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
26323
- function terminalInitScript(cwd, binDir, resumeAgentSessionId, provider, cardFile) {
26324
- const prelude = terminalPrelude(cwd, binDir);
26325
- const shell = `exec bash --norc -i`;
26326
- if (!resumeAgentSessionId || !SAFE_SESSION_ID.test(resumeAgentSessionId)) {
26327
- if (!cardFile) return `${prelude}; ${shell}`;
26328
- const prompt2 = cardContextPrompt(cardFile);
26329
- const start = provider === "codex" ? `codex '${prompt2}'` : `claude ${CLAUDE_PERMISSION_MODE} '${prompt2}'`;
26330
- return `${prelude}; ${start}; ${shell}`;
26331
- }
26332
- const resume = provider === "codex" ? `codex resume ${resumeAgentSessionId} '${RESUME_SEED}'` : `claude ${CLAUDE_PERMISSION_MODE} --resume ${resumeAgentSessionId} '${RESUME_SEED}'`;
26333
- return `${prelude}; ${resume}; ${shell}`;
26983
+ // src/agent-adapter.ts
26984
+ var import_fs10 = __toESM(require("fs"));
26985
+ var import_path22 = __toESM(require("path"));
26986
+ function credsDir(spaceSlug) {
26987
+ return import_path22.default.join(spaceDirPath(spaceSlug), ".creds");
26334
26988
  }
26335
- function tmuxName(terminalKey) {
26336
- return `terminal-${safeTmuxName(terminalKey)}`;
26989
+ function githubPath(spaceSlug) {
26990
+ return import_path22.default.join(credsDir(spaceSlug), "github.json");
26337
26991
  }
26338
- var TerminalManager = class {
26339
- constructor(send, getCardTarget, getCardWorkdir) {
26340
- this.send = send;
26341
- this.getCardTarget = getCardTarget;
26342
- this.getCardWorkdir = getCardWorkdir;
26343
- this.sessions = /* @__PURE__ */ new Map();
26344
- }
26345
- handle(msg) {
26346
- switch (msg.op) {
26347
- case "open":
26348
- void this.open(msg);
26349
- return;
26350
- case "stdin":
26351
- this.stdin(msg.sessionId, msg.data);
26352
- return;
26353
- case "resize":
26354
- void this.resize(msg.sessionId, msg.cols, msg.rows);
26355
- return;
26356
- case "close":
26357
- this.close(msg.sessionId);
26358
- return;
26359
- case "kill":
26360
- void this.killTmuxSession(msg.postId, msg.terminalKey);
26361
- return;
26362
- }
26363
- }
26364
- async open(msg) {
26365
- if (this.sessions.has(msg.sessionId)) {
26366
- this.emitError(msg.sessionId, "Session already open.");
26367
- return;
26368
- }
26369
- const target = this.getCardTarget(msg.postId);
26370
- if (!target) {
26371
- this.emitError(
26372
- msg.sessionId,
26373
- `No active card session for postId=${msg.postId}.`
26374
- );
26375
- return;
26376
- }
26377
- const tmuxSession = tmuxName(msg.terminalKey);
26378
- const wantsCardContext = !msg.resumeAgentSessionId && !!msg.cardContext;
26379
- const cardFile = wantsCardContext ? cardContextPath(target, msg.postId) : void 0;
26380
- let wroteCardFile = false;
26381
- if (cardFile) {
26382
- wroteCardFile = await target.writeFile(cardFile, msg.cardContext).then(() => true).catch(() => false);
26383
- }
26384
- try {
26385
- const session = await startTmuxSession({
26386
- target,
26387
- session: tmuxSession,
26388
- // Attach if it exists, else create. This is what makes the shell
26389
- // survive modal close/reopen: the server holds it, we just view.
26390
- attachExisting: true,
26391
- size: { cols: msg.cols, rows: msg.rows },
26392
- // Without these, `claude` in the shell reads the default ~/.claude and
26393
- // asks to sign in, while the agent spawns (which pass agentEnv) don't.
26394
- env: target.stateEnv(),
26395
- argv: [
26396
- "bash",
26397
- "-c",
26398
- terminalInitScript(
26399
- this.getCardWorkdir(msg.postId) || target.paths.workspace,
26400
- target.paths.bin,
26401
- msg.resumeAgentSessionId,
26402
- msg.resumeProvider,
26403
- wroteCardFile ? cardFile : void 0
26404
- )
26405
- ]
26406
- });
26407
- this.sessions.set(msg.sessionId, session);
26408
- session.onData(
26409
- (data) => this.emit({
26410
- type: "terminal",
26411
- op: "stdout",
26412
- sessionId: msg.sessionId,
26413
- data: data.toString("base64")
26414
- })
26415
- );
26416
- session.onError((err) => {
26417
- this.cleanup(msg.sessionId);
26418
- this.emitError(msg.sessionId, err.message);
26419
- });
26420
- session.onClose((code) => {
26421
- if (!this.sessions.has(msg.sessionId)) return;
26422
- this.cleanup(msg.sessionId);
26423
- this.emit({
26424
- type: "terminal",
26425
- op: "exit",
26426
- sessionId: msg.sessionId,
26427
- code
26428
- });
26429
- });
26430
- this.emit({ type: "terminal", op: "opened", sessionId: msg.sessionId });
26431
- } catch (err) {
26432
- this.emitError(
26433
- msg.sessionId,
26434
- err instanceof Error ? err.message : String(err)
26435
- );
26436
- }
26437
- }
26438
- stdin(sessionId, b64) {
26439
- this.sessions.get(sessionId)?.write(Buffer.from(b64, "base64"));
26440
- }
26441
- async resize(sessionId, cols, rows) {
26442
- const session = this.sessions.get(sessionId);
26443
- if (!session) return;
26444
- if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
26445
- if (cols < 1 || rows < 1) return;
26446
- await session.resize(cols, rows).catch(() => {
26447
- });
26448
- }
26449
- close(sessionId) {
26450
- const session = this.sessions.get(sessionId);
26451
- if (!session) return;
26452
- this.sessions.delete(sessionId);
26453
- session.detach();
26454
- }
26455
- /** Tear down the tmux session for a tab. Used when the user explicitly
26456
- * removes a tab (the X button), not on transient detach. */
26457
- async killTmuxSession(postId, terminalKey) {
26458
- const target = this.getCardTarget(postId);
26459
- if (!target) return;
26460
- await target.sh(
26461
- `${tmuxCli(target)} kill-session -t ${tmuxName(terminalKey)} 2>/dev/null || true`
26462
- ).catch(() => {
26463
- });
26464
- }
26465
- destroyAll() {
26466
- for (const sessionId of [...this.sessions.keys()]) {
26467
- this.close(sessionId);
26468
- }
26469
- }
26470
- cleanup(sessionId) {
26471
- this.sessions.delete(sessionId);
26472
- }
26473
- emitError(sessionId, message) {
26474
- this.emit({ type: "terminal", op: "error", sessionId, message });
26475
- }
26476
- emit(msg) {
26477
- const wire = JSON.stringify(msg);
26478
- for (const frame of chunkWire(wire)) this.send(frame);
26479
- }
26480
- };
26481
-
26482
- // src/agent-adapter.ts
26483
- var import_fs9 = __toESM(require("fs"));
26484
- var import_path20 = __toESM(require("path"));
26485
- var RUNNER_CONFIG_PATH = import_path20.default.join(CONFIG_DIR, "runner-config.json");
26486
26992
  function connectionPath(spaceSlug) {
26487
- return import_path20.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26993
+ return import_path22.default.join(credsDir(spaceSlug), "onedrive.json");
26488
26994
  }
26995
+ var RUNNER_CONFIG_PATH = import_path22.default.join(CONFIG_DIR, "runner-config.json");
26489
26996
  function writeJson(filePath, value2) {
26490
26997
  writeSecureJson(filePath, value2);
26491
26998
  }
26492
- var CONFIG_FIELDS = [
26493
- "repoUrl",
26494
- "mainBranch",
26495
- "gitName",
26496
- "gitEmail",
26497
- "githubToken",
26498
- "provider"
26499
- ];
26500
26999
  function fillDefaults(config) {
26501
27000
  return {
26502
27001
  repoUrl: config.repoUrl || "",
@@ -26507,29 +27006,107 @@ function fillDefaults(config) {
26507
27006
  provider: config.provider || "claude"
26508
27007
  };
26509
27008
  }
26510
- function migrateLegacyConfigs() {
26511
- const file = { boards: {} };
26512
- let legacy = [];
27009
+ function configDirNames() {
27010
+ try {
27011
+ return import_fs10.default.readdirSync(CONFIG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
27012
+ } catch {
27013
+ return [];
27014
+ }
27015
+ }
27016
+ function provisionedSpaceSlugs() {
27017
+ return configDirNames().filter(
27018
+ (name) => import_fs10.default.existsSync(import_path22.default.join(CONFIG_DIR, name, "bin"))
27019
+ );
27020
+ }
27021
+ var migrated = false;
27022
+ function migrateToSpaceFolders() {
27023
+ if (migrated) return;
27024
+ migrated = true;
27025
+ try {
27026
+ migrateOnce();
27027
+ } catch {
27028
+ migrated = false;
27029
+ }
27030
+ }
27031
+ function readLegacy(filePath) {
27032
+ if (!import_fs10.default.existsSync(filePath)) return { readable: true, value: null };
27033
+ const value2 = readSecureJson(filePath);
27034
+ return { readable: value2 !== null, value: value2 };
27035
+ }
27036
+ function migrateOnce() {
27037
+ const legacy = readLegacy(RUNNER_CONFIG_PATH);
27038
+ if (!legacy.readable) {
27039
+ throw new Error(
27040
+ `Cannot read ${RUNNER_CONFIG_PATH} \u2014 leaving the legacy stores in place.`
27041
+ );
27042
+ }
27043
+ const store = legacy.value ?? {};
27044
+ const base = store.default ?? {};
27045
+ const boards = store.boards ?? {};
27046
+ const strays = {};
27047
+ const consumed = [];
27048
+ let strayFiles = [];
26513
27049
  try {
26514
- legacy = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
27050
+ strayFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
26515
27051
  } catch {
26516
- return file;
27052
+ strayFiles = [];
26517
27053
  }
26518
- for (const name of legacy) {
27054
+ for (const name of strayFiles) {
26519
27055
  const slug = name.slice("space-".length, -".json".length);
26520
- const parsed = readSecureJson(
26521
- import_path20.default.join(CONFIG_DIR, name)
27056
+ const parsed = readLegacy(
27057
+ import_path22.default.join(CONFIG_DIR, name)
26522
27058
  );
26523
- if (!parsed) continue;
26524
- if (!file.defaultSlug) {
26525
- file.defaultSlug = slug;
26526
- file.default = parsed;
26527
- } else {
26528
- file.boards[slug] = parsed;
27059
+ if (!parsed.readable) continue;
27060
+ if (parsed.value) strays[slug] = parsed.value;
27061
+ consumed.push(name);
27062
+ }
27063
+ const slugs = /* @__PURE__ */ new Set([
27064
+ ...Object.keys(boards),
27065
+ ...Object.keys(strays),
27066
+ ...store.defaultSlug ? [store.defaultSlug] : [],
27067
+ ...provisionedSpaceSlugs()
27068
+ ]);
27069
+ for (const slug of slugs) {
27070
+ if (import_fs10.default.existsSync(githubPath(slug))) continue;
27071
+ const over = boards[slug] ?? strays[slug] ?? {};
27072
+ if (!Object.keys(over).length && !Object.keys(base).length) continue;
27073
+ const merged = fillDefaults({
27074
+ repoUrl: over.repoUrl || base.repoUrl,
27075
+ mainBranch: over.mainBranch || base.mainBranch,
27076
+ gitName: over.gitName || base.gitName,
27077
+ gitEmail: over.gitEmail || base.gitEmail,
27078
+ githubToken: over.githubToken || base.githubToken,
27079
+ provider: over.provider || base.provider
27080
+ });
27081
+ import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27082
+ writeJson(githubPath(slug), merged);
27083
+ }
27084
+ let odFiles = [];
27085
+ try {
27086
+ odFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".onedrive.json"));
27087
+ } catch {
27088
+ odFiles = [];
27089
+ }
27090
+ for (const name of odFiles) {
27091
+ const slug = name.slice("space-".length, -".onedrive.json".length);
27092
+ if (import_fs10.default.existsSync(connectionPath(slug))) {
27093
+ consumed.push(name);
27094
+ continue;
27095
+ }
27096
+ const conn = readLegacy(import_path22.default.join(CONFIG_DIR, name));
27097
+ if (!conn.readable) continue;
27098
+ if (conn.value) {
27099
+ import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27100
+ writeJson(connectionPath(slug), conn.value);
27101
+ }
27102
+ consumed.push(name);
27103
+ }
27104
+ for (const name of [...consumed, "runner-config.json"]) {
27105
+ try {
27106
+ import_fs10.default.rmSync(import_path22.default.join(CONFIG_DIR, name), { force: true });
27107
+ } catch {
26529
27108
  }
26530
27109
  }
26531
- if (file.defaultSlug) writeJson(RUNNER_CONFIG_PATH, file);
26532
- return file;
26533
27110
  }
26534
27111
  var sweptStrays = false;
26535
27112
  function encryptStrayPlaintextConfigs() {
@@ -26538,14 +27115,14 @@ function encryptStrayPlaintextConfigs() {
26538
27115
  const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
26539
27116
  let names = [];
26540
27117
  try {
26541
- names = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
27118
+ names = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
26542
27119
  } catch {
26543
27120
  return;
26544
27121
  }
26545
27122
  for (const name of names) {
26546
- const filePath = import_path20.default.join(CONFIG_DIR, name);
27123
+ const filePath = import_path22.default.join(CONFIG_DIR, name);
26547
27124
  try {
26548
- const raw = import_fs9.default.readFileSync(filePath, "utf-8");
27125
+ const raw = import_fs10.default.readFileSync(filePath, "utf-8");
26549
27126
  if (!raw.startsWith("{")) continue;
26550
27127
  writeSecureJson(filePath, JSON.parse(raw));
26551
27128
  } catch {
@@ -26557,84 +27134,50 @@ function encryptStrayPlaintextConfigs() {
26557
27134
  function resetStraySweep() {
26558
27135
  sweptStrays = false;
26559
27136
  }
26560
- function readRunnerConfig2() {
27137
+ function readBoard(spaceSlug) {
26561
27138
  encryptStrayPlaintextConfigs();
26562
- const file = readSecureJson(RUNNER_CONFIG_PATH);
26563
- return file ?? migrateLegacyConfigs();
27139
+ migrateToSpaceFolders();
27140
+ return readSecureJson(githubPath(spaceSlug)) ?? {};
26564
27141
  }
26565
27142
  async function getConfiguredConnections() {
26566
27143
  const kinds = /* @__PURE__ */ new Set();
26567
- let file;
26568
27144
  try {
26569
- file = readRunnerConfig2();
27145
+ for (const slug of listConfiguredSlugs()) {
27146
+ if (kinds.has("git") && kinds.has("onedrive")) break;
27147
+ if (readBoard(slug).githubToken) kinds.add("git");
27148
+ if (import_fs10.default.existsSync(connectionPath(slug))) kinds.add("onedrive");
27149
+ }
26570
27150
  } catch {
26571
27151
  return [];
26572
27152
  }
26573
- const configs = [file.default ?? {}, ...Object.values(file.boards ?? {})];
26574
- for (const c of configs) {
26575
- if (c.githubToken) kinds.add("git");
26576
- }
26577
27153
  if (await isSignedIn(hostTarget(CONFIG_DIR), "claude")) kinds.add("claude");
26578
- try {
26579
- const hasOneDrive = import_fs9.default.readdirSync(CONFIG_DIR).some((f) => f.endsWith(".onedrive.json"));
26580
- if (hasOneDrive) kinds.add("onedrive");
26581
- } catch {
26582
- }
26583
27154
  return [...kinds];
26584
27155
  }
26585
27156
  function listConfiguredSlugs() {
26586
27157
  try {
26587
- const file = readRunnerConfig2();
26588
- return [
26589
- .../* @__PURE__ */ new Set([
26590
- ...Object.keys(file.boards ?? {}),
26591
- ...file.defaultSlug ? [file.defaultSlug] : []
26592
- ])
26593
- ];
27158
+ migrateToSpaceFolders();
27159
+ return configDirNames().filter(
27160
+ (slug) => import_fs10.default.existsSync(githubPath(slug)) || import_fs10.default.existsSync(connectionPath(slug))
27161
+ );
26594
27162
  } catch {
26595
27163
  return [];
26596
27164
  }
26597
27165
  }
26598
27166
  var localConfigProvider = {
26599
27167
  readConfig(spaceSlug) {
26600
- let file;
26601
27168
  try {
26602
- file = readRunnerConfig2();
27169
+ return fillDefaults(readBoard(spaceSlug));
26603
27170
  } catch (err) {
26604
27171
  if (!isSecureStoreError(err)) throw err;
26605
27172
  return fillDefaults({});
26606
27173
  }
26607
- const base = file.default ?? {};
26608
- const over = file.boards?.[spaceSlug] ?? {};
26609
- return fillDefaults({
26610
- repoUrl: over.repoUrl || base.repoUrl,
26611
- mainBranch: over.mainBranch || base.mainBranch,
26612
- gitName: over.gitName || base.gitName,
26613
- gitEmail: over.gitEmail || base.gitEmail,
26614
- githubToken: over.githubToken || base.githubToken,
26615
- provider: over.provider || base.provider
26616
- });
26617
27174
  },
27175
+ // The whole config, every time: a cleared field has to persist as cleared,
27176
+ // which is what makes disconnect stick.
26618
27177
  writeConfig(spaceSlug, config) {
26619
- const file = readRunnerConfig2();
26620
- file.boards ??= {};
26621
- if (!file.defaultSlug) {
26622
- file.defaultSlug = spaceSlug;
26623
- file.default = config;
26624
- } else if (spaceSlug === file.defaultSlug) {
26625
- file.default = config;
26626
- } else {
26627
- const base = file.default ?? {};
26628
- const diff = {};
26629
- for (const f of CONFIG_FIELDS) {
26630
- if (config[f] && config[f] !== base[f]) {
26631
- diff[f] = config[f];
26632
- }
26633
- }
26634
- if (Object.keys(diff).length) file.boards[spaceSlug] = diff;
26635
- else delete file.boards[spaceSlug];
26636
- }
26637
- writeJson(RUNNER_CONFIG_PATH, file);
27178
+ migrateToSpaceFolders();
27179
+ import_fs10.default.mkdirSync(credsDir(spaceSlug), { recursive: true });
27180
+ writeJson(githubPath(spaceSlug), config);
26638
27181
  },
26639
27182
  readOneDriveConnection(spaceSlug) {
26640
27183
  try {
@@ -26647,7 +27190,7 @@ var localConfigProvider = {
26647
27190
  writeOneDriveConnection(spaceSlug, connection) {
26648
27191
  const filePath = connectionPath(spaceSlug);
26649
27192
  if (!connection) {
26650
- import_fs9.default.rmSync(filePath, { force: true });
27193
+ import_fs10.default.rmSync(filePath, { force: true });
26651
27194
  return;
26652
27195
  }
26653
27196
  writeJson(filePath, connection);
@@ -26672,7 +27215,7 @@ var localConfigProvider = {
26672
27215
  "claude",
26673
27216
  "codex"
26674
27217
  ]) {
26675
- import_fs9.default.mkdirSync(import_path20.default.join(dir, sub), { recursive: true });
27218
+ import_fs10.default.mkdirSync(import_path22.default.join(dir, sub), { recursive: true });
26676
27219
  }
26677
27220
  return dir;
26678
27221
  }
@@ -26846,15 +27389,15 @@ async function startDaemon(config) {
26846
27389
  }
26847
27390
  );
26848
27391
  const indexCandidates = [
26849
- import_path21.default.join(__dirname, "..", "index"),
26850
- import_path21.default.join(__dirname, "..", "..", "index", "image")
27392
+ import_path23.default.join(__dirname, "..", "index"),
27393
+ import_path23.default.join(__dirname, "..", "..", "index", "image")
26851
27394
  ];
26852
- const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
27395
+ const indexDir = indexCandidates.find((p) => import_fs11.default.existsSync(p)) ?? indexCandidates[0];
26853
27396
  const odIndex = new IndexService({
26854
- controlToken: (0, import_crypto9.createHash)("sha256").update(config.authToken).digest("hex"),
27397
+ controlToken: (0, import_crypto11.createHash)("sha256").update(config.authToken).digest("hex"),
26855
27398
  embedUrl: () => config.serverUrl,
26856
27399
  indexDir,
26857
- dataDir: import_path21.default.join(CONFIG_DIR, "index-data")
27400
+ dataDir: import_path23.default.join(CONFIG_DIR, "index-data")
26858
27401
  });
26859
27402
  void odIndex.ensureRunning().catch((err) => {
26860
27403
  console.error(
@@ -26951,7 +27494,9 @@ async function startDaemon(config) {
26951
27494
  // Board MCP is server-hosted, so the base URL is all it needs.
26952
27495
  serverUrl: config.serverUrl,
26953
27496
  // Point the agent at the index MCP endpoint (lazy for the port).
26954
- mcp: () => odIndex.mcpBridge(spaceSlug)
27497
+ mcp: () => odIndex.mcpBridge(spaceSlug),
27498
+ // The bearer alone, available before the service has a port for a URL.
27499
+ mcpToken: () => odIndex.bearerToken(spaceSlug)
26955
27500
  });
26956
27501
  engines.set(spaceSlug, engine);
26957
27502
  void engine.reattachAndHydrate();