@factiii/runner 0.10.1 → 0.10.2

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
@@ -915,18 +915,18 @@ var require_suggestSimilar = __commonJS({
915
915
  }
916
916
  return d[a.length][b.length];
917
917
  }
918
- function suggestSimilar(word, candidates) {
919
- if (!candidates || candidates.length === 0) return "";
920
- candidates = Array.from(new Set(candidates));
918
+ function suggestSimilar(word, candidates2) {
919
+ if (!candidates2 || candidates2.length === 0) return "";
920
+ candidates2 = Array.from(new Set(candidates2));
921
921
  const searchingOptions = word.startsWith("--");
922
922
  if (searchingOptions) {
923
923
  word = word.slice(2);
924
- candidates = candidates.map((candidate) => candidate.slice(2));
924
+ candidates2 = candidates2.map((candidate) => candidate.slice(2));
925
925
  }
926
926
  let similar = [];
927
927
  let bestDistance = maxDistance;
928
928
  const minSimilarity = 0.4;
929
- candidates.forEach((candidate) => {
929
+ candidates2.forEach((candidate) => {
930
930
  if (candidate.length <= 1) return;
931
931
  const distance = editDistance(word, candidate);
932
932
  const length = Math.max(word.length, candidate.length);
@@ -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 path17 = require("node:path");
966
+ var path18 = require("node:path");
967
967
  var fs14 = 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 = path17.resolve(baseDir, baseName);
1899
+ const localBin = path18.resolve(baseDir, baseName);
1900
1900
  if (fs14.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path17.extname(baseName))) return void 0;
1901
+ if (sourceExt.includes(path18.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
1903
  (ext) => fs14.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 = path17.resolve(
1920
- path17.dirname(resolvedScriptPath),
1919
+ executableDir = path18.resolve(
1920
+ path18.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 = path17.basename(
1927
+ const legacyName = path18.basename(
1928
1928
  this._scriptPath,
1929
- path17.extname(this._scriptPath)
1929
+ path18.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(path17.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path18.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 = path17.basename(filename, path17.extname(filename));
2780
+ this._name = path18.basename(filename, path18.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(path18) {
2795
- if (path18 === void 0) return this._executableDir;
2796
- this._executableDir = path18;
2794
+ executableDir(path19) {
2795
+ if (path19 === void 0) return this._executableDir;
2796
+ this._executableDir = path19;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
3984
3984
  var require_supports_color = __commonJS({
3985
3985
  "../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
3986
3986
  "use strict";
3987
- var os6 = require("os");
3987
+ var os7 = require("os");
3988
3988
  var hasFlag = require_has_flag();
3989
3989
  var env = process.env;
3990
3990
  var forceColor;
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
4022
4022
  }
4023
4023
  const min = forceColor ? 1 : 0;
4024
4024
  if (process.platform === "win32") {
4025
- const osRelease = os6.release().split(".");
4025
+ const osRelease = os7.release().split(".");
4026
4026
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
4027
4027
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
4028
4028
  }
@@ -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: path17, errorMaps, issueData } = params;
8432
- const fullPath = [...path17, ...issueData.path || []];
8431
+ const { data, path: path18, errorMaps, issueData } = params;
8432
+ const fullPath = [...path18, ...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, path17, key) {
8548
+ constructor(parent, value2, path18, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path17;
8552
+ this._path = path18;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12908,7 +12908,7 @@ ${diff.slice(0, 8e3)}`,
12908
12908
  }
12909
12909
 
12910
12910
  // ../../shared/all/helpers/board-agent-core/engine.ts
12911
- var import_path9 = require("path");
12911
+ var import_path10 = require("path");
12912
12912
 
12913
12913
  // ../../shared/all/domains/electron.ts
12914
12914
  var SETUP_LOG_PREFIX = "log ";
@@ -13017,7 +13017,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13017
13017
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13018
13018
  }
13019
13019
  async function addWorktree(target, workName, baseBranch, kind) {
13020
- const path17 = worktreeFor(target.paths, workName);
13020
+ const path18 = worktreeFor(target.paths, workName);
13021
13021
  const branch = workBranch(workName, kind);
13022
13022
  await gitInDir(
13023
13023
  target,
@@ -13027,7 +13027,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13027
13027
  baseBranch
13028
13028
  ).catch(() => {
13029
13029
  });
13030
- await removeWorktree(target, path17).catch(() => {
13030
+ await removeWorktree(target, path18).catch(() => {
13031
13031
  });
13032
13032
  await gitInDir(
13033
13033
  target,
@@ -13036,30 +13036,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13036
13036
  "add",
13037
13037
  "-B",
13038
13038
  branch,
13039
- path17,
13039
+ path18,
13040
13040
  `origin/${baseBranch}`
13041
13041
  );
13042
- return { path: path17, branch };
13042
+ return { path: path18, branch };
13043
13043
  }
13044
- async function removeWorktree(target, path17) {
13044
+ async function removeWorktree(target, path18) {
13045
13045
  await gitInDir(
13046
13046
  target,
13047
13047
  target.paths.repo,
13048
13048
  "worktree",
13049
13049
  "remove",
13050
13050
  "--force",
13051
- path17
13051
+ path18
13052
13052
  );
13053
13053
  }
13054
- async function removeWorktreeAndBranch(target, path17) {
13054
+ async function removeWorktreeAndBranch(target, path18) {
13055
13055
  const branch = await gitInDir(
13056
13056
  target,
13057
- path17,
13057
+ path18,
13058
13058
  "rev-parse",
13059
13059
  "--abbrev-ref",
13060
13060
  "HEAD"
13061
13061
  ).then((out) => out.trim()).catch(() => "");
13062
- await removeWorktree(target, path17);
13062
+ await removeWorktree(target, path18);
13063
13063
  if (branch && branch !== "HEAD") {
13064
13064
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13065
13065
  () => {
@@ -13073,8 +13073,8 @@ async function pruneWorktrees(target) {
13073
13073
  }
13074
13074
  );
13075
13075
  }
13076
- function isWorktreePath(target, path17) {
13077
- return path17.startsWith(`${target.paths.worktrees}/`);
13076
+ function isWorktreePath(target, path18) {
13077
+ return path18.startsWith(`${target.paths.worktrees}/`);
13078
13078
  }
13079
13079
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13080
13080
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13100,13 +13100,13 @@ function parseStatus(raw) {
13100
13100
  const y = line[1];
13101
13101
  const rest = line.slice(3);
13102
13102
  const arrow = rest.indexOf(" -> ");
13103
- const path17 = arrow === -1 ? rest : rest.slice(arrow + 4);
13103
+ const path18 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
13104
  if (x === "?" || y === "?") {
13105
- unstaged.push({ path: path17, code: "A" });
13105
+ unstaged.push({ path: path18, code: "A" });
13106
13106
  continue;
13107
13107
  }
13108
- if (x !== " ") staged.push({ path: path17, code: x });
13109
- if (y !== " ") unstaged.push({ path: path17, code: y });
13108
+ if (x !== " ") staged.push({ path: path18, code: x });
13109
+ if (y !== " ") unstaged.push({ path: path18, code: y });
13110
13110
  }
13111
13111
  return { staged, unstaged };
13112
13112
  }
@@ -13300,16 +13300,18 @@ function cardKey(postId) {
13300
13300
  return `card:${safeId(postId)}`;
13301
13301
  }
13302
13302
  var INDEX_KEY = "cards";
13303
- function socketPath(target) {
13303
+ function redisSocketPath(target) {
13304
13304
  return `${target.paths.state}/redis.sock`;
13305
13305
  }
13306
13306
  async function redisCli(target, args) {
13307
- const out = await target.sh(`redis-cli -s ${socketPath(target)} ${args}`);
13307
+ const out = await target.sh(
13308
+ `redis-cli -s ${redisSocketPath(target)} ${args}`
13309
+ );
13308
13310
  return out.replace(/\n$/, "");
13309
13311
  }
13310
13312
  function redisCliStdin(target, args, stdin) {
13311
13313
  return target.runWithStdin(
13312
- ["redis-cli", "-s", socketPath(target), "-x", ...args],
13314
+ ["redis-cli", "-s", redisSocketPath(target), "-x", ...args],
13313
13315
  stdin,
13314
13316
  `redis-cli ${args.join(" ")}`
13315
13317
  );
@@ -13322,7 +13324,7 @@ async function ensureRedis(target) {
13322
13324
  }
13323
13325
  const stateDir = target.paths.state;
13324
13326
  await target.sh(
13325
- `mkdir -p ${stateDir} && redis-server --daemonize yes --appendonly yes --save '' --dir ${stateDir} --port 0 --unixsocket ${socketPath(target)} --unixsocketperm 700`
13327
+ `mkdir -p ${stateDir} && redis-server --daemonize yes --appendonly yes --save '' --dir ${stateDir} --port 0 --unixsocket ${redisSocketPath(target)} --unixsocketperm 700`
13326
13328
  );
13327
13329
  for (let i = 0; i < 20; i += 1) {
13328
13330
  try {
@@ -13688,6 +13690,7 @@ function toSummary(session) {
13688
13690
  pendingQuestion: session.pendingQuestion,
13689
13691
  error: session.error,
13690
13692
  startedAt: session.startedAt,
13693
+ agentState: session.agentState,
13691
13694
  targetBranch: session.targetBranch,
13692
13695
  taskTitle: session.taskTitle,
13693
13696
  todos: session.todos,
@@ -15917,18 +15920,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
15917
15920
  await this.core.ensureSpaceWorkspace();
15918
15921
  await assertAgentSignedIn(session.provider, this.core.target());
15919
15922
  if (hasGit) {
15920
- const { path: path17 } = await addWorktree(
15923
+ const { path: path18 } = await addWorktree(
15921
15924
  this.core.target(),
15922
15925
  session.taskId,
15923
15926
  baseBranch || config.mainBranch || "main",
15924
15927
  "card"
15925
15928
  );
15926
- session.workspacePath = path17;
15929
+ session.workspacePath = path18;
15927
15930
  await setGitIdentity(
15928
15931
  this.core.target(),
15929
15932
  config.gitName,
15930
15933
  config.gitEmail,
15931
- path17
15934
+ path18
15932
15935
  );
15933
15936
  this.emitter.addLog(session, "system", "Worktree ready.");
15934
15937
  } else {
@@ -16150,18 +16153,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16150
16153
  this.emitter.addLog(session, "system", "Preparing environment...");
16151
16154
  await this.core.ensureSpaceWorkspace();
16152
16155
  await assertAgentSignedIn(session.provider, this.core.target());
16153
- const { path: path17 } = await addWorktree(
16156
+ const { path: path18 } = await addWorktree(
16154
16157
  this.core.target(),
16155
16158
  session.taskId,
16156
16159
  config.mainBranch || "main",
16157
16160
  "card"
16158
16161
  );
16159
- session.workspacePath = path17;
16162
+ session.workspacePath = path18;
16160
16163
  await setGitIdentity(
16161
16164
  this.core.target(),
16162
16165
  config.gitName,
16163
16166
  config.gitEmail,
16164
- path17
16167
+ path18
16165
16168
  );
16166
16169
  await this.putCard(session);
16167
16170
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16456,6 +16459,50 @@ async function startTmuxSession(opts) {
16456
16459
  };
16457
16460
  }
16458
16461
 
16462
+ // ../../shared/all/helpers/terminal-ai/agentState.ts
16463
+ var STATES = /* @__PURE__ */ new Set(["working", "idle", "blocked"]);
16464
+ var AGENT_STATE_CHANNEL = "agent-state";
16465
+ function agentStateKey(paneId) {
16466
+ return `agent-state:${paneId}`;
16467
+ }
16468
+ function parseAgentMessage(payload) {
16469
+ const at = payload.indexOf(" ");
16470
+ if (at <= 0) return null;
16471
+ const state = parseAgentState(payload.slice(at + 1));
16472
+ return state ? { paneId: payload.slice(0, at), state } : null;
16473
+ }
16474
+ function postIdForSessionName(sessionName, postIds) {
16475
+ for (const postId of postIds) {
16476
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
16477
+ if (sessionName.startsWith(prefix)) return postId;
16478
+ }
16479
+ return null;
16480
+ }
16481
+ function parseAgentState(text) {
16482
+ const word = text.trim();
16483
+ return STATES.has(word) ? word : null;
16484
+ }
16485
+ function drainStateMessages(buffer) {
16486
+ const lines = buffer.split("\n");
16487
+ const partial = lines.pop() ?? "";
16488
+ const states = [];
16489
+ let i = 0;
16490
+ let consumed = 0;
16491
+ while (i < lines.length) {
16492
+ if (lines[i] !== "message") {
16493
+ i += 1;
16494
+ consumed = i;
16495
+ continue;
16496
+ }
16497
+ if (i + 2 >= lines.length) break;
16498
+ const parsed = parseAgentMessage(lines[i + 2]);
16499
+ if (parsed) states.push(parsed);
16500
+ i += 3;
16501
+ consumed = i;
16502
+ }
16503
+ return { states, rest: [...lines.slice(consumed), partial].join("\n") };
16504
+ }
16505
+
16459
16506
  // ../../shared/all/helpers/terminal-ai/processes.ts
16460
16507
  var PS_FORMAT = "pid=,ppid=,pcpu=,rss=,etime=,args=";
16461
16508
  var PS_FIELDS = 5;
@@ -16501,6 +16548,45 @@ function descendantsOf(rows, roots) {
16501
16548
  }
16502
16549
  return found.sort((a, b) => a.pid - b.pid);
16503
16550
  }
16551
+ function panesByPid(raw) {
16552
+ const found = /* @__PURE__ */ new Map();
16553
+ for (const line of raw.split("\n")) {
16554
+ const trimmed = line.trim();
16555
+ const pid = Number(trimmed.split(/\s+/)[0]);
16556
+ if (!Number.isInteger(pid) || pid <= 0) continue;
16557
+ const match = /(?:^|\s)TMUX_PANE=(%\d+)(?:\s|$)/.exec(trimmed);
16558
+ if (match) found.set(pid, match[1]);
16559
+ }
16560
+ return found;
16561
+ }
16562
+ async function processesForCard(panes, table, lookupPanes) {
16563
+ const mine = /* @__PURE__ */ new Map();
16564
+ for (const pane of panes) {
16565
+ for (const row of processesForPane(table, pane.pid)) {
16566
+ mine.set(row.pid, { tabId: pane.tabId, row });
16567
+ }
16568
+ }
16569
+ const shells = new Set(panes.map((pane) => pane.pid));
16570
+ const rest = table.filter((row) => row.pid > 1 && !mine.has(row.pid) && !shells.has(row.pid)).map((row) => row.pid);
16571
+ if (rest.length === 0) return mine;
16572
+ const paneOf = await lookupPanes(rest);
16573
+ const tabByPane = new Map(panes.map((pane) => [pane.paneId, pane.tabId]));
16574
+ const adopted = /* @__PURE__ */ new Map();
16575
+ for (const row of table) {
16576
+ const tabId = tabByPane.get(paneOf.get(row.pid) ?? "");
16577
+ if (!tabId || mine.has(row.pid)) continue;
16578
+ mine.set(row.pid, { tabId, row });
16579
+ const roots = adopted.get(tabId);
16580
+ if (roots) roots.push(row.pid);
16581
+ else adopted.set(tabId, [row.pid]);
16582
+ }
16583
+ for (const [tabId, roots] of adopted) {
16584
+ for (const row of descendantsOf(table, roots)) {
16585
+ if (!mine.has(row.pid)) mine.set(row.pid, { tabId, row });
16586
+ }
16587
+ }
16588
+ return mine;
16589
+ }
16504
16590
  function commandOf(args) {
16505
16591
  const first = args.trim().split(/\s+/)[0] ?? "";
16506
16592
  const base = first.split("/").pop() ?? first;
@@ -16625,12 +16711,12 @@ var FLUSH_MS = 120;
16625
16711
  var SELF_WRITE_MS = 2e3;
16626
16712
  var dynamicImport = new Function("s", "return import(s)");
16627
16713
  var importFs = new Function("s", "return import(s)");
16628
- async function resolveReal(path17) {
16714
+ async function resolveReal(path18) {
16629
16715
  try {
16630
16716
  const { realpath } = await importFs("fs/promises");
16631
- return await realpath(path17);
16717
+ return await realpath(path18);
16632
16718
  } catch {
16633
- return path17;
16719
+ return path18;
16634
16720
  }
16635
16721
  }
16636
16722
  var loadSubscribe = null;
@@ -16744,19 +16830,19 @@ var WorkspaceWatcher = class {
16744
16830
  }
16745
16831
  record(kind, relPath) {
16746
16832
  if (this.closed) return;
16747
- const path17 = "/" + relPath;
16748
- const expiry = this.selfWrites.get(path17);
16833
+ const path18 = "/" + relPath;
16834
+ const expiry = this.selfWrites.get(path18);
16749
16835
  if (expiry !== void 0) {
16750
16836
  if (expiry > Date.now()) return;
16751
- this.selfWrites.delete(path17);
16837
+ this.selfWrites.delete(path18);
16752
16838
  }
16753
16839
  const { added, changed, removed } = this.pending;
16754
- added.delete(path17);
16755
- changed.delete(path17);
16756
- removed.delete(path17);
16757
- if (kind === "added") added.add(path17);
16758
- else if (kind === "changed") changed.add(path17);
16759
- else removed.add(path17);
16840
+ added.delete(path18);
16841
+ changed.delete(path18);
16842
+ removed.delete(path18);
16843
+ if (kind === "added") added.add(path18);
16844
+ else if (kind === "changed") changed.add(path18);
16845
+ else removed.add(path18);
16760
16846
  if (added.size + changed.size + removed.size > MAX_BATCH) {
16761
16847
  this.pending = { ...emptyPending(), resync: true };
16762
16848
  }
@@ -16775,8 +16861,8 @@ var WorkspaceWatcher = class {
16775
16861
  this.pending = emptyPending();
16776
16862
  if (!resync && !added.size && !changed.size && !removed.size) return;
16777
16863
  const now = Date.now();
16778
- for (const [path17, expiry] of this.selfWrites) {
16779
- if (expiry <= now) this.selfWrites.delete(path17);
16864
+ for (const [path18, expiry] of this.selfWrites) {
16865
+ if (expiry <= now) this.selfWrites.delete(path18);
16780
16866
  }
16781
16867
  this.emit({
16782
16868
  added: resync ? [] : [...added],
@@ -16822,6 +16908,7 @@ async function writeBase64File(target, abs, b64, opts = {}) {
16822
16908
  `Write ${abs}`
16823
16909
  );
16824
16910
  }
16911
+ var PANE_REREAD_MS = 1e3;
16825
16912
  var TerminalModeEngine = class {
16826
16913
  constructor(core, emitter) {
16827
16914
  this.core = core;
@@ -16832,12 +16919,117 @@ var TerminalModeEngine = class {
16832
16919
  /** Worktree name each session claimed, keyed by postId. Covers the window
16833
16920
  * `claimWorkName`'s disk scan cannot: a name taken but not yet created. */
16834
16921
  this.reservedNames = /* @__PURE__ */ new Map();
16922
+ // ── agent activity ──
16923
+ /** Long-lived `redis-cli subscribe` on the space's own socket. One for every
16924
+ * session: the channel is space-wide and the pane rides in the payload, so
16925
+ * a session starting does not mean re-subscribing. */
16926
+ this.stateSub = null;
16927
+ /** Applies states one at a time; see the comment at the push site. */
16928
+ this.stateQueue = Promise.resolve();
16929
+ /** pane id -> tmux session name, refreshed only when a pane we have not seen
16930
+ * turns up. Panes outlive individual messages, so re-reading the whole map
16931
+ * per message would be a tmux call per hook. */
16932
+ this.paneSessions = /* @__PURE__ */ new Map();
16933
+ this.paneReadAt = 0;
16934
+ }
16935
+ /** Idempotent, like startWatching: re-entering a session must not stack
16936
+ * subscribers. */
16937
+ async startStateSub() {
16938
+ if (this.stateSub) return;
16939
+ const target = this.core.target();
16940
+ const child = await target.spawn([
16941
+ "redis-cli",
16942
+ "-s",
16943
+ redisSocketPath(target),
16944
+ "subscribe",
16945
+ AGENT_STATE_CHANNEL
16946
+ ]).catch(() => null);
16947
+ if (!child) return;
16948
+ this.stateSub = child;
16949
+ let buffer = "";
16950
+ child.stdout?.on("data", (chunk) => {
16951
+ buffer += String(chunk);
16952
+ const { states, rest } = drainStateMessages(buffer);
16953
+ buffer = rest;
16954
+ for (const { paneId, state } of states) {
16955
+ this.stateQueue = this.stateQueue.then(() => this.applyPaneState(paneId, state)).catch(() => {
16956
+ });
16957
+ }
16958
+ });
16959
+ child.once("exit", () => {
16960
+ if (this.stateSub === child) this.stateSub = null;
16961
+ });
16962
+ void this.hydrateAgentStates();
16963
+ }
16964
+ /** Read the last reported state for every live session's pane.
16965
+ *
16966
+ * Subscribing only catches what happens next. After a runner restart the
16967
+ * sessions come back but no hook has fired yet, so an idle session would
16968
+ * show as working until someone typed in it — which for an idle session may
16969
+ * be never. This is what the hook's SET alongside its PUBLISH is for. */
16970
+ async hydrateAgentStates() {
16971
+ if (this.sessions.size === 0) return;
16972
+ await this.refreshPanes();
16973
+ const target = this.core.target();
16974
+ for (const [paneId, name] of this.paneSessions) {
16975
+ const postId = postIdForSessionName(name, this.sessions.keys());
16976
+ const session = postId ? this.sessions.get(postId) : null;
16977
+ if (!session || session.agentState) continue;
16978
+ const raw = await target.sh(
16979
+ `redis-cli -s ${redisSocketPath(target)} get '${agentStateKey(paneId)}'`
16980
+ ).catch(() => "");
16981
+ const state = parseAgentState(raw);
16982
+ if (!state) continue;
16983
+ session.agentState = state;
16984
+ this.emitter.emitSessionUpdate(session);
16985
+ }
16986
+ }
16987
+ /** Called after a session is removed, never before: every stopWatching call
16988
+ * site runs while the session is still in the map, so keying this off
16989
+ * stopWatching would leave the subscriber running for the life of the
16990
+ * process. */
16991
+ releaseStateSub() {
16992
+ if (this.sessions.size > 0) return;
16993
+ this.stateSub?.kill();
16994
+ this.stateSub = null;
16995
+ }
16996
+ async applyPaneState(paneId, state) {
16997
+ let name = this.paneSessions.get(paneId);
16998
+ if (!name) {
16999
+ await this.refreshPanes();
17000
+ name = this.paneSessions.get(paneId);
17001
+ }
17002
+ if (!name) return;
17003
+ const postId = postIdForSessionName(name, this.sessions.keys());
17004
+ if (!postId) return;
17005
+ const session = this.sessions.get(postId);
17006
+ if (!session || session.agentState === state) return;
17007
+ session.agentState = state;
17008
+ this.emitter.emitSessionUpdate(session);
17009
+ }
17010
+ /** Rate-limited: a pane that will never resolve — one whose session has
17011
+ * since closed — would otherwise re-list on every message it sent, which is
17012
+ * a tmux subprocess per hook. */
17013
+ async refreshPanes() {
17014
+ const now = Date.now();
17015
+ if (now - this.paneReadAt < PANE_REREAD_MS) return;
17016
+ this.paneReadAt = now;
17017
+ const target = this.core.target();
17018
+ const out = await target.sh(
17019
+ `${tmuxCli(target)} list-panes -a -F '#{pane_id}|#{session_name}' 2>/dev/null || true`
17020
+ ).catch(() => "");
17021
+ this.paneSessions.clear();
17022
+ for (const raw of out.split("\n")) {
17023
+ const [pane, name] = raw.trim().split("|");
17024
+ if (pane && name) this.paneSessions.set(pane, name);
17025
+ }
16835
17026
  }
16836
17027
  // ── working-tree watching ──
16837
17028
  /** Start watching a session's tree. Idempotent: re-entering a session (the
16838
17029
  * modal reopening, a hydrate after restart) must not stack watchers. */
16839
17030
  startWatching(session) {
16840
17031
  const postId = session.postId;
17032
+ void this.startStateSub();
16841
17033
  if (this.watchers.has(postId)) return;
16842
17034
  const paths = this.core.target().paths;
16843
17035
  const root = session.workspacePath || paths.root;
@@ -16874,8 +17066,8 @@ var TerminalModeEngine = class {
16874
17066
  /** Suppress the change event our own write is about to produce, so a save
16875
17067
  * never comes back to the editor as an outside edit. Normalized to the form
16876
17068
  * the watcher emits, since callers spell the path either way. */
16877
- expectWrite(postId, path17) {
16878
- this.watchers.get(postId)?.expectWrite(posixNormalize(path17));
17069
+ expectWrite(postId, path18) {
17070
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path18));
16879
17071
  }
16880
17072
  // ── Lifecycle / ModeEngine ──
16881
17073
  start(base, args) {
@@ -16921,6 +17113,7 @@ var TerminalModeEngine = class {
16921
17113
  this.stopWatching(postId);
16922
17114
  this.reservedNames.delete(postId);
16923
17115
  this.sessions.delete(postId);
17116
+ this.releaseStateSub();
16924
17117
  }
16925
17118
  async kill(postId) {
16926
17119
  let session = this.sessions.get(postId);
@@ -16938,6 +17131,7 @@ var TerminalModeEngine = class {
16938
17131
  const removedPostId = session.postId;
16939
17132
  this.reservedNames.delete(removedPostId);
16940
17133
  this.sessions.delete(removedPostId);
17134
+ this.releaseStateSub();
16941
17135
  this.emitter.emitSessionRemoved(removedPostId);
16942
17136
  }
16943
17137
  destroy() {
@@ -17070,18 +17264,18 @@ var TerminalModeEngine = class {
17070
17264
  this.reservedNames.values()
17071
17265
  );
17072
17266
  this.reservedNames.set(session.postId, workName);
17073
- const { path: path17 } = await addWorktree(
17267
+ const { path: path18 } = await addWorktree(
17074
17268
  this.core.target(),
17075
17269
  workName,
17076
17270
  branch,
17077
17271
  "terminal"
17078
17272
  );
17079
- session.workspacePath = path17;
17273
+ session.workspacePath = path18;
17080
17274
  await setGitIdentity(
17081
17275
  this.core.target(),
17082
17276
  config.gitName,
17083
17277
  config.gitEmail,
17084
- path17
17278
+ path18
17085
17279
  );
17086
17280
  this.emitter.addLog(session, "system", "Worktree ready.");
17087
17281
  } else {
@@ -17143,11 +17337,11 @@ var TerminalModeEngine = class {
17143
17337
  }
17144
17338
  async bareFsList({
17145
17339
  postId,
17146
- path: path17
17340
+ path: path18
17147
17341
  }) {
17148
17342
  const target = this.getBareTarget(postId);
17149
17343
  const root = this.getBareRoot(postId);
17150
- const abs = this.resolveBarePath(path17, root);
17344
+ const abs = this.resolveBarePath(path18, root);
17151
17345
  const relRoot = abs.slice(root.length) || "/";
17152
17346
  const out = await target.run([
17153
17347
  "sh",
@@ -17197,30 +17391,30 @@ var TerminalModeEngine = class {
17197
17391
  }
17198
17392
  async bareFsRead({
17199
17393
  postId,
17200
- path: path17
17394
+ path: path18
17201
17395
  }) {
17202
17396
  const target = this.getBareTarget(postId);
17203
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17397
+ const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17204
17398
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17205
17399
  '`)).trim();
17206
17400
  return { content: b64, encoding: "base64" };
17207
17401
  }
17208
17402
  async bareFsWrite({
17209
17403
  postId,
17210
- path: path17,
17404
+ path: path18,
17211
17405
  content,
17212
17406
  encoding
17213
17407
  }) {
17214
17408
  const target = this.getBareTarget(postId);
17215
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17409
+ const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17216
17410
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17217
17411
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17218
17412
  throw new Error("Invalid base64 payload.");
17219
17413
  }
17220
- this.expectWrite(String(postId), path17);
17414
+ this.expectWrite(String(postId), path18);
17221
17415
  await writeBase64File(target, abs, b64);
17222
17416
  const session = this.sessions.get(String(postId));
17223
- if (session) delete session.buffers[path17];
17417
+ if (session) delete session.buffers[path18];
17224
17418
  }
17225
17419
  /** Append one chunk of a dropped image to the per-space drops dir
17226
17420
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17270,39 +17464,39 @@ var TerminalModeEngine = class {
17270
17464
  }
17271
17465
  bareOpenFile({
17272
17466
  postId: rawPostId,
17273
- path: path17
17467
+ path: path18
17274
17468
  }) {
17275
17469
  const session = this.sessions.get(String(rawPostId));
17276
17470
  if (!session || session.mode !== "bare") return;
17277
- if (!session.openedFiles.includes(path17)) {
17278
- session.openedFiles.push(path17);
17471
+ if (!session.openedFiles.includes(path18)) {
17472
+ session.openedFiles.push(path18);
17279
17473
  }
17280
17474
  }
17281
17475
  bareCloseFile({
17282
17476
  postId: rawPostId,
17283
- path: path17
17477
+ path: path18
17284
17478
  }) {
17285
17479
  const session = this.sessions.get(String(rawPostId));
17286
17480
  if (!session || session.mode !== "bare") return;
17287
- session.openedFiles = session.openedFiles.filter((p) => p !== path17);
17288
- delete session.buffers[path17];
17481
+ session.openedFiles = session.openedFiles.filter((p) => p !== path18);
17482
+ delete session.buffers[path18];
17289
17483
  }
17290
17484
  bareBufferSet({
17291
17485
  postId: rawPostId,
17292
- path: path17,
17486
+ path: path18,
17293
17487
  content
17294
17488
  }) {
17295
17489
  const session = this.sessions.get(String(rawPostId));
17296
17490
  if (!session || session.mode !== "bare") return;
17297
- session.buffers[path17] = content;
17491
+ session.buffers[path18] = content;
17298
17492
  }
17299
17493
  async bareFsDelete({
17300
17494
  postId,
17301
- path: path17
17495
+ path: path18
17302
17496
  }) {
17303
17497
  const target = this.getBareTarget(postId);
17304
17498
  const root = this.getBareRoot(postId);
17305
- const abs = this.resolveBarePath(path17, root);
17499
+ const abs = this.resolveBarePath(path18, root);
17306
17500
  if (abs === root) {
17307
17501
  throw new Error("Refusing to delete the repo root.");
17308
17502
  }
@@ -17310,10 +17504,10 @@ var TerminalModeEngine = class {
17310
17504
  }
17311
17505
  async bareFsMkdir({
17312
17506
  postId,
17313
- path: path17
17507
+ path: path18
17314
17508
  }) {
17315
17509
  const target = this.getBareTarget(postId);
17316
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17510
+ const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17317
17511
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17318
17512
  }
17319
17513
  async bareReview({
@@ -17456,53 +17650,63 @@ var TerminalModeEngine = class {
17456
17650
  return [];
17457
17651
  }
17458
17652
  }
17459
- /** Pane pids for this card's tabs, keyed by tab id. The pane's own shell is
17460
- * the root of everything the user started in that tab. */
17653
+ /** This card's panes: the pid its tree hangs off, and the pane id its
17654
+ * processes carry in their environment. */
17461
17655
  async panePids(target, postId) {
17462
17656
  const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
17463
17657
  const stdout = await target.sh(
17464
- `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_pid}' 2>/dev/null || true`
17658
+ `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_pid}|#{pane_id}' 2>/dev/null || true`
17465
17659
  ).catch(() => "");
17466
17660
  const panes = [];
17467
17661
  for (const raw of stdout.split("\n")) {
17468
17662
  const line = raw.trim();
17469
17663
  if (!line.startsWith(prefix)) continue;
17470
- const rest = line.slice(prefix.length);
17471
- const sep = rest.indexOf("|");
17472
- if (sep === -1) continue;
17473
- const pid = Number(rest.slice(sep + 1));
17474
- if (!Number.isInteger(pid) || pid <= 0) continue;
17475
- panes.push({ tabId: rest.slice(0, sep), pid });
17664
+ const [tabId, rawPid, paneId] = line.slice(prefix.length).split("|");
17665
+ const pid = Number(rawPid);
17666
+ if (!tabId || !paneId || !Number.isInteger(pid) || pid <= 0) continue;
17667
+ panes.push({ tabId, pid, paneId });
17476
17668
  }
17477
17669
  return panes;
17478
17670
  }
17671
+ /**
17672
+ * Every process this card is running, by pid. Shared with bareKillProcess,
17673
+ * so stopping a process accepts exactly the set the panel shows.
17674
+ *
17675
+ * The `-p <pids>` on the environment call is required, not a saving: macOS
17676
+ * omits environments from an unscoped `ps -axeww` — see panesByPid.
17677
+ */
17678
+ async cardProcesses(target, postId) {
17679
+ const panes = await this.panePids(target, postId);
17680
+ if (panes.length === 0) return /* @__PURE__ */ new Map();
17681
+ const table = parsePsTable(
17682
+ await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17683
+ );
17684
+ return processesForCard(
17685
+ panes,
17686
+ table,
17687
+ async (pids) => panesByPid(
17688
+ await target.run(["ps", "eww", "-o", "pid=,command=", "-p", pids.join(",")]).catch(() => "")
17689
+ )
17690
+ );
17691
+ }
17479
17692
  async bareListProcesses({
17480
17693
  postId: rawPostId
17481
17694
  }) {
17482
17695
  const postId = String(rawPostId);
17483
17696
  const target = this.getBareTarget(postId);
17484
- const panes = await this.panePids(target, postId);
17485
- if (panes.length === 0) return [];
17486
- const table = parsePsTable(
17487
- await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17488
- );
17489
- const found = [];
17490
- for (const pane of panes) {
17491
- for (const row of processesForPane(table, pane.pid)) {
17492
- found.push({
17493
- pid: row.pid,
17494
- tabId: pane.tabId,
17495
- command: commandOf(row.args),
17496
- args: row.args,
17497
- cpu: row.cpu,
17498
- memoryKb: row.rssKb,
17499
- elapsed: row.elapsed,
17500
- ports: [],
17501
- connections: 0
17502
- });
17503
- }
17504
- }
17505
- if (found.length === 0) return [];
17697
+ const mine = await this.cardProcesses(target, postId);
17698
+ if (mine.size === 0) return [];
17699
+ const found = Array.from(mine.values(), ({ tabId, row }) => ({
17700
+ pid: row.pid,
17701
+ tabId,
17702
+ command: commandOf(row.args),
17703
+ args: row.args,
17704
+ cpu: row.cpu,
17705
+ memoryKb: row.rssKb,
17706
+ elapsed: row.elapsed,
17707
+ ports: [],
17708
+ connections: 0
17709
+ }));
17506
17710
  const sockets = await socketsFor(
17507
17711
  target,
17508
17712
  found.map((p) => p.pid)
@@ -17525,14 +17729,8 @@ var TerminalModeEngine = class {
17525
17729
  if (!Number.isInteger(pid) || pid <= 1) {
17526
17730
  throw new Error("Not a process this card can stop.");
17527
17731
  }
17528
- const panes = await this.panePids(target, postId);
17529
- const table = parsePsTable(
17530
- await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17531
- );
17532
- const mine = panes.some(
17533
- (pane) => processesForPane(table, pane.pid).some((row) => row.pid === pid)
17534
- );
17535
- if (!mine) {
17732
+ const mine = await this.cardProcesses(target, postId);
17733
+ if (!mine.has(pid)) {
17536
17734
  throw new Error("That process is not running in this card.");
17537
17735
  }
17538
17736
  await target.run(["kill", "-TERM", String(pid)]);
@@ -17991,8 +18189,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
17991
18189
  };
17992
18190
  }
17993
18191
  }
17994
- async function githubApi(token, path17) {
17995
- const res = await fetch(`${API_BASE}${path17}`, {
18192
+ async function githubApi(token, path18) {
18193
+ const res = await fetch(`${API_BASE}${path18}`, {
17996
18194
  headers: {
17997
18195
  Authorization: `Bearer ${token}`,
17998
18196
  Accept: "application/vnd.github+json",
@@ -18001,7 +18199,7 @@ async function githubApi(token, path17) {
18001
18199
  });
18002
18200
  if (!res.ok) {
18003
18201
  throw new Error(
18004
- `GitHub ${path17} failed (${res.status}): ${await res.text()}`
18202
+ `GitHub ${path18} failed (${res.status}): ${await res.text()}`
18005
18203
  );
18006
18204
  }
18007
18205
  return await res.json();
@@ -18508,6 +18706,59 @@ async function getDriveRoot(accessToken) {
18508
18706
  };
18509
18707
  }
18510
18708
 
18709
+ // ../../shared/all/helpers/board-agent-core/plugin.ts
18710
+ var import_fs3 = require("fs");
18711
+ var import_promises6 = require("fs/promises");
18712
+ var import_os3 = __toESM(require("os"));
18713
+ var import_path9 = __toESM(require("path"));
18714
+ var PLUGIN_ID = "factiii@factiii-runner";
18715
+ function candidates() {
18716
+ return [
18717
+ import_path9.default.join(__dirname, "..", "plugin"),
18718
+ import_path9.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
18719
+ import_path9.default.join(process.cwd(), "apps", "runner", "plugin")
18720
+ ];
18721
+ }
18722
+ function pluginRoot() {
18723
+ for (const dir of candidates()) {
18724
+ if ((0, import_fs3.existsSync)(import_path9.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18725
+ return dir;
18726
+ }
18727
+ }
18728
+ return null;
18729
+ }
18730
+ function renderedPluginRoot() {
18731
+ return import_path9.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18732
+ }
18733
+ async function renderPlugin(boardMcpUrl) {
18734
+ const source = pluginRoot();
18735
+ if (!source) return null;
18736
+ const dest = renderedPluginRoot();
18737
+ const manifest = import_path9.default.join(dest, "factiii", ".mcp.json");
18738
+ const want = `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
18739
+ `;
18740
+ const have = await (0, import_promises6.readFile)(manifest, "utf8").catch(() => null);
18741
+ if (have === want) return dest;
18742
+ await (0, import_promises6.mkdir)(import_path9.default.dirname(dest), { recursive: true });
18743
+ await (0, import_promises6.cp)(source, dest, { recursive: true, force: true });
18744
+ await (0, import_promises6.writeFile)(manifest, want);
18745
+ return dest;
18746
+ }
18747
+ function boardMcpConfig(url2) {
18748
+ return {
18749
+ mcpServers: {
18750
+ "factiii-board": {
18751
+ type: "http",
18752
+ url: url2,
18753
+ bearer_token_env_var: BOARD_MCP_TOKEN_ENV,
18754
+ // Written literally, for claude to expand at connect time. Not a
18755
+ // template string: the `${…}` has to survive into the file.
18756
+ headers: { Authorization: "Bearer ${" + BOARD_MCP_TOKEN_ENV + "}" }
18757
+ }
18758
+ }
18759
+ };
18760
+ }
18761
+
18511
18762
  // ../../shared/all/helpers/board-agent-core/toolchain.ts
18512
18763
  var TOOLS = [
18513
18764
  { bin: "git" },
@@ -18765,10 +19016,6 @@ var SpaceCore = class {
18765
19016
  enabled: this.isOneDriveConnected()
18766
19017
  });
18767
19018
  }
18768
- const board = this.boardMcp();
18769
- if (board) {
18770
- lanes.push({ mcp: board, tokenEnv: BOARD_MCP_TOKEN_ENV, enabled: true });
18771
- }
18772
19019
  return lanes;
18773
19020
  }
18774
19021
  isReady() {
@@ -18919,6 +19166,7 @@ var SpaceCore = class {
18919
19166
  await Promise.all([
18920
19167
  this.provisionGitLane(status),
18921
19168
  this.provisionMcpLane(),
19169
+ this.provisionPluginLane(),
18922
19170
  // Covers a host signed in outside the one-click button, which would
18923
19171
  // otherwise meet the wizard the first time it opens a terminal.
18924
19172
  markOnboardingComplete(target).catch(() => {
@@ -18961,17 +19209,53 @@ var SpaceCore = class {
18961
19209
  target.paths.repo
18962
19210
  );
18963
19211
  }
19212
+ /** Install the factiii plugin into both CLIs: the hooks behind the session
19213
+ * status dot, the skills this workspace provides, and the board MCP.
19214
+ *
19215
+ * A plugin rather than hand-written config because it is additive — unlike
19216
+ * pointing CLAUDE_CONFIG_DIR or CODEX_HOME at our own directory, which
19217
+ * would hide the user's own settings, skills, MCP entries and auth.
19218
+ *
19219
+ * Both CLIs read the same manifest, so one marketplace serves both, and
19220
+ * adding a marketplace or plugin that is already there is a no-op in each —
19221
+ * which is what makes this safe on every ensure. */
19222
+ async provisionPluginLane() {
19223
+ const board = this.boardMcp();
19224
+ const root = board ? await renderPlugin(board.url).catch(() => null) : null;
19225
+ if (!root) return;
19226
+ const q = `'${root}'`;
19227
+ const cmd = [
19228
+ `claude plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19229
+ `claude plugin install ${PLUGIN_ID} --scope user >/dev/null 2>&1 || true`,
19230
+ `codex plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19231
+ `codex plugin add ${PLUGIN_ID} >/dev/null 2>&1 || true`
19232
+ ].join("; ");
19233
+ try {
19234
+ await this.target().sh(cmd);
19235
+ } catch {
19236
+ }
19237
+ }
18964
19238
  // Register this space's MCPs with both CLIs, user-scoped, and clear the ones
18965
19239
  // it cannot currently offer. Registering regardless left every space holding
18966
19240
  // an entry it never uses, which the CLI then fails to handshake with on every
18967
19241
  // run. Removal is unconditional so a disconnect cleans itself up.
19242
+ //
19243
+ // No early return on an empty lane list: since the board lane moved into the
19244
+ // plugin, a host with no OneDrive has nothing to add and still has the old
19245
+ // board entries to drop.
18968
19246
  async provisionMcpLane() {
18969
19247
  const lanes = this.mcpLanes();
18970
- if (!lanes.length) return;
19248
+ const board = this.boardMcp();
19249
+ const boardCodexTable = board?.name.replace(/-/g, "_");
18971
19250
  const cmd = [
18972
19251
  // Pre-rename entries, when the name was the same for every space.
18973
19252
  `claude mcp remove factiii-onedrive --scope user >/dev/null 2>&1 || true`,
18974
19253
  `codex mcp remove factiii_onedrive >/dev/null 2>&1 || true`,
19254
+ // The board lane moved into the plugin; drop what the old path wrote.
19255
+ ...board ? [
19256
+ `claude mcp remove ${board.name} --scope user >/dev/null 2>&1 || true`,
19257
+ `codex mcp remove ${boardCodexTable} >/dev/null 2>&1 || true`
19258
+ ] : [],
18975
19259
  ...lanes.flatMap(({ mcp, tokenEnv, enabled }) => {
18976
19260
  const codexTable = mcp.name.replace(/-/g, "_");
18977
19261
  const claudeJson = JSON.stringify({
@@ -19588,7 +19872,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19588
19872
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
19589
19873
  secretsDirs() {
19590
19874
  const spaceDir = this.core.spaceDir();
19591
- return { configDir: (0, import_path9.dirname)(spaceDir), spaceDir };
19875
+ return { configDir: (0, import_path10.dirname)(spaceDir), spaceDir };
19592
19876
  }
19593
19877
  async deploySecretsStatus() {
19594
19878
  return {
@@ -20646,10 +20930,10 @@ ${c.content.trim() || "(no description)"}`
20646
20930
  // ../../shared/all/helpers/board-agent-core/index-service.ts
20647
20931
  var import_child_process5 = require("child_process");
20648
20932
  var import_crypto3 = require("crypto");
20649
- var import_fs3 = require("fs");
20650
- var import_promises6 = __toESM(require("fs/promises"));
20933
+ var import_fs4 = require("fs");
20934
+ var import_promises7 = __toESM(require("fs/promises"));
20651
20935
  var import_net2 = require("net");
20652
- var import_path10 = require("path");
20936
+ var import_path11 = require("path");
20653
20937
  var import_util7 = require("util");
20654
20938
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
20655
20939
  var IndexService = class {
@@ -20708,7 +20992,7 @@ var IndexService = class {
20708
20992
  const log = await this.openLog();
20709
20993
  this.proc = (0, import_child_process5.spawn)(
20710
20994
  process.execPath,
20711
- [(0, import_path10.join)(this.opts.indexDir, "index.js")],
20995
+ [(0, import_path11.join)(this.opts.indexDir, "index.js")],
20712
20996
  {
20713
20997
  cwd: this.opts.indexDir,
20714
20998
  env: {
@@ -20736,8 +21020,8 @@ var IndexService = class {
20736
21020
  * opened (a missing log must never stop the service from starting). */
20737
21021
  async openLog() {
20738
21022
  try {
20739
- await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true });
20740
- return (0, import_fs3.openSync)((0, import_path10.join)(this.opts.dataDir, "index.log"), "a");
21023
+ await import_promises7.default.mkdir(this.opts.dataDir, { recursive: true });
21024
+ return (0, import_fs4.openSync)((0, import_path11.join)(this.opts.dataDir, "index.log"), "a");
20741
21025
  } catch {
20742
21026
  return null;
20743
21027
  }
@@ -20746,10 +21030,10 @@ var IndexService = class {
20746
21030
  * platform needing node-gyp fails here rather than at query time. */
20747
21031
  async installDeps() {
20748
21032
  const dir = this.opts.indexDir;
20749
- if ((0, import_fs3.existsSync)((0, import_path10.join)(dir, "node_modules"))) return;
20750
- await import_promises6.default.copyFile(
20751
- (0, import_path10.join)(dir, "image-package.json"),
20752
- (0, import_path10.join)(dir, "package.json")
21033
+ if ((0, import_fs4.existsSync)((0, import_path11.join)(dir, "node_modules"))) return;
21034
+ await import_promises7.default.copyFile(
21035
+ (0, import_path11.join)(dir, "image-package.json"),
21036
+ (0, import_path11.join)(dir, "package.json")
20753
21037
  );
20754
21038
  await execFileAsync4(
20755
21039
  "npm",
@@ -20761,15 +21045,15 @@ var IndexService = class {
20761
21045
  * CLIs' persistent MCP config, so a fresh port each start would leave every
20762
21046
  * registration pointing at a dead one. */
20763
21047
  async stablePort() {
20764
- const file = (0, import_path10.join)(this.opts.dataDir, ".port");
20765
- const saved = Number(await import_promises6.default.readFile(file, "utf-8").catch(() => ""));
21048
+ const file = (0, import_path11.join)(this.opts.dataDir, ".port");
21049
+ const saved = Number(await import_promises7.default.readFile(file, "utf-8").catch(() => ""));
20766
21050
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
20767
21051
  return saved;
20768
21052
  }
20769
21053
  const port = await freePort();
20770
- await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21054
+ await import_promises7.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
20771
21055
  });
20772
- await import_promises6.default.writeFile(file, String(port), "utf-8").catch(() => {
21056
+ await import_promises7.default.writeFile(file, String(port), "utf-8").catch(() => {
20773
21057
  });
20774
21058
  return port;
20775
21059
  }
@@ -20936,25 +21220,25 @@ async function pairWithBrowser(serverUrl) {
20936
21220
  }
20937
21221
 
20938
21222
  // src/config.ts
20939
- var import_fs7 = __toESM(require("fs"));
20940
- var import_os5 = __toESM(require("os"));
20941
- var import_path15 = __toESM(require("path"));
21223
+ var import_fs8 = __toESM(require("fs"));
21224
+ var import_os6 = __toESM(require("os"));
21225
+ var import_path16 = __toESM(require("path"));
20942
21226
 
20943
21227
  // src/secureFile.ts
20944
21228
  var import_crypto6 = __toESM(require("crypto"));
20945
- var import_fs6 = __toESM(require("fs"));
20946
- var import_path14 = __toESM(require("path"));
21229
+ var import_fs7 = __toESM(require("fs"));
21230
+ var import_path15 = __toESM(require("path"));
20947
21231
 
20948
21232
  // src/keychain.ts
20949
21233
  var import_child_process7 = require("child_process");
20950
21234
  var import_crypto4 = __toESM(require("crypto"));
20951
- var import_fs4 = __toESM(require("fs"));
20952
- var import_os3 = __toESM(require("os"));
20953
- var import_path11 = __toESM(require("path"));
21235
+ var import_fs5 = __toESM(require("fs"));
21236
+ var import_os4 = __toESM(require("os"));
21237
+ var import_path12 = __toESM(require("path"));
20954
21238
  var SERVICE = "factiii-runner";
20955
21239
  var ACCOUNT = "config-encryption-key";
20956
- var DPAPI_KEY_FILE = import_path11.default.join(
20957
- import_os3.default.homedir(),
21240
+ var DPAPI_KEY_FILE = import_path12.default.join(
21241
+ import_os4.default.homedir(),
20958
21242
  ".factiii-runner",
20959
21243
  "config-key.dpapi"
20960
21244
  );
@@ -21034,7 +21318,7 @@ function linuxWrite(key) {
21034
21318
  function winRead() {
21035
21319
  let wrapped;
21036
21320
  try {
21037
- wrapped = import_fs4.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21321
+ wrapped = import_fs5.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21038
21322
  } catch {
21039
21323
  return { state: "missing" };
21040
21324
  }
@@ -21059,8 +21343,8 @@ function winWrite(key) {
21059
21343
  "-Command",
21060
21344
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
21061
21345
  ]);
21062
- import_fs4.default.mkdirSync(import_path11.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21063
- import_fs4.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21346
+ import_fs5.default.mkdirSync(import_path12.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21347
+ import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21064
21348
  `, { mode: 384 });
21065
21349
  }
21066
21350
  function keychainBackend() {
@@ -21222,22 +21506,22 @@ function keychainStatus() {
21222
21506
 
21223
21507
  // src/secureStore.ts
21224
21508
  var import_crypto5 = __toESM(require("crypto"));
21225
- var import_fs5 = __toESM(require("fs"));
21226
- var import_path13 = __toESM(require("path"));
21509
+ var import_fs6 = __toESM(require("fs"));
21510
+ var import_path14 = __toESM(require("path"));
21227
21511
 
21228
21512
  // src/paths.ts
21229
- var import_os4 = __toESM(require("os"));
21230
- var import_path12 = __toESM(require("path"));
21231
- var CONFIG_DIR = import_path12.default.join(import_os4.default.homedir(), ".factiii-runner");
21513
+ var import_os5 = __toESM(require("os"));
21514
+ var import_path13 = __toESM(require("path"));
21515
+ var CONFIG_DIR = import_path13.default.join(import_os5.default.homedir(), ".factiii-runner");
21232
21516
  function safeSlug(spaceSlug) {
21233
21517
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
21234
21518
  }
21235
21519
  function spaceDirPath(spaceSlug) {
21236
- return import_path12.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21520
+ return import_path13.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21237
21521
  }
21238
21522
 
21239
21523
  // src/secureStore.ts
21240
- var VAULT_KEY_FILE = import_path13.default.join(CONFIG_DIR, "vault-key.json");
21524
+ var VAULT_KEY_FILE = import_path14.default.join(CONFIG_DIR, "vault-key.json");
21241
21525
  var SCRYPT_N2 = 1 << 15;
21242
21526
  var MIN_PASSWORD_LENGTH = 8;
21243
21527
  var SecureStoreError = class extends Error {
@@ -21310,7 +21594,7 @@ function unseal(box, key) {
21310
21594
  function readVaultFile() {
21311
21595
  try {
21312
21596
  const parsed = JSON.parse(
21313
- import_fs5.default.readFileSync(VAULT_KEY_FILE, "utf-8")
21597
+ import_fs6.default.readFileSync(VAULT_KEY_FILE, "utf-8")
21314
21598
  );
21315
21599
  return parsed.v === 1 ? parsed : null;
21316
21600
  } catch {
@@ -21318,11 +21602,11 @@ function readVaultFile() {
21318
21602
  }
21319
21603
  }
21320
21604
  function writeVaultFile(file) {
21321
- import_fs5.default.mkdirSync(CONFIG_DIR, { recursive: true });
21605
+ import_fs6.default.mkdirSync(CONFIG_DIR, { recursive: true });
21322
21606
  const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
21323
- import_fs5.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
21607
+ import_fs6.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
21324
21608
  `, { mode: 384 });
21325
- import_fs5.default.renameSync(tmp, VAULT_KEY_FILE);
21609
+ import_fs6.default.renameSync(tmp, VAULT_KEY_FILE);
21326
21610
  }
21327
21611
  var unlockedDek = null;
21328
21612
  function createVault(machineKey) {
@@ -21559,15 +21843,15 @@ function parseEnvelope(raw) {
21559
21843
  }
21560
21844
  }
21561
21845
  function atomicWrite(filePath, body) {
21562
- import_fs6.default.mkdirSync(import_path14.default.dirname(filePath), { recursive: true });
21846
+ import_fs7.default.mkdirSync(import_path15.default.dirname(filePath), { recursive: true });
21563
21847
  const tmp = `${filePath}.${process.pid}.tmp`;
21564
- import_fs6.default.writeFileSync(tmp, body, { mode: 384 });
21565
- import_fs6.default.renameSync(tmp, filePath);
21848
+ import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
21849
+ import_fs7.default.renameSync(tmp, filePath);
21566
21850
  }
21567
21851
  function readSecureBuffer(filePath) {
21568
21852
  let raw;
21569
21853
  try {
21570
- raw = import_fs6.default.readFileSync(filePath, "utf-8");
21854
+ raw = import_fs7.default.readFileSync(filePath, "utf-8");
21571
21855
  } catch {
21572
21856
  return null;
21573
21857
  }
@@ -21624,7 +21908,7 @@ function writeSecureJson(filePath, value2) {
21624
21908
  function readBootJson(filePath) {
21625
21909
  let raw;
21626
21910
  try {
21627
- raw = import_fs6.default.readFileSync(filePath, "utf-8");
21911
+ raw = import_fs7.default.readFileSync(filePath, "utf-8");
21628
21912
  } catch {
21629
21913
  return null;
21630
21914
  }
@@ -21674,8 +21958,8 @@ function writeBootJson(filePath, value2) {
21674
21958
  }
21675
21959
 
21676
21960
  // src/config.ts
21677
- var CONFIG_DIR2 = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
21678
- var CONFIG_FILE = import_path15.default.join(CONFIG_DIR2, "config.json");
21961
+ var CONFIG_DIR2 = import_path16.default.join(import_os6.default.homedir(), ".factiii-runner");
21962
+ var CONFIG_FILE = import_path16.default.join(CONFIG_DIR2, "config.json");
21679
21963
  function readRunnerConfig() {
21680
21964
  return readBootJson(CONFIG_FILE);
21681
21965
  }
@@ -21683,7 +21967,7 @@ function writeRunnerConfig(config) {
21683
21967
  writeBootJson(CONFIG_FILE, config);
21684
21968
  }
21685
21969
  function configExists() {
21686
- return import_fs7.default.existsSync(CONFIG_FILE);
21970
+ return import_fs8.default.existsSync(CONFIG_FILE);
21687
21971
  }
21688
21972
 
21689
21973
  // src/promptPassword.ts
@@ -21845,9 +22129,9 @@ function makeChunkReassembler() {
21845
22129
 
21846
22130
  // src/daemon.ts
21847
22131
  var import_crypto7 = require("crypto");
21848
- var import_fs9 = __toESM(require("fs"));
22132
+ var import_fs10 = __toESM(require("fs"));
21849
22133
  var import_node_datachannel = require("node-datachannel");
21850
- var import_path17 = __toESM(require("path"));
22134
+ var import_path18 = __toESM(require("path"));
21851
22135
 
21852
22136
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
21853
22137
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23027,12 +23311,12 @@ function parse2(str) {
23027
23311
  uri.queryKey = queryKey(uri, uri["query"]);
23028
23312
  return uri;
23029
23313
  }
23030
- function pathNames(obj, path17) {
23031
- const regx = /\/{2,9}/g, names = path17.replace(regx, "/").split("/");
23032
- if (path17.slice(0, 1) == "/" || path17.length === 0) {
23314
+ function pathNames(obj, path18) {
23315
+ const regx = /\/{2,9}/g, names = path18.replace(regx, "/").split("/");
23316
+ if (path18.slice(0, 1) == "/" || path18.length === 0) {
23033
23317
  names.splice(0, 1);
23034
23318
  }
23035
- if (path17.slice(-1) == "/") {
23319
+ if (path18.slice(-1) == "/") {
23036
23320
  names.splice(names.length - 1, 1);
23037
23321
  }
23038
23322
  return names;
@@ -23651,7 +23935,7 @@ var protocol2 = Socket.protocol;
23651
23935
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
23652
23936
  var import_debug7 = __toESM(require_src(), 1);
23653
23937
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
23654
- function url(uri, path17 = "", loc) {
23938
+ function url(uri, path18 = "", loc) {
23655
23939
  let obj = uri;
23656
23940
  loc = loc || typeof location !== "undefined" && location;
23657
23941
  if (null == uri)
@@ -23685,7 +23969,7 @@ function url(uri, path17 = "", loc) {
23685
23969
  obj.path = obj.path || "/";
23686
23970
  const ipv6 = obj.host.indexOf(":") !== -1;
23687
23971
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
23688
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path17;
23972
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path18;
23689
23973
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
23690
23974
  return obj;
23691
23975
  }
@@ -25319,8 +25603,8 @@ function lookup(uri, opts) {
25319
25603
  const parsed = url(uri, opts.path || "/socket.io");
25320
25604
  const source = parsed.source;
25321
25605
  const id = parsed.id;
25322
- const path17 = parsed.path;
25323
- const sameNamespace = cache[id] && path17 in cache[id]["nsps"];
25606
+ const path18 = parsed.path;
25607
+ const sameNamespace = cache[id] && path18 in cache[id]["nsps"];
25324
25608
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
25325
25609
  let io;
25326
25610
  if (newConnection) {
@@ -25362,8 +25646,8 @@ function cardContextPath(target, postId) {
25362
25646
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
25363
25647
  return `${target.paths.root}/.cards/${safe}.md`;
25364
25648
  }
25365
- function cardContextPrompt(path17) {
25366
- return `Read ${path17} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
25649
+ function cardContextPrompt(path18) {
25650
+ return `Read ${path18} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
25367
25651
  }
25368
25652
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
25369
25653
  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.";
@@ -25527,11 +25811,11 @@ var TerminalManager = class {
25527
25811
  };
25528
25812
 
25529
25813
  // src/agent-adapter.ts
25530
- var import_fs8 = __toESM(require("fs"));
25531
- var import_path16 = __toESM(require("path"));
25532
- var RUNNER_CONFIG_PATH = import_path16.default.join(CONFIG_DIR, "runner-config.json");
25814
+ var import_fs9 = __toESM(require("fs"));
25815
+ var import_path17 = __toESM(require("path"));
25816
+ var RUNNER_CONFIG_PATH = import_path17.default.join(CONFIG_DIR, "runner-config.json");
25533
25817
  function connectionPath(spaceSlug) {
25534
- return import_path16.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
25818
+ return import_path17.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
25535
25819
  }
25536
25820
  function writeJson(filePath, value2) {
25537
25821
  writeSecureJson(filePath, value2);
@@ -25558,14 +25842,14 @@ function migrateLegacyConfigs() {
25558
25842
  const file = { boards: {} };
25559
25843
  let legacy = [];
25560
25844
  try {
25561
- legacy = import_fs8.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
25845
+ legacy = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
25562
25846
  } catch {
25563
25847
  return file;
25564
25848
  }
25565
25849
  for (const name of legacy) {
25566
25850
  const slug = name.slice("space-".length, -".json".length);
25567
25851
  const parsed = readSecureJson(
25568
- import_path16.default.join(CONFIG_DIR, name)
25852
+ import_path17.default.join(CONFIG_DIR, name)
25569
25853
  );
25570
25854
  if (!parsed) continue;
25571
25855
  if (!file.defaultSlug) {
@@ -25585,14 +25869,14 @@ function encryptStrayPlaintextConfigs() {
25585
25869
  const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
25586
25870
  let names = [];
25587
25871
  try {
25588
- names = import_fs8.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
25872
+ names = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
25589
25873
  } catch {
25590
25874
  return;
25591
25875
  }
25592
25876
  for (const name of names) {
25593
- const filePath = import_path16.default.join(CONFIG_DIR, name);
25877
+ const filePath = import_path17.default.join(CONFIG_DIR, name);
25594
25878
  try {
25595
- const raw = import_fs8.default.readFileSync(filePath, "utf-8");
25879
+ const raw = import_fs9.default.readFileSync(filePath, "utf-8");
25596
25880
  if (!raw.startsWith("{")) continue;
25597
25881
  writeSecureJson(filePath, JSON.parse(raw));
25598
25882
  } catch {
@@ -25623,7 +25907,7 @@ async function getConfiguredConnections() {
25623
25907
  }
25624
25908
  if (await isSignedIn(hostTarget(CONFIG_DIR), "claude")) kinds.add("claude");
25625
25909
  try {
25626
- const hasOneDrive = import_fs8.default.readdirSync(CONFIG_DIR).some((f) => f.endsWith(".onedrive.json"));
25910
+ const hasOneDrive = import_fs9.default.readdirSync(CONFIG_DIR).some((f) => f.endsWith(".onedrive.json"));
25627
25911
  if (hasOneDrive) kinds.add("onedrive");
25628
25912
  } catch {
25629
25913
  }
@@ -25694,7 +25978,7 @@ var localConfigProvider = {
25694
25978
  writeOneDriveConnection(spaceSlug, connection) {
25695
25979
  const filePath = connectionPath(spaceSlug);
25696
25980
  if (!connection) {
25697
- import_fs8.default.rmSync(filePath, { force: true });
25981
+ import_fs9.default.rmSync(filePath, { force: true });
25698
25982
  return;
25699
25983
  }
25700
25984
  writeJson(filePath, connection);
@@ -25719,7 +26003,7 @@ var localConfigProvider = {
25719
26003
  "claude",
25720
26004
  "codex"
25721
26005
  ]) {
25722
- import_fs8.default.mkdirSync(import_path16.default.join(dir, sub), { recursive: true });
26006
+ import_fs9.default.mkdirSync(import_path17.default.join(dir, sub), { recursive: true });
25723
26007
  }
25724
26008
  return dir;
25725
26009
  }
@@ -25893,15 +26177,15 @@ async function startDaemon(config) {
25893
26177
  }
25894
26178
  );
25895
26179
  const indexCandidates = [
25896
- import_path17.default.join(__dirname, "..", "index"),
25897
- import_path17.default.join(__dirname, "..", "..", "index", "image")
26180
+ import_path18.default.join(__dirname, "..", "index"),
26181
+ import_path18.default.join(__dirname, "..", "..", "index", "image")
25898
26182
  ];
25899
- const indexDir = indexCandidates.find((p) => import_fs9.default.existsSync(p)) ?? indexCandidates[0];
26183
+ const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
25900
26184
  const odIndex = new IndexService({
25901
26185
  controlToken: (0, import_crypto7.createHash)("sha256").update(config.authToken).digest("hex"),
25902
26186
  embedUrl: () => config.serverUrl,
25903
26187
  indexDir,
25904
- dataDir: import_path17.default.join(CONFIG_DIR, "index-data")
26188
+ dataDir: import_path18.default.join(CONFIG_DIR, "index-data")
25905
26189
  });
25906
26190
  void odIndex.ensureRunning().catch((err) => {
25907
26191
  console.error(