@factiii/runner 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  /**
@@ -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,313 @@ 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_fs2 = require("fs");
13809
+ var import_promises2 = __toESM(require("fs/promises"));
13810
+ var import_net = __toESM(require("net"));
13811
+ var import_path4 = __toESM(require("path"));
13812
+ var import_util4 = require("util");
13545
13813
  var execFileAsync = (0, import_util4.promisify)(import_child_process2.execFile);
13546
13814
  var MAX_BUFFER = 10 * 1024 * 1024;
13547
13815
  var PORTS_FILE = ".ports";
13548
13816
  var NUM_PORTS = 6;
13549
13817
  function portsFilePath(spaceDir) {
13550
- return import_path3.default.join(spaceDir, PORTS_FILE);
13818
+ return import_path4.default.join(spaceDir, PORTS_FILE);
13551
13819
  }
13552
13820
  async function allocateSpacePorts(spaceDir) {
13553
13821
  const existing = readSpacePorts(spaceDir);
@@ -13573,19 +13841,19 @@ async function allocateSpacePorts(spaceDir) {
13573
13841
  seen.add(port);
13574
13842
  ports.push(port);
13575
13843
  }
13576
- await import_promises.default.mkdir(spaceDir, { recursive: true });
13577
- await import_promises.default.writeFile(portsFilePath(spaceDir), ports.join(","), "utf-8");
13844
+ await import_promises2.default.mkdir(spaceDir, { recursive: true });
13845
+ await import_promises2.default.writeFile(portsFilePath(spaceDir), ports.join(","), "utf-8");
13578
13846
  return ports;
13579
13847
  }
13580
13848
  function readSpacePorts(spaceDir) {
13581
13849
  try {
13582
- return (0, import_fs.readFileSync)(portsFilePath(spaceDir), "utf-8").split(",").map((p) => Number(p.trim())).filter((p) => Number.isInteger(p) && p > 0);
13850
+ return (0, import_fs2.readFileSync)(portsFilePath(spaceDir), "utf-8").split(",").map((p) => Number(p.trim())).filter((p) => Number.isInteger(p) && p > 0);
13583
13851
  } catch {
13584
13852
  return [];
13585
13853
  }
13586
13854
  }
13587
13855
  function usableCwd(dir) {
13588
- return dir && (0, import_fs.existsSync)(dir) ? dir : void 0;
13856
+ return dir && (0, import_fs2.existsSync)(dir) ? dir : void 0;
13589
13857
  }
13590
13858
  function hostTarget(spaceDir, extraEnv) {
13591
13859
  const paths = hostSpacePaths(spaceDir);
@@ -13651,8 +13919,8 @@ function hostTarget(spaceDir, extraEnv) {
13651
13919
  });
13652
13920
  },
13653
13921
  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");
13922
+ await import_promises2.default.mkdir(import_path4.default.dirname(filePath), { recursive: true });
13923
+ await import_promises2.default.writeFile(filePath, content, "utf-8");
13656
13924
  },
13657
13925
  async step(cmd, opts) {
13658
13926
  const res = await execLogin(cmd, {
@@ -13867,14 +14135,14 @@ function createEventSink(send, spaceSlug, notify) {
13867
14135
  }
13868
14136
 
13869
14137
  // ../../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"));
14138
+ var import_fs3 = require("fs");
14139
+ var import_promises6 = __toESM(require("fs/promises"));
14140
+ var import_path8 = __toESM(require("path"));
13873
14141
 
13874
14142
  // ../../shared/all/helpers/board-agent-core/host-skills.ts
13875
- var import_promises4 = __toESM(require("fs/promises"));
14143
+ var import_promises5 = __toESM(require("fs/promises"));
13876
14144
  var import_os2 = __toESM(require("os"));
13877
- var import_path6 = __toESM(require("path"));
14145
+ var import_path7 = __toESM(require("path"));
13878
14146
 
13879
14147
  // ../../shared/all/domains/posts.ts
13880
14148
  var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
@@ -14384,17 +14652,17 @@ var skillScopeSchema = external_exports.enum(["global", "repo"]);
14384
14652
 
14385
14653
  // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14386
14654
  var import_child_process3 = require("child_process");
14387
- var import_promises3 = __toESM(require("fs/promises"));
14388
- var import_path5 = __toESM(require("path"));
14655
+ var import_promises4 = __toESM(require("fs/promises"));
14656
+ var import_path6 = __toESM(require("path"));
14389
14657
  var import_util5 = require("util");
14390
14658
 
14391
14659
  // ../../shared/all/helpers/board-agent-core/secrets-bridge.ts
14392
- var import_promises2 = __toESM(require("fs/promises"));
14660
+ var import_promises3 = __toESM(require("fs/promises"));
14393
14661
  var import_net2 = __toESM(require("net"));
14394
- var import_path4 = __toESM(require("path"));
14662
+ var import_path5 = __toESM(require("path"));
14395
14663
  var SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14396
14664
  function secretsSocketPath(spaceDir) {
14397
- return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14665
+ return import_path5.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14398
14666
  }
14399
14667
  function redactor(values) {
14400
14668
  const significant = values.filter((v) => v.length >= 4);
@@ -14448,8 +14716,8 @@ async function handleRun(request, handlers, reply) {
14448
14716
  }
14449
14717
  async function startSecretsBridge(opts) {
14450
14718
  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 });
14719
+ await import_promises3.default.mkdir(import_path5.default.dirname(socketPath), { recursive: true });
14720
+ await import_promises3.default.rm(socketPath, { force: true });
14453
14721
  const { handlers } = opts;
14454
14722
  let queue = Promise.resolve();
14455
14723
  let queueDepth = 0;
@@ -14514,13 +14782,13 @@ async function startSecretsBridge(opts) {
14514
14782
  resolve2();
14515
14783
  });
14516
14784
  });
14517
- await import_promises2.default.chmod(socketPath, 384).catch(() => void 0);
14785
+ await import_promises3.default.chmod(socketPath, 384).catch(() => void 0);
14518
14786
  return {
14519
14787
  close: async () => {
14520
14788
  for (const conn of live) conn.destroy();
14521
14789
  live.clear();
14522
14790
  await new Promise((resolve2) => server.close(() => resolve2()));
14523
- await import_promises2.default.rm(socketPath, { force: true });
14791
+ await import_promises3.default.rm(socketPath, { force: true });
14524
14792
  }
14525
14793
  };
14526
14794
  }
@@ -14537,14 +14805,14 @@ var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
14537
14805
  var CLAUDE_SKILLS_DIR = ".claude/skills";
14538
14806
  var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
14539
14807
  async function ensureClaudeSkillLink(workspace) {
14540
- const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
14808
+ const linkPath = import_path6.default.join(workspace, CLAUDE_LINK);
14541
14809
  try {
14542
- await import_promises3.default.lstat(linkPath);
14810
+ await import_promises4.default.lstat(linkPath);
14543
14811
  return;
14544
14812
  } catch {
14545
14813
  }
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");
14814
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14815
+ await import_promises4.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14548
14816
  }
14549
14817
  var GIT_NO_HELPER = ["-c", "credential.helper="];
14550
14818
  var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
@@ -14589,7 +14857,7 @@ async function gitCatFileBatch(cwd, shas) {
14589
14857
  return out;
14590
14858
  }
14591
14859
  async function ensureWorkspaceClone(opts) {
14592
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14860
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14593
14861
  const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
14594
14862
  const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
14595
14863
  let cloned = false;
@@ -14597,14 +14865,14 @@ async function ensureWorkspaceClone(opts) {
14597
14865
  await hostGit(workspace, "rev-parse", "--git-dir");
14598
14866
  await hostGit(workspace, "remote", "set-url", "origin", url2);
14599
14867
  } catch {
14600
- const leftovers = await import_promises3.default.readdir(workspace).catch(() => []);
14868
+ const leftovers = await import_promises4.default.readdir(workspace).catch(() => []);
14601
14869
  if (leftovers.includes(".git")) {
14602
14870
  throw new Error(
14603
14871
  `${workspace} holds a git repository that cannot be read. Remove it and retry.`
14604
14872
  );
14605
14873
  }
14606
14874
  for (const name of leftovers) {
14607
- await import_promises3.default.rm(import_path5.default.join(workspace, name), { recursive: true, force: true });
14875
+ await import_promises4.default.rm(import_path6.default.join(workspace, name), { recursive: true, force: true });
14608
14876
  }
14609
14877
  await execFileAsync2(
14610
14878
  "git",
@@ -14756,15 +15024,15 @@ function spaceEnv(spaceDir) {
14756
15024
  };
14757
15025
  }
14758
15026
  async function readContract(workspace) {
14759
- const skill = await import_promises3.default.readFile(import_path5.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
15027
+ const skill = await import_promises4.default.readFile(import_path6.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14760
15028
  throw new Error("The creator wrote no SKILL.md.");
14761
15029
  });
14762
- const checker = await import_promises3.default.readFile(import_path5.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
15030
+ const checker = await import_promises4.default.readFile(import_path6.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14763
15031
  throw new Error("The creator wrote no check-environment.sh.");
14764
15032
  });
14765
15033
  try {
14766
15034
  const raw = JSON.parse(
14767
- await import_promises3.default.readFile(import_path5.default.join(workspace, VARIABLES_PATH), "utf-8")
15035
+ await import_promises4.default.readFile(import_path6.default.join(workspace, VARIABLES_PATH), "utf-8")
14768
15036
  );
14769
15037
  return {
14770
15038
  skill,
@@ -14781,7 +15049,7 @@ async function generateDeploySkill(opts) {
14781
15049
  throw new Error("Describe your deployment process first.");
14782
15050
  }
14783
15051
  const status = await checkDeployStatus(opts);
14784
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15052
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14785
15053
  sendLog({
14786
15054
  type: "system",
14787
15055
  content: `Resetting workspace to origin/${status.branch}...`
@@ -14824,7 +15092,7 @@ async function generateDeploySkill(opts) {
14824
15092
  async function reviseDeploySkill(opts) {
14825
15093
  const { feedback, sendLog } = opts;
14826
15094
  if (!feedback.trim()) throw new Error("Corrections are empty.");
14827
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15095
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14828
15096
  sendLog({ type: "system", content: "Applying your corrections..." });
14829
15097
  const { result } = await spawnAgent(
14830
15098
  opts.provider,
@@ -14852,7 +15120,7 @@ async function loadSkillDraft(opts) {
14852
15120
  throw new Error("Not a deploy-skill branch.");
14853
15121
  }
14854
15122
  await checkDeployStatus(opts);
14855
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15123
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14856
15124
  await hostGit(
14857
15125
  workspace,
14858
15126
  "fetch",
@@ -14864,7 +15132,7 @@ async function loadSkillDraft(opts) {
14864
15132
  return readContract(workspace);
14865
15133
  }
14866
15134
  async function approveDeploySkill(opts) {
14867
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15135
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14868
15136
  await readContract(workspace);
14869
15137
  await ensureClaudeSkillLink(workspace);
14870
15138
  if (opts.gitName)
@@ -14907,19 +15175,19 @@ async function runEnvironmentCheck(opts) {
14907
15175
  "No environment checker on the default branch. Generate the deploy skill first."
14908
15176
  );
14909
15177
  }
14910
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15178
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14911
15179
  const script = await hostGit(
14912
15180
  workspace,
14913
15181
  "cat-file",
14914
15182
  "-p",
14915
15183
  `origin/${status.branch}:${CHECKER_PATH}`
14916
15184
  );
14917
- const tmp = import_path5.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14918
- await import_promises3.default.writeFile(tmp, script, { mode: 448 });
15185
+ const tmp = import_path6.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
15186
+ await import_promises4.default.writeFile(tmp, script, { mode: 448 });
14919
15187
  let ok = true;
14920
15188
  let output = "";
14921
15189
  try {
14922
- const scriptArg = tmp.split(import_path5.default.sep).join("/");
15190
+ const scriptArg = tmp.split(import_path6.default.sep).join("/");
14923
15191
  const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
14924
15192
  cwd: workspace,
14925
15193
  env: { ...process.env, ...spaceEnv(opts.spaceDir) },
@@ -14928,7 +15196,7 @@ async function runEnvironmentCheck(opts) {
14928
15196
  output = res.output;
14929
15197
  if (res.code !== 0) ok = false;
14930
15198
  } finally {
14931
- await import_promises3.default.rm(tmp, { force: true });
15199
+ await import_promises4.default.rm(tmp, { force: true });
14932
15200
  }
14933
15201
  const checks = [];
14934
15202
  for (const line of output.split("\n")) {
@@ -14950,7 +15218,7 @@ async function fixEnvironment(opts) {
14950
15218
  const before = await runEnvironmentCheck(opts);
14951
15219
  if (before.ok) return before;
14952
15220
  const status = await checkDeployStatus(opts);
14953
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15221
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14954
15222
  const script = await hostGit(
14955
15223
  workspace,
14956
15224
  "cat-file",
@@ -14985,7 +15253,7 @@ async function fixEnvironment(opts) {
14985
15253
  return runEnvironmentCheck(opts);
14986
15254
  }
14987
15255
  async function discardDeploySkill(opts) {
14988
- const workspace = import_path5.default.join(opts.spaceDir, "workspace");
15256
+ const workspace = import_path6.default.join(opts.spaceDir, "workspace");
14989
15257
  await hostGit(workspace, "checkout", "--", ".").catch(() => null);
14990
15258
  await hostGit(workspace, "clean", "-fd");
14991
15259
  return { success: true };
@@ -15104,21 +15372,22 @@ var AGENTS_ROOT = ".agents/skills";
15104
15372
  var CLAUDE_ROOT = ".claude/skills";
15105
15373
  var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
15106
15374
  function globalAgentsRoot() {
15107
- return import_path6.default.join(import_os2.default.homedir(), ".agents", "skills");
15375
+ return import_path7.default.join(import_os2.default.homedir(), ".agents", "skills");
15108
15376
  }
15109
15377
  var AGENTS_LINK_TARGET = "../.agents/skills";
15110
15378
  var BRANCH_PREFIX = "skill/";
15379
+ var MIGRATE_BRANCH = `${BRANCH_PREFIX}migrate`;
15111
15380
  var SYMLINK_MODE = "120000";
15112
15381
  function summarize(raw, base) {
15113
15382
  const { frontmatter, errors } = parseSkill(raw);
15114
15383
  return { ...base, description: frontmatter.description, errors };
15115
15384
  }
15116
15385
  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);
15386
+ const linkPath = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15387
+ const stat = await import_promises5.default.lstat(linkPath).catch(() => null);
15119
15388
  if (!stat) return "missing";
15120
15389
  if (!stat.isSymbolicLink()) return "blocked";
15121
- const target = await import_promises4.default.readlink(linkPath).catch(() => "");
15390
+ const target = await import_promises5.default.readlink(linkPath).catch(() => "");
15122
15391
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
15123
15392
  }
15124
15393
  async function globalLinkState() {
@@ -15128,12 +15397,12 @@ async function globalLinkState() {
15128
15397
  }
15129
15398
  async function listGlobal() {
15130
15399
  const root = globalAgentsRoot();
15131
- const entries = await import_promises4.default.readdir(root, { withFileTypes: true }).catch(() => []);
15400
+ const entries = await import_promises5.default.readdir(root, { withFileTypes: true }).catch(() => []);
15132
15401
  const out = [];
15133
15402
  for (const entry of entries) {
15134
15403
  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(() => "");
15404
+ const dir = import_path7.default.join(root, entry.name);
15405
+ const raw = await import_promises5.default.readFile(import_path7.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
15137
15406
  if (!raw) continue;
15138
15407
  out.push(
15139
15408
  summarize(raw, {
@@ -15302,7 +15571,7 @@ async function ensureSkillsWorktree(spaceDir, branch) {
15302
15571
  } catch {
15303
15572
  await hostGit(repo, "worktree", "prune").catch(() => {
15304
15573
  });
15305
- await import_promises4.default.rm(worktree, { recursive: true, force: true });
15574
+ await import_promises5.default.rm(worktree, { recursive: true, force: true });
15306
15575
  await hostGit(
15307
15576
  repo,
15308
15577
  "worktree",
@@ -15367,29 +15636,34 @@ async function listSkillsImpl(opts) {
15367
15636
  globalLink,
15368
15637
  repoLink: "linked",
15369
15638
  repoBranch: "",
15370
- repoAvailable: false
15639
+ repoAvailable: false,
15640
+ repoMigrationBranch: "",
15641
+ repoMigrationMissing: []
15371
15642
  };
15372
15643
  }
15373
15644
  const ctx = await repoContext(opts);
15374
15645
  const repo = await repoSkills(ctx);
15646
+ const migration = openMigration(ctx.liveBlobs, ctx.pending);
15375
15647
  return {
15376
15648
  global: global2,
15377
15649
  repo,
15378
15650
  globalLink,
15379
15651
  repoLink: repoLinkState(ctx),
15380
15652
  repoBranch: ctx.branch,
15381
- repoAvailable: true
15653
+ repoAvailable: true,
15654
+ repoMigrationBranch: migration.branch,
15655
+ repoMigrationMissing: migration.missing
15382
15656
  };
15383
15657
  }
15384
15658
  async function bundleFilesOnDisk(dir) {
15385
15659
  const out = [];
15386
15660
  const walk = async (current, prefix, depth) => {
15387
15661
  if (depth > 3 || out.length >= 200) return;
15388
- const entries = await import_promises4.default.readdir(current, { withFileTypes: true }).catch(() => []);
15662
+ const entries = await import_promises5.default.readdir(current, { withFileTypes: true }).catch(() => []);
15389
15663
  for (const entry of entries) {
15390
15664
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
15391
15665
  if (entry.isDirectory()) {
15392
- await walk(import_path6.default.join(current, entry.name), rel, depth + 1);
15666
+ await walk(import_path7.default.join(current, entry.name), rel, depth + 1);
15393
15667
  } else if (rel !== "SKILL.md") {
15394
15668
  out.push(rel);
15395
15669
  }
@@ -15401,7 +15675,7 @@ async function bundleFilesOnDisk(dir) {
15401
15675
  async function readSkillImpl(opts) {
15402
15676
  if (opts.scope === "global") {
15403
15677
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15404
- const content2 = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8");
15678
+ const content2 = await import_promises5.default.readFile(import_path7.default.join(dir, "SKILL.md"), "utf-8");
15405
15679
  const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
15406
15680
  return {
15407
15681
  name: opts.name,
@@ -15436,71 +15710,102 @@ async function readSkillImpl(opts) {
15436
15710
  };
15437
15711
  }
15438
15712
  async function moveIntoAgents(from, to) {
15439
- if (await import_promises4.default.access(to).then(
15713
+ if (await import_promises5.default.access(to).then(
15440
15714
  () => true,
15441
15715
  () => false
15442
15716
  )) {
15443
15717
  throw new Error(
15444
- `${import_path6.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15718
+ `${import_path7.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15445
15719
  );
15446
15720
  }
15447
- await import_promises4.default.mkdir(import_path6.default.dirname(to), { recursive: true });
15721
+ await import_promises5.default.mkdir(import_path7.default.dirname(to), { recursive: true });
15448
15722
  try {
15449
- await import_promises4.default.rename(from, to);
15723
+ await import_promises5.default.rename(from, to);
15450
15724
  } catch {
15451
- await import_promises4.default.cp(from, to, { recursive: true });
15452
- await import_promises4.default.rm(from, { recursive: true, force: true });
15725
+ await import_promises5.default.cp(from, to, { recursive: true });
15726
+ await import_promises5.default.rm(from, { recursive: true, force: true });
15453
15727
  }
15454
15728
  }
15455
15729
  async function migrateGlobal() {
15456
15730
  const moved = [];
15457
15731
  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);
15732
+ const dir = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15733
+ const stat = await import_promises5.default.lstat(dir).catch(() => null);
15460
15734
  if (!stat || stat.isSymbolicLink()) continue;
15461
- const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15735
+ const entries = await import_promises5.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15462
15736
  for (const entry of entries) {
15463
15737
  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(
15738
+ const src = import_path7.default.join(dir, entry.name);
15739
+ if (!await import_promises5.default.access(import_path7.default.join(src, "SKILL.md")).then(
15466
15740
  () => true,
15467
15741
  () => false
15468
15742
  )) {
15469
15743
  continue;
15470
15744
  }
15471
- await moveIntoAgents(src, import_path6.default.join(globalAgentsRoot(), entry.name));
15745
+ await moveIntoAgents(src, import_path7.default.join(globalAgentsRoot(), entry.name));
15472
15746
  moved.push(entry.name);
15473
15747
  }
15474
- await import_promises4.default.rmdir(dir).catch(() => {
15748
+ await import_promises5.default.rmdir(dir).catch(() => {
15475
15749
  });
15476
15750
  }
15477
15751
  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 });
15752
+ const linkPath = import_path7.default.join(import_os2.default.homedir(), ...root.split("/"));
15753
+ const after = await import_promises5.default.lstat(linkPath).catch(() => null);
15754
+ if (after?.isSymbolicLink()) await import_promises5.default.rm(linkPath, { force: true });
15481
15755
  else if (after) {
15482
15756
  throw new Error(
15483
15757
  `~/${root} still has files that are not skills. Empty it, then migrate.`
15484
15758
  );
15485
15759
  }
15486
- await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15487
- await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15760
+ await import_promises5.default.mkdir(import_path7.default.dirname(linkPath), { recursive: true });
15761
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15488
15762
  }
15489
15763
  return moved;
15490
15764
  }
15765
+ var MIGRATE_SUBJECT = /^Migrate \d+ skill\(s\) to /;
15766
+ function migrationCommitsOnly(log) {
15767
+ return log.split("\n").map((line) => line.trim()).filter(Boolean).every((subject) => MIGRATE_SUBJECT.test(subject));
15768
+ }
15769
+ function openMigration(liveBlobs, pending) {
15770
+ const onBranch = pending.find(
15771
+ (entry) => entry.branch === MIGRATE_BRANCH
15772
+ )?.blobs;
15773
+ if (!onBranch) return { branch: "", missing: [] };
15774
+ const missing = /* @__PURE__ */ new Set();
15775
+ for (const stray of strayRepoSkills(liveBlobs)) {
15776
+ const name = stray.slice(stray.lastIndexOf("/") + 1);
15777
+ if (!onBranch.has(`${AGENTS_ROOT}/${name}`)) missing.add(name);
15778
+ }
15779
+ return { branch: MIGRATE_BRANCH, missing: [...missing].sort() };
15780
+ }
15491
15781
  async function migrateSkillsImpl(opts) {
15492
15782
  if (opts.scope === "global") {
15493
- return { branch: "", moved: await migrateGlobal() };
15783
+ return { branch: "", moved: await migrateGlobal(), pushed: false };
15494
15784
  }
15495
15785
  const ctx = await repoContext(opts);
15496
15786
  const strays = strayRepoSkills(ctx.liveBlobs);
15497
15787
  const linked = ctx.links.get(CLAUDE_ROOT);
15498
15788
  if (!strays.length && linked === AGENTS_LINK_TARGET) {
15499
- return { branch: "", moved: [] };
15789
+ return { branch: "", moved: [], pushed: false };
15500
15790
  }
15501
15791
  const { branch } = ctx;
15502
15792
  const workspace = await ensureSkillsWorktree(opts.spaceDir, branch);
15503
- const target = `${BRANCH_PREFIX}migrate`;
15793
+ const target = MIGRATE_BRANCH;
15794
+ const existing = await hostGit(
15795
+ workspace,
15796
+ "ls-remote",
15797
+ "--heads",
15798
+ "origin",
15799
+ target
15800
+ ).catch(() => "");
15801
+ if (existing.trim()) {
15802
+ await hostGit(
15803
+ workspace,
15804
+ "fetch",
15805
+ "origin",
15806
+ `+refs/heads/${target}:refs/remotes/origin/${target}`
15807
+ );
15808
+ }
15504
15809
  await hostGit(workspace, "checkout", "-B", target, `origin/${branch}`);
15505
15810
  await hostGit(workspace, "clean", "-fd");
15506
15811
  if (opts.gitName) {
@@ -15518,24 +15823,46 @@ async function migrateSkillsImpl(opts) {
15518
15823
  `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
15519
15824
  );
15520
15825
  }
15521
- await import_promises4.default.mkdir(import_path6.default.join(workspace, AGENTS_ROOT), { recursive: true });
15826
+ await import_promises5.default.mkdir(import_path7.default.join(workspace, AGENTS_ROOT), { recursive: true });
15522
15827
  await hostGit(workspace, "mv", stray, dest);
15523
15828
  moved.push(name);
15524
15829
  }
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");
15830
+ const linkPath = import_path7.default.join(workspace, CLAUDE_ROOT);
15831
+ await import_promises5.default.rm(linkPath, { recursive: true, force: true });
15832
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15528
15833
  await hostGit(workspace, "add", "-A");
15529
15834
  const dirty = await hostGit(workspace, "status", "--porcelain");
15530
- if (!dirty.trim()) return { branch: "", moved: [] };
15835
+ if (!dirty.trim()) return { branch: "", moved: [], pushed: false };
15531
15836
  await hostGit(
15532
15837
  workspace,
15533
15838
  "commit",
15534
15839
  "-m",
15535
15840
  `Migrate ${moved.length} skill(s) to ${AGENTS_ROOT} and link ${CLAUDE_ROOT}`
15536
15841
  );
15537
- await hostGit(workspace, "push", "origin", target);
15538
- return { branch: target, moved };
15842
+ if (!existing.trim()) {
15843
+ await hostGit(workspace, "push", "origin", target);
15844
+ return { branch: target, moved, pushed: true };
15845
+ }
15846
+ const [rebuilt, onBranch] = await Promise.all([
15847
+ hostGit(workspace, "rev-parse", "HEAD^{tree}"),
15848
+ hostGit(workspace, "rev-parse", `origin/${target}^{tree}`)
15849
+ ]);
15850
+ if (rebuilt.trim() === onBranch.trim()) {
15851
+ return { branch: target, moved, pushed: false };
15852
+ }
15853
+ const log = await hostGit(
15854
+ workspace,
15855
+ "log",
15856
+ "--format=%s",
15857
+ `origin/${branch}..origin/${target}`
15858
+ );
15859
+ if (!migrationCommitsOnly(log)) {
15860
+ throw new Error(
15861
+ `${target} carries commits that are not this migration. Merge or delete that branch, then migrate.`
15862
+ );
15863
+ }
15864
+ await hostGit(workspace, "push", "--force-with-lease", "origin", target);
15865
+ return { branch: target, moved, pushed: true };
15539
15866
  }
15540
15867
  function assertWritable(name, content) {
15541
15868
  const parsed = parseSkill(content);
@@ -15550,16 +15877,16 @@ function assertWritable(name, content) {
15550
15877
  }
15551
15878
  async function writeSkillFile(opts) {
15552
15879
  const dir = resolveSkillDir(opts.root, opts.name);
15553
- const file = import_path6.default.join(dir, "SKILL.md");
15880
+ const file = import_path7.default.join(dir, "SKILL.md");
15554
15881
  if (opts.keepExisting) {
15555
- const exists = await import_promises4.default.access(file).then(
15882
+ const exists = await import_promises5.default.access(file).then(
15556
15883
  () => true,
15557
15884
  () => false
15558
15885
  );
15559
15886
  if (exists) return;
15560
15887
  }
15561
- await import_promises4.default.mkdir(dir, { recursive: true });
15562
- await import_promises4.default.writeFile(file, opts.content, "utf-8");
15888
+ await import_promises5.default.mkdir(dir, { recursive: true });
15889
+ await import_promises5.default.writeFile(file, opts.content, "utf-8");
15563
15890
  }
15564
15891
  async function writeGlobalSkill(opts) {
15565
15892
  assertWritable(opts.name, opts.content);
@@ -15574,14 +15901,14 @@ async function installBundledSkill(opts) {
15574
15901
  content: opts.content,
15575
15902
  keepExisting: true
15576
15903
  });
15577
- await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15904
+ await write(import_path7.default.join(home, ...AGENTS_ROOT.split("/")));
15578
15905
  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);
15906
+ const linkPath = import_path7.default.join(home, ...root.split("/"));
15907
+ const stat = await import_promises5.default.lstat(linkPath).catch(() => null);
15581
15908
  if (stat?.isSymbolicLink()) continue;
15582
15909
  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);
15910
+ await import_promises5.default.mkdir(import_path7.default.dirname(linkPath), { recursive: true });
15911
+ await import_promises5.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15585
15912
  continue;
15586
15913
  }
15587
15914
  await write(linkPath);
@@ -15589,7 +15916,7 @@ async function installBundledSkill(opts) {
15589
15916
  }
15590
15917
  async function deleteGlobalSkill(opts) {
15591
15918
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15592
- await import_promises4.default.rm(dir, { recursive: true, force: true });
15919
+ await import_promises5.default.rm(dir, { recursive: true, force: true });
15593
15920
  }
15594
15921
  async function proposeRepoSkillImpl(opts) {
15595
15922
  assertWritable(opts.name, opts.content);
@@ -15627,9 +15954,9 @@ async function proposeRepoSkillImpl(opts) {
15627
15954
  if (opts.gitEmail) {
15628
15955
  await hostGit(workspace, "config", "user.email", opts.gitEmail);
15629
15956
  }
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"),
15957
+ await import_promises5.default.mkdir(import_path7.default.join(workspace, dir), { recursive: true });
15958
+ await import_promises5.default.writeFile(
15959
+ import_path7.default.join(workspace, dir, "SKILL.md"),
15633
15960
  opts.content,
15634
15961
  "utf-8"
15635
15962
  );
@@ -15736,18 +16063,18 @@ function vendoredBinaryName() {
15736
16063
  async function installFrpcBinary(target) {
15737
16064
  const p = frpcPaths(target);
15738
16065
  const binaryName = vendoredBinaryName();
15739
- const src = import_path7.default.join(__dirname, "..", "vendor", binaryName);
15740
- if (!(0, import_fs2.existsSync)(src)) {
16066
+ const src = import_path8.default.join(__dirname, "..", "vendor", binaryName);
16067
+ if (!(0, import_fs3.existsSync)(src)) {
15741
16068
  throw new Error(
15742
16069
  `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
15743
16070
  );
15744
16071
  }
15745
- const staged = await import_promises5.default.stat(p.binary).catch(() => null);
15746
- const source = await import_promises5.default.stat(src);
16072
+ const staged = await import_promises6.default.stat(p.binary).catch(() => null);
16073
+ const source = await import_promises6.default.stat(src);
15747
16074
  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);
16075
+ await import_promises6.default.mkdir(p.binDir, { recursive: true });
16076
+ await import_promises6.default.copyFile(src, p.binary);
16077
+ await import_promises6.default.chmod(p.binary, 493);
15751
16078
  }
15752
16079
  var EXPOSE_SKILL_NAME = "share-a-port";
15753
16080
  var FENCE = "```";
@@ -16137,18 +16464,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16137
16464
  await this.core.ensureSpaceWorkspace();
16138
16465
  await assertAgentSignedIn(session.provider, this.core.target());
16139
16466
  if (hasGit) {
16140
- const { path: path21 } = await addWorktree(
16467
+ const { path: path23 } = await addWorktree(
16141
16468
  this.core.target(),
16142
16469
  session.taskId,
16143
16470
  baseBranch || config.mainBranch || "main",
16144
16471
  "card"
16145
16472
  );
16146
- session.workspacePath = path21;
16473
+ session.workspacePath = path23;
16147
16474
  await setGitIdentity(
16148
16475
  this.core.target(),
16149
16476
  config.gitName,
16150
16477
  config.gitEmail,
16151
- path21
16478
+ path23
16152
16479
  );
16153
16480
  this.emitter.addLog(session, "system", "Worktree ready.");
16154
16481
  } else {
@@ -16370,18 +16697,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16370
16697
  this.emitter.addLog(session, "system", "Preparing environment...");
16371
16698
  await this.core.ensureSpaceWorkspace();
16372
16699
  await assertAgentSignedIn(session.provider, this.core.target());
16373
- const { path: path21 } = await addWorktree(
16700
+ const { path: path23 } = await addWorktree(
16374
16701
  this.core.target(),
16375
16702
  session.taskId,
16376
16703
  config.mainBranch || "main",
16377
16704
  "card"
16378
16705
  );
16379
- session.workspacePath = path21;
16706
+ session.workspacePath = path23;
16380
16707
  await setGitIdentity(
16381
16708
  this.core.target(),
16382
16709
  config.gitName,
16383
16710
  config.gitEmail,
16384
- path21
16711
+ path23
16385
16712
  );
16386
16713
  await this.putCard(session);
16387
16714
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16576,7 +16903,7 @@ ${buildPrompt(prompts_default.implement)}`;
16576
16903
  };
16577
16904
 
16578
16905
  // ../../shared/all/helpers/terminal-ai/terminal-mode.ts
16579
- var import_crypto = require("crypto");
16906
+ var import_crypto3 = require("crypto");
16580
16907
 
16581
16908
  // ../../shared/all/helpers/board-agent-core/boardTerminal.ts
16582
16909
  function boardTerminalKey(spaceSlug) {
@@ -16587,95 +16914,6 @@ function isCardPostId(id) {
16587
16914
  return UUID_RE.test(id);
16588
16915
  }
16589
16916
 
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
16917
  // ../../shared/all/helpers/terminal-ai/agentState.ts
16680
16918
  var STATES = /* @__PURE__ */ new Set(["working", "idle", "blocked"]);
16681
16919
  var AGENT_STATE_CHANNEL = "agent-state";
@@ -16720,6 +16958,189 @@ function drainStateMessages(buffer) {
16720
16958
  return { states, rest: [...lines.slice(consumed), partial].join("\n") };
16721
16959
  }
16722
16960
 
16961
+ // ../../shared/all/helpers/terminal-ai/digest.ts
16962
+ var import_crypto2 = __toESM(require("crypto"));
16963
+ var import_promises8 = require("fs/promises");
16964
+ var import_path10 = __toESM(require("path"));
16965
+
16966
+ // ../../shared/all/helpers/board-agent-core/plugin.ts
16967
+ var import_crypto = __toESM(require("crypto"));
16968
+ var import_fs4 = require("fs");
16969
+ var import_promises7 = require("fs/promises");
16970
+ var import_os3 = __toESM(require("os"));
16971
+ var import_path9 = __toESM(require("path"));
16972
+ var MARKETPLACE_NAME = "factiii-runner";
16973
+ var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
16974
+ var SECRETS_BIN = "factiii-secrets";
16975
+ function candidates() {
16976
+ return [
16977
+ import_path9.default.join(__dirname, "..", "plugin"),
16978
+ import_path9.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
16979
+ import_path9.default.join(process.cwd(), "apps", "runner", "plugin")
16980
+ ];
16981
+ }
16982
+ function pluginRoot() {
16983
+ for (const dir of candidates()) {
16984
+ if ((0, import_fs4.existsSync)(import_path9.default.join(dir, ".claude-plugin", "marketplace.json"))) {
16985
+ return dir;
16986
+ }
16987
+ }
16988
+ return null;
16989
+ }
16990
+ function renderedPluginRoot() {
16991
+ return import_path9.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
16992
+ }
16993
+ async function runnerVersion(source) {
16994
+ const pkg = import_path9.default.join(source, "..", "package.json");
16995
+ const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
16996
+ if (!raw) return FALLBACK_VERSION;
16997
+ try {
16998
+ const { version } = JSON.parse(raw);
16999
+ return version || FALLBACK_VERSION;
17000
+ } catch {
17001
+ return FALLBACK_VERSION;
17002
+ }
17003
+ }
17004
+ var FALLBACK_VERSION = "0.0.0-unknown";
17005
+ async function treeFiles(dir) {
17006
+ const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
17007
+ const files = [];
17008
+ for (const entry of entries) {
17009
+ const full = import_path9.default.join(dir, entry.name);
17010
+ if (entry.isDirectory()) files.push(...await treeFiles(full));
17011
+ else if (entry.isFile()) files.push(full);
17012
+ }
17013
+ return files.sort();
17014
+ }
17015
+ async function fingerprint(source, url2, version = "") {
17016
+ const hash = import_crypto.default.createHash("sha256").update(url2).update(version);
17017
+ for (const file of await treeFiles(source)) {
17018
+ hash.update(import_path9.default.relative(source, file));
17019
+ hash.update(await (0, import_promises7.readFile)(file));
17020
+ }
17021
+ return hash.digest("hex");
17022
+ }
17023
+ function stampPath(dest) {
17024
+ return `${dest}.stamp`;
17025
+ }
17026
+ var inFlight = /* @__PURE__ */ new Map();
17027
+ function renderPlugin(boardMcpUrl) {
17028
+ const running = inFlight.get(boardMcpUrl);
17029
+ if (running) return running;
17030
+ const started = renderPluginImpl(boardMcpUrl).finally(
17031
+ () => inFlight.delete(boardMcpUrl)
17032
+ );
17033
+ inFlight.set(boardMcpUrl, started);
17034
+ return started;
17035
+ }
17036
+ async function renderPluginImpl(boardMcpUrl) {
17037
+ const source = pluginRoot();
17038
+ if (!source) return null;
17039
+ const dest = renderedPluginRoot();
17040
+ const version = await runnerVersion(source);
17041
+ const want = await fingerprint(source, boardMcpUrl, version);
17042
+ const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
17043
+ if (have === want) return dest;
17044
+ await (0, import_promises7.mkdir)(import_path9.default.dirname(dest), { recursive: true });
17045
+ await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
17046
+ await (0, import_promises7.writeFile)(
17047
+ import_path9.default.join(dest, "factiii", ".mcp.json"),
17048
+ `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
17049
+ `
17050
+ );
17051
+ await stampVersion(dest, version);
17052
+ await (0, import_promises7.writeFile)(stampPath(dest), want);
17053
+ return dest;
17054
+ }
17055
+ async function stampVersion(dest, version) {
17056
+ const manifest = import_path9.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
17057
+ const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
17058
+ if (!raw) return;
17059
+ const parsed = JSON.parse(raw);
17060
+ await (0, import_promises7.writeFile)(
17061
+ manifest,
17062
+ `${JSON.stringify({ ...parsed, version }, null, 2)}
17063
+ `
17064
+ );
17065
+ }
17066
+ function boardMcpConfig(url2) {
17067
+ return {
17068
+ mcpServers: {
17069
+ "factiii-board": {
17070
+ type: "http",
17071
+ url: url2,
17072
+ bearer_token_env_var: BOARD_MCP_TOKEN_ENV,
17073
+ // Written literally, for claude to expand at connect time. Not a
17074
+ // template string: the `${…}` has to survive into the file.
17075
+ headers: { Authorization: "Bearer ${" + BOARD_MCP_TOKEN_ENV + "}" }
17076
+ }
17077
+ }
17078
+ };
17079
+ }
17080
+
17081
+ // ../../shared/all/helpers/terminal-ai/initScript.ts
17082
+ function terminalPrelude(cwd, binDir) {
17083
+ return [
17084
+ // cd in-script, falling back to $HOME. Must match the agent's worktree:
17085
+ // the CLIs key transcripts by cwd, so --resume needs it.
17086
+ `cd ${cwd} 2>/dev/null || cd "$HOME" 2>/dev/null || cd /`,
17087
+ // Prepended here rather than via the session env, so the login shell's
17088
+ // own PATH survives.
17089
+ `export PATH="${binDir}:$PATH"`,
17090
+ `export PS1='\\[\\e[1;32m\\]user@bare\\[\\e[0m\\]:\\w\\$ '`
17091
+ ].join("; ");
17092
+ }
17093
+ var SAFE_SESSION_ID = /^[a-zA-Z0-9_-]{1,128}$/;
17094
+ function cardBriefPrompt(postId) {
17095
+ 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.`;
17096
+ }
17097
+ var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
17098
+ 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.";
17099
+ function terminalInitScript(cwd, binDir, resumeAgentSessionId, provider, cardPostId) {
17100
+ const prelude = terminalPrelude(cwd, binDir);
17101
+ const shell = `exec bash --norc -i`;
17102
+ if (!resumeAgentSessionId || !SAFE_SESSION_ID.test(resumeAgentSessionId)) {
17103
+ if (!cardPostId) return `${prelude}; ${shell}`;
17104
+ const prompt2 = cardBriefPrompt(cardPostId);
17105
+ const start = provider === "codex" ? `codex '${prompt2}'` : `claude ${CLAUDE_PERMISSION_MODE} '${prompt2}'`;
17106
+ return `${prelude}; ${start}; ${shell}`;
17107
+ }
17108
+ const resume = provider === "codex" ? `codex resume ${resumeAgentSessionId} '${RESUME_SEED}'` : `claude ${CLAUDE_PERMISSION_MODE} --resume ${resumeAgentSessionId} '${RESUME_SEED}'`;
17109
+ return `${prelude}; ${resume}; ${shell}`;
17110
+ }
17111
+
17112
+ // ../../shared/all/helpers/terminal-ai/digest.ts
17113
+ var TERMINAL_FP_OPTION = "@factiii-fp";
17114
+ var CWD_PLACEHOLDER = "/__cwd__";
17115
+ var VOLATILE_ENV_KEYS = /* @__PURE__ */ new Set(["FACTIII_PORTS"]);
17116
+ var PLUGIN_CACHE_MS = 3e4;
17117
+ var pluginCache = null;
17118
+ async function pluginContribution() {
17119
+ const now = Date.now();
17120
+ if (pluginCache && now - pluginCache.at < PLUGIN_CACHE_MS) {
17121
+ return pluginCache.value;
17122
+ }
17123
+ const source = pluginRoot();
17124
+ const shipped = source ? await fingerprint(source, "", "").catch(() => "") : "";
17125
+ const mcp = await (0, import_promises8.readFile)(
17126
+ import_path10.default.join(renderedPluginRoot(), "factiii", ".mcp.json"),
17127
+ "utf-8"
17128
+ ).catch(() => "");
17129
+ const value2 = `${shipped}|${mcp}`;
17130
+ pluginCache = { at: now, value: value2 };
17131
+ return value2;
17132
+ }
17133
+ async function terminalDigest(target) {
17134
+ const hash = import_crypto2.default.createHash("sha256").update(TMUX_CONF).update(terminalPrelude(CWD_PLACEHOLDER, target.paths.bin));
17135
+ const env = target.stateEnv();
17136
+ for (const key of Object.keys(env).sort()) {
17137
+ if (VOLATILE_ENV_KEYS.has(key)) continue;
17138
+ hash.update(`${key}=${env[key]}`);
17139
+ }
17140
+ hash.update(await pluginContribution());
17141
+ return hash.digest("hex");
17142
+ }
17143
+
16723
17144
  // ../../shared/all/helpers/terminal-ai/processes.ts
16724
17145
  var PS_FORMAT = "pid=,ppid=,pcpu=,rss=,etime=,args=";
16725
17146
  var PS_FIELDS = 5;
@@ -16889,19 +17310,306 @@ function parseSsListen(raw) {
16889
17310
  use.set(pid, found);
16890
17311
  }
16891
17312
  }
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
- }
17313
+ return use;
17314
+ }
17315
+ async function socketsFor(target, pids) {
17316
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
17317
+ const lsof = await target.run(["lsof", "-a", "-p", pids.join(","), "-nP", "-iTCP", "-F", "pnT"]).catch((err) => String(err.stdout ?? ""));
17318
+ if (lsof.trim()) return parseLsofSockets(lsof);
17319
+ const ss = await target.run(["ss", "-ltnpH"]).catch(() => "");
17320
+ if (!ss.trim()) return /* @__PURE__ */ new Map();
17321
+ const all = parseSsListen(ss);
17322
+ const wanted = new Set(pids);
17323
+ for (const pid of all.keys()) if (!wanted.has(pid)) all.delete(pid);
17324
+ return all;
17325
+ }
17326
+
17327
+ // ../../shared/all/helpers/terminal-ai/tabNames.ts
17328
+ var TAB_LIST_FORMAT = "#{session_name}|#{pane_current_command}|#{host}|#{@factiii-fp}|#{pane_dead}|#{pane_title}";
17329
+ var SHELL_COMMANDS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "fish"]);
17330
+ var tabName = (cmd, host, title) => {
17331
+ const command = cmd.replace(/\.exe$/, "") || "bash";
17332
+ if (!title || title === host || SHELL_COMMANDS.has(command)) return command;
17333
+ return title;
17334
+ };
17335
+ var parseTerminalTabs = (stdout, prefix) => {
17336
+ const byId = /* @__PURE__ */ new Map();
17337
+ for (const raw of stdout.split("\n")) {
17338
+ const line = raw.trim();
17339
+ if (!line.startsWith(prefix)) continue;
17340
+ const parts2 = line.slice(prefix.length).split("|");
17341
+ if (parts2.length < 6) continue;
17342
+ const id = parts2[0].trim();
17343
+ if (!id || byId.has(id)) continue;
17344
+ byId.set(id, {
17345
+ id,
17346
+ name: tabName(
17347
+ parts2[1].trim(),
17348
+ parts2[2].trim(),
17349
+ parts2.slice(5).join("|").trim()
17350
+ ),
17351
+ fp: parts2[3].trim(),
17352
+ dead: parts2[4].trim() === "1"
17353
+ });
17354
+ }
17355
+ return Array.from(byId.values()).sort(
17356
+ (a, b) => (parseInt(a.id, 10) || 0) - (parseInt(b.id, 10) || 0)
17357
+ );
17358
+ };
17359
+
17360
+ // ../../shared/all/helpers/wireChunk.ts
17361
+ var MAX_INLINE_BYTES = 14e3;
17362
+ var chunkCounter = 0;
17363
+ function nextChunkId() {
17364
+ return `c-${++chunkCounter}-${Date.now()}`;
17365
+ }
17366
+ function chunkWire(wire) {
17367
+ if (wire.length <= MAX_INLINE_BYTES) return [wire];
17368
+ const id = nextChunkId();
17369
+ const total = Math.ceil(wire.length / MAX_INLINE_BYTES);
17370
+ const frames = [];
17371
+ for (let i = 0; i < total; i++) {
17372
+ const start = i * MAX_INLINE_BYTES;
17373
+ const slice = wire.slice(start, start + MAX_INLINE_BYTES);
17374
+ const frame = { __c: id, __i: i, __n: total, d: slice };
17375
+ frames.push(JSON.stringify(frame));
17376
+ }
17377
+ return frames;
17378
+ }
17379
+ function makeChunkReassembler() {
17380
+ const buffers = /* @__PURE__ */ new Map();
17381
+ return {
17382
+ feed(raw) {
17383
+ if (!raw.startsWith('{"__c"')) return raw;
17384
+ let frame;
17385
+ try {
17386
+ frame = JSON.parse(raw);
17387
+ } catch {
17388
+ return raw;
17389
+ }
17390
+ if (typeof frame.__c !== "string" || typeof frame.__i !== "number" || typeof frame.__n !== "number" || typeof frame.d !== "string") {
17391
+ return raw;
17392
+ }
17393
+ let buf = buffers.get(frame.__c);
17394
+ if (!buf) {
17395
+ buf = { received: new Array(frame.__n), total: frame.__n };
17396
+ buffers.set(frame.__c, buf);
17397
+ }
17398
+ buf.received[frame.__i] = frame.d;
17399
+ for (let i = 0; i < buf.total; i++) {
17400
+ if (buf.received[i] === void 0) return null;
17401
+ }
17402
+ buffers.delete(frame.__c);
17403
+ return buf.received.join("");
17404
+ }
17405
+ };
17406
+ }
17407
+
17408
+ // ../../shared/all/helpers/terminal-ai/terminal.ts
17409
+ function tmuxName(terminalKey) {
17410
+ return `terminal-${safeTmuxName(terminalKey)}`;
17411
+ }
17412
+ async function restartTerminalSession(opts) {
17413
+ const { target, terminalKey, cwd } = opts;
17414
+ await respawnTmuxPane({
17415
+ target,
17416
+ session: tmuxName(terminalKey),
17417
+ argv: ["bash", "-c", terminalInitScript(cwd, target.paths.bin)],
17418
+ cwd,
17419
+ // Rebuilt rather than inherited: picking up an env the session was created
17420
+ // too early to see is half the reason to restart at all.
17421
+ env: target.stateEnv()
17422
+ }).catch(() => {
17423
+ });
17424
+ await terminalDigest(target).then(
17425
+ (digest) => setTmuxSessionOption(
17426
+ target,
17427
+ tmuxName(terminalKey),
17428
+ TERMINAL_FP_OPTION,
17429
+ digest
17430
+ )
17431
+ ).catch(() => {
17432
+ });
17433
+ }
17434
+ var TerminalManager = class {
17435
+ constructor(send, getCardTarget, getCardWorkdir) {
17436
+ this.send = send;
17437
+ this.getCardTarget = getCardTarget;
17438
+ this.getCardWorkdir = getCardWorkdir;
17439
+ this.sessions = /* @__PURE__ */ new Map();
17440
+ }
17441
+ handle(msg) {
17442
+ switch (msg.op) {
17443
+ case "open":
17444
+ void this.open(msg);
17445
+ return;
17446
+ case "stdin":
17447
+ this.stdin(msg.sessionId, msg.data);
17448
+ return;
17449
+ case "resize":
17450
+ void this.resize(msg.sessionId, msg.cols, msg.rows);
17451
+ return;
17452
+ case "close":
17453
+ this.close(msg.sessionId);
17454
+ return;
17455
+ case "kill":
17456
+ void this.killTmuxSession(msg.postId, msg.terminalKey);
17457
+ return;
17458
+ case "restart":
17459
+ void this.restart(msg.postId, msg.terminalKey);
17460
+ return;
17461
+ }
17462
+ }
17463
+ async open(msg) {
17464
+ if (this.sessions.has(msg.sessionId)) {
17465
+ this.emitError(msg.sessionId, "Session already open.");
17466
+ return;
17467
+ }
17468
+ const target = this.getCardTarget(msg.postId);
17469
+ if (!target) {
17470
+ this.emitError(
17471
+ msg.sessionId,
17472
+ `No active card session for postId=${msg.postId}.`
17473
+ );
17474
+ return;
17475
+ }
17476
+ const tmuxSession = tmuxName(msg.terminalKey);
17477
+ const cardPostId = msg.briefCard && !msg.resumeAgentSessionId && isCardPostId(msg.postId) ? msg.postId : void 0;
17478
+ const init = this.buildInit(target, msg.postId, {
17479
+ resumeAgentSessionId: msg.resumeAgentSessionId,
17480
+ provider: msg.resumeProvider,
17481
+ cardPostId
17482
+ });
17483
+ const creating = !await tmuxSessionExists(target, tmuxSession);
17484
+ try {
17485
+ const session = await startTmuxSession({
17486
+ target,
17487
+ session: tmuxSession,
17488
+ // Attach if it exists, else create. This is what makes the shell
17489
+ // survive modal close/reopen: the server holds it, we just view.
17490
+ attachExisting: true,
17491
+ size: { cols: msg.cols, rows: msg.rows },
17492
+ // Without these, `claude` in the shell reads the default ~/.claude and
17493
+ // asks to sign in, while the agent spawns (which pass agentEnv) don't.
17494
+ env: target.stateEnv(),
17495
+ argv: init.argv
17496
+ });
17497
+ this.sessions.set(msg.sessionId, session);
17498
+ if (creating) {
17499
+ void resurrectSave(target);
17500
+ void terminalDigest(target).then(
17501
+ (digest) => setTmuxSessionOption(
17502
+ target,
17503
+ tmuxSession,
17504
+ TERMINAL_FP_OPTION,
17505
+ digest
17506
+ )
17507
+ ).catch(() => {
17508
+ });
17509
+ }
17510
+ session.onData(
17511
+ (data) => this.emit({
17512
+ type: "terminal",
17513
+ op: "stdout",
17514
+ sessionId: msg.sessionId,
17515
+ data: data.toString("base64")
17516
+ })
17517
+ );
17518
+ session.onError((err) => {
17519
+ this.cleanup(msg.sessionId);
17520
+ this.emitError(msg.sessionId, err.message);
17521
+ });
17522
+ session.onClose((code) => {
17523
+ if (!this.sessions.has(msg.sessionId)) return;
17524
+ this.cleanup(msg.sessionId);
17525
+ this.emit({
17526
+ type: "terminal",
17527
+ op: "exit",
17528
+ sessionId: msg.sessionId,
17529
+ code
17530
+ });
17531
+ });
17532
+ this.emit({ type: "terminal", op: "opened", sessionId: msg.sessionId });
17533
+ } catch (err) {
17534
+ this.emitError(
17535
+ msg.sessionId,
17536
+ err instanceof Error ? err.message : String(err)
17537
+ );
17538
+ }
17539
+ }
17540
+ /** What tmux runs for a tab, and where. One builder so `open` and `restart`
17541
+ * cannot drift. */
17542
+ buildInit(target, postId, agent) {
17543
+ const cwd = this.getCardWorkdir(postId) || target.paths.workspace;
17544
+ return {
17545
+ cwd,
17546
+ argv: [
17547
+ "bash",
17548
+ "-c",
17549
+ terminalInitScript(
17550
+ cwd,
17551
+ target.paths.bin,
17552
+ agent?.resumeAgentSessionId,
17553
+ agent?.provider,
17554
+ agent?.cardPostId
17555
+ )
17556
+ ]
17557
+ };
17558
+ }
17559
+ /** Restart the tab's process while keeping the tab. */
17560
+ async restart(postId, terminalKey) {
17561
+ const target = this.getCardTarget(postId);
17562
+ if (!target) return;
17563
+ await restartTerminalSession({
17564
+ target,
17565
+ terminalKey,
17566
+ cwd: this.getCardWorkdir(postId) || target.paths.workspace
17567
+ });
17568
+ }
17569
+ stdin(sessionId, b64) {
17570
+ this.sessions.get(sessionId)?.write(Buffer.from(b64, "base64"));
17571
+ }
17572
+ async resize(sessionId, cols, rows) {
17573
+ const session = this.sessions.get(sessionId);
17574
+ if (!session) return;
17575
+ if (!Number.isInteger(cols) || !Number.isInteger(rows)) return;
17576
+ if (cols < 1 || rows < 1) return;
17577
+ await session.resize(cols, rows).catch(() => {
17578
+ });
17579
+ }
17580
+ close(sessionId) {
17581
+ const session = this.sessions.get(sessionId);
17582
+ if (!session) return;
17583
+ this.sessions.delete(sessionId);
17584
+ session.detach();
17585
+ }
17586
+ /** Tear down the tmux session for a tab. Used when the user explicitly
17587
+ * removes a tab (the X button), not on transient detach. */
17588
+ async killTmuxSession(postId, terminalKey) {
17589
+ const target = this.getCardTarget(postId);
17590
+ if (!target) return;
17591
+ await target.sh(
17592
+ `${tmuxCli(target)} kill-session -t ${tmuxName(terminalKey)} 2>/dev/null || true`
17593
+ ).catch(() => {
17594
+ });
17595
+ await resurrectSave(target);
17596
+ }
17597
+ destroyAll() {
17598
+ for (const sessionId of [...this.sessions.keys()]) {
17599
+ this.close(sessionId);
17600
+ }
17601
+ }
17602
+ cleanup(sessionId) {
17603
+ this.sessions.delete(sessionId);
17604
+ }
17605
+ emitError(sessionId, message) {
17606
+ this.emit({ type: "terminal", op: "error", sessionId, message });
17607
+ }
17608
+ emit(msg) {
17609
+ const wire = JSON.stringify(msg);
17610
+ for (const frame of chunkWire(wire)) this.send(frame);
17611
+ }
17612
+ };
16905
17613
 
16906
17614
  // ../../shared/all/helpers/terminal-ai/watcher.ts
16907
17615
  var PRUNED_DIRS = [
@@ -16928,12 +17636,12 @@ var FLUSH_MS = 120;
16928
17636
  var SELF_WRITE_MS = 2e3;
16929
17637
  var dynamicImport = new Function("s", "return import(s)");
16930
17638
  var importFs = new Function("s", "return import(s)");
16931
- async function resolveReal(path21) {
17639
+ async function resolveReal(path23) {
16932
17640
  try {
16933
17641
  const { realpath } = await importFs("fs/promises");
16934
- return await realpath(path21);
17642
+ return await realpath(path23);
16935
17643
  } catch {
16936
- return path21;
17644
+ return path23;
16937
17645
  }
16938
17646
  }
16939
17647
  var loadSubscribe = null;
@@ -17047,19 +17755,19 @@ var WorkspaceWatcher = class {
17047
17755
  }
17048
17756
  record(kind, relPath) {
17049
17757
  if (this.closed) return;
17050
- const path21 = "/" + relPath;
17051
- const expiry = this.selfWrites.get(path21);
17758
+ const path23 = "/" + relPath;
17759
+ const expiry = this.selfWrites.get(path23);
17052
17760
  if (expiry !== void 0) {
17053
17761
  if (expiry > Date.now()) return;
17054
- this.selfWrites.delete(path21);
17762
+ this.selfWrites.delete(path23);
17055
17763
  }
17056
17764
  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);
17765
+ added.delete(path23);
17766
+ changed.delete(path23);
17767
+ removed.delete(path23);
17768
+ if (kind === "added") added.add(path23);
17769
+ else if (kind === "changed") changed.add(path23);
17770
+ else removed.add(path23);
17063
17771
  if (added.size + changed.size + removed.size > MAX_BATCH) {
17064
17772
  this.pending = { ...emptyPending(), resync: true };
17065
17773
  }
@@ -17078,8 +17786,8 @@ var WorkspaceWatcher = class {
17078
17786
  this.pending = emptyPending();
17079
17787
  if (!resync && !added.size && !changed.size && !removed.size) return;
17080
17788
  const now = Date.now();
17081
- for (const [path21, expiry] of this.selfWrites) {
17082
- if (expiry <= now) this.selfWrites.delete(path21);
17789
+ for (const [path23, expiry] of this.selfWrites) {
17790
+ if (expiry <= now) this.selfWrites.delete(path23);
17083
17791
  }
17084
17792
  this.emit({
17085
17793
  added: resync ? [] : [...added],
@@ -17283,8 +17991,8 @@ var TerminalModeEngine = class {
17283
17991
  /** Suppress the change event our own write is about to produce, so a save
17284
17992
  * never comes back to the editor as an outside edit. Normalized to the form
17285
17993
  * the watcher emits, since callers spell the path either way. */
17286
- expectWrite(postId, path21) {
17287
- this.watchers.get(postId)?.expectWrite(posixNormalize(path21));
17994
+ expectWrite(postId, path23) {
17995
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path23));
17288
17996
  }
17289
17997
  // ── Lifecycle / ModeEngine ──
17290
17998
  start(base, args) {
@@ -17344,7 +18052,7 @@ var TerminalModeEngine = class {
17344
18052
  }
17345
18053
  if (!session) return;
17346
18054
  this.stopWatching(session.postId);
17347
- await this.teardown(session);
18055
+ await teardownSession(session, this.core.target());
17348
18056
  const removedPostId = session.postId;
17349
18057
  this.reservedNames.delete(removedPostId);
17350
18058
  this.sessions.delete(removedPostId);
@@ -17357,33 +18065,6 @@ var TerminalModeEngine = class {
17357
18065
  this.reservedNames.clear();
17358
18066
  this.sessions.clear();
17359
18067
  }
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
18068
  putCard(s) {
17388
18069
  return putSessionCard(s, this.core.target());
17389
18070
  }
@@ -17481,18 +18162,18 @@ var TerminalModeEngine = class {
17481
18162
  this.reservedNames.values()
17482
18163
  );
17483
18164
  this.reservedNames.set(session.postId, workName);
17484
- const { path: path21 } = await addWorktree(
18165
+ const { path: path23 } = await addWorktree(
17485
18166
  this.core.target(),
17486
18167
  workName,
17487
18168
  branch,
17488
18169
  "terminal"
17489
18170
  );
17490
- session.workspacePath = path21;
18171
+ session.workspacePath = path23;
17491
18172
  await setGitIdentity(
17492
18173
  this.core.target(),
17493
18174
  config.gitName,
17494
18175
  config.gitEmail,
17495
- path21
18176
+ path23
17496
18177
  );
17497
18178
  this.emitter.addLog(session, "system", "Worktree ready.");
17498
18179
  } else {
@@ -17554,11 +18235,11 @@ var TerminalModeEngine = class {
17554
18235
  }
17555
18236
  async bareFsList({
17556
18237
  postId,
17557
- path: path21
18238
+ path: path23
17558
18239
  }) {
17559
18240
  const target = this.getBareTarget(postId);
17560
18241
  const root = this.getBareRoot(postId);
17561
- const abs = this.resolveBarePath(path21, root);
18242
+ const abs = this.resolveBarePath(path23, root);
17562
18243
  const relRoot = abs.slice(root.length) || "/";
17563
18244
  const out = await target.run([
17564
18245
  "sh",
@@ -17608,30 +18289,30 @@ var TerminalModeEngine = class {
17608
18289
  }
17609
18290
  async bareFsRead({
17610
18291
  postId,
17611
- path: path21
18292
+ path: path23
17612
18293
  }) {
17613
18294
  const target = this.getBareTarget(postId);
17614
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18295
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17615
18296
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17616
18297
  '`)).trim();
17617
18298
  return { content: b64, encoding: "base64" };
17618
18299
  }
17619
18300
  async bareFsWrite({
17620
18301
  postId,
17621
- path: path21,
18302
+ path: path23,
17622
18303
  content,
17623
18304
  encoding
17624
18305
  }) {
17625
18306
  const target = this.getBareTarget(postId);
17626
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18307
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17627
18308
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17628
18309
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17629
18310
  throw new Error("Invalid base64 payload.");
17630
18311
  }
17631
- this.expectWrite(String(postId), path21);
18312
+ this.expectWrite(String(postId), path23);
17632
18313
  await writeBase64File(target, abs, b64);
17633
18314
  const session = this.sessions.get(String(postId));
17634
- if (session) delete session.buffers[path21];
18315
+ if (session) delete session.buffers[path23];
17635
18316
  }
17636
18317
  /** Append one chunk of a dropped image to the per-space drops dir
17637
18318
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17649,7 +18330,7 @@ var TerminalModeEngine = class {
17649
18330
  }
17650
18331
  const safeExt = (ext || "png").toLowerCase().replace(/[^a-z0-9]/g, "") || "png";
17651
18332
  const oneShot = !dropId;
17652
- const safeId2 = oneShot ? (0, import_crypto.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
18333
+ const safeId2 = oneShot ? (0, import_crypto3.randomUUID)() : dropId.toLowerCase().replace(/[^a-z0-9-]/g, "");
17653
18334
  if (!safeId2) throw new Error("Invalid drop id.");
17654
18335
  const dropsDir = `${target.paths.root}/drops`;
17655
18336
  const abs = `${dropsDir}/${safeId2}.${safeExt}`;
@@ -17681,39 +18362,39 @@ var TerminalModeEngine = class {
17681
18362
  }
17682
18363
  bareOpenFile({
17683
18364
  postId: rawPostId,
17684
- path: path21
18365
+ path: path23
17685
18366
  }) {
17686
18367
  const session = this.sessions.get(String(rawPostId));
17687
18368
  if (!session || session.mode !== "bare") return;
17688
- if (!session.openedFiles.includes(path21)) {
17689
- session.openedFiles.push(path21);
18369
+ if (!session.openedFiles.includes(path23)) {
18370
+ session.openedFiles.push(path23);
17690
18371
  }
17691
18372
  }
17692
18373
  bareCloseFile({
17693
18374
  postId: rawPostId,
17694
- path: path21
18375
+ path: path23
17695
18376
  }) {
17696
18377
  const session = this.sessions.get(String(rawPostId));
17697
18378
  if (!session || session.mode !== "bare") return;
17698
- session.openedFiles = session.openedFiles.filter((p) => p !== path21);
17699
- delete session.buffers[path21];
18379
+ session.openedFiles = session.openedFiles.filter((p) => p !== path23);
18380
+ delete session.buffers[path23];
17700
18381
  }
17701
18382
  bareBufferSet({
17702
18383
  postId: rawPostId,
17703
- path: path21,
18384
+ path: path23,
17704
18385
  content
17705
18386
  }) {
17706
18387
  const session = this.sessions.get(String(rawPostId));
17707
18388
  if (!session || session.mode !== "bare") return;
17708
- session.buffers[path21] = content;
18389
+ session.buffers[path23] = content;
17709
18390
  }
17710
18391
  async bareFsDelete({
17711
18392
  postId,
17712
- path: path21
18393
+ path: path23
17713
18394
  }) {
17714
18395
  const target = this.getBareTarget(postId);
17715
18396
  const root = this.getBareRoot(postId);
17716
- const abs = this.resolveBarePath(path21, root);
18397
+ const abs = this.resolveBarePath(path23, root);
17717
18398
  if (abs === root) {
17718
18399
  throw new Error("Refusing to delete the repo root.");
17719
18400
  }
@@ -17721,10 +18402,10 @@ var TerminalModeEngine = class {
17721
18402
  }
17722
18403
  async bareFsMkdir({
17723
18404
  postId,
17724
- path: path21
18405
+ path: path23
17725
18406
  }) {
17726
18407
  const target = this.getBareTarget(postId);
17727
- const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
18408
+ const abs = this.resolveBarePath(path23, this.getBareRoot(postId));
17728
18409
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17729
18410
  }
17730
18411
  async bareReview({
@@ -17847,26 +18528,76 @@ var TerminalModeEngine = class {
17847
18528
  const prefix = `terminal-${sanitizedPostId}-`;
17848
18529
  try {
17849
18530
  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)
18531
+ `${tmuxCli(target)} list-panes -a -F '${TAB_LIST_FORMAT}' 2>/dev/null || true`
17865
18532
  );
18533
+ const rows = parseTerminalTabs(stdout, prefix);
18534
+ const current = await terminalDigest(target).catch(() => "");
18535
+ return rows.map(({ id, name, fp, dead }) => ({
18536
+ id,
18537
+ name,
18538
+ // No stamp means a session older than the stamp, as blind to the
18539
+ // current build as a mismatched one.
18540
+ stale: !!current && fp !== current,
18541
+ dead
18542
+ }));
17866
18543
  } catch {
17867
18544
  return [];
17868
18545
  }
17869
18546
  }
18547
+ /** Terminal tabs across several posts in one sweep, so the sessions view can
18548
+ * ask about a whole board without a round trip per card. */
18549
+ async staleTabsFor(target, postIds, current) {
18550
+ if (!current) return [];
18551
+ const stdout = await target.sh(
18552
+ `${tmuxCli(target)} list-panes -a -F '${TAB_LIST_FORMAT}' 2>/dev/null || true`
18553
+ ).catch(() => "");
18554
+ const stale = [];
18555
+ for (const postId of postIds) {
18556
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
18557
+ for (const row of parseTerminalTabs(stdout, prefix)) {
18558
+ if (row.fp !== current) stale.push({ postId, tabId: row.id });
18559
+ }
18560
+ }
18561
+ return stale;
18562
+ }
18563
+ async bareTerminalHealth({
18564
+ postIds
18565
+ }) {
18566
+ const target = this.core.target();
18567
+ const digest = await terminalDigest(target).catch(() => "");
18568
+ const acked = await getTerminalAck(target);
18569
+ return {
18570
+ digest,
18571
+ acked: !!digest && acked === digest,
18572
+ staleTabs: await this.staleTabsFor(target, postIds, digest)
18573
+ };
18574
+ }
18575
+ async bareAckTerminalDigest() {
18576
+ const target = this.core.target();
18577
+ const digest = await terminalDigest(target).catch(() => "");
18578
+ if (!digest) return;
18579
+ await setTerminalAck(target, digest);
18580
+ }
18581
+ async bareRestartStaleTerminals({
18582
+ postIds
18583
+ }) {
18584
+ const target = this.core.target();
18585
+ const digest = await terminalDigest(target).catch(() => "");
18586
+ const stale = await this.staleTabsFor(target, postIds, digest);
18587
+ for (const { postId, tabId } of stale) {
18588
+ let cwd = target.paths.workspace;
18589
+ try {
18590
+ cwd = this.gitWorkdir(postId) || cwd;
18591
+ } catch {
18592
+ }
18593
+ await restartTerminalSession({
18594
+ target,
18595
+ terminalKey: `${postId}-${tabId}`,
18596
+ cwd
18597
+ });
18598
+ }
18599
+ return { restarted: stale.length };
18600
+ }
17870
18601
  /** This card's panes: the pid its tree hangs off, and the pane id its
17871
18602
  * processes carry in their environment. */
17872
18603
  async panePids(target, postId) {
@@ -18406,8 +19137,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
18406
19137
  };
18407
19138
  }
18408
19139
  }
18409
- async function githubApi(token, path21) {
18410
- const res = await fetch(`${API_BASE}${path21}`, {
19140
+ async function githubApi(token, path23) {
19141
+ const res = await fetch(`${API_BASE}${path23}`, {
18411
19142
  headers: {
18412
19143
  Authorization: `Bearer ${token}`,
18413
19144
  Accept: "application/vnd.github+json",
@@ -18416,7 +19147,7 @@ async function githubApi(token, path21) {
18416
19147
  });
18417
19148
  if (!res.ok) {
18418
19149
  throw new Error(
18419
- `GitHub ${path21} failed (${res.status}): ${await res.text()}`
19150
+ `GitHub ${path23} failed (${res.status}): ${await res.text()}`
18420
19151
  );
18421
19152
  }
18422
19153
  return await res.json();
@@ -18461,10 +19192,10 @@ async function listGithubBranches(token, fullName) {
18461
19192
  }
18462
19193
 
18463
19194
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18464
- var import_path10 = __toESM(require("path"));
19195
+ var import_path13 = __toESM(require("path"));
18465
19196
 
18466
19197
  // ../../shared/all/helpers/board-agent-core/secrets-gate.ts
18467
- var import_crypto2 = __toESM(require("crypto"));
19198
+ var import_crypto4 = __toESM(require("crypto"));
18468
19199
  var DEPLOY_ORIGIN = "Deploy run";
18469
19200
  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
19201
  var SecretsGate = class {
@@ -18489,7 +19220,7 @@ var SecretsGate = class {
18489
19220
  /** Mint a token for one session, labelled with what the owner should see on
18490
19221
  * the prompt - the label is most of the decision, so name the session. */
18491
19222
  issueToken(origin) {
18492
- const token = import_crypto2.default.randomBytes(24).toString("hex");
19223
+ const token = import_crypto4.default.randomBytes(24).toString("hex");
18493
19224
  this.tokens.set(token, origin);
18494
19225
  return token;
18495
19226
  }
@@ -18513,7 +19244,7 @@ var SecretsGate = class {
18513
19244
  return new Promise((resolve2) => {
18514
19245
  this.waiting = {
18515
19246
  request: {
18516
- id: import_crypto2.default.randomUUID(),
19247
+ id: import_crypto4.default.randomUUID(),
18517
19248
  command: opts.command,
18518
19249
  keys: opts.keys,
18519
19250
  origin: opts.origin,
@@ -18553,13 +19284,13 @@ var SecretsGate = class {
18553
19284
  };
18554
19285
 
18555
19286
  // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18556
- var import_path9 = __toESM(require("path"));
19287
+ var import_path12 = __toESM(require("path"));
18557
19288
 
18558
19289
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18559
19290
  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"));
19291
+ var import_crypto5 = __toESM(require("crypto"));
19292
+ var import_promises9 = __toESM(require("fs/promises"));
19293
+ var import_path11 = __toESM(require("path"));
18563
19294
  var import_util6 = require("util");
18564
19295
  var execFileAsync3 = (0, import_util6.promisify)(import_child_process4.execFile);
18565
19296
  var IDENTITY_FILE = "age-identity.enc";
@@ -18585,14 +19316,14 @@ async function assertBinaries() {
18585
19316
  }
18586
19317
  }
18587
19318
  function deriveKey(password, salt) {
18588
- return import_crypto3.default.scryptSync(password, salt, 32, {
19319
+ return import_crypto5.default.scryptSync(password, salt, 32, {
18589
19320
  N: SCRYPT_N,
18590
19321
  maxmem: 64 * 1024 * 1024
18591
19322
  });
18592
19323
  }
18593
19324
  async function secretsInitialized(configDir) {
18594
19325
  try {
18595
- await import_promises6.default.access(import_path8.default.join(configDir, IDENTITY_FILE));
19326
+ await import_promises9.default.access(import_path11.default.join(configDir, IDENTITY_FILE));
18596
19327
  return true;
18597
19328
  } catch {
18598
19329
  return false;
@@ -18614,9 +19345,9 @@ async function initSecrets(configDir, password) {
18614
19345
  if (!identity || !recipient) {
18615
19346
  throw new Error("age-keygen returned no usable keypair.");
18616
19347
  }
18617
- const salt = import_crypto3.default.randomBytes(16);
18618
- const iv = import_crypto3.default.randomBytes(12);
18619
- const cipher = import_crypto3.default.createCipheriv(
19348
+ const salt = import_crypto5.default.randomBytes(16);
19349
+ const iv = import_crypto5.default.randomBytes(12);
19350
+ const cipher = import_crypto5.default.createCipheriv(
18620
19351
  "aes-256-gcm",
18621
19352
  deriveKey(password, salt),
18622
19353
  iv
@@ -18631,17 +19362,17 @@ async function initSecrets(configDir, password) {
18631
19362
  tag: cipher.getAuthTag().toString("base64"),
18632
19363
  data: data.toString("base64")
18633
19364
  };
18634
- await import_promises6.default.writeFile(import_path8.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
19365
+ await import_promises9.default.writeFile(import_path11.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18635
19366
  mode: 384
18636
19367
  });
18637
- await import_promises6.default.writeFile(import_path8.default.join(configDir, RECIPIENT_FILE), `${recipient}
19368
+ await import_promises9.default.writeFile(import_path11.default.join(configDir, RECIPIENT_FILE), `${recipient}
18638
19369
  `, {
18639
19370
  mode: 384
18640
19371
  });
18641
19372
  return { success: true };
18642
19373
  }
18643
19374
  function decryptIdentity(box, password) {
18644
- const decipher = import_crypto3.default.createDecipheriv(
19375
+ const decipher = import_crypto5.default.createDecipheriv(
18645
19376
  "aes-256-gcm",
18646
19377
  deriveKey(password, Buffer.from(box.salt, "base64")),
18647
19378
  Buffer.from(box.iv, "base64")
@@ -18657,17 +19388,17 @@ function decryptIdentity(box, password) {
18657
19388
  }
18658
19389
  }
18659
19390
  function envPath(spaceDir) {
18660
- return import_path8.default.join(spaceDir, "secrets", ENV_FILE);
19391
+ return import_path11.default.join(spaceDir, "secrets", ENV_FILE);
18661
19392
  }
18662
19393
  async function identityFor(configDir, password) {
18663
19394
  const box = JSON.parse(
18664
- await import_promises6.default.readFile(import_path8.default.join(configDir, IDENTITY_FILE), "utf-8")
19395
+ await import_promises9.default.readFile(import_path11.default.join(configDir, IDENTITY_FILE), "utf-8")
18665
19396
  );
18666
19397
  return decryptIdentity(box, password);
18667
19398
  }
18668
19399
  async function readEnv(spaceDir, identity) {
18669
19400
  try {
18670
- await import_promises6.default.access(envPath(spaceDir));
19401
+ await import_promises9.default.access(envPath(spaceDir));
18671
19402
  } catch {
18672
19403
  return {};
18673
19404
  }
@@ -18696,14 +19427,14 @@ async function readEnv(spaceDir, identity) {
18696
19427
  return values;
18697
19428
  }
18698
19429
  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();
19430
+ const recipient = (await import_promises9.default.readFile(import_path11.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18700
19431
  const plain = `${Object.entries(values).map(([k, v]) => `${k}=${v}`).join("\n")}
18701
19432
  `;
18702
19433
  const tmp = envPath(spaceDir).replace(
18703
19434
  /\.enc\.env$/,
18704
19435
  `.${process.pid}.tmp.env`
18705
19436
  );
18706
- await import_promises6.default.writeFile(tmp, plain, { mode: 384 });
19437
+ await import_promises9.default.writeFile(tmp, plain, { mode: 384 });
18707
19438
  try {
18708
19439
  const { stdout } = await execFileAsync3(
18709
19440
  "sops",
@@ -18722,13 +19453,13 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18722
19453
  maxBuffer: 4 * 1024 * 1024
18723
19454
  }
18724
19455
  );
18725
- await import_promises6.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
19456
+ await import_promises9.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18726
19457
  } finally {
18727
- await import_promises6.default.rm(tmp, { force: true });
19458
+ await import_promises9.default.rm(tmp, { force: true });
18728
19459
  }
18729
19460
  }
18730
19461
  async function storedSecretKeys(spaceDir) {
18731
- const raw = await import_promises6.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
19462
+ const raw = await import_promises9.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
18732
19463
  const keys = [];
18733
19464
  for (const line of raw.split("\n")) {
18734
19465
  const eq = line.indexOf("=");
@@ -18772,9 +19503,9 @@ var COMMAND_TIMEOUT_MS = 6 * 60 * 6e4;
18772
19503
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18773
19504
  function containedCwd(cwd, spaceDir) {
18774
19505
  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;
19506
+ const resolved = import_path12.default.resolve(cwd);
19507
+ const root = import_path12.default.resolve(spaceDir);
19508
+ return resolved === root || resolved.startsWith(root + import_path12.default.sep) ? resolved : null;
18778
19509
  }
18779
19510
  async function authorizeAndRun(opts) {
18780
19511
  const { command, gate } = opts;
@@ -18859,8 +19590,8 @@ function tailMarker(text) {
18859
19590
  }
18860
19591
  async function startDeployRun(opts) {
18861
19592
  const { hooks } = opts;
18862
- const workspace = import_path10.default.join(opts.spaceDir, "workspace");
18863
- const configDir = import_path10.default.dirname(opts.spaceDir);
19593
+ const workspace = import_path13.default.join(opts.spaceDir, "workspace");
19594
+ const configDir = import_path13.default.dirname(opts.spaceDir);
18864
19595
  const status = await checkDeployStatus(opts);
18865
19596
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18866
19597
  throw new Error(
@@ -19079,123 +19810,8 @@ async function getDriveRoot(accessToken) {
19079
19810
  }
19080
19811
 
19081
19812
  // ../../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
- }
19813
+ var import_promises10 = require("fs/promises");
19814
+ var import_path14 = __toESM(require("path"));
19199
19815
 
19200
19816
  // ../../shared/all/helpers/board-agent-core/toolchain.ts
19201
19817
  var TOOLS = [
@@ -19464,7 +20080,7 @@ var SpaceCore = class {
19464
20080
  return { code: 3, output: "" };
19465
20081
  }
19466
20082
  return authorizeAndRun({
19467
- configDir: import_path12.default.dirname(spaceDir),
20083
+ configDir: import_path14.default.dirname(spaceDir),
19468
20084
  spaceDir,
19469
20085
  // A step legitimately cds into subdirectories, so the caller's own
19470
20086
  // cwd is right - but only inside the space. Anywhere else is a
@@ -19717,6 +20333,8 @@ var SpaceCore = class {
19717
20333
  markOnboardingComplete(target).catch(() => {
19718
20334
  })
19719
20335
  ]);
20336
+ await this.restoreTerminals();
20337
+ await this.applyTerminalConfig();
19720
20338
  status("");
19721
20339
  this.ready = true;
19722
20340
  } catch (err) {
@@ -19802,17 +20420,17 @@ var SpaceCore = class {
19802
20420
  const source = pluginRoot();
19803
20421
  if (!source) return;
19804
20422
  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);
20423
+ const from = import_path14.default.join(source, "factiii", "bin", SECRETS_BIN);
20424
+ const to = import_path14.default.join(bin, SECRETS_BIN);
19807
20425
  try {
19808
20426
  const [src, staged] = await Promise.all([
19809
- (0, import_promises8.readFile)(from),
19810
- (0, import_promises8.readFile)(to).catch(() => null)
20427
+ (0, import_promises10.readFile)(from),
20428
+ (0, import_promises10.readFile)(to).catch(() => null)
19811
20429
  ]);
19812
20430
  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);
20431
+ await (0, import_promises10.mkdir)(bin, { recursive: true });
20432
+ await (0, import_promises10.copyFile)(from, to);
20433
+ await (0, import_promises10.chmod)(to, 493);
19816
20434
  } catch {
19817
20435
  }
19818
20436
  }
@@ -19875,9 +20493,79 @@ var SpaceCore = class {
19875
20493
  });
19876
20494
  await Promise.all([this.provisionPluginLane(), this.provisionBinLane()]);
19877
20495
  await this.provisionMcpLane();
20496
+ await this.restoreTerminals();
20497
+ await this.applyTerminalConfig();
19878
20498
  this.ready = true;
19879
20499
  return true;
19880
20500
  }
20501
+ /** Push a changed tmux config into the server that is already running.
20502
+ *
20503
+ * tmux reads `-f` only when it STARTS, so a config a runner update changed
20504
+ * would otherwise sit unread until every session on the host died. Sourcing
20505
+ * it reaches the live sessions and costs no process its life, which is why
20506
+ * this half never has to bother the user - unlike the env half, where a
20507
+ * restart is the only way in.
20508
+ *
20509
+ * Runs AFTER the plugin and helper lanes above, deliberately: those are two
20510
+ * of the inputs the digest covers, and recording one before they finish
20511
+ * would stamp a state the runner is about to invalidate. */
20512
+ /** Bring this space's tabs back after the tmux server died (a reboot, or a
20513
+ * kill). A runner restart needs none of this — tmux outlives us.
20514
+ *
20515
+ * Structure only: every tab returns as a fresh shell.
20516
+ *
20517
+ * Each restored pane is respawned, because resurrect rebuilds panes on the
20518
+ * DEFAULT shell with no bin dir on PATH, and stamped, because it saves no
20519
+ * user options and an unstamped session would read as stale on every boot. */
20520
+ async restoreTerminals() {
20521
+ try {
20522
+ const target = this.target();
20523
+ await ensureTmuxResurrect(target);
20524
+ const root = target.paths.root;
20525
+ const before = await tmuxPanePaths(target);
20526
+ const mineUp = [...before].some(
20527
+ ([name, dir]) => name.startsWith("terminal-") && dir.startsWith(root)
20528
+ );
20529
+ if (mineUp) return;
20530
+ const restored = await resurrectRestore(target);
20531
+ const paths = await tmuxPanePaths(target);
20532
+ const revived = restored.filter(
20533
+ (name) => !before.has(name) && (paths.get(name) ?? "").startsWith(root)
20534
+ );
20535
+ if (revived.length === 0) return;
20536
+ const digest = await terminalDigest(target).catch(() => "");
20537
+ for (const session of revived) {
20538
+ await respawnTmuxPane({
20539
+ target,
20540
+ session,
20541
+ argv: [
20542
+ "bash",
20543
+ "-c",
20544
+ terminalInitScript(
20545
+ paths.get(session) || target.paths.workspace,
20546
+ target.paths.bin
20547
+ )
20548
+ ],
20549
+ cwd: paths.get(session) || target.paths.workspace,
20550
+ env: target.stateEnv()
20551
+ }).catch(() => {
20552
+ });
20553
+ if (digest) {
20554
+ await setTmuxSessionOption(
20555
+ target,
20556
+ session,
20557
+ TERMINAL_FP_OPTION,
20558
+ digest
20559
+ );
20560
+ }
20561
+ }
20562
+ } catch {
20563
+ }
20564
+ }
20565
+ async applyTerminalConfig() {
20566
+ await sourceTmuxConf(this.target()).catch(() => {
20567
+ });
20568
+ }
19881
20569
  async ensureSetup() {
19882
20570
  await this.ensureSpaceWorkspace();
19883
20571
  return { ready: true };
@@ -20528,7 +21216,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20528
21216
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
20529
21217
  secretsDirs() {
20530
21218
  const spaceDir = this.core.spaceDir();
20531
- return { configDir: (0, import_path13.dirname)(spaceDir), spaceDir };
21219
+ return { configDir: (0, import_path15.dirname)(spaceDir), spaceDir };
20532
21220
  }
20533
21221
  async deploySecretsStatus() {
20534
21222
  return {
@@ -20919,6 +21607,15 @@ var BoardAgentEngine = class _BoardAgentEngine {
20919
21607
  this.getCardTarget(String(p.postId)) ?? void 0
20920
21608
  );
20921
21609
  }
21610
+ bareTerminalHealth(p) {
21611
+ return this.terminal.bareTerminalHealth(p);
21612
+ }
21613
+ bareAckTerminalDigest() {
21614
+ return this.terminal.bareAckTerminalDigest();
21615
+ }
21616
+ bareRestartStaleTerminals(p) {
21617
+ return this.terminal.bareRestartStaleTerminals(p);
21618
+ }
20922
21619
  bareEditorState(p) {
20923
21620
  return this.terminal.bareEditorState(p);
20924
21621
  }
@@ -21556,6 +22253,17 @@ ${c.content.trim() || "(no description)"}`
21556
22253
  return await this.bareListTerminals(
21557
22254
  payload
21558
22255
  );
22256
+ case "bareTerminalHealth":
22257
+ return await this.bareTerminalHealth(
22258
+ payload
22259
+ );
22260
+ case "bareAckTerminalDigest":
22261
+ await this.bareAckTerminalDigest();
22262
+ return void 0;
22263
+ case "bareRestartStaleTerminals":
22264
+ return await this.bareRestartStaleTerminals(
22265
+ payload
22266
+ );
21559
22267
  case "bareEditorState":
21560
22268
  return this.bareEditorState(
21561
22269
  payload
@@ -21598,11 +22306,11 @@ ${c.content.trim() || "(no description)"}`
21598
22306
 
21599
22307
  // ../../shared/all/helpers/board-agent-core/index-service.ts
21600
22308
  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"));
22309
+ var import_crypto6 = require("crypto");
22310
+ var import_fs5 = require("fs");
22311
+ var import_promises11 = __toESM(require("fs/promises"));
21604
22312
  var import_net3 = require("net");
21605
- var import_path14 = require("path");
22313
+ var import_path16 = require("path");
21606
22314
  var import_util7 = require("util");
21607
22315
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
21608
22316
  var IndexService = class {
@@ -21616,7 +22324,7 @@ var IndexService = class {
21616
22324
  /** Per-space bearer for MCP + file, derived from the control token (the
21617
22325
  * service computes the same). */
21618
22326
  bearerToken(spaceSlug) {
21619
- return (0, import_crypto5.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
22327
+ return (0, import_crypto6.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21620
22328
  }
21621
22329
  /** MCP bridge for a space. Null until the service is up (which it kicks
21622
22330
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -21661,7 +22369,7 @@ var IndexService = class {
21661
22369
  const log = await this.openLog();
21662
22370
  this.proc = (0, import_child_process5.spawn)(
21663
22371
  process.execPath,
21664
- [(0, import_path14.join)(this.opts.indexDir, "index.js")],
22372
+ [(0, import_path16.join)(this.opts.indexDir, "index.js")],
21665
22373
  {
21666
22374
  cwd: this.opts.indexDir,
21667
22375
  env: {
@@ -21689,8 +22397,8 @@ var IndexService = class {
21689
22397
  * opened (a missing log must never stop the service from starting). */
21690
22398
  async openLog() {
21691
22399
  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");
22400
+ await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true });
22401
+ return (0, import_fs5.openSync)((0, import_path16.join)(this.opts.dataDir, "index.log"), "a");
21694
22402
  } catch {
21695
22403
  return null;
21696
22404
  }
@@ -21699,10 +22407,10 @@ var IndexService = class {
21699
22407
  * platform needing node-gyp fails here rather than at query time. */
21700
22408
  async installDeps() {
21701
22409
  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")
22410
+ if ((0, import_fs5.existsSync)((0, import_path16.join)(dir, "node_modules"))) return;
22411
+ await import_promises11.default.copyFile(
22412
+ (0, import_path16.join)(dir, "image-package.json"),
22413
+ (0, import_path16.join)(dir, "package.json")
21706
22414
  );
21707
22415
  await execFileAsync4(
21708
22416
  "npm",
@@ -21714,15 +22422,15 @@ var IndexService = class {
21714
22422
  * CLIs' persistent MCP config, so a fresh port each start would leave every
21715
22423
  * registration pointing at a dead one. */
21716
22424
  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(() => ""));
22425
+ const file = (0, import_path16.join)(this.opts.dataDir, ".port");
22426
+ const saved = Number(await import_promises11.default.readFile(file, "utf-8").catch(() => ""));
21719
22427
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
21720
22428
  return saved;
21721
22429
  }
21722
22430
  const port = await freePort();
21723
- await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
22431
+ await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21724
22432
  });
21725
- await import_promises9.default.writeFile(file, String(port), "utf-8").catch(() => {
22433
+ await import_promises11.default.writeFile(file, String(port), "utf-8").catch(() => {
21726
22434
  });
21727
22435
  return port;
21728
22436
  }
@@ -21889,24 +22597,24 @@ async function pairWithBrowser(serverUrl) {
21889
22597
  }
21890
22598
 
21891
22599
  // src/config.ts
21892
- var import_fs8 = __toESM(require("fs"));
22600
+ var import_fs9 = __toESM(require("fs"));
21893
22601
  var import_os6 = __toESM(require("os"));
21894
- var import_path19 = __toESM(require("path"));
22602
+ var import_path21 = __toESM(require("path"));
21895
22603
 
21896
22604
  // src/secureFile.ts
21897
- var import_crypto8 = __toESM(require("crypto"));
21898
- var import_fs7 = __toESM(require("fs"));
21899
- var import_path18 = __toESM(require("path"));
22605
+ var import_crypto9 = __toESM(require("crypto"));
22606
+ var import_fs8 = __toESM(require("fs"));
22607
+ var import_path20 = __toESM(require("path"));
21900
22608
 
21901
22609
  // src/keychain.ts
21902
22610
  var import_child_process7 = require("child_process");
21903
- var import_crypto6 = __toESM(require("crypto"));
21904
- var import_fs5 = __toESM(require("fs"));
22611
+ var import_crypto7 = __toESM(require("crypto"));
22612
+ var import_fs6 = __toESM(require("fs"));
21905
22613
  var import_os4 = __toESM(require("os"));
21906
- var import_path15 = __toESM(require("path"));
22614
+ var import_path17 = __toESM(require("path"));
21907
22615
  var SERVICE = "factiii-runner";
21908
22616
  var ACCOUNT = "config-encryption-key";
21909
- var DPAPI_KEY_FILE = import_path15.default.join(
22617
+ var DPAPI_KEY_FILE = import_path17.default.join(
21910
22618
  import_os4.default.homedir(),
21911
22619
  ".factiii-runner",
21912
22620
  "config-key.dpapi"
@@ -21987,7 +22695,7 @@ function linuxWrite(key) {
21987
22695
  function winRead() {
21988
22696
  let wrapped;
21989
22697
  try {
21990
- wrapped = import_fs5.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
22698
+ wrapped = import_fs6.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21991
22699
  } catch {
21992
22700
  return { state: "missing" };
21993
22701
  }
@@ -22012,8 +22720,8 @@ function winWrite(key) {
22012
22720
  "-Command",
22013
22721
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
22014
22722
  ]);
22015
- import_fs5.default.mkdirSync(import_path15.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22016
- import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22723
+ import_fs6.default.mkdirSync(import_path17.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22724
+ import_fs6.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
22017
22725
  `, { mode: 384 });
22018
22726
  }
22019
22727
  function keychainBackend() {
@@ -22152,7 +22860,7 @@ function resolve() {
22152
22860
  cached = { key: null, status: diagnose() };
22153
22861
  return cached;
22154
22862
  }
22155
- const fresh = import_crypto6.default.randomBytes(32);
22863
+ const fresh = import_crypto7.default.randomBytes(32);
22156
22864
  writeKey(fresh);
22157
22865
  const verify = readKey();
22158
22866
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -22174,23 +22882,23 @@ function keychainStatus() {
22174
22882
  }
22175
22883
 
22176
22884
  // src/secureStore.ts
22177
- var import_crypto7 = __toESM(require("crypto"));
22178
- var import_fs6 = __toESM(require("fs"));
22179
- var import_path17 = __toESM(require("path"));
22885
+ var import_crypto8 = __toESM(require("crypto"));
22886
+ var import_fs7 = __toESM(require("fs"));
22887
+ var import_path19 = __toESM(require("path"));
22180
22888
 
22181
22889
  // src/paths.ts
22182
22890
  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");
22891
+ var import_path18 = __toESM(require("path"));
22892
+ var CONFIG_DIR = import_path18.default.join(import_os5.default.homedir(), ".factiii-runner");
22185
22893
  function safeSlug(spaceSlug) {
22186
22894
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
22187
22895
  }
22188
22896
  function spaceDirPath(spaceSlug) {
22189
- return import_path16.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22897
+ return import_path18.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22190
22898
  }
22191
22899
 
22192
22900
  // src/secureStore.ts
22193
- var VAULT_KEY_FILE = import_path17.default.join(CONFIG_DIR, "vault-key.json");
22901
+ var VAULT_KEY_FILE = import_path19.default.join(CONFIG_DIR, "vault-key.json");
22194
22902
  var SCRYPT_N2 = 1 << 15;
22195
22903
  var MIN_PASSWORD_LENGTH = 8;
22196
22904
  var SecureStoreError = class extends Error {
@@ -22217,7 +22925,7 @@ function requireKeychainKey() {
22217
22925
  );
22218
22926
  }
22219
22927
  function passwordKey(password, salt) {
22220
- return import_crypto7.default.scryptSync(password, salt, 32, {
22928
+ return import_crypto8.default.scryptSync(password, salt, 32, {
22221
22929
  N: SCRYPT_N2,
22222
22930
  maxmem: 64 * 1024 * 1024
22223
22931
  });
@@ -22225,7 +22933,7 @@ function passwordKey(password, salt) {
22225
22933
  function wrapKey(machineKey, pwKey) {
22226
22934
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
22227
22935
  return Buffer.from(
22228
- import_crypto7.default.hkdfSync(
22936
+ import_crypto8.default.hkdfSync(
22229
22937
  "sha256",
22230
22938
  ikm,
22231
22939
  Buffer.alloc(0),
@@ -22235,8 +22943,8 @@ function wrapKey(machineKey, pwKey) {
22235
22943
  );
22236
22944
  }
22237
22945
  function seal(plaintext, key) {
22238
- const iv = import_crypto7.default.randomBytes(12);
22239
- const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
22946
+ const iv = import_crypto8.default.randomBytes(12);
22947
+ const cipher = import_crypto8.default.createCipheriv("aes-256-gcm", key, iv);
22240
22948
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
22241
22949
  return {
22242
22950
  iv: iv.toString("base64"),
@@ -22246,7 +22954,7 @@ function seal(plaintext, key) {
22246
22954
  }
22247
22955
  function unseal(box, key) {
22248
22956
  try {
22249
- const decipher = import_crypto7.default.createDecipheriv(
22957
+ const decipher = import_crypto8.default.createDecipheriv(
22250
22958
  "aes-256-gcm",
22251
22959
  key,
22252
22960
  Buffer.from(box.iv, "base64")
@@ -22263,7 +22971,7 @@ function unseal(box, key) {
22263
22971
  function readVaultFile() {
22264
22972
  try {
22265
22973
  const parsed = JSON.parse(
22266
- import_fs6.default.readFileSync(VAULT_KEY_FILE, "utf-8")
22974
+ import_fs7.default.readFileSync(VAULT_KEY_FILE, "utf-8")
22267
22975
  );
22268
22976
  return parsed.v === 1 ? parsed : null;
22269
22977
  } catch {
@@ -22271,15 +22979,15 @@ function readVaultFile() {
22271
22979
  }
22272
22980
  }
22273
22981
  function writeVaultFile(file) {
22274
- import_fs6.default.mkdirSync(CONFIG_DIR, { recursive: true });
22982
+ import_fs7.default.mkdirSync(CONFIG_DIR, { recursive: true });
22275
22983
  const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
22276
- import_fs6.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
22984
+ import_fs7.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
22277
22985
  `, { mode: 384 });
22278
- import_fs6.default.renameSync(tmp, VAULT_KEY_FILE);
22986
+ import_fs7.default.renameSync(tmp, VAULT_KEY_FILE);
22279
22987
  }
22280
22988
  var unlockedDek = null;
22281
22989
  function createVault(machineKey) {
22282
- const dek = import_crypto7.default.randomBytes(32);
22990
+ const dek = import_crypto8.default.randomBytes(32);
22283
22991
  writeVaultFile({
22284
22992
  v: 1,
22285
22993
  passwordEnabled: false,
@@ -22374,7 +23082,7 @@ function enablePassword(password) {
22374
23082
  );
22375
23083
  }
22376
23084
  const dek = dataKey();
22377
- const salt = import_crypto7.default.randomBytes(16);
23085
+ const salt = import_crypto8.default.randomBytes(16);
22378
23086
  writeVaultFile({
22379
23087
  v: 1,
22380
23088
  passwordEnabled: true,
@@ -22402,7 +23110,7 @@ function changePassword(current, next) {
22402
23110
  "The current password is not correct."
22403
23111
  );
22404
23112
  }
22405
- const salt = import_crypto7.default.randomBytes(16);
23113
+ const salt = import_crypto8.default.randomBytes(16);
22406
23114
  writeVaultFile({
22407
23115
  v: 1,
22408
23116
  passwordEnabled: true,
@@ -22479,8 +23187,8 @@ function storeStatus() {
22479
23187
  // src/secureFile.ts
22480
23188
  var MAGIC = "FACTIII1";
22481
23189
  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);
23190
+ const iv = import_crypto9.default.randomBytes(12);
23191
+ const cipher = import_crypto9.default.createCipheriv("aes-256-gcm", key, iv);
22484
23192
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
22485
23193
  const envelope = {
22486
23194
  v: version,
@@ -22491,7 +23199,7 @@ function encrypt(plain, key, version) {
22491
23199
  return `${MAGIC}${JSON.stringify(envelope)}`;
22492
23200
  }
22493
23201
  function decrypt(envelope, key) {
22494
- const decipher = import_crypto8.default.createDecipheriv(
23202
+ const decipher = import_crypto9.default.createDecipheriv(
22495
23203
  "aes-256-gcm",
22496
23204
  key,
22497
23205
  Buffer.from(envelope.iv, "base64")
@@ -22512,15 +23220,15 @@ function parseEnvelope(raw) {
22512
23220
  }
22513
23221
  }
22514
23222
  function atomicWrite(filePath, body) {
22515
- import_fs7.default.mkdirSync(import_path18.default.dirname(filePath), { recursive: true });
23223
+ import_fs8.default.mkdirSync(import_path20.default.dirname(filePath), { recursive: true });
22516
23224
  const tmp = `${filePath}.${process.pid}.tmp`;
22517
- import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
22518
- import_fs7.default.renameSync(tmp, filePath);
23225
+ import_fs8.default.writeFileSync(tmp, body, { mode: 384 });
23226
+ import_fs8.default.renameSync(tmp, filePath);
22519
23227
  }
22520
23228
  function readSecureBuffer(filePath) {
22521
23229
  let raw;
22522
23230
  try {
22523
- raw = import_fs7.default.readFileSync(filePath, "utf-8");
23231
+ raw = import_fs8.default.readFileSync(filePath, "utf-8");
22524
23232
  } catch {
22525
23233
  return null;
22526
23234
  }
@@ -22577,7 +23285,7 @@ function writeSecureJson(filePath, value2) {
22577
23285
  function readBootJson(filePath) {
22578
23286
  let raw;
22579
23287
  try {
22580
- raw = import_fs7.default.readFileSync(filePath, "utf-8");
23288
+ raw = import_fs8.default.readFileSync(filePath, "utf-8");
22581
23289
  } catch {
22582
23290
  return null;
22583
23291
  }
@@ -22627,8 +23335,8 @@ function writeBootJson(filePath, value2) {
22627
23335
  }
22628
23336
 
22629
23337
  // 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");
23338
+ var CONFIG_DIR2 = import_path21.default.join(import_os6.default.homedir(), ".factiii-runner");
23339
+ var CONFIG_FILE = import_path21.default.join(CONFIG_DIR2, "config.json");
22632
23340
  function readRunnerConfig() {
22633
23341
  return readBootJson(CONFIG_FILE);
22634
23342
  }
@@ -22636,7 +23344,7 @@ function writeRunnerConfig(config) {
22636
23344
  writeBootJson(CONFIG_FILE, config);
22637
23345
  }
22638
23346
  function configExists() {
22639
- return import_fs8.default.existsSync(CONFIG_FILE);
23347
+ return import_fs9.default.existsSync(CONFIG_FILE);
22640
23348
  }
22641
23349
 
22642
23350
  // src/promptPassword.ts
@@ -22742,65 +23450,17 @@ function installLatest() {
22742
23450
  );
22743
23451
  return false;
22744
23452
  }
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));
22767
- }
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
- };
23453
+ return true;
22797
23454
  }
22798
23455
 
23456
+ // src/supervisor.ts
23457
+ var import_node_child_process2 = require("node:child_process");
23458
+
22799
23459
  // src/daemon.ts
22800
- var import_crypto9 = require("crypto");
22801
- var import_fs10 = __toESM(require("fs"));
23460
+ var import_crypto10 = require("crypto");
23461
+ var import_fs11 = __toESM(require("fs"));
22802
23462
  var import_node_datachannel = require("node-datachannel");
22803
- var import_path21 = __toESM(require("path"));
23463
+ var import_path23 = __toESM(require("path"));
22804
23464
 
22805
23465
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
22806
23466
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23980,12 +24640,12 @@ function parse2(str) {
23980
24640
  uri.queryKey = queryKey(uri, uri["query"]);
23981
24641
  return uri;
23982
24642
  }
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) {
24643
+ function pathNames(obj, path23) {
24644
+ const regx = /\/{2,9}/g, names = path23.replace(regx, "/").split("/");
24645
+ if (path23.slice(0, 1) == "/" || path23.length === 0) {
23986
24646
  names.splice(0, 1);
23987
24647
  }
23988
- if (path21.slice(-1) == "/") {
24648
+ if (path23.slice(-1) == "/") {
23989
24649
  names.splice(names.length - 1, 1);
23990
24650
  }
23991
24651
  return names;
@@ -24604,7 +25264,7 @@ var protocol2 = Socket.protocol;
24604
25264
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
24605
25265
  var import_debug7 = __toESM(require_src(), 1);
24606
25266
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
24607
- function url(uri, path21 = "", loc) {
25267
+ function url(uri, path23 = "", loc) {
24608
25268
  let obj = uri;
24609
25269
  loc = loc || typeof location !== "undefined" && location;
24610
25270
  if (null == uri)
@@ -24638,7 +25298,7 @@ function url(uri, path21 = "", loc) {
24638
25298
  obj.path = obj.path || "/";
24639
25299
  const ipv6 = obj.host.indexOf(":") !== -1;
24640
25300
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
24641
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path21;
25301
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path23;
24642
25302
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
24643
25303
  return obj;
24644
25304
  }
@@ -26272,8 +26932,8 @@ function lookup(uri, opts) {
26272
26932
  const parsed = url(uri, opts.path || "/socket.io");
26273
26933
  const source = parsed.source;
26274
26934
  const id = parsed.id;
26275
- const path21 = parsed.path;
26276
- const sameNamespace = cache[id] && path21 in cache[id]["nsps"];
26935
+ const path23 = parsed.path;
26936
+ const sameNamespace = cache[id] && path23 in cache[id]["nsps"];
26277
26937
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
26278
26938
  let io;
26279
26939
  if (newConnection) {
@@ -26298,205 +26958,22 @@ Object.assign(lookup, {
26298
26958
  connect: lookup
26299
26959
  });
26300
26960
 
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}`;
26961
+ // src/agent-adapter.ts
26962
+ var import_fs10 = __toESM(require("fs"));
26963
+ var import_path22 = __toESM(require("path"));
26964
+ function credsDir(spaceSlug) {
26965
+ return import_path22.default.join(spaceDirPath(spaceSlug), ".creds");
26334
26966
  }
26335
- function tmuxName(terminalKey) {
26336
- return `terminal-${safeTmuxName(terminalKey)}`;
26967
+ function githubPath(spaceSlug) {
26968
+ return import_path22.default.join(credsDir(spaceSlug), "github.json");
26337
26969
  }
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
26970
  function connectionPath(spaceSlug) {
26487
- return import_path20.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26971
+ return import_path22.default.join(credsDir(spaceSlug), "onedrive.json");
26488
26972
  }
26973
+ var RUNNER_CONFIG_PATH = import_path22.default.join(CONFIG_DIR, "runner-config.json");
26489
26974
  function writeJson(filePath, value2) {
26490
26975
  writeSecureJson(filePath, value2);
26491
26976
  }
26492
- var CONFIG_FIELDS = [
26493
- "repoUrl",
26494
- "mainBranch",
26495
- "gitName",
26496
- "gitEmail",
26497
- "githubToken",
26498
- "provider"
26499
- ];
26500
26977
  function fillDefaults(config) {
26501
26978
  return {
26502
26979
  repoUrl: config.repoUrl || "",
@@ -26507,29 +26984,107 @@ function fillDefaults(config) {
26507
26984
  provider: config.provider || "claude"
26508
26985
  };
26509
26986
  }
26510
- function migrateLegacyConfigs() {
26511
- const file = { boards: {} };
26512
- let legacy = [];
26987
+ function configDirNames() {
26988
+ try {
26989
+ return import_fs10.default.readdirSync(CONFIG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
26990
+ } catch {
26991
+ return [];
26992
+ }
26993
+ }
26994
+ function provisionedSpaceSlugs() {
26995
+ return configDirNames().filter(
26996
+ (name) => import_fs10.default.existsSync(import_path22.default.join(CONFIG_DIR, name, "bin"))
26997
+ );
26998
+ }
26999
+ var migrated = false;
27000
+ function migrateToSpaceFolders() {
27001
+ if (migrated) return;
27002
+ migrated = true;
27003
+ try {
27004
+ migrateOnce();
27005
+ } catch {
27006
+ migrated = false;
27007
+ }
27008
+ }
27009
+ function readLegacy(filePath) {
27010
+ if (!import_fs10.default.existsSync(filePath)) return { readable: true, value: null };
27011
+ const value2 = readSecureJson(filePath);
27012
+ return { readable: value2 !== null, value: value2 };
27013
+ }
27014
+ function migrateOnce() {
27015
+ const legacy = readLegacy(RUNNER_CONFIG_PATH);
27016
+ if (!legacy.readable) {
27017
+ throw new Error(
27018
+ `Cannot read ${RUNNER_CONFIG_PATH} \u2014 leaving the legacy stores in place.`
27019
+ );
27020
+ }
27021
+ const store = legacy.value ?? {};
27022
+ const base = store.default ?? {};
27023
+ const boards = store.boards ?? {};
27024
+ const strays = {};
27025
+ const consumed = [];
27026
+ let strayFiles = [];
26513
27027
  try {
26514
- legacy = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
27028
+ strayFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
26515
27029
  } catch {
26516
- return file;
27030
+ strayFiles = [];
26517
27031
  }
26518
- for (const name of legacy) {
27032
+ for (const name of strayFiles) {
26519
27033
  const slug = name.slice("space-".length, -".json".length);
26520
- const parsed = readSecureJson(
26521
- import_path20.default.join(CONFIG_DIR, name)
27034
+ const parsed = readLegacy(
27035
+ import_path22.default.join(CONFIG_DIR, name)
26522
27036
  );
26523
- if (!parsed) continue;
26524
- if (!file.defaultSlug) {
26525
- file.defaultSlug = slug;
26526
- file.default = parsed;
26527
- } else {
26528
- file.boards[slug] = parsed;
27037
+ if (!parsed.readable) continue;
27038
+ if (parsed.value) strays[slug] = parsed.value;
27039
+ consumed.push(name);
27040
+ }
27041
+ const slugs = /* @__PURE__ */ new Set([
27042
+ ...Object.keys(boards),
27043
+ ...Object.keys(strays),
27044
+ ...store.defaultSlug ? [store.defaultSlug] : [],
27045
+ ...provisionedSpaceSlugs()
27046
+ ]);
27047
+ for (const slug of slugs) {
27048
+ if (import_fs10.default.existsSync(githubPath(slug))) continue;
27049
+ const over = boards[slug] ?? strays[slug] ?? {};
27050
+ if (!Object.keys(over).length && !Object.keys(base).length) continue;
27051
+ const merged = fillDefaults({
27052
+ repoUrl: over.repoUrl || base.repoUrl,
27053
+ mainBranch: over.mainBranch || base.mainBranch,
27054
+ gitName: over.gitName || base.gitName,
27055
+ gitEmail: over.gitEmail || base.gitEmail,
27056
+ githubToken: over.githubToken || base.githubToken,
27057
+ provider: over.provider || base.provider
27058
+ });
27059
+ import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27060
+ writeJson(githubPath(slug), merged);
27061
+ }
27062
+ let odFiles = [];
27063
+ try {
27064
+ odFiles = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".onedrive.json"));
27065
+ } catch {
27066
+ odFiles = [];
27067
+ }
27068
+ for (const name of odFiles) {
27069
+ const slug = name.slice("space-".length, -".onedrive.json".length);
27070
+ if (import_fs10.default.existsSync(connectionPath(slug))) {
27071
+ consumed.push(name);
27072
+ continue;
27073
+ }
27074
+ const conn = readLegacy(import_path22.default.join(CONFIG_DIR, name));
27075
+ if (!conn.readable) continue;
27076
+ if (conn.value) {
27077
+ import_fs10.default.mkdirSync(credsDir(slug), { recursive: true });
27078
+ writeJson(connectionPath(slug), conn.value);
27079
+ }
27080
+ consumed.push(name);
27081
+ }
27082
+ for (const name of [...consumed, "runner-config.json"]) {
27083
+ try {
27084
+ import_fs10.default.rmSync(import_path22.default.join(CONFIG_DIR, name), { force: true });
27085
+ } catch {
26529
27086
  }
26530
27087
  }
26531
- if (file.defaultSlug) writeJson(RUNNER_CONFIG_PATH, file);
26532
- return file;
26533
27088
  }
26534
27089
  var sweptStrays = false;
26535
27090
  function encryptStrayPlaintextConfigs() {
@@ -26538,14 +27093,14 @@ function encryptStrayPlaintextConfigs() {
26538
27093
  const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
26539
27094
  let names = [];
26540
27095
  try {
26541
- names = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
27096
+ names = import_fs10.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
26542
27097
  } catch {
26543
27098
  return;
26544
27099
  }
26545
27100
  for (const name of names) {
26546
- const filePath = import_path20.default.join(CONFIG_DIR, name);
27101
+ const filePath = import_path22.default.join(CONFIG_DIR, name);
26547
27102
  try {
26548
- const raw = import_fs9.default.readFileSync(filePath, "utf-8");
27103
+ const raw = import_fs10.default.readFileSync(filePath, "utf-8");
26549
27104
  if (!raw.startsWith("{")) continue;
26550
27105
  writeSecureJson(filePath, JSON.parse(raw));
26551
27106
  } catch {
@@ -26557,84 +27112,50 @@ function encryptStrayPlaintextConfigs() {
26557
27112
  function resetStraySweep() {
26558
27113
  sweptStrays = false;
26559
27114
  }
26560
- function readRunnerConfig2() {
27115
+ function readBoard(spaceSlug) {
26561
27116
  encryptStrayPlaintextConfigs();
26562
- const file = readSecureJson(RUNNER_CONFIG_PATH);
26563
- return file ?? migrateLegacyConfigs();
27117
+ migrateToSpaceFolders();
27118
+ return readSecureJson(githubPath(spaceSlug)) ?? {};
26564
27119
  }
26565
27120
  async function getConfiguredConnections() {
26566
27121
  const kinds = /* @__PURE__ */ new Set();
26567
- let file;
26568
27122
  try {
26569
- file = readRunnerConfig2();
27123
+ for (const slug of listConfiguredSlugs()) {
27124
+ if (kinds.has("git") && kinds.has("onedrive")) break;
27125
+ if (readBoard(slug).githubToken) kinds.add("git");
27126
+ if (import_fs10.default.existsSync(connectionPath(slug))) kinds.add("onedrive");
27127
+ }
26570
27128
  } catch {
26571
27129
  return [];
26572
27130
  }
26573
- const configs = [file.default ?? {}, ...Object.values(file.boards ?? {})];
26574
- for (const c of configs) {
26575
- if (c.githubToken) kinds.add("git");
26576
- }
26577
27131
  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
27132
  return [...kinds];
26584
27133
  }
26585
27134
  function listConfiguredSlugs() {
26586
27135
  try {
26587
- const file = readRunnerConfig2();
26588
- return [
26589
- .../* @__PURE__ */ new Set([
26590
- ...Object.keys(file.boards ?? {}),
26591
- ...file.defaultSlug ? [file.defaultSlug] : []
26592
- ])
26593
- ];
27136
+ migrateToSpaceFolders();
27137
+ return configDirNames().filter(
27138
+ (slug) => import_fs10.default.existsSync(githubPath(slug)) || import_fs10.default.existsSync(connectionPath(slug))
27139
+ );
26594
27140
  } catch {
26595
27141
  return [];
26596
27142
  }
26597
27143
  }
26598
27144
  var localConfigProvider = {
26599
27145
  readConfig(spaceSlug) {
26600
- let file;
26601
27146
  try {
26602
- file = readRunnerConfig2();
27147
+ return fillDefaults(readBoard(spaceSlug));
26603
27148
  } catch (err) {
26604
27149
  if (!isSecureStoreError(err)) throw err;
26605
27150
  return fillDefaults({});
26606
27151
  }
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
27152
  },
27153
+ // The whole config, every time: a cleared field has to persist as cleared,
27154
+ // which is what makes disconnect stick.
26618
27155
  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);
27156
+ migrateToSpaceFolders();
27157
+ import_fs10.default.mkdirSync(credsDir(spaceSlug), { recursive: true });
27158
+ writeJson(githubPath(spaceSlug), config);
26638
27159
  },
26639
27160
  readOneDriveConnection(spaceSlug) {
26640
27161
  try {
@@ -26647,7 +27168,7 @@ var localConfigProvider = {
26647
27168
  writeOneDriveConnection(spaceSlug, connection) {
26648
27169
  const filePath = connectionPath(spaceSlug);
26649
27170
  if (!connection) {
26650
- import_fs9.default.rmSync(filePath, { force: true });
27171
+ import_fs10.default.rmSync(filePath, { force: true });
26651
27172
  return;
26652
27173
  }
26653
27174
  writeJson(filePath, connection);
@@ -26672,7 +27193,7 @@ var localConfigProvider = {
26672
27193
  "claude",
26673
27194
  "codex"
26674
27195
  ]) {
26675
- import_fs9.default.mkdirSync(import_path20.default.join(dir, sub), { recursive: true });
27196
+ import_fs10.default.mkdirSync(import_path22.default.join(dir, sub), { recursive: true });
26676
27197
  }
26677
27198
  return dir;
26678
27199
  }
@@ -26846,15 +27367,15 @@ async function startDaemon(config) {
26846
27367
  }
26847
27368
  );
26848
27369
  const indexCandidates = [
26849
- import_path21.default.join(__dirname, "..", "index"),
26850
- import_path21.default.join(__dirname, "..", "..", "index", "image")
27370
+ import_path23.default.join(__dirname, "..", "index"),
27371
+ import_path23.default.join(__dirname, "..", "..", "index", "image")
26851
27372
  ];
26852
- const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
27373
+ const indexDir = indexCandidates.find((p) => import_fs11.default.existsSync(p)) ?? indexCandidates[0];
26853
27374
  const odIndex = new IndexService({
26854
- controlToken: (0, import_crypto9.createHash)("sha256").update(config.authToken).digest("hex"),
27375
+ controlToken: (0, import_crypto10.createHash)("sha256").update(config.authToken).digest("hex"),
26855
27376
  embedUrl: () => config.serverUrl,
26856
27377
  indexDir,
26857
- dataDir: import_path21.default.join(CONFIG_DIR, "index-data")
27378
+ dataDir: import_path23.default.join(CONFIG_DIR, "index-data")
26858
27379
  });
26859
27380
  void odIndex.ensureRunning().catch((err) => {
26860
27381
  console.error(
@@ -27468,11 +27989,27 @@ ${err.fix}`);
27468
27989
  console.error("");
27469
27990
  process.exit(1);
27470
27991
  }
27992
+ var MAX_SUPPORTED_NODE_MAJOR = 24;
27993
+ function assertSupportedNode() {
27994
+ const major = Number(process.versions.node.split(".")[0]);
27995
+ if (Number.isNaN(major) || major <= MAX_SUPPORTED_NODE_MAJOR) return;
27996
+ console.error(
27997
+ `
27998
+ factiii-runner needs Node 20\u2013${MAX_SUPPORTED_NODE_MAJOR} (pinned 24.11.1); you are on Node ${process.versions.node}.
27999
+ The bundled OneDrive index compiles better-sqlite3 from source, and that build fails on Node 25+.
28000
+
28001
+ Switch with nvm: nvm install 24.11.1 && nvm use 24.11.1
28002
+ then relaunch: factiii-runner start
28003
+ `
28004
+ );
28005
+ process.exit(1);
28006
+ }
27471
28007
  import_node_dns.default.setDefaultResultOrder("ipv4first");
27472
28008
  var program2 = new Command();
27473
28009
  program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
27474
28010
  program2.command("setup").description("Verify the host toolchain and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
27475
28011
  try {
28012
+ assertSupportedNode();
27476
28013
  await setup(opts.server);
27477
28014
  } catch (err) {
27478
28015
  console.error(
@@ -27501,6 +28038,7 @@ program2.command("connect").description("Pair this runner with a Factiii account
27501
28038
  }
27502
28039
  });
27503
28040
  program2.command("start").description("Start the runner daemon").action(async () => {
28041
+ assertSupportedNode();
27504
28042
  if (!configExists()) {
27505
28043
  console.error('No config found. Run "factiii-runner setup" first.');
27506
28044
  process.exit(1);
@@ -27546,6 +28084,9 @@ program2.command("status").description("Show current runner configuration").acti
27546
28084
  console.log(`Token: \xB7\xB7\xB7\xB7${config.authToken.slice(-4)}`);
27547
28085
  });
27548
28086
  program2.command("doctor").description("Show where each host tool resolves, for a failed environment").action(async () => {
28087
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
28088
+ const nodeNote = nodeMajor > MAX_SUPPORTED_NODE_MAJOR ? " (UNSUPPORTED \u2014 use 24.11.1)" : "";
28089
+ console.log(`${"node".padEnd(14)} ${process.execPath} v${process.versions.node}${nodeNote}`);
27549
28090
  const report = await reportToolchain(hostTarget(CONFIG_DIR));
27550
28091
  for (const tool of report.tools) {
27551
28092
  console.log(`${tool.bin.padEnd(14)} ${tool.location || "NOT FOUND"}`);