@factiii/runner 0.10.2 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -963,8 +963,8 @@ 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 path18 = require("node:path");
967
- var fs14 = require("node:fs");
966
+ var path20 = require("node:path");
967
+ var fs15 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
970
970
  var { CommanderError: CommanderError2 } = require_error();
@@ -1896,11 +1896,11 @@ 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 = path18.resolve(baseDir, baseName);
1900
- if (fs14.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path18.extname(baseName))) return void 0;
1899
+ const localBin = path20.resolve(baseDir, baseName);
1900
+ if (fs15.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path20.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs14.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs15.existsSync(`${localBin}${ext}`)
1904
1904
  );
1905
1905
  if (foundExt) return `${localBin}${foundExt}`;
1906
1906
  return void 0;
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1912
1912
  if (this._scriptPath) {
1913
1913
  let resolvedScriptPath;
1914
1914
  try {
1915
- resolvedScriptPath = fs14.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs15.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path18.resolve(
1920
- path18.dirname(resolvedScriptPath),
1919
+ executableDir = path20.resolve(
1920
+ path20.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 = path18.basename(
1927
+ const legacyName = path20.basename(
1928
1928
  this._scriptPath,
1929
- path18.extname(this._scriptPath)
1929
+ path20.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(path18.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path20.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 = path18.basename(filename, path18.extname(filename));
2780
+ this._name = path20.basename(filename, path20.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(path19) {
2795
- if (path19 === void 0) return this._executableDir;
2796
- this._executableDir = path19;
2794
+ executableDir(path21) {
2795
+ if (path21 === void 0) return this._executableDir;
2796
+ this._executableDir = path21;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -3026,7 +3026,7 @@ var require_commander = __commonJS({
3026
3026
  // ../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3027
3027
  var require_XMLHttpRequest = __commonJS({
3028
3028
  "../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports2, module2) {
3029
- var fs14 = require("fs");
3029
+ var fs15 = require("fs");
3030
3030
  var Url = require("url");
3031
3031
  var spawn5 = require("child_process").spawn;
3032
3032
  module2.exports = XMLHttpRequest3;
@@ -3184,7 +3184,7 @@ var require_XMLHttpRequest = __commonJS({
3184
3184
  throw new Error("XMLHttpRequest: Only GET method is supported");
3185
3185
  }
3186
3186
  if (settings.async) {
3187
- fs14.readFile(unescape(url2.pathname), function(error, data2) {
3187
+ fs15.readFile(unescape(url2.pathname), function(error, data2) {
3188
3188
  if (error) {
3189
3189
  self.handleError(error, error.errno || -1);
3190
3190
  } else {
@@ -3196,7 +3196,7 @@ var require_XMLHttpRequest = __commonJS({
3196
3196
  });
3197
3197
  } else {
3198
3198
  try {
3199
- this.response = fs14.readFileSync(unescape(url2.pathname));
3199
+ this.response = fs15.readFileSync(unescape(url2.pathname));
3200
3200
  this.responseText = this.response.toString("utf8");
3201
3201
  this.status = 200;
3202
3202
  setState(self.DONE);
@@ -3322,15 +3322,15 @@ var require_XMLHttpRequest = __commonJS({
3322
3322
  } else {
3323
3323
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
3324
3324
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
3325
- fs14.writeFileSync(syncFile, "", "utf8");
3325
+ fs15.writeFileSync(syncFile, "", "utf8");
3326
3326
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
3327
3327
  var syncProc = spawn5(process.argv[0], ["-e", execString]);
3328
3328
  var statusText;
3329
- while (fs14.existsSync(syncFile)) {
3329
+ while (fs15.existsSync(syncFile)) {
3330
3330
  }
3331
- self.responseText = fs14.readFileSync(contentFile, "utf8");
3331
+ self.responseText = fs15.readFileSync(contentFile, "utf8");
3332
3332
  syncProc.stdin.end();
3333
- fs14.unlinkSync(contentFile);
3333
+ fs15.unlinkSync(contentFile);
3334
3334
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
3335
3335
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
3336
3336
  self.handleError(errorObj, 503);
@@ -6497,7 +6497,7 @@ var require_websocket = __commonJS({
6497
6497
  var EventEmitter = require("events");
6498
6498
  var https = require("https");
6499
6499
  var http = require("http");
6500
- var net2 = require("net");
6500
+ var net3 = require("net");
6501
6501
  var tls = require("tls");
6502
6502
  var { randomBytes, createHash: createHash2 } = require("crypto");
6503
6503
  var { Duplex, Readable } = require("stream");
@@ -7241,12 +7241,12 @@ var require_websocket = __commonJS({
7241
7241
  }
7242
7242
  function netConnect(options) {
7243
7243
  options.path = options.socketPath;
7244
- return net2.connect(options);
7244
+ return net3.connect(options);
7245
7245
  }
7246
7246
  function tlsConnect(options) {
7247
7247
  options.path = void 0;
7248
7248
  if (!options.servername && options.servername !== "") {
7249
- options.servername = net2.isIP(options.host) ? "" : options.host;
7249
+ options.servername = net3.isIP(options.host) ? "" : options.host;
7250
7250
  }
7251
7251
  return tls.connect(options);
7252
7252
  }
@@ -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: path18, errorMaps, issueData } = params;
8432
- const fullPath = [...path18, ...issueData.path || []];
8431
+ const { data, path: path20, errorMaps, issueData } = params;
8432
+ const fullPath = [...path20, ...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, path18, key) {
8548
+ constructor(parent, value2, path20, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path18;
8552
+ this._path = path20;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12197,9 +12197,9 @@ function processLine(trimmed, state, cb) {
12197
12197
  if (e.type === "result" /* Result */) {
12198
12198
  if (e.is_error) {
12199
12199
  state.isError = true;
12200
- const errorText = e.result || "Claude encountered an error";
12201
- state.fullText = errorText;
12202
- cb.onLog?.({ type: "error", content: errorText });
12200
+ const errorText2 = e.result || "Claude encountered an error";
12201
+ state.fullText = errorText2;
12202
+ cb.onLog?.({ type: "error", content: errorText2 });
12203
12203
  return;
12204
12204
  }
12205
12205
  if (e.result && !state.fullText) {
@@ -12647,7 +12647,8 @@ var prompts_default = {
12647
12647
  "- $WORKSPACE - the repo checkout (the working directory), reset to the default branch at run start",
12648
12648
  "- $BACKUPS_DIR - durable backups; every backup (db dumps etc) is written here and nowhere else",
12649
12649
  "- $BUILDS_DIR - build artifacts; each build writes into $BUILDS_DIR/<commit-hash>/",
12650
- "Reference paths ONLY via these variables or relative to the working directory. NEVER hardcode absolute machine paths (like /Users/...) anywhere in the skill - it must run unchanged on any runner. Secrets live in an encrypted store you never touch: their values reach commands only via runner-side injection of the keys declared in required-variables.json.",
12650
+ "Reference paths ONLY via these variables or relative to the working directory. NEVER hardcode absolute machine paths (like /Users/...) anywhere in the skill - it must run unchanged on any runner.",
12651
+ "Secrets live in an encrypted store you never touch, and `factiii-secrets` is how a command gets them - it is on the PATH of every run. Load the `factiii-secrets` skill installed on this machine (it is not part of this repo) for exactly how it behaves, and write the skill's commands in that shape.",
12651
12652
  "",
12652
12653
  "## Produce exactly three files",
12653
12654
  "1. .agents/skills/deploy/SKILL.md - the runbook, written for a competent operator. It MUST start with YAML frontmatter (--- name: deploy / description: one line saying this is the repository's release runbook and when to run it ---) so agent CLIs discover it as a skill, followed by: named steps in ship order, the exact commands, what success looks like per step, and explicit stop-and-ask points wherever human judgment or confirmation belongs (e.g. 'confirm before the production cutover'). Plain language; no secret values or key material anywhere in the file - reference secrets ONLY as $KEY env names.",
@@ -12658,7 +12659,7 @@ var prompts_default = {
12658
12659
  "- Write ONLY those three files under .agents/skills/deploy/.",
12659
12660
  "- Do NOT git commit or git push; the harness pushes for review.",
12660
12661
  "- Cloud CLIs on this machine (aws, vercel, wrangler, gh...) may already be signed in. You may use them READ-ONLY to verify what exists (project names, repo names, regions) so the skill is accurate - but NEVER create, modify, deploy, or delete anything, and never echo a credential or token value.",
12661
- "- The harness injects required variables per-command at execution time (the owner authorizes each secret-bearing command individually). Write the skill accordingly: commands simply reference the variables; NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.",
12662
+ '- Secret-bearing commands: any command needing a variable declared in required-variables.json MUST be written in the skill as `factiii-secrets run -- <command>`, with the variables referenced unexpanded and quoted (e.g. "$DATABASE_URL"). The owner authorizes each one at run time and the runner injects the values. Wrap only the commands that need a secret. NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.',
12662
12663
  "- No secret values in any file, ever."
12663
12664
  ],
12664
12665
  deployCreateRevise: [
@@ -12666,18 +12667,18 @@ var prompts_default = {
12666
12667
  "",
12667
12668
  "{{FEEDBACK}}",
12668
12669
  "",
12669
- "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12670
+ "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, every secret-bearing command still written as `factiii-secrets run -- <command>`, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12670
12671
  ],
12671
12672
  deployRun: [
12672
12673
  "## Deploy run protocol",
12673
12674
  "You are executing this repository's deploy skill NOW, on the owner's machine, for real. Invoke it and follow it exactly - the skill is the program; do not improvise steps it doesn't contain or change any step's intent.",
12674
12675
  "",
12675
- "## The secrets model (read this before anything else)",
12676
- "Every variable declared in required-variables.json is UNSET in your shell BY DESIGN, and will stay unset for the whole run. That is the security model working, not a problem: the owner already stored the values encrypted on this machine, and the runner injects them into commands you request through the NEED_SECRETS gate below. So:",
12677
- "- NEVER check whether these variables are set (no `env`, `printenv`, `test -n`, echo checks). Their absence tells you nothing.",
12678
- "- NEVER report them as missing, ask the owner to configure them, or abort the run because your shell lacks them. Aborting for unset declared variables is ALWAYS wrong - the gate is how every one of them gets used.",
12679
- "- If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
12680
- '- To run a command that needs any declared variable, print as the LAST line of your message exactly: NEED_SECRETS: <the complete command> - then END your turn. Write the variables unexpanded in that command (e.g. "$DATABASE_URL"). The owner inspects and authorizes it with their password; the runner executes it with every declared variable injected, and your next message contains its redacted output. One command per request.',
12676
+ "## Secrets (read this before anything else)",
12677
+ "Every variable declared in required-variables.json is UNSET in your shell BY DESIGN, and will stay unset for the whole run. The values are stored encrypted on this machine, and `factiii-secrets` is how a command gets them - it is on your PATH, and the `factiii-secrets` skill describes it in full.",
12678
+ "- To run a command that needs any declared variable, prefix it: `factiii-secrets run -- <command>`. The owner authorizes it with their password, the runner runs it with every declared value injected, and you get the output back with the values redacted. It exits with the command's own code, so `&&`, `||` and `set -e` behave normally. Exit 77 means the owner declined - adapt or stop, and never retry the same command.",
12679
+ '- Quote the variables (`"$DATABASE_URL"`): they expand on the runner\'s side, not in your shell.',
12680
+ "- NEVER check whether these variables are set (no `env`, `printenv`, `test -n`, echo checks), never report them as missing, and never abort because your shell lacks them. Their absence tells you nothing. If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
12681
+ "- Wrap ONLY the commands that actually need a secret. Everything else runs unwrapped and needs no authorization.",
12681
12682
  "",
12682
12683
  "- Track the release with your todo/plan tool: one todo per step in ship order, updated as you go. This is the owner's live timeline.",
12683
12684
  "- HUMAN GATES: wherever the skill says to stop and ask (and before anything irreversible the skill flags), print as the LAST line: AWAITING_INPUT: <the question> - then END your turn. The owner's answer arrives as your next message.",
@@ -12908,7 +12909,7 @@ ${diff.slice(0, 8e3)}`,
12908
12909
  }
12909
12910
 
12910
12911
  // ../../shared/all/helpers/board-agent-core/engine.ts
12911
- var import_path10 = require("path");
12912
+ var import_path12 = require("path");
12912
12913
 
12913
12914
  // ../../shared/all/domains/electron.ts
12914
12915
  var SETUP_LOG_PREFIX = "log ";
@@ -13017,7 +13018,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13017
13018
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13018
13019
  }
13019
13020
  async function addWorktree(target, workName, baseBranch, kind) {
13020
- const path18 = worktreeFor(target.paths, workName);
13021
+ const path20 = worktreeFor(target.paths, workName);
13021
13022
  const branch = workBranch(workName, kind);
13022
13023
  await gitInDir(
13023
13024
  target,
@@ -13027,7 +13028,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13027
13028
  baseBranch
13028
13029
  ).catch(() => {
13029
13030
  });
13030
- await removeWorktree(target, path18).catch(() => {
13031
+ await removeWorktree(target, path20).catch(() => {
13031
13032
  });
13032
13033
  await gitInDir(
13033
13034
  target,
@@ -13036,30 +13037,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13036
13037
  "add",
13037
13038
  "-B",
13038
13039
  branch,
13039
- path18,
13040
+ path20,
13040
13041
  `origin/${baseBranch}`
13041
13042
  );
13042
- return { path: path18, branch };
13043
+ return { path: path20, branch };
13043
13044
  }
13044
- async function removeWorktree(target, path18) {
13045
+ async function removeWorktree(target, path20) {
13045
13046
  await gitInDir(
13046
13047
  target,
13047
13048
  target.paths.repo,
13048
13049
  "worktree",
13049
13050
  "remove",
13050
13051
  "--force",
13051
- path18
13052
+ path20
13052
13053
  );
13053
13054
  }
13054
- async function removeWorktreeAndBranch(target, path18) {
13055
+ async function removeWorktreeAndBranch(target, path20) {
13055
13056
  const branch = await gitInDir(
13056
13057
  target,
13057
- path18,
13058
+ path20,
13058
13059
  "rev-parse",
13059
13060
  "--abbrev-ref",
13060
13061
  "HEAD"
13061
13062
  ).then((out) => out.trim()).catch(() => "");
13062
- await removeWorktree(target, path18);
13063
+ await removeWorktree(target, path20);
13063
13064
  if (branch && branch !== "HEAD") {
13064
13065
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13065
13066
  () => {
@@ -13073,8 +13074,8 @@ async function pruneWorktrees(target) {
13073
13074
  }
13074
13075
  );
13075
13076
  }
13076
- function isWorktreePath(target, path18) {
13077
- return path18.startsWith(`${target.paths.worktrees}/`);
13077
+ function isWorktreePath(target, path20) {
13078
+ return path20.startsWith(`${target.paths.worktrees}/`);
13078
13079
  }
13079
13080
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13080
13081
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13100,13 +13101,13 @@ function parseStatus(raw) {
13100
13101
  const y = line[1];
13101
13102
  const rest = line.slice(3);
13102
13103
  const arrow = rest.indexOf(" -> ");
13103
- const path18 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
+ const path20 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
13105
  if (x === "?" || y === "?") {
13105
- unstaged.push({ path: path18, code: "A" });
13106
+ unstaged.push({ path: path20, code: "A" });
13106
13107
  continue;
13107
13108
  }
13108
- if (x !== " ") staged.push({ path: path18, code: x });
13109
- if (y !== " ") unstaged.push({ path: path18, code: y });
13109
+ if (x !== " ") staged.push({ path: path20, code: x });
13110
+ if (y !== " ") unstaged.push({ path: path20, code: y });
13110
13111
  }
13111
13112
  return { staged, unstaged };
13112
13113
  }
@@ -13464,10 +13465,12 @@ function execLogin(command, opts) {
13464
13465
  let output = "";
13465
13466
  let settled = false;
13466
13467
  let timer;
13468
+ let idleTimer;
13467
13469
  const done = (code, extra = "") => {
13468
13470
  if (settled) return;
13469
13471
  settled = true;
13470
13472
  if (timer) clearTimeout(timer);
13473
+ if (idleTimer) clearTimeout(idleTimer);
13471
13474
  resolve2({ code, output: (output + extra).slice(-cap) });
13472
13475
  };
13473
13476
  let proc;
@@ -13489,9 +13492,22 @@ function execLogin(command, opts) {
13489
13492
  done(-1, `
13490
13493
  [timed out after ${Math.round(opts.timeout / 1e3)}s]`);
13491
13494
  }, opts.timeout);
13495
+ const idleMs = opts.idleTimeout;
13496
+ const bumpIdle = () => {
13497
+ if (!idleMs) return;
13498
+ if (idleTimer) clearTimeout(idleTimer);
13499
+ idleTimer = setTimeout(() => {
13500
+ killTree(proc);
13501
+ const secs = Math.round(idleMs / 1e3);
13502
+ done(-1, `
13503
+ [no output for ${secs}s - timed out]`);
13504
+ }, idleMs);
13505
+ };
13506
+ bumpIdle();
13492
13507
  const collect = (chunk) => {
13493
13508
  output += chunk.toString("utf-8");
13494
13509
  if (output.length > cap * 2) output = output.slice(-cap);
13510
+ bumpIdle();
13495
13511
  opts.onOutput?.(output);
13496
13512
  };
13497
13513
  proc.stdout.on("data", collect);
@@ -13848,13 +13864,13 @@ function createEventSink(send, spaceSlug, notify) {
13848
13864
 
13849
13865
  // ../../shared/all/helpers/card-ai/frpc.ts
13850
13866
  var import_fs2 = require("fs");
13851
- var import_promises4 = __toESM(require("fs/promises"));
13852
- var import_path6 = __toESM(require("path"));
13867
+ var import_promises5 = __toESM(require("fs/promises"));
13868
+ var import_path7 = __toESM(require("path"));
13853
13869
 
13854
13870
  // ../../shared/all/helpers/board-agent-core/host-skills.ts
13855
- var import_promises3 = __toESM(require("fs/promises"));
13871
+ var import_promises4 = __toESM(require("fs/promises"));
13856
13872
  var import_os2 = __toESM(require("os"));
13857
- var import_path5 = __toESM(require("path"));
13873
+ var import_path6 = __toESM(require("path"));
13858
13874
 
13859
13875
  // ../../shared/all/domains/posts.ts
13860
13876
  var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
@@ -14364,9 +14380,134 @@ var skillScopeSchema = external_exports.enum(["global", "repo"]);
14364
14380
 
14365
14381
  // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14366
14382
  var import_child_process3 = require("child_process");
14383
+ var import_promises3 = __toESM(require("fs/promises"));
14384
+ var import_path5 = __toESM(require("path"));
14385
+ var import_util5 = require("util");
14386
+
14387
+ // ../../shared/all/helpers/board-agent-core/secrets-bridge.ts
14367
14388
  var import_promises2 = __toESM(require("fs/promises"));
14389
+ var import_net2 = __toESM(require("net"));
14368
14390
  var import_path4 = __toESM(require("path"));
14369
- var import_util5 = require("util");
14391
+ function secretsSocketPath(spaceDir) {
14392
+ return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14393
+ }
14394
+ function redactor(values) {
14395
+ const significant = values.filter((v) => v.length >= 4);
14396
+ return (text) => {
14397
+ let out = text;
14398
+ for (const value2 of significant) {
14399
+ out = out.split(value2).join("[REDACTED]");
14400
+ }
14401
+ return out;
14402
+ };
14403
+ }
14404
+ function replier(conn) {
14405
+ return (message) => {
14406
+ if (conn.destroyed) return;
14407
+ conn.write(`${JSON.stringify(message)}
14408
+ `);
14409
+ };
14410
+ }
14411
+ async function handleRun(request, handlers, reply) {
14412
+ const command = typeof request.command === "string" ? request.command.trim() : "";
14413
+ if (!command) {
14414
+ reply({ type: "error", message: "No command was sent." });
14415
+ return;
14416
+ }
14417
+ const cwd = typeof request.cwd === "string" ? request.cwd : "";
14418
+ const result = await handlers.run({
14419
+ command,
14420
+ cwd,
14421
+ status: (text) => reply({ type: "status", text })
14422
+ });
14423
+ if ("denied" in result) {
14424
+ reply({ type: "denied" });
14425
+ return;
14426
+ }
14427
+ if (result.output) reply({ type: "output", chunk: result.output });
14428
+ reply({ type: "exit", code: result.code });
14429
+ }
14430
+ async function startSecretsBridge(opts) {
14431
+ const socketPath = secretsSocketPath(opts.spaceDir);
14432
+ await import_promises2.default.mkdir(import_path4.default.dirname(socketPath), { recursive: true });
14433
+ await import_promises2.default.rm(socketPath, { force: true });
14434
+ const { handlers } = opts;
14435
+ let queue = Promise.resolve();
14436
+ let queueDepth = 0;
14437
+ const live = /* @__PURE__ */ new Set();
14438
+ const server = import_net2.default.createServer((conn) => {
14439
+ live.add(conn);
14440
+ conn.on("close", () => live.delete(conn));
14441
+ const reply = replier(conn);
14442
+ let buffer = "";
14443
+ let handled = false;
14444
+ conn.on("error", () => {
14445
+ });
14446
+ conn.on("data", (chunk) => {
14447
+ if (handled) return;
14448
+ buffer += chunk.toString("utf-8");
14449
+ const newline = buffer.indexOf("\n");
14450
+ if (newline === -1) return;
14451
+ handled = true;
14452
+ let request;
14453
+ try {
14454
+ request = JSON.parse(buffer.slice(0, newline));
14455
+ } catch {
14456
+ reply({ type: "error", message: "Malformed request." });
14457
+ conn.end();
14458
+ return;
14459
+ }
14460
+ if (request.op === "list") {
14461
+ void handlers.list().then((keys) => reply({ type: "keys", keys })).catch(
14462
+ (err) => reply({ type: "error", message: errorText(err) })
14463
+ ).finally(() => conn.end());
14464
+ return;
14465
+ }
14466
+ if (request.op !== "run") {
14467
+ reply({ type: "error", message: "Unknown operation." });
14468
+ conn.end();
14469
+ return;
14470
+ }
14471
+ if (queueDepth > 0) {
14472
+ reply({
14473
+ type: "status",
14474
+ text: "queued behind another command awaiting authorization"
14475
+ });
14476
+ }
14477
+ queueDepth += 1;
14478
+ queue = queue.then(() => {
14479
+ if (conn.destroyed) return void 0;
14480
+ return handleRun(request, handlers, reply);
14481
+ }).catch(
14482
+ (err) => reply({ type: "error", message: errorText(err) })
14483
+ ).finally(() => {
14484
+ queueDepth -= 1;
14485
+ conn.end();
14486
+ });
14487
+ });
14488
+ });
14489
+ await new Promise((resolve2, reject) => {
14490
+ server.once("error", reject);
14491
+ server.listen(socketPath, () => {
14492
+ server.removeListener("error", reject);
14493
+ resolve2();
14494
+ });
14495
+ });
14496
+ await import_promises2.default.chmod(socketPath, 384).catch(() => void 0);
14497
+ return {
14498
+ close: async () => {
14499
+ for (const conn of live) conn.destroy();
14500
+ live.clear();
14501
+ await new Promise((resolve2) => server.close(() => resolve2()));
14502
+ await import_promises2.default.rm(socketPath, { force: true });
14503
+ }
14504
+ };
14505
+ }
14506
+ function errorText(err) {
14507
+ return err instanceof Error ? err.message : String(err);
14508
+ }
14509
+
14510
+ // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14370
14511
  var execFileAsync2 = (0, import_util5.promisify)(import_child_process3.execFile);
14371
14512
  var SKILL_DIR = ".agents/skills/deploy";
14372
14513
  var SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
@@ -14375,14 +14516,14 @@ var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
14375
14516
  var CLAUDE_SKILLS_DIR = ".claude/skills";
14376
14517
  var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
14377
14518
  async function ensureClaudeSkillLink(workspace) {
14378
- const linkPath = import_path4.default.join(workspace, CLAUDE_LINK);
14519
+ const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
14379
14520
  try {
14380
- await import_promises2.default.lstat(linkPath);
14521
+ await import_promises3.default.lstat(linkPath);
14381
14522
  return;
14382
14523
  } catch {
14383
14524
  }
14384
- await import_promises2.default.mkdir(import_path4.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14385
- await import_promises2.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14525
+ await import_promises3.default.mkdir(import_path5.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14526
+ await import_promises3.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14386
14527
  }
14387
14528
  var GIT_NO_HELPER = ["-c", "credential.helper="];
14388
14529
  var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
@@ -14427,7 +14568,7 @@ async function gitCatFileBatch(cwd, shas) {
14427
14568
  return out;
14428
14569
  }
14429
14570
  async function ensureWorkspaceClone(opts) {
14430
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14571
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14431
14572
  const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
14432
14573
  const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
14433
14574
  let cloned = false;
@@ -14435,14 +14576,14 @@ async function ensureWorkspaceClone(opts) {
14435
14576
  await hostGit(workspace, "rev-parse", "--git-dir");
14436
14577
  await hostGit(workspace, "remote", "set-url", "origin", url2);
14437
14578
  } catch {
14438
- const leftovers = await import_promises2.default.readdir(workspace).catch(() => []);
14579
+ const leftovers = await import_promises3.default.readdir(workspace).catch(() => []);
14439
14580
  if (leftovers.includes(".git")) {
14440
14581
  throw new Error(
14441
14582
  `${workspace} holds a git repository that cannot be read. Remove it and retry.`
14442
14583
  );
14443
14584
  }
14444
14585
  for (const name of leftovers) {
14445
- await import_promises2.default.rm(import_path4.default.join(workspace, name), { recursive: true, force: true });
14586
+ await import_promises3.default.rm(import_path5.default.join(workspace, name), { recursive: true, force: true });
14446
14587
  }
14447
14588
  await execFileAsync2(
14448
14589
  "git",
@@ -14513,6 +14654,60 @@ async function checkDeployStatus(opts) {
14513
14654
  variables
14514
14655
  };
14515
14656
  }
14657
+ var RELEASE_BRANCH_LIMIT = 25;
14658
+ var BRANCH_FIELDS = "%(refname:short)%09%(committerdate:unix)";
14659
+ async function listReleaseBranches(opts) {
14660
+ const { workspace, branch } = await ensureWorkspaceClone(opts);
14661
+ await hostGit(workspace, "fetch", "origin", "--prune");
14662
+ let counted = true;
14663
+ let raw = "";
14664
+ try {
14665
+ raw = await hostGit(
14666
+ workspace,
14667
+ "for-each-ref",
14668
+ "--sort=-committerdate",
14669
+ `--format=${BRANCH_FIELDS}%09%(ahead-behind:refs/remotes/origin/${branch})%09%(contents:subject)`,
14670
+ "refs/remotes/origin"
14671
+ );
14672
+ } catch {
14673
+ counted = false;
14674
+ raw = await hostGit(
14675
+ workspace,
14676
+ "for-each-ref",
14677
+ "--sort=-committerdate",
14678
+ `--format=${BRANCH_FIELDS}%09%(contents:subject)`,
14679
+ "refs/remotes/origin"
14680
+ );
14681
+ }
14682
+ const branches = [];
14683
+ for (const row of raw.split("\n")) {
14684
+ if (branches.length >= RELEASE_BRANCH_LIMIT) break;
14685
+ const [ref = "", unix = "", ...rest] = row.split(" ");
14686
+ if (!ref.startsWith("origin/")) continue;
14687
+ const name = ref.slice("origin/".length);
14688
+ if (name === "HEAD" || name === branch) continue;
14689
+ let ahead = counted ? Number(rest.shift()?.split(" ")[0]) || 0 : 0;
14690
+ if (!counted) {
14691
+ try {
14692
+ const count = await hostGit(
14693
+ workspace,
14694
+ "rev-list",
14695
+ "--count",
14696
+ `origin/${branch}..${ref}`
14697
+ );
14698
+ ahead = Number(count.trim()) || 0;
14699
+ } catch {
14700
+ }
14701
+ }
14702
+ branches.push({
14703
+ name,
14704
+ ahead,
14705
+ updated: Number(unix) || 0,
14706
+ subject: rest.join(" ").trim()
14707
+ });
14708
+ }
14709
+ return branches;
14710
+ }
14516
14711
  var requiredVariableSchema = external_exports.object({
14517
14712
  key: external_exports.string().min(1),
14518
14713
  title: external_exports.string().catch(""),
@@ -14526,6 +14721,10 @@ function spaceEnv(spaceDir) {
14526
14721
  WORKSPACE: paths.workspace,
14527
14722
  BACKUPS_DIR: paths.backups,
14528
14723
  BUILDS_DIR: paths.builds,
14724
+ // Where `factiii-secrets` finds the runner. It can also walk up for the
14725
+ // socket, but only from inside the space tree - a step that cd'd into a
14726
+ // build dir elsewhere would not find it.
14727
+ FACTIII_SECRETS_SOCK: secretsSocketPath(spaceDir),
14529
14728
  // No keychain credential helper and no interactive git prompts anywhere
14530
14729
  // in the deploy surface (agent sessions + authorized commands): auth
14531
14730
  // comes from the secrets store or the tokened URL, never the keychain.
@@ -14536,15 +14735,15 @@ function spaceEnv(spaceDir) {
14536
14735
  };
14537
14736
  }
14538
14737
  async function readContract(workspace) {
14539
- const skill = await import_promises2.default.readFile(import_path4.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14738
+ const skill = await import_promises3.default.readFile(import_path5.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14540
14739
  throw new Error("The creator wrote no SKILL.md.");
14541
14740
  });
14542
- const checker = await import_promises2.default.readFile(import_path4.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14741
+ const checker = await import_promises3.default.readFile(import_path5.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14543
14742
  throw new Error("The creator wrote no check-environment.sh.");
14544
14743
  });
14545
14744
  try {
14546
14745
  const raw = JSON.parse(
14547
- await import_promises2.default.readFile(import_path4.default.join(workspace, VARIABLES_PATH), "utf-8")
14746
+ await import_promises3.default.readFile(import_path5.default.join(workspace, VARIABLES_PATH), "utf-8")
14548
14747
  );
14549
14748
  return {
14550
14749
  skill,
@@ -14561,7 +14760,7 @@ async function generateDeploySkill(opts) {
14561
14760
  throw new Error("Describe your deployment process first.");
14562
14761
  }
14563
14762
  const status = await checkDeployStatus(opts);
14564
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14763
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14565
14764
  sendLog({
14566
14765
  type: "system",
14567
14766
  content: `Resetting workspace to origin/${status.branch}...`
@@ -14586,13 +14785,12 @@ async function generateDeploySkill(opts) {
14586
14785
  prompt: buildPrompt(prompts_default.deployCreate, {
14587
14786
  INSTRUCTIONS: instructions
14588
14787
  }),
14589
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14788
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch,Skill",
14590
14789
  // auto: the per-action classifier blocks escalations/unrecognized-infra
14591
14790
  // actions - a host session with no human reviewing each command should
14592
14791
  // not run bypassPermissions. (Codex maps this to a workspace-write
14593
14792
  // sandbox; see agent.ts.)
14594
14793
  permissionMode: "auto",
14595
- model: "claude-opus-4-6",
14596
14794
  includePartialMessages: true,
14597
14795
  env: spaceEnv(opts.spaceDir),
14598
14796
  callbacks: { onLog: (entry) => sendLog(entry) }
@@ -14605,7 +14803,7 @@ async function generateDeploySkill(opts) {
14605
14803
  async function reviseDeploySkill(opts) {
14606
14804
  const { feedback, sendLog } = opts;
14607
14805
  if (!feedback.trim()) throw new Error("Corrections are empty.");
14608
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14806
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14609
14807
  sendLog({ type: "system", content: "Applying your corrections..." });
14610
14808
  const { result } = await spawnAgent(
14611
14809
  opts.provider,
@@ -14615,9 +14813,8 @@ async function reviseDeploySkill(opts) {
14615
14813
  prompt: buildPrompt(prompts_default.deployCreateRevise, {
14616
14814
  FEEDBACK: feedback
14617
14815
  }),
14618
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14816
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch,Skill",
14619
14817
  permissionMode: "auto",
14620
- model: "claude-opus-4-6",
14621
14818
  includePartialMessages: true,
14622
14819
  resumeSessionId: opts.resumeSessionId || void 0,
14623
14820
  env: spaceEnv(opts.spaceDir),
@@ -14634,7 +14831,7 @@ async function loadSkillDraft(opts) {
14634
14831
  throw new Error("Not a deploy-skill branch.");
14635
14832
  }
14636
14833
  await checkDeployStatus(opts);
14637
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14834
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14638
14835
  await hostGit(
14639
14836
  workspace,
14640
14837
  "fetch",
@@ -14646,7 +14843,7 @@ async function loadSkillDraft(opts) {
14646
14843
  return readContract(workspace);
14647
14844
  }
14648
14845
  async function approveDeploySkill(opts) {
14649
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14846
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14650
14847
  await readContract(workspace);
14651
14848
  await ensureClaudeSkillLink(workspace);
14652
14849
  if (opts.gitName)
@@ -14689,19 +14886,19 @@ async function runEnvironmentCheck(opts) {
14689
14886
  "No environment checker on the default branch. Generate the deploy skill first."
14690
14887
  );
14691
14888
  }
14692
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14889
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14693
14890
  const script = await hostGit(
14694
14891
  workspace,
14695
14892
  "cat-file",
14696
14893
  "-p",
14697
14894
  `origin/${status.branch}:${CHECKER_PATH}`
14698
14895
  );
14699
- const tmp = import_path4.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14700
- await import_promises2.default.writeFile(tmp, script, { mode: 448 });
14896
+ const tmp = import_path5.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14897
+ await import_promises3.default.writeFile(tmp, script, { mode: 448 });
14701
14898
  let ok = true;
14702
14899
  let output = "";
14703
14900
  try {
14704
- const scriptArg = tmp.split(import_path4.default.sep).join("/");
14901
+ const scriptArg = tmp.split(import_path5.default.sep).join("/");
14705
14902
  const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
14706
14903
  cwd: workspace,
14707
14904
  env: { ...process.env, ...spaceEnv(opts.spaceDir) },
@@ -14710,7 +14907,7 @@ async function runEnvironmentCheck(opts) {
14710
14907
  output = res.output;
14711
14908
  if (res.code !== 0) ok = false;
14712
14909
  } finally {
14713
- await import_promises2.default.rm(tmp, { force: true });
14910
+ await import_promises3.default.rm(tmp, { force: true });
14714
14911
  }
14715
14912
  const checks = [];
14716
14913
  for (const line of output.split("\n")) {
@@ -14732,7 +14929,7 @@ async function fixEnvironment(opts) {
14732
14929
  const before = await runEnvironmentCheck(opts);
14733
14930
  if (before.ok) return before;
14734
14931
  const status = await checkDeployStatus(opts);
14735
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14932
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14736
14933
  const script = await hostGit(
14737
14934
  workspace,
14738
14935
  "cat-file",
@@ -14757,7 +14954,6 @@ async function fixEnvironment(opts) {
14757
14954
  // auto: installs are in-scope of the owner's explicit "fix environment"
14758
14955
  // request; the classifier still blocks anything beyond it.
14759
14956
  permissionMode: "auto",
14760
- model: "claude-opus-4-6",
14761
14957
  includePartialMessages: true,
14762
14958
  env: spaceEnv(opts.spaceDir),
14763
14959
  callbacks: { onLog: (entry) => sendLog(entry) }
@@ -14768,7 +14964,7 @@ async function fixEnvironment(opts) {
14768
14964
  return runEnvironmentCheck(opts);
14769
14965
  }
14770
14966
  async function discardDeploySkill(opts) {
14771
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14967
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14772
14968
  await hostGit(workspace, "checkout", "--", ".").catch(() => null);
14773
14969
  await hostGit(workspace, "clean", "-fd");
14774
14970
  return { success: true };
@@ -14887,7 +15083,7 @@ var AGENTS_ROOT = ".agents/skills";
14887
15083
  var CLAUDE_ROOT = ".claude/skills";
14888
15084
  var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
14889
15085
  function globalAgentsRoot() {
14890
- return import_path5.default.join(import_os2.default.homedir(), ".agents", "skills");
15086
+ return import_path6.default.join(import_os2.default.homedir(), ".agents", "skills");
14891
15087
  }
14892
15088
  var AGENTS_LINK_TARGET = "../.agents/skills";
14893
15089
  var BRANCH_PREFIX = "skill/";
@@ -14897,11 +15093,11 @@ function summarize(raw, base) {
14897
15093
  return { ...base, description: frontmatter.description, errors };
14898
15094
  }
14899
15095
  async function rootLinkState(root) {
14900
- const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
14901
- const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
14902
- if (!stat) return "missing";
14903
- if (!stat.isSymbolicLink()) return "blocked";
14904
- const target = await import_promises3.default.readlink(linkPath).catch(() => "");
15096
+ const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15097
+ const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15098
+ if (!stat2) return "missing";
15099
+ if (!stat2.isSymbolicLink()) return "blocked";
15100
+ const target = await import_promises4.default.readlink(linkPath).catch(() => "");
14905
15101
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
14906
15102
  }
14907
15103
  async function globalLinkState() {
@@ -14911,12 +15107,12 @@ async function globalLinkState() {
14911
15107
  }
14912
15108
  async function listGlobal() {
14913
15109
  const root = globalAgentsRoot();
14914
- const entries = await import_promises3.default.readdir(root, { withFileTypes: true }).catch(() => []);
15110
+ const entries = await import_promises4.default.readdir(root, { withFileTypes: true }).catch(() => []);
14915
15111
  const out = [];
14916
15112
  for (const entry of entries) {
14917
15113
  if (entry.isFile()) continue;
14918
- const dir = import_path5.default.join(root, entry.name);
14919
- const raw = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
15114
+ const dir = import_path6.default.join(root, entry.name);
15115
+ const raw = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
14920
15116
  if (!raw) continue;
14921
15117
  out.push(
14922
15118
  summarize(raw, {
@@ -15085,7 +15281,7 @@ async function ensureSkillsWorktree(spaceDir, branch) {
15085
15281
  } catch {
15086
15282
  await hostGit(repo, "worktree", "prune").catch(() => {
15087
15283
  });
15088
- await import_promises3.default.rm(worktree, { recursive: true, force: true });
15284
+ await import_promises4.default.rm(worktree, { recursive: true, force: true });
15089
15285
  await hostGit(
15090
15286
  repo,
15091
15287
  "worktree",
@@ -15168,11 +15364,11 @@ async function bundleFilesOnDisk(dir) {
15168
15364
  const out = [];
15169
15365
  const walk = async (current, prefix, depth) => {
15170
15366
  if (depth > 3 || out.length >= 200) return;
15171
- const entries = await import_promises3.default.readdir(current, { withFileTypes: true }).catch(() => []);
15367
+ const entries = await import_promises4.default.readdir(current, { withFileTypes: true }).catch(() => []);
15172
15368
  for (const entry of entries) {
15173
15369
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
15174
15370
  if (entry.isDirectory()) {
15175
- await walk(import_path5.default.join(current, entry.name), rel, depth + 1);
15371
+ await walk(import_path6.default.join(current, entry.name), rel, depth + 1);
15176
15372
  } else if (rel !== "SKILL.md") {
15177
15373
  out.push(rel);
15178
15374
  }
@@ -15184,7 +15380,7 @@ async function bundleFilesOnDisk(dir) {
15184
15380
  async function readSkillImpl(opts) {
15185
15381
  if (opts.scope === "global") {
15186
15382
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15187
- const content2 = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8");
15383
+ const content2 = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8");
15188
15384
  const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
15189
15385
  return {
15190
15386
  name: opts.name,
@@ -15219,55 +15415,55 @@ async function readSkillImpl(opts) {
15219
15415
  };
15220
15416
  }
15221
15417
  async function moveIntoAgents(from, to) {
15222
- if (await import_promises3.default.access(to).then(
15418
+ if (await import_promises4.default.access(to).then(
15223
15419
  () => true,
15224
15420
  () => false
15225
15421
  )) {
15226
15422
  throw new Error(
15227
- `${import_path5.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15423
+ `${import_path6.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15228
15424
  );
15229
15425
  }
15230
- await import_promises3.default.mkdir(import_path5.default.dirname(to), { recursive: true });
15426
+ await import_promises4.default.mkdir(import_path6.default.dirname(to), { recursive: true });
15231
15427
  try {
15232
- await import_promises3.default.rename(from, to);
15428
+ await import_promises4.default.rename(from, to);
15233
15429
  } catch {
15234
- await import_promises3.default.cp(from, to, { recursive: true });
15235
- await import_promises3.default.rm(from, { recursive: true, force: true });
15430
+ await import_promises4.default.cp(from, to, { recursive: true });
15431
+ await import_promises4.default.rm(from, { recursive: true, force: true });
15236
15432
  }
15237
15433
  }
15238
15434
  async function migrateGlobal() {
15239
15435
  const moved = [];
15240
15436
  for (const root of CLI_ROOTS) {
15241
- const dir = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15242
- const stat = await import_promises3.default.lstat(dir).catch(() => null);
15243
- if (!stat || stat.isSymbolicLink()) continue;
15244
- const entries = await import_promises3.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15437
+ const dir = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15438
+ const stat2 = await import_promises4.default.lstat(dir).catch(() => null);
15439
+ if (!stat2 || stat2.isSymbolicLink()) continue;
15440
+ const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15245
15441
  for (const entry of entries) {
15246
15442
  if (entry.isFile() || entry.isSymbolicLink()) continue;
15247
- const src = import_path5.default.join(dir, entry.name);
15248
- if (!await import_promises3.default.access(import_path5.default.join(src, "SKILL.md")).then(
15443
+ const src = import_path6.default.join(dir, entry.name);
15444
+ if (!await import_promises4.default.access(import_path6.default.join(src, "SKILL.md")).then(
15249
15445
  () => true,
15250
15446
  () => false
15251
15447
  )) {
15252
15448
  continue;
15253
15449
  }
15254
- await moveIntoAgents(src, import_path5.default.join(globalAgentsRoot(), entry.name));
15450
+ await moveIntoAgents(src, import_path6.default.join(globalAgentsRoot(), entry.name));
15255
15451
  moved.push(entry.name);
15256
15452
  }
15257
- await import_promises3.default.rmdir(dir).catch(() => {
15453
+ await import_promises4.default.rmdir(dir).catch(() => {
15258
15454
  });
15259
15455
  }
15260
15456
  for (const root of CLI_ROOTS) {
15261
- const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15262
- const after = await import_promises3.default.lstat(linkPath).catch(() => null);
15263
- if (after?.isSymbolicLink()) await import_promises3.default.rm(linkPath, { force: true });
15457
+ const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15458
+ const after = await import_promises4.default.lstat(linkPath).catch(() => null);
15459
+ if (after?.isSymbolicLink()) await import_promises4.default.rm(linkPath, { force: true });
15264
15460
  else if (after) {
15265
15461
  throw new Error(
15266
15462
  `~/${root} still has files that are not skills. Empty it, then migrate.`
15267
15463
  );
15268
15464
  }
15269
- await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15270
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15465
+ await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15466
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15271
15467
  }
15272
15468
  return moved;
15273
15469
  }
@@ -15301,13 +15497,13 @@ async function migrateSkillsImpl(opts) {
15301
15497
  `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
15302
15498
  );
15303
15499
  }
15304
- await import_promises3.default.mkdir(import_path5.default.join(workspace, AGENTS_ROOT), { recursive: true });
15500
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, AGENTS_ROOT), { recursive: true });
15305
15501
  await hostGit(workspace, "mv", stray, dest);
15306
15502
  moved.push(name);
15307
15503
  }
15308
- const linkPath = import_path5.default.join(workspace, CLAUDE_ROOT);
15309
- await import_promises3.default.rm(linkPath, { recursive: true, force: true });
15310
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15504
+ const linkPath = import_path6.default.join(workspace, CLAUDE_ROOT);
15505
+ await import_promises4.default.rm(linkPath, { recursive: true, force: true });
15506
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15311
15507
  await hostGit(workspace, "add", "-A");
15312
15508
  const dirty = await hostGit(workspace, "status", "--porcelain");
15313
15509
  if (!dirty.trim()) return { branch: "", moved: [] };
@@ -15333,16 +15529,16 @@ function assertWritable(name, content) {
15333
15529
  }
15334
15530
  async function writeSkillFile(opts) {
15335
15531
  const dir = resolveSkillDir(opts.root, opts.name);
15336
- const file = import_path5.default.join(dir, "SKILL.md");
15532
+ const file = import_path6.default.join(dir, "SKILL.md");
15337
15533
  if (opts.keepExisting) {
15338
- const exists = await import_promises3.default.access(file).then(
15534
+ const exists = await import_promises4.default.access(file).then(
15339
15535
  () => true,
15340
15536
  () => false
15341
15537
  );
15342
15538
  if (exists) return;
15343
15539
  }
15344
- await import_promises3.default.mkdir(dir, { recursive: true });
15345
- await import_promises3.default.writeFile(file, opts.content, "utf-8");
15540
+ await import_promises4.default.mkdir(dir, { recursive: true });
15541
+ await import_promises4.default.writeFile(file, opts.content, "utf-8");
15346
15542
  }
15347
15543
  async function writeGlobalSkill(opts) {
15348
15544
  assertWritable(opts.name, opts.content);
@@ -15357,14 +15553,14 @@ async function installBundledSkill(opts) {
15357
15553
  content: opts.content,
15358
15554
  keepExisting: true
15359
15555
  });
15360
- await write(import_path5.default.join(home, ...AGENTS_ROOT.split("/")));
15556
+ await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15361
15557
  for (const root of CLI_ROOTS) {
15362
- const linkPath = import_path5.default.join(home, ...root.split("/"));
15363
- const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
15364
- if (stat?.isSymbolicLink()) continue;
15365
- if (!stat) {
15366
- await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15367
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15558
+ const linkPath = import_path6.default.join(home, ...root.split("/"));
15559
+ const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15560
+ if (stat2?.isSymbolicLink()) continue;
15561
+ if (!stat2) {
15562
+ await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15563
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15368
15564
  continue;
15369
15565
  }
15370
15566
  await write(linkPath);
@@ -15372,7 +15568,7 @@ async function installBundledSkill(opts) {
15372
15568
  }
15373
15569
  async function deleteGlobalSkill(opts) {
15374
15570
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15375
- await import_promises3.default.rm(dir, { recursive: true, force: true });
15571
+ await import_promises4.default.rm(dir, { recursive: true, force: true });
15376
15572
  }
15377
15573
  async function proposeRepoSkillImpl(opts) {
15378
15574
  assertWritable(opts.name, opts.content);
@@ -15410,9 +15606,9 @@ async function proposeRepoSkillImpl(opts) {
15410
15606
  if (opts.gitEmail) {
15411
15607
  await hostGit(workspace, "config", "user.email", opts.gitEmail);
15412
15608
  }
15413
- await import_promises3.default.mkdir(import_path5.default.join(workspace, dir), { recursive: true });
15414
- await import_promises3.default.writeFile(
15415
- import_path5.default.join(workspace, dir, "SKILL.md"),
15609
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, dir), { recursive: true });
15610
+ await import_promises4.default.writeFile(
15611
+ import_path6.default.join(workspace, dir, "SKILL.md"),
15416
15612
  opts.content,
15417
15613
  "utf-8"
15418
15614
  );
@@ -15519,18 +15715,18 @@ function vendoredBinaryName() {
15519
15715
  async function installFrpcBinary(target) {
15520
15716
  const p = frpcPaths(target);
15521
15717
  const binaryName = vendoredBinaryName();
15522
- const src = import_path6.default.join(__dirname, "..", "vendor", binaryName);
15718
+ const src = import_path7.default.join(__dirname, "..", "vendor", binaryName);
15523
15719
  if (!(0, import_fs2.existsSync)(src)) {
15524
15720
  throw new Error(
15525
15721
  `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
15526
15722
  );
15527
15723
  }
15528
- const staged = await import_promises4.default.stat(p.binary).catch(() => null);
15529
- const source = await import_promises4.default.stat(src);
15724
+ const staged = await import_promises5.default.stat(p.binary).catch(() => null);
15725
+ const source = await import_promises5.default.stat(src);
15530
15726
  if (staged?.size === source.size) return;
15531
- await import_promises4.default.mkdir(p.binDir, { recursive: true });
15532
- await import_promises4.default.copyFile(src, p.binary);
15533
- await import_promises4.default.chmod(p.binary, 493);
15727
+ await import_promises5.default.mkdir(p.binDir, { recursive: true });
15728
+ await import_promises5.default.copyFile(src, p.binary);
15729
+ await import_promises5.default.chmod(p.binary, 493);
15534
15730
  }
15535
15731
  var EXPOSE_SKILL_NAME = "share-a-port";
15536
15732
  var FENCE = "```";
@@ -15920,18 +16116,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
15920
16116
  await this.core.ensureSpaceWorkspace();
15921
16117
  await assertAgentSignedIn(session.provider, this.core.target());
15922
16118
  if (hasGit) {
15923
- const { path: path18 } = await addWorktree(
16119
+ const { path: path20 } = await addWorktree(
15924
16120
  this.core.target(),
15925
16121
  session.taskId,
15926
16122
  baseBranch || config.mainBranch || "main",
15927
16123
  "card"
15928
16124
  );
15929
- session.workspacePath = path18;
16125
+ session.workspacePath = path20;
15930
16126
  await setGitIdentity(
15931
16127
  this.core.target(),
15932
16128
  config.gitName,
15933
16129
  config.gitEmail,
15934
- path18
16130
+ path20
15935
16131
  );
15936
16132
  this.emitter.addLog(session, "system", "Worktree ready.");
15937
16133
  } else {
@@ -16153,18 +16349,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16153
16349
  this.emitter.addLog(session, "system", "Preparing environment...");
16154
16350
  await this.core.ensureSpaceWorkspace();
16155
16351
  await assertAgentSignedIn(session.provider, this.core.target());
16156
- const { path: path18 } = await addWorktree(
16352
+ const { path: path20 } = await addWorktree(
16157
16353
  this.core.target(),
16158
16354
  session.taskId,
16159
16355
  config.mainBranch || "main",
16160
16356
  "card"
16161
16357
  );
16162
- session.workspacePath = path18;
16358
+ session.workspacePath = path20;
16163
16359
  await setGitIdentity(
16164
16360
  this.core.target(),
16165
16361
  config.gitName,
16166
16362
  config.gitEmail,
16167
- path18
16363
+ path20
16168
16364
  );
16169
16365
  await this.putCard(session);
16170
16366
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16711,12 +16907,12 @@ var FLUSH_MS = 120;
16711
16907
  var SELF_WRITE_MS = 2e3;
16712
16908
  var dynamicImport = new Function("s", "return import(s)");
16713
16909
  var importFs = new Function("s", "return import(s)");
16714
- async function resolveReal(path18) {
16910
+ async function resolveReal(path20) {
16715
16911
  try {
16716
16912
  const { realpath } = await importFs("fs/promises");
16717
- return await realpath(path18);
16913
+ return await realpath(path20);
16718
16914
  } catch {
16719
- return path18;
16915
+ return path20;
16720
16916
  }
16721
16917
  }
16722
16918
  var loadSubscribe = null;
@@ -16830,19 +17026,19 @@ var WorkspaceWatcher = class {
16830
17026
  }
16831
17027
  record(kind, relPath) {
16832
17028
  if (this.closed) return;
16833
- const path18 = "/" + relPath;
16834
- const expiry = this.selfWrites.get(path18);
17029
+ const path20 = "/" + relPath;
17030
+ const expiry = this.selfWrites.get(path20);
16835
17031
  if (expiry !== void 0) {
16836
17032
  if (expiry > Date.now()) return;
16837
- this.selfWrites.delete(path18);
17033
+ this.selfWrites.delete(path20);
16838
17034
  }
16839
17035
  const { added, changed, removed } = this.pending;
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);
17036
+ added.delete(path20);
17037
+ changed.delete(path20);
17038
+ removed.delete(path20);
17039
+ if (kind === "added") added.add(path20);
17040
+ else if (kind === "changed") changed.add(path20);
17041
+ else removed.add(path20);
16846
17042
  if (added.size + changed.size + removed.size > MAX_BATCH) {
16847
17043
  this.pending = { ...emptyPending(), resync: true };
16848
17044
  }
@@ -16861,8 +17057,8 @@ var WorkspaceWatcher = class {
16861
17057
  this.pending = emptyPending();
16862
17058
  if (!resync && !added.size && !changed.size && !removed.size) return;
16863
17059
  const now = Date.now();
16864
- for (const [path18, expiry] of this.selfWrites) {
16865
- if (expiry <= now) this.selfWrites.delete(path18);
17060
+ for (const [path20, expiry] of this.selfWrites) {
17061
+ if (expiry <= now) this.selfWrites.delete(path20);
16866
17062
  }
16867
17063
  this.emit({
16868
17064
  added: resync ? [] : [...added],
@@ -17066,8 +17262,8 @@ var TerminalModeEngine = class {
17066
17262
  /** Suppress the change event our own write is about to produce, so a save
17067
17263
  * never comes back to the editor as an outside edit. Normalized to the form
17068
17264
  * the watcher emits, since callers spell the path either way. */
17069
- expectWrite(postId, path18) {
17070
- this.watchers.get(postId)?.expectWrite(posixNormalize(path18));
17265
+ expectWrite(postId, path20) {
17266
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path20));
17071
17267
  }
17072
17268
  // ── Lifecycle / ModeEngine ──
17073
17269
  start(base, args) {
@@ -17264,18 +17460,18 @@ var TerminalModeEngine = class {
17264
17460
  this.reservedNames.values()
17265
17461
  );
17266
17462
  this.reservedNames.set(session.postId, workName);
17267
- const { path: path18 } = await addWorktree(
17463
+ const { path: path20 } = await addWorktree(
17268
17464
  this.core.target(),
17269
17465
  workName,
17270
17466
  branch,
17271
17467
  "terminal"
17272
17468
  );
17273
- session.workspacePath = path18;
17469
+ session.workspacePath = path20;
17274
17470
  await setGitIdentity(
17275
17471
  this.core.target(),
17276
17472
  config.gitName,
17277
17473
  config.gitEmail,
17278
- path18
17474
+ path20
17279
17475
  );
17280
17476
  this.emitter.addLog(session, "system", "Worktree ready.");
17281
17477
  } else {
@@ -17337,11 +17533,11 @@ var TerminalModeEngine = class {
17337
17533
  }
17338
17534
  async bareFsList({
17339
17535
  postId,
17340
- path: path18
17536
+ path: path20
17341
17537
  }) {
17342
17538
  const target = this.getBareTarget(postId);
17343
17539
  const root = this.getBareRoot(postId);
17344
- const abs = this.resolveBarePath(path18, root);
17540
+ const abs = this.resolveBarePath(path20, root);
17345
17541
  const relRoot = abs.slice(root.length) || "/";
17346
17542
  const out = await target.run([
17347
17543
  "sh",
@@ -17391,30 +17587,30 @@ var TerminalModeEngine = class {
17391
17587
  }
17392
17588
  async bareFsRead({
17393
17589
  postId,
17394
- path: path18
17590
+ path: path20
17395
17591
  }) {
17396
17592
  const target = this.getBareTarget(postId);
17397
- const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17593
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17398
17594
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17399
17595
  '`)).trim();
17400
17596
  return { content: b64, encoding: "base64" };
17401
17597
  }
17402
17598
  async bareFsWrite({
17403
17599
  postId,
17404
- path: path18,
17600
+ path: path20,
17405
17601
  content,
17406
17602
  encoding
17407
17603
  }) {
17408
17604
  const target = this.getBareTarget(postId);
17409
- const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17605
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17410
17606
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17411
17607
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17412
17608
  throw new Error("Invalid base64 payload.");
17413
17609
  }
17414
- this.expectWrite(String(postId), path18);
17610
+ this.expectWrite(String(postId), path20);
17415
17611
  await writeBase64File(target, abs, b64);
17416
17612
  const session = this.sessions.get(String(postId));
17417
- if (session) delete session.buffers[path18];
17613
+ if (session) delete session.buffers[path20];
17418
17614
  }
17419
17615
  /** Append one chunk of a dropped image to the per-space drops dir
17420
17616
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17464,39 +17660,39 @@ var TerminalModeEngine = class {
17464
17660
  }
17465
17661
  bareOpenFile({
17466
17662
  postId: rawPostId,
17467
- path: path18
17663
+ path: path20
17468
17664
  }) {
17469
17665
  const session = this.sessions.get(String(rawPostId));
17470
17666
  if (!session || session.mode !== "bare") return;
17471
- if (!session.openedFiles.includes(path18)) {
17472
- session.openedFiles.push(path18);
17667
+ if (!session.openedFiles.includes(path20)) {
17668
+ session.openedFiles.push(path20);
17473
17669
  }
17474
17670
  }
17475
17671
  bareCloseFile({
17476
17672
  postId: rawPostId,
17477
- path: path18
17673
+ path: path20
17478
17674
  }) {
17479
17675
  const session = this.sessions.get(String(rawPostId));
17480
17676
  if (!session || session.mode !== "bare") return;
17481
- session.openedFiles = session.openedFiles.filter((p) => p !== path18);
17482
- delete session.buffers[path18];
17677
+ session.openedFiles = session.openedFiles.filter((p) => p !== path20);
17678
+ delete session.buffers[path20];
17483
17679
  }
17484
17680
  bareBufferSet({
17485
17681
  postId: rawPostId,
17486
- path: path18,
17682
+ path: path20,
17487
17683
  content
17488
17684
  }) {
17489
17685
  const session = this.sessions.get(String(rawPostId));
17490
17686
  if (!session || session.mode !== "bare") return;
17491
- session.buffers[path18] = content;
17687
+ session.buffers[path20] = content;
17492
17688
  }
17493
17689
  async bareFsDelete({
17494
17690
  postId,
17495
- path: path18
17691
+ path: path20
17496
17692
  }) {
17497
17693
  const target = this.getBareTarget(postId);
17498
17694
  const root = this.getBareRoot(postId);
17499
- const abs = this.resolveBarePath(path18, root);
17695
+ const abs = this.resolveBarePath(path20, root);
17500
17696
  if (abs === root) {
17501
17697
  throw new Error("Refusing to delete the repo root.");
17502
17698
  }
@@ -17504,10 +17700,10 @@ var TerminalModeEngine = class {
17504
17700
  }
17505
17701
  async bareFsMkdir({
17506
17702
  postId,
17507
- path: path18
17703
+ path: path20
17508
17704
  }) {
17509
17705
  const target = this.getBareTarget(postId);
17510
- const abs = this.resolveBarePath(path18, this.getBareRoot(postId));
17706
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17511
17707
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17512
17708
  }
17513
17709
  async bareReview({
@@ -18189,8 +18385,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
18189
18385
  };
18190
18386
  }
18191
18387
  }
18192
- async function githubApi(token, path18) {
18193
- const res = await fetch(`${API_BASE}${path18}`, {
18388
+ async function githubApi(token, path20) {
18389
+ const res = await fetch(`${API_BASE}${path20}`, {
18194
18390
  headers: {
18195
18391
  Authorization: `Bearer ${token}`,
18196
18392
  Accept: "application/vnd.github+json",
@@ -18199,7 +18395,7 @@ async function githubApi(token, path18) {
18199
18395
  });
18200
18396
  if (!res.ok) {
18201
18397
  throw new Error(
18202
- `GitHub ${path18} failed (${res.status}): ${await res.text()}`
18398
+ `GitHub ${path20} failed (${res.status}): ${await res.text()}`
18203
18399
  );
18204
18400
  }
18205
18401
  return await res.json();
@@ -18244,13 +18440,13 @@ async function listGithubBranches(token, fullName) {
18244
18440
  }
18245
18441
 
18246
18442
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18247
- var import_path8 = __toESM(require("path"));
18443
+ var import_path9 = __toESM(require("path"));
18248
18444
 
18249
18445
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18250
18446
  var import_child_process4 = require("child_process");
18251
18447
  var import_crypto2 = __toESM(require("crypto"));
18252
- var import_promises5 = __toESM(require("fs/promises"));
18253
- var import_path7 = __toESM(require("path"));
18448
+ var import_promises6 = __toESM(require("fs/promises"));
18449
+ var import_path8 = __toESM(require("path"));
18254
18450
  var import_util6 = require("util");
18255
18451
  var execFileAsync3 = (0, import_util6.promisify)(import_child_process4.execFile);
18256
18452
  var IDENTITY_FILE = "age-identity.enc";
@@ -18283,7 +18479,7 @@ function deriveKey(password, salt) {
18283
18479
  }
18284
18480
  async function secretsInitialized(configDir) {
18285
18481
  try {
18286
- await import_promises5.default.access(import_path7.default.join(configDir, IDENTITY_FILE));
18482
+ await import_promises6.default.access(import_path8.default.join(configDir, IDENTITY_FILE));
18287
18483
  return true;
18288
18484
  } catch {
18289
18485
  return false;
@@ -18322,10 +18518,10 @@ async function initSecrets(configDir, password) {
18322
18518
  tag: cipher.getAuthTag().toString("base64"),
18323
18519
  data: data.toString("base64")
18324
18520
  };
18325
- await import_promises5.default.writeFile(import_path7.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18521
+ await import_promises6.default.writeFile(import_path8.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18326
18522
  mode: 384
18327
18523
  });
18328
- await import_promises5.default.writeFile(import_path7.default.join(configDir, RECIPIENT_FILE), `${recipient}
18524
+ await import_promises6.default.writeFile(import_path8.default.join(configDir, RECIPIENT_FILE), `${recipient}
18329
18525
  `, {
18330
18526
  mode: 384
18331
18527
  });
@@ -18348,17 +18544,17 @@ function decryptIdentity(box, password) {
18348
18544
  }
18349
18545
  }
18350
18546
  function envPath(spaceDir) {
18351
- return import_path7.default.join(spaceDir, "secrets", ENV_FILE);
18547
+ return import_path8.default.join(spaceDir, "secrets", ENV_FILE);
18352
18548
  }
18353
18549
  async function identityFor(configDir, password) {
18354
18550
  const box = JSON.parse(
18355
- await import_promises5.default.readFile(import_path7.default.join(configDir, IDENTITY_FILE), "utf-8")
18551
+ await import_promises6.default.readFile(import_path8.default.join(configDir, IDENTITY_FILE), "utf-8")
18356
18552
  );
18357
18553
  return decryptIdentity(box, password);
18358
18554
  }
18359
18555
  async function readEnv(spaceDir, identity) {
18360
18556
  try {
18361
- await import_promises5.default.access(envPath(spaceDir));
18557
+ await import_promises6.default.access(envPath(spaceDir));
18362
18558
  } catch {
18363
18559
  return {};
18364
18560
  }
@@ -18387,14 +18583,14 @@ async function readEnv(spaceDir, identity) {
18387
18583
  return values;
18388
18584
  }
18389
18585
  async function writeEnv(configDir, spaceDir, identity, values) {
18390
- const recipient = (await import_promises5.default.readFile(import_path7.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18586
+ const recipient = (await import_promises6.default.readFile(import_path8.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18391
18587
  const plain = `${Object.entries(values).map(([k, v]) => `${k}=${v}`).join("\n")}
18392
18588
  `;
18393
18589
  const tmp = envPath(spaceDir).replace(
18394
18590
  /\.enc\.env$/,
18395
18591
  `.${process.pid}.tmp.env`
18396
18592
  );
18397
- await import_promises5.default.writeFile(tmp, plain, { mode: 384 });
18593
+ await import_promises6.default.writeFile(tmp, plain, { mode: 384 });
18398
18594
  try {
18399
18595
  const { stdout } = await execFileAsync3(
18400
18596
  "sops",
@@ -18413,9 +18609,9 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18413
18609
  maxBuffer: 4 * 1024 * 1024
18414
18610
  }
18415
18611
  );
18416
- await import_promises5.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18612
+ await import_promises6.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18417
18613
  } finally {
18418
- await import_promises5.default.rm(tmp, { force: true });
18614
+ await import_promises6.default.rm(tmp, { force: true });
18419
18615
  }
18420
18616
  }
18421
18617
  async function listSecretKeys(configDir, spaceDir, password) {
@@ -18451,18 +18647,9 @@ var SECRETS_MARKER = "NEED_SECRETS:";
18451
18647
  var COMPLETE_MARKER = "DEPLOY_COMPLETE:";
18452
18648
  var FAILED_MARKER = "DEPLOY_FAILED:";
18453
18649
  var MAX_MARKERLESS_TURNS = 3;
18454
- var COMMAND_TIMEOUT_MS = 45 * 6e4;
18650
+ var COMMAND_IDLE_TIMEOUT_MS = 45 * 6e4;
18651
+ var COMMAND_TIMEOUT_MS = 6 * 60 * 6e4;
18455
18652
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18456
- function redactor(values) {
18457
- const significant = values.filter((v) => v.length >= 4);
18458
- return (text) => {
18459
- let out = text;
18460
- for (const value2 of significant) {
18461
- out = out.split(value2).join("[REDACTED]");
18462
- }
18463
- return out;
18464
- };
18465
- }
18466
18653
  function tailMarker(text) {
18467
18654
  const zone = text.slice(-2e3);
18468
18655
  let best = null;
@@ -18479,10 +18666,53 @@ function tailMarker(text) {
18479
18666
  const value2 = zone.slice(best.at + best.marker.length).trim();
18480
18667
  return value2 ? { marker: best.marker, value: value2 } : null;
18481
18668
  }
18669
+ function containedCwd(cwd, spaceDir) {
18670
+ if (!cwd) return null;
18671
+ const resolved = import_path9.default.resolve(cwd);
18672
+ const root = import_path9.default.resolve(spaceDir);
18673
+ return resolved === root || resolved.startsWith(root + import_path9.default.sep) ? resolved : null;
18674
+ }
18675
+ async function authorizeAndRun(opts) {
18676
+ const { command, hooks } = opts;
18677
+ opts.status?.("waiting for the owner to authorize this command");
18678
+ let secrets = null;
18679
+ let authError = "";
18680
+ while (!secrets) {
18681
+ const auth = await hooks.requestAuth(command, authError);
18682
+ if ("denied" in auth) return { denied: true };
18683
+ try {
18684
+ secrets = await secretsForInjection(
18685
+ opts.configDir,
18686
+ opts.spaceDir,
18687
+ auth.password
18688
+ );
18689
+ } catch (err) {
18690
+ authError = err instanceof Error ? err.message : String(err);
18691
+ opts.status?.(`authorization failed: ${authError}`);
18692
+ }
18693
+ }
18694
+ const redact = redactor(Object.values(secrets));
18695
+ opts.status?.("authorized, running");
18696
+ hooks.sendLog({
18697
+ type: "system",
18698
+ content: "Authorized. Running the command with secrets injected..."
18699
+ });
18700
+ const res = await execLogin(command, {
18701
+ cwd: opts.cwd,
18702
+ env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18703
+ timeout: COMMAND_TIMEOUT_MS,
18704
+ idleTimeout: COMMAND_IDLE_TIMEOUT_MS,
18705
+ maxBuffer: 32 * 1024 * 1024
18706
+ });
18707
+ const output = redact(res.output);
18708
+ hooks.sendLog({ type: "tool", content: `$ ${command}
18709
+ ${tail(output)}` });
18710
+ return { code: res.code, output };
18711
+ }
18482
18712
  async function startDeployRun(opts) {
18483
18713
  const { hooks } = opts;
18484
- const workspace = import_path8.default.join(opts.spaceDir, "workspace");
18485
- const configDir = import_path8.default.dirname(opts.spaceDir);
18714
+ const workspace = import_path9.default.join(opts.spaceDir, "workspace");
18715
+ const configDir = import_path9.default.dirname(opts.spaceDir);
18486
18716
  const status = await checkDeployStatus(opts);
18487
18717
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18488
18718
  throw new Error(
@@ -18502,119 +18732,120 @@ async function startDeployRun(opts) {
18502
18732
  );
18503
18733
  await hostGit(workspace, "clean", "-fd");
18504
18734
  await ensureClaudeSkillLink(workspace);
18735
+ const release = (opts.releaseBranch || "").replace(/[^\w./-]/g, "");
18736
+ const releaseNote = release && release !== status.branch ? `The release branch for this run is \`${release}\` - it was chosen in the Deploy modal, so do not ask which branch to ship. Check it out as your first step.` : "";
18737
+ if (releaseNote) {
18738
+ hooks.sendLog({
18739
+ type: "system",
18740
+ content: `Release branch: ${release}`
18741
+ });
18742
+ }
18505
18743
  const contract = prompts_default.deployRun.join("\n");
18506
- const reminder = "Reminder: every declared deploy variable is intentionally unset in your shell. Never check for them or abort over them - route any command that needs one through NEED_SECRETS and the runner injects the values.";
18507
- let prompt2 = opts.provider === "codex" ? `Use the "deploy" skill: load it and execute it step by step.
18508
-
18509
- ${reminder}` : `/deploy
18510
-
18511
- ${reminder}`;
18744
+ const reminder = "Reminder: every declared deploy variable is intentionally unset in your shell. Never check for them or abort over them - prefix any command that needs one with `factiii-secrets run --` and the runner injects the values.";
18745
+ const opening = opts.provider === "codex" ? 'Use the "deploy" skill: load it and execute it step by step.' : "/deploy";
18746
+ let prompt2 = [opening, releaseNote, reminder].filter(Boolean).join("\n\n");
18512
18747
  let sessionId = "";
18513
18748
  let markerless = 0;
18514
18749
  let todos = [];
18515
- for (; ; ) {
18516
- if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18517
- const { proc, result } = await spawnAgent(
18518
- opts.provider,
18519
- hostTarget(opts.spaceDir),
18520
- {
18521
- cwd: workspace,
18522
- prompt: prompt2,
18523
- allowedTools: "Bash,Read,Grep,Glob,TodoWrite,WebSearch,WebFetch",
18524
- // The run's guardrails are structural (merge-gated skill, per-command
18525
- // authorization, runner-side injection); the auto classifier fights
18526
- // legitimate infrastructure commands, so the session itself runs open.
18527
- permissionMode: "bypassPermissions",
18528
- model: "claude-opus-4-6",
18529
- includePartialMessages: true,
18530
- resumeSessionId: sessionId || void 0,
18531
- appendSystemPrompt: sessionId ? void 0 : contract,
18532
- env: spaceEnv(opts.spaceDir),
18533
- callbacks: {
18534
- initialTodos: todos,
18535
- onLog: (entry) => hooks.sendLog(entry),
18536
- onTodos: (updated) => {
18537
- todos = updated;
18538
- hooks.onTodos(updated);
18750
+ const bridge = await startSecretsBridge({
18751
+ spaceDir: opts.spaceDir,
18752
+ handlers: {
18753
+ run: ({ command, cwd, status: report }) => authorizeAndRun({
18754
+ configDir,
18755
+ spaceDir: opts.spaceDir,
18756
+ // A skill legitimately cds into subdirectories, so the CLI's own cwd
18757
+ // is the right place to run - but only while it is still inside the
18758
+ // space. Anywhere else is a confused caller, not a step, and the
18759
+ // workspace is the safe reading of what it meant.
18760
+ cwd: containedCwd(cwd, opts.spaceDir) ?? workspace,
18761
+ command,
18762
+ hooks,
18763
+ status: report
18764
+ }),
18765
+ // Names only. The contract on the default branch already declares them
18766
+ // in public, so this reveals nothing the repo does not.
18767
+ list: () => Promise.resolve(status.variables.map((v) => v.key))
18768
+ }
18769
+ });
18770
+ try {
18771
+ for (; ; ) {
18772
+ if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18773
+ const { proc, result } = await spawnAgent(
18774
+ opts.provider,
18775
+ hostTarget(opts.spaceDir),
18776
+ {
18777
+ cwd: workspace,
18778
+ prompt: prompt2,
18779
+ allowedTools: "Bash,Read,Grep,Glob,TodoWrite,WebSearch,WebFetch,Skill",
18780
+ // The run's guardrails are structural (merge-gated skill, per-command
18781
+ // authorization, runner-side injection); the auto classifier fights
18782
+ // legitimate infrastructure commands, so the session itself runs open.
18783
+ permissionMode: "bypassPermissions",
18784
+ includePartialMessages: true,
18785
+ resumeSessionId: sessionId || void 0,
18786
+ appendSystemPrompt: sessionId ? void 0 : contract,
18787
+ env: spaceEnv(opts.spaceDir),
18788
+ callbacks: {
18789
+ initialTodos: todos,
18790
+ onLog: (entry) => hooks.sendLog(entry),
18791
+ onTodos: (updated) => {
18792
+ todos = updated;
18793
+ hooks.onTodos(updated);
18794
+ }
18539
18795
  }
18540
18796
  }
18541
- }
18542
- );
18543
- hooks.registerProc(proc);
18544
- const { text, sessionId: sid } = await result;
18545
- hooks.registerProc(null);
18546
- sessionId = sid || sessionId;
18547
- if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18548
- const found = tailMarker(text);
18549
- if (found?.marker === SECRETS_MARKER) {
18550
- const command = found.value.split("\n")[0].trim();
18551
- let secrets = null;
18552
- let denied = false;
18553
- let authError = "";
18554
- while (!secrets && !denied) {
18555
- const auth = await hooks.requestAuth(command, authError);
18556
- if ("denied" in auth) {
18557
- denied = true;
18558
- break;
18559
- }
18560
- try {
18561
- secrets = await secretsForInjection(
18562
- configDir,
18563
- opts.spaceDir,
18564
- auth.password
18565
- );
18566
- } catch (err) {
18567
- authError = err instanceof Error ? err.message : String(err);
18797
+ );
18798
+ hooks.registerProc(proc);
18799
+ const { text, sessionId: sid } = await result;
18800
+ hooks.registerProc(null);
18801
+ sessionId = sid || sessionId;
18802
+ if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18803
+ const found = tailMarker(text);
18804
+ if (found?.marker === SECRETS_MARKER) {
18805
+ const command = found.value.split("\n")[0].trim();
18806
+ const res = await authorizeAndRun({
18807
+ configDir,
18808
+ spaceDir: opts.spaceDir,
18809
+ cwd: workspace,
18810
+ command,
18811
+ hooks
18812
+ });
18813
+ if ("denied" in res) {
18814
+ prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18815
+ continue;
18568
18816
  }
18817
+ markerless = 0;
18818
+ const clean = tail(res.output);
18819
+ prompt2 = `The authorized command finished (exit ${res.code}). Redacted output:
18820
+
18821
+ ${clean}`;
18822
+ continue;
18569
18823
  }
18570
- if (denied || !secrets) {
18571
- prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18824
+ if (found?.marker === HUMAN_MARKER) {
18825
+ markerless = 0;
18826
+ prompt2 = await hooks.requestHuman(found.value);
18572
18827
  continue;
18573
18828
  }
18574
- const redact = redactor(Object.values(secrets));
18829
+ if (found?.marker === COMPLETE_MARKER) {
18830
+ return { shipped: true, summary: found.value };
18831
+ }
18832
+ if (found?.marker === FAILED_MARKER) {
18833
+ return { shipped: false, summary: found.value };
18834
+ }
18835
+ markerless += 1;
18836
+ if (markerless >= MAX_MARKERLESS_TURNS) {
18837
+ throw new Error(
18838
+ "The run ended without declaring completion (no DEPLOY_COMPLETE after repeated turns)."
18839
+ );
18840
+ }
18575
18841
  hooks.sendLog({
18576
18842
  type: "system",
18577
- content: `Authorized. Running the command with secrets injected...`
18578
- });
18579
- const res = await execLogin(command, {
18580
- cwd: workspace,
18581
- env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18582
- timeout: COMMAND_TIMEOUT_MS,
18583
- maxBuffer: 32 * 1024 * 1024
18843
+ content: "Turn ended without a protocol marker - resuming the run..."
18584
18844
  });
18585
- const output = res.output;
18586
- const exitNote = `exit ${res.code}`;
18587
- markerless = 0;
18588
- const clean = redact(tail(output));
18589
- hooks.sendLog({ type: "tool", content: `$ ${command}
18590
- ${clean}` });
18591
- prompt2 = `The authorized command finished (${exitNote}). Redacted output:
18592
-
18593
- ${clean}`;
18594
- continue;
18595
- }
18596
- if (found?.marker === HUMAN_MARKER) {
18597
- markerless = 0;
18598
- prompt2 = await hooks.requestHuman(found.value);
18599
- continue;
18845
+ prompt2 = "Your turn ended with no protocol marker. If the release fully shipped, end your message with DEPLOY_COMPLETE: followed by your run report; if you are stopping without shipping, end with DEPLOY_FAILED: followed by the report. Otherwise CONTINUE executing the skill from where you left off. If you had background tasks running, they were killed when your turn ended - rerun them, and reap every background task before ending a turn.";
18600
18846
  }
18601
- if (found?.marker === COMPLETE_MARKER) {
18602
- return { shipped: true, summary: found.value };
18603
- }
18604
- if (found?.marker === FAILED_MARKER) {
18605
- return { shipped: false, summary: found.value };
18606
- }
18607
- markerless += 1;
18608
- if (markerless >= MAX_MARKERLESS_TURNS) {
18609
- throw new Error(
18610
- "The run ended without declaring completion (no DEPLOY_COMPLETE after repeated turns)."
18611
- );
18612
- }
18613
- hooks.sendLog({
18614
- type: "system",
18615
- content: "Turn ended without a protocol marker - resuming the run..."
18616
- });
18617
- prompt2 = "Your turn ended with no protocol marker. If the release fully shipped, end your message with DEPLOY_COMPLETE: followed by your run report; if you are stopping without shipping, end with DEPLOY_FAILED: followed by the report. Otherwise CONTINUE executing the skill from where you left off. If you had background tasks running, they were killed when your turn ended - rerun them, and reap every background task before ending a turn.";
18847
+ } finally {
18848
+ await bridge.close().catch(() => void 0);
18618
18849
  }
18619
18850
  }
18620
18851
 
@@ -18706,44 +18937,110 @@ async function getDriveRoot(accessToken) {
18706
18937
  };
18707
18938
  }
18708
18939
 
18940
+ // ../../shared/all/helpers/board-agent-core/space-core.ts
18941
+ var import_promises8 = require("fs/promises");
18942
+ var import_path11 = __toESM(require("path"));
18943
+
18709
18944
  // ../../shared/all/helpers/board-agent-core/plugin.ts
18945
+ var import_crypto3 = __toESM(require("crypto"));
18710
18946
  var import_fs3 = require("fs");
18711
- var import_promises6 = require("fs/promises");
18947
+ var import_promises7 = require("fs/promises");
18712
18948
  var import_os3 = __toESM(require("os"));
18713
- var import_path9 = __toESM(require("path"));
18714
- var PLUGIN_ID = "factiii@factiii-runner";
18949
+ var import_path10 = __toESM(require("path"));
18950
+ var MARKETPLACE_NAME = "factiii-runner";
18951
+ var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
18952
+ var SECRETS_BIN = "factiii-secrets";
18715
18953
  function candidates() {
18716
18954
  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")
18955
+ import_path10.default.join(__dirname, "..", "plugin"),
18956
+ import_path10.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
18957
+ import_path10.default.join(process.cwd(), "apps", "runner", "plugin")
18720
18958
  ];
18721
18959
  }
18722
18960
  function pluginRoot() {
18723
18961
  for (const dir of candidates()) {
18724
- if ((0, import_fs3.existsSync)(import_path9.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18962
+ if ((0, import_fs3.existsSync)(import_path10.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18725
18963
  return dir;
18726
18964
  }
18727
18965
  }
18728
18966
  return null;
18729
18967
  }
18730
18968
  function renderedPluginRoot() {
18731
- return import_path9.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18969
+ return import_path10.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18970
+ }
18971
+ async function runnerVersion(source) {
18972
+ const pkg = import_path10.default.join(source, "..", "package.json");
18973
+ const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
18974
+ if (!raw) return FALLBACK_VERSION;
18975
+ try {
18976
+ const { version } = JSON.parse(raw);
18977
+ return version || FALLBACK_VERSION;
18978
+ } catch {
18979
+ return FALLBACK_VERSION;
18980
+ }
18981
+ }
18982
+ var FALLBACK_VERSION = "0.0.0-unknown";
18983
+ async function treeFiles(dir) {
18984
+ const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
18985
+ const files = [];
18986
+ for (const entry of entries) {
18987
+ const full = import_path10.default.join(dir, entry.name);
18988
+ if (entry.isDirectory()) files.push(...await treeFiles(full));
18989
+ else if (entry.isFile()) files.push(full);
18990
+ }
18991
+ return files.sort();
18992
+ }
18993
+ async function fingerprint(source, url2, version = "") {
18994
+ const hash = import_crypto3.default.createHash("sha256").update(url2).update(version);
18995
+ for (const file of await treeFiles(source)) {
18996
+ hash.update(import_path10.default.relative(source, file));
18997
+ hash.update(await (0, import_promises7.readFile)(file));
18998
+ }
18999
+ return hash.digest("hex");
19000
+ }
19001
+ function stampPath(dest) {
19002
+ return `${dest}.stamp`;
19003
+ }
19004
+ var inFlight = /* @__PURE__ */ new Map();
19005
+ function renderPlugin(boardMcpUrl) {
19006
+ const running = inFlight.get(boardMcpUrl);
19007
+ if (running) return running;
19008
+ const started = renderPluginImpl(boardMcpUrl).finally(
19009
+ () => inFlight.delete(boardMcpUrl)
19010
+ );
19011
+ inFlight.set(boardMcpUrl, started);
19012
+ return started;
18732
19013
  }
18733
- async function renderPlugin(boardMcpUrl) {
19014
+ async function renderPluginImpl(boardMcpUrl) {
18734
19015
  const source = pluginRoot();
18735
19016
  if (!source) return null;
18736
19017
  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);
19018
+ const version = await runnerVersion(source);
19019
+ const want = await fingerprint(source, boardMcpUrl, version);
19020
+ const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
18741
19021
  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);
19022
+ await (0, import_promises7.mkdir)(import_path10.default.dirname(dest), { recursive: true });
19023
+ await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
19024
+ await (0, import_promises7.writeFile)(
19025
+ import_path10.default.join(dest, "factiii", ".mcp.json"),
19026
+ `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
19027
+ `
19028
+ );
19029
+ await stampVersion(dest, version);
19030
+ await (0, import_promises7.writeFile)(stampPath(dest), want);
18745
19031
  return dest;
18746
19032
  }
19033
+ async function stampVersion(dest, version) {
19034
+ const manifest = import_path10.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
19035
+ const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
19036
+ if (!raw) return;
19037
+ const parsed = JSON.parse(raw);
19038
+ await (0, import_promises7.writeFile)(
19039
+ manifest,
19040
+ `${JSON.stringify({ ...parsed, version }, null, 2)}
19041
+ `
19042
+ );
19043
+ }
18747
19044
  function boardMcpConfig(url2) {
18748
19045
  return {
18749
19046
  mcpServers: {
@@ -18987,6 +19284,8 @@ var SpaceCore = class {
18987
19284
  this.execTarget ??= hostTarget(this.spaceDir(), () => {
18988
19285
  const env = {};
18989
19286
  for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
19287
+ const board = this.boardMcp();
19288
+ if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
18990
19289
  return env;
18991
19290
  });
18992
19291
  return this.execTarget;
@@ -19167,6 +19466,7 @@ var SpaceCore = class {
19167
19466
  this.provisionGitLane(status),
19168
19467
  this.provisionMcpLane(),
19169
19468
  this.provisionPluginLane(),
19469
+ this.provisionBinLane(),
19170
19470
  // Covers a host signed in outside the one-click button, which would
19171
19471
  // otherwise meet the wizard the first time it opens a terminal.
19172
19472
  markOnboardingComplete(target).catch(() => {
@@ -19227,6 +19527,13 @@ var SpaceCore = class {
19227
19527
  const cmd = [
19228
19528
  `claude plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19229
19529
  `claude plugin install ${PLUGIN_ID} --scope user >/dev/null 2>&1 || true`,
19530
+ // Both CLIs install by COPYING the plugin into a cache keyed by its
19531
+ // version, and that copy is what loads. claude's `install` refuses once
19532
+ // something is installed ("already installed"), so without these two an
19533
+ // upgrade renders new files into a directory nothing reads again. Codex
19534
+ // needs no equivalent: its `add` re-installs from a local marketplace.
19535
+ `claude plugin marketplace update ${MARKETPLACE_NAME} >/dev/null 2>&1 || true`,
19536
+ `claude plugin update ${PLUGIN_ID} >/dev/null 2>&1 || true`,
19230
19537
  `codex plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19231
19538
  `codex plugin add ${PLUGIN_ID} >/dev/null 2>&1 || true`
19232
19539
  ].join("; ");
@@ -19235,6 +19542,35 @@ var SpaceCore = class {
19235
19542
  } catch {
19236
19543
  }
19237
19544
  }
19545
+ /** Stage the plugin's `factiii-secrets` onto the agent's PATH.
19546
+ *
19547
+ * A skill can only teach a command that exists. `paths.bin` is already the
19548
+ * PATH prefix `agentEnv()` hands every spawn (it is how `factiii-expose`
19549
+ * resolves), but nothing put anything there for a deploy run - so the
19550
+ * skill would name a command the shell could not find.
19551
+ *
19552
+ * Copied rather than symlinked: an `npm i -g @factiii/runner` upgrade
19553
+ * replaces the package directory, and a link into the old one dangles.
19554
+ * Re-copied whenever the sizes differ, which is what carries an upgrade
19555
+ * through without re-copying on every ensure. */
19556
+ async provisionBinLane() {
19557
+ const source = pluginRoot();
19558
+ if (!source) return;
19559
+ const { bin } = this.target().paths;
19560
+ const from = import_path11.default.join(source, "factiii", "bin", SECRETS_BIN);
19561
+ const to = import_path11.default.join(bin, SECRETS_BIN);
19562
+ try {
19563
+ const [src, staged] = await Promise.all([
19564
+ (0, import_promises8.stat)(from),
19565
+ (0, import_promises8.stat)(to).catch(() => null)
19566
+ ]);
19567
+ if (staged?.size === src.size) return;
19568
+ await (0, import_promises8.mkdir)(bin, { recursive: true });
19569
+ await (0, import_promises8.copyFile)(from, to);
19570
+ await (0, import_promises8.chmod)(to, 493);
19571
+ } catch {
19572
+ }
19573
+ }
19238
19574
  // Register this space's MCPs with both CLIs, user-scoped, and clear the ones
19239
19575
  // it cannot currently offer. Registering regardless left every space holding
19240
19576
  // an entry it never uses, which the CLI then fails to handshake with on every
@@ -19335,6 +19671,10 @@ var BoardAgentEngine = class _BoardAgentEngine {
19335
19671
  // In-flight creator session, memory-only (the repo is the source of truth
19336
19672
  // once merged). Survives modal reopen via deployCreatorState.
19337
19673
  this.skillCreatorSessionId = null;
19674
+ // Which CLI produced that session. A resume id belongs to ONE provider, so
19675
+ // switching the picker between drafting and revising has to start fresh
19676
+ // rather than hand a claude session id to codex.
19677
+ this.skillCreatorProvider = null;
19338
19678
  // Set when the draft was resumed from a pushed deploy-skill branch:
19339
19679
  // approve commits onto that branch instead of cutting a new one.
19340
19680
  this.skillDraftBranch = null;
@@ -19355,6 +19695,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19355
19695
  // ── Deploy run (host executor; see host-run.ts + .specs/deploy.md) ──
19356
19696
  // Memory-only: reopened clients reattach, a restart loses the run and says so.
19357
19697
  this.runState = {
19698
+ provider: "claude",
19358
19699
  phase: "idle",
19359
19700
  todos: [],
19360
19701
  logs: [],
@@ -19448,6 +19789,20 @@ var BoardAgentEngine = class _BoardAgentEngine {
19448
19789
  githubToken: config.githubToken
19449
19790
  });
19450
19791
  }
19792
+ // What the Deploy modal's release picker offers: branches on origin that
19793
+ // could ship, newest first. See listReleaseBranches.
19794
+ async deployBranches() {
19795
+ const config = this.core.readConfig();
19796
+ if (!config.repoUrl || !config.githubToken) {
19797
+ throw new Error("Set the repository URL and GitHub token first.");
19798
+ }
19799
+ return listReleaseBranches({
19800
+ spaceDir: this.core.spaceDir(),
19801
+ repoUrl: config.repoUrl,
19802
+ mainBranch: config.mainBranch,
19803
+ githubToken: config.githubToken
19804
+ });
19805
+ }
19451
19806
  // ── Agent skills (SKILL.md) ──
19452
19807
  // Repo opts are optional here, unlike deploy: the global roots are readable
19453
19808
  // with no repo configured, and listSkills reports repoAvailable: false.
@@ -19498,6 +19853,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19498
19853
  });
19499
19854
  const draft = await loadSkillDraft({ ...opts, branch: payload.branch });
19500
19855
  this.skillCreatorSessionId = null;
19856
+ this.skillCreatorProvider = null;
19501
19857
  this.skillDraftBranch = payload.branch;
19502
19858
  this.creatorState = { phase: "review", logs: [], draft, error: "" };
19503
19859
  return draft;
@@ -19543,8 +19899,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
19543
19899
  throw err;
19544
19900
  }
19545
19901
  }
19546
- deployFixEnvironment(sendLog) {
19547
- const opts = this.deployCreatorOpts(sendLog);
19902
+ deployFixEnvironment(sendLog, provider) {
19903
+ const opts = this.deployCreatorOpts(sendLog, provider);
19548
19904
  return this.runEnvPass(
19549
19905
  "fixing",
19550
19906
  () => fixEnvironment({
@@ -19608,13 +19964,14 @@ var BoardAgentEngine = class _BoardAgentEngine {
19608
19964
  return this.activity.clear("deploy-run");
19609
19965
  }
19610
19966
  }
19611
- deployRunStart(sendLog) {
19967
+ deployRunStart(sendLog, provider, releaseBranch) {
19612
19968
  if (this.runState.phase === "running" || this.runState.phase === "awaiting-input" || this.runState.phase === "awaiting-auth") {
19613
19969
  throw new Error("A deploy run is already in progress.");
19614
19970
  }
19615
- const opts = this.deployCreatorOpts(sendLog);
19971
+ const opts = this.deployCreatorOpts(sendLog, provider);
19616
19972
  this.runCancelled = false;
19617
19973
  this.runState = {
19974
+ provider: opts.provider,
19618
19975
  phase: "running",
19619
19976
  todos: [],
19620
19977
  logs: [],
@@ -19650,6 +20007,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19650
20007
  mainBranch: opts.mainBranch,
19651
20008
  githubToken: opts.githubToken,
19652
20009
  provider: opts.provider,
20010
+ releaseBranch,
19653
20011
  hooks: {
19654
20012
  sendLog: bufferLog,
19655
20013
  onTodos: (todos) => this.setRun({ todos }),
@@ -19735,6 +20093,9 @@ var BoardAgentEngine = class _BoardAgentEngine {
19735
20093
  throw new Error("A deploy run is still in progress.");
19736
20094
  }
19737
20095
  this.runState = {
20096
+ // Unread while idle - the next run sets its own. Carried rather than
20097
+ // reset so nothing has to invent a provider that means nothing yet.
20098
+ provider: this.runState.provider,
19738
20099
  phase: "idle",
19739
20100
  todos: [],
19740
20101
  logs: [],
@@ -19833,7 +20194,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19833
20194
  throw err;
19834
20195
  }
19835
20196
  }
19836
- deployCreatorOpts(sendLog) {
20197
+ deployCreatorOpts(sendLog, provider) {
19837
20198
  const config = this.core.readConfig();
19838
20199
  if (!config.repoUrl || !config.githubToken) {
19839
20200
  throw new Error("Set the repository URL and GitHub token first.");
@@ -19843,36 +20204,51 @@ var BoardAgentEngine = class _BoardAgentEngine {
19843
20204
  repoUrl: config.repoUrl,
19844
20205
  mainBranch: config.mainBranch,
19845
20206
  githubToken: config.githubToken,
19846
- provider: this.core.agentProvider(),
20207
+ // resolveProvider coerces anything unknown to claude, so it can only be
20208
+ // asked about a value that is actually there.
20209
+ provider: provider ? resolveProvider(provider) : this.core.agentProvider(),
19847
20210
  sendLog
19848
20211
  };
19849
20212
  }
19850
20213
  deployCreateSkill(payload, sendLog) {
19851
20214
  this.skillDraftBranch = null;
19852
20215
  return this.runCreatorPass(async () => {
20216
+ const opts = this.deployCreatorOpts(
20217
+ this.creatorLogger(sendLog),
20218
+ payload.provider
20219
+ );
19853
20220
  const { sessionId, ...draft } = await generateDeploySkill({
19854
- ...this.deployCreatorOpts(this.creatorLogger(sendLog)),
20221
+ ...opts,
19855
20222
  instructions: payload.instructions
19856
20223
  });
19857
20224
  this.skillCreatorSessionId = sessionId || null;
20225
+ this.skillCreatorProvider = opts.provider;
19858
20226
  return draft;
19859
20227
  });
19860
20228
  }
19861
20229
  deployReviseSkill(payload, sendLog) {
19862
20230
  return this.runCreatorPass(async () => {
20231
+ const opts = this.deployCreatorOpts(
20232
+ this.creatorLogger(sendLog),
20233
+ payload.provider
20234
+ );
20235
+ const resumable = this.skillCreatorProvider === opts.provider;
19863
20236
  const { sessionId, ...draft } = await reviseDeploySkill({
19864
- ...this.deployCreatorOpts(this.creatorLogger(sendLog)),
20237
+ ...opts,
19865
20238
  feedback: payload.feedback,
19866
- resumeSessionId: this.skillCreatorSessionId ?? void 0
20239
+ // A different CLI than drafted this: its session id means nothing to
20240
+ // the new one, so the revision starts from the files on disk.
20241
+ resumeSessionId: resumable ? this.skillCreatorSessionId ?? void 0 : void 0
19867
20242
  });
19868
20243
  this.skillCreatorSessionId = sessionId || this.skillCreatorSessionId;
20244
+ this.skillCreatorProvider = opts.provider;
19869
20245
  return draft;
19870
20246
  });
19871
20247
  }
19872
20248
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
19873
20249
  secretsDirs() {
19874
20250
  const spaceDir = this.core.spaceDir();
19875
- return { configDir: (0, import_path10.dirname)(spaceDir), spaceDir };
20251
+ return { configDir: (0, import_path12.dirname)(spaceDir), spaceDir };
19876
20252
  }
19877
20253
  async deploySecretsStatus() {
19878
20254
  return {
@@ -19906,6 +20282,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19906
20282
  const config = this.core.readConfig();
19907
20283
  const onBranch = this.skillDraftBranch ?? void 0;
19908
20284
  this.skillCreatorSessionId = null;
20285
+ this.skillCreatorProvider = null;
19909
20286
  this.skillDraftBranch = null;
19910
20287
  this.creatorState = { phase: "idle", logs: [], draft: null, error: "" };
19911
20288
  this.activity.clear("deploy-skill");
@@ -19918,6 +20295,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19918
20295
  }
19919
20296
  deployDiscardSkill() {
19920
20297
  this.skillCreatorSessionId = null;
20298
+ this.skillCreatorProvider = null;
19921
20299
  this.skillDraftBranch = null;
19922
20300
  this.creatorState = { phase: "idle", logs: [], draft: null, error: "" };
19923
20301
  this.activity.clear("deploy-skill");
@@ -20677,6 +21055,8 @@ ${c.content.trim() || "(no description)"}`
20677
21055
  return await this.bareWorkNames();
20678
21056
  case "deployStatus":
20679
21057
  return await this.deployStatus();
21058
+ case "deployBranches":
21059
+ return await this.deployBranches();
20680
21060
  case "skillsList":
20681
21061
  return await this.skillsList();
20682
21062
  case "skillRead":
@@ -20722,14 +21102,17 @@ ${c.content.trim() || "(no description)"}`
20722
21102
  case "deployFixEnvironment":
20723
21103
  return await this.deployFixEnvironment(
20724
21104
  askLogSender ?? (() => {
20725
- })
21105
+ }),
21106
+ payload?.provider
20726
21107
  );
20727
21108
  case "deployEnvState":
20728
21109
  return this.deployEnvState();
20729
21110
  case "deployRunStart":
20730
21111
  return this.deployRunStart(
20731
21112
  askLogSender ?? (() => {
20732
- })
21113
+ }),
21114
+ payload?.provider,
21115
+ payload?.branch
20733
21116
  );
20734
21117
  case "deployRunState":
20735
21118
  return this.deployRunState();
@@ -20929,11 +21312,11 @@ ${c.content.trim() || "(no description)"}`
20929
21312
 
20930
21313
  // ../../shared/all/helpers/board-agent-core/index-service.ts
20931
21314
  var import_child_process5 = require("child_process");
20932
- var import_crypto3 = require("crypto");
21315
+ var import_crypto4 = require("crypto");
20933
21316
  var import_fs4 = require("fs");
20934
- var import_promises7 = __toESM(require("fs/promises"));
20935
- var import_net2 = require("net");
20936
- var import_path11 = require("path");
21317
+ var import_promises9 = __toESM(require("fs/promises"));
21318
+ var import_net3 = require("net");
21319
+ var import_path13 = require("path");
20937
21320
  var import_util7 = require("util");
20938
21321
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
20939
21322
  var IndexService = class {
@@ -20947,7 +21330,7 @@ var IndexService = class {
20947
21330
  /** Per-space bearer for MCP + file, derived from the control token (the
20948
21331
  * service computes the same). */
20949
21332
  bearerToken(spaceSlug) {
20950
- return (0, import_crypto3.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21333
+ return (0, import_crypto4.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
20951
21334
  }
20952
21335
  /** MCP bridge for a space. Null until the service is up (which it kicks
20953
21336
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -20992,7 +21375,7 @@ var IndexService = class {
20992
21375
  const log = await this.openLog();
20993
21376
  this.proc = (0, import_child_process5.spawn)(
20994
21377
  process.execPath,
20995
- [(0, import_path11.join)(this.opts.indexDir, "index.js")],
21378
+ [(0, import_path13.join)(this.opts.indexDir, "index.js")],
20996
21379
  {
20997
21380
  cwd: this.opts.indexDir,
20998
21381
  env: {
@@ -21020,8 +21403,8 @@ var IndexService = class {
21020
21403
  * opened (a missing log must never stop the service from starting). */
21021
21404
  async openLog() {
21022
21405
  try {
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");
21406
+ await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true });
21407
+ return (0, import_fs4.openSync)((0, import_path13.join)(this.opts.dataDir, "index.log"), "a");
21025
21408
  } catch {
21026
21409
  return null;
21027
21410
  }
@@ -21030,10 +21413,10 @@ var IndexService = class {
21030
21413
  * platform needing node-gyp fails here rather than at query time. */
21031
21414
  async installDeps() {
21032
21415
  const dir = this.opts.indexDir;
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")
21416
+ if ((0, import_fs4.existsSync)((0, import_path13.join)(dir, "node_modules"))) return;
21417
+ await import_promises9.default.copyFile(
21418
+ (0, import_path13.join)(dir, "image-package.json"),
21419
+ (0, import_path13.join)(dir, "package.json")
21037
21420
  );
21038
21421
  await execFileAsync4(
21039
21422
  "npm",
@@ -21045,15 +21428,15 @@ var IndexService = class {
21045
21428
  * CLIs' persistent MCP config, so a fresh port each start would leave every
21046
21429
  * registration pointing at a dead one. */
21047
21430
  async stablePort() {
21048
- const file = (0, import_path11.join)(this.opts.dataDir, ".port");
21049
- const saved = Number(await import_promises7.default.readFile(file, "utf-8").catch(() => ""));
21431
+ const file = (0, import_path13.join)(this.opts.dataDir, ".port");
21432
+ const saved = Number(await import_promises9.default.readFile(file, "utf-8").catch(() => ""));
21050
21433
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
21051
21434
  return saved;
21052
21435
  }
21053
21436
  const port = await freePort();
21054
- await import_promises7.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21437
+ await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21055
21438
  });
21056
- await import_promises7.default.writeFile(file, String(port), "utf-8").catch(() => {
21439
+ await import_promises9.default.writeFile(file, String(port), "utf-8").catch(() => {
21057
21440
  });
21058
21441
  return port;
21059
21442
  }
@@ -21147,14 +21530,14 @@ var IndexService = class {
21147
21530
  };
21148
21531
  function isFree(port) {
21149
21532
  return new Promise((resolve2) => {
21150
- const srv = (0, import_net2.createServer)();
21533
+ const srv = (0, import_net3.createServer)();
21151
21534
  srv.once("error", () => resolve2(false));
21152
21535
  srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
21153
21536
  });
21154
21537
  }
21155
21538
  function freePort() {
21156
21539
  return new Promise((resolve2, reject) => {
21157
- const srv = (0, import_net2.createServer)();
21540
+ const srv = (0, import_net3.createServer)();
21158
21541
  srv.on("error", reject);
21159
21542
  srv.listen(0, "127.0.0.1", () => {
21160
21543
  const addr = srv.address();
@@ -21222,22 +21605,22 @@ async function pairWithBrowser(serverUrl) {
21222
21605
  // src/config.ts
21223
21606
  var import_fs8 = __toESM(require("fs"));
21224
21607
  var import_os6 = __toESM(require("os"));
21225
- var import_path16 = __toESM(require("path"));
21608
+ var import_path18 = __toESM(require("path"));
21226
21609
 
21227
21610
  // src/secureFile.ts
21228
- var import_crypto6 = __toESM(require("crypto"));
21611
+ var import_crypto7 = __toESM(require("crypto"));
21229
21612
  var import_fs7 = __toESM(require("fs"));
21230
- var import_path15 = __toESM(require("path"));
21613
+ var import_path17 = __toESM(require("path"));
21231
21614
 
21232
21615
  // src/keychain.ts
21233
21616
  var import_child_process7 = require("child_process");
21234
- var import_crypto4 = __toESM(require("crypto"));
21617
+ var import_crypto5 = __toESM(require("crypto"));
21235
21618
  var import_fs5 = __toESM(require("fs"));
21236
21619
  var import_os4 = __toESM(require("os"));
21237
- var import_path12 = __toESM(require("path"));
21620
+ var import_path14 = __toESM(require("path"));
21238
21621
  var SERVICE = "factiii-runner";
21239
21622
  var ACCOUNT = "config-encryption-key";
21240
- var DPAPI_KEY_FILE = import_path12.default.join(
21623
+ var DPAPI_KEY_FILE = import_path14.default.join(
21241
21624
  import_os4.default.homedir(),
21242
21625
  ".factiii-runner",
21243
21626
  "config-key.dpapi"
@@ -21343,7 +21726,7 @@ function winWrite(key) {
21343
21726
  "-Command",
21344
21727
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
21345
21728
  ]);
21346
- import_fs5.default.mkdirSync(import_path12.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21729
+ import_fs5.default.mkdirSync(import_path14.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21347
21730
  import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21348
21731
  `, { mode: 384 });
21349
21732
  }
@@ -21483,7 +21866,7 @@ function resolve() {
21483
21866
  cached = { key: null, status: diagnose() };
21484
21867
  return cached;
21485
21868
  }
21486
- const fresh = import_crypto4.default.randomBytes(32);
21869
+ const fresh = import_crypto5.default.randomBytes(32);
21487
21870
  writeKey(fresh);
21488
21871
  const verify = readKey();
21489
21872
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -21505,23 +21888,23 @@ function keychainStatus() {
21505
21888
  }
21506
21889
 
21507
21890
  // src/secureStore.ts
21508
- var import_crypto5 = __toESM(require("crypto"));
21891
+ var import_crypto6 = __toESM(require("crypto"));
21509
21892
  var import_fs6 = __toESM(require("fs"));
21510
- var import_path14 = __toESM(require("path"));
21893
+ var import_path16 = __toESM(require("path"));
21511
21894
 
21512
21895
  // src/paths.ts
21513
21896
  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");
21897
+ var import_path15 = __toESM(require("path"));
21898
+ var CONFIG_DIR = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
21516
21899
  function safeSlug(spaceSlug) {
21517
21900
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
21518
21901
  }
21519
21902
  function spaceDirPath(spaceSlug) {
21520
- return import_path13.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21903
+ return import_path15.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21521
21904
  }
21522
21905
 
21523
21906
  // src/secureStore.ts
21524
- var VAULT_KEY_FILE = import_path14.default.join(CONFIG_DIR, "vault-key.json");
21907
+ var VAULT_KEY_FILE = import_path16.default.join(CONFIG_DIR, "vault-key.json");
21525
21908
  var SCRYPT_N2 = 1 << 15;
21526
21909
  var MIN_PASSWORD_LENGTH = 8;
21527
21910
  var SecureStoreError = class extends Error {
@@ -21548,7 +21931,7 @@ function requireKeychainKey() {
21548
21931
  );
21549
21932
  }
21550
21933
  function passwordKey(password, salt) {
21551
- return import_crypto5.default.scryptSync(password, salt, 32, {
21934
+ return import_crypto6.default.scryptSync(password, salt, 32, {
21552
21935
  N: SCRYPT_N2,
21553
21936
  maxmem: 64 * 1024 * 1024
21554
21937
  });
@@ -21556,7 +21939,7 @@ function passwordKey(password, salt) {
21556
21939
  function wrapKey(machineKey, pwKey) {
21557
21940
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
21558
21941
  return Buffer.from(
21559
- import_crypto5.default.hkdfSync(
21942
+ import_crypto6.default.hkdfSync(
21560
21943
  "sha256",
21561
21944
  ikm,
21562
21945
  Buffer.alloc(0),
@@ -21566,8 +21949,8 @@ function wrapKey(machineKey, pwKey) {
21566
21949
  );
21567
21950
  }
21568
21951
  function seal(plaintext, key) {
21569
- const iv = import_crypto5.default.randomBytes(12);
21570
- const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
21952
+ const iv = import_crypto6.default.randomBytes(12);
21953
+ const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
21571
21954
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
21572
21955
  return {
21573
21956
  iv: iv.toString("base64"),
@@ -21577,7 +21960,7 @@ function seal(plaintext, key) {
21577
21960
  }
21578
21961
  function unseal(box, key) {
21579
21962
  try {
21580
- const decipher = import_crypto5.default.createDecipheriv(
21963
+ const decipher = import_crypto6.default.createDecipheriv(
21581
21964
  "aes-256-gcm",
21582
21965
  key,
21583
21966
  Buffer.from(box.iv, "base64")
@@ -21610,7 +21993,7 @@ function writeVaultFile(file) {
21610
21993
  }
21611
21994
  var unlockedDek = null;
21612
21995
  function createVault(machineKey) {
21613
- const dek = import_crypto5.default.randomBytes(32);
21996
+ const dek = import_crypto6.default.randomBytes(32);
21614
21997
  writeVaultFile({
21615
21998
  v: 1,
21616
21999
  passwordEnabled: false,
@@ -21705,7 +22088,7 @@ function enablePassword(password) {
21705
22088
  );
21706
22089
  }
21707
22090
  const dek = dataKey();
21708
- const salt = import_crypto5.default.randomBytes(16);
22091
+ const salt = import_crypto6.default.randomBytes(16);
21709
22092
  writeVaultFile({
21710
22093
  v: 1,
21711
22094
  passwordEnabled: true,
@@ -21733,7 +22116,7 @@ function changePassword(current, next) {
21733
22116
  "The current password is not correct."
21734
22117
  );
21735
22118
  }
21736
- const salt = import_crypto5.default.randomBytes(16);
22119
+ const salt = import_crypto6.default.randomBytes(16);
21737
22120
  writeVaultFile({
21738
22121
  v: 1,
21739
22122
  passwordEnabled: true,
@@ -21810,8 +22193,8 @@ function storeStatus() {
21810
22193
  // src/secureFile.ts
21811
22194
  var MAGIC = "FACTIII1";
21812
22195
  function encrypt(plain, key, version) {
21813
- const iv = import_crypto6.default.randomBytes(12);
21814
- const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
22196
+ const iv = import_crypto7.default.randomBytes(12);
22197
+ const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
21815
22198
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
21816
22199
  const envelope = {
21817
22200
  v: version,
@@ -21822,7 +22205,7 @@ function encrypt(plain, key, version) {
21822
22205
  return `${MAGIC}${JSON.stringify(envelope)}`;
21823
22206
  }
21824
22207
  function decrypt(envelope, key) {
21825
- const decipher = import_crypto6.default.createDecipheriv(
22208
+ const decipher = import_crypto7.default.createDecipheriv(
21826
22209
  "aes-256-gcm",
21827
22210
  key,
21828
22211
  Buffer.from(envelope.iv, "base64")
@@ -21843,7 +22226,7 @@ function parseEnvelope(raw) {
21843
22226
  }
21844
22227
  }
21845
22228
  function atomicWrite(filePath, body) {
21846
- import_fs7.default.mkdirSync(import_path15.default.dirname(filePath), { recursive: true });
22229
+ import_fs7.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
21847
22230
  const tmp = `${filePath}.${process.pid}.tmp`;
21848
22231
  import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
21849
22232
  import_fs7.default.renameSync(tmp, filePath);
@@ -21958,8 +22341,8 @@ function writeBootJson(filePath, value2) {
21958
22341
  }
21959
22342
 
21960
22343
  // src/config.ts
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");
22344
+ var CONFIG_DIR2 = import_path18.default.join(import_os6.default.homedir(), ".factiii-runner");
22345
+ var CONFIG_FILE = import_path18.default.join(CONFIG_DIR2, "config.json");
21963
22346
  function readRunnerConfig() {
21964
22347
  return readBootJson(CONFIG_FILE);
21965
22348
  }
@@ -22128,10 +22511,10 @@ function makeChunkReassembler() {
22128
22511
  }
22129
22512
 
22130
22513
  // src/daemon.ts
22131
- var import_crypto7 = require("crypto");
22514
+ var import_crypto8 = require("crypto");
22132
22515
  var import_fs10 = __toESM(require("fs"));
22133
22516
  var import_node_datachannel = require("node-datachannel");
22134
- var import_path18 = __toESM(require("path"));
22517
+ var import_path20 = __toESM(require("path"));
22135
22518
 
22136
22519
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
22137
22520
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23311,12 +23694,12 @@ function parse2(str) {
23311
23694
  uri.queryKey = queryKey(uri, uri["query"]);
23312
23695
  return uri;
23313
23696
  }
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) {
23697
+ function pathNames(obj, path20) {
23698
+ const regx = /\/{2,9}/g, names = path20.replace(regx, "/").split("/");
23699
+ if (path20.slice(0, 1) == "/" || path20.length === 0) {
23317
23700
  names.splice(0, 1);
23318
23701
  }
23319
- if (path18.slice(-1) == "/") {
23702
+ if (path20.slice(-1) == "/") {
23320
23703
  names.splice(names.length - 1, 1);
23321
23704
  }
23322
23705
  return names;
@@ -23935,7 +24318,7 @@ var protocol2 = Socket.protocol;
23935
24318
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
23936
24319
  var import_debug7 = __toESM(require_src(), 1);
23937
24320
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
23938
- function url(uri, path18 = "", loc) {
24321
+ function url(uri, path20 = "", loc) {
23939
24322
  let obj = uri;
23940
24323
  loc = loc || typeof location !== "undefined" && location;
23941
24324
  if (null == uri)
@@ -23969,7 +24352,7 @@ function url(uri, path18 = "", loc) {
23969
24352
  obj.path = obj.path || "/";
23970
24353
  const ipv6 = obj.host.indexOf(":") !== -1;
23971
24354
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
23972
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path18;
24355
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path20;
23973
24356
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
23974
24357
  return obj;
23975
24358
  }
@@ -25603,8 +25986,8 @@ function lookup(uri, opts) {
25603
25986
  const parsed = url(uri, opts.path || "/socket.io");
25604
25987
  const source = parsed.source;
25605
25988
  const id = parsed.id;
25606
- const path18 = parsed.path;
25607
- const sameNamespace = cache[id] && path18 in cache[id]["nsps"];
25989
+ const path20 = parsed.path;
25990
+ const sameNamespace = cache[id] && path20 in cache[id]["nsps"];
25608
25991
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
25609
25992
  let io;
25610
25993
  if (newConnection) {
@@ -25646,8 +26029,8 @@ function cardContextPath(target, postId) {
25646
26029
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
25647
26030
  return `${target.paths.root}/.cards/${safe}.md`;
25648
26031
  }
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.`;
26032
+ function cardContextPrompt(path20) {
26033
+ return `Read ${path20} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
25651
26034
  }
25652
26035
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
25653
26036
  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.";
@@ -25812,10 +26195,10 @@ var TerminalManager = class {
25812
26195
 
25813
26196
  // src/agent-adapter.ts
25814
26197
  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");
26198
+ var import_path19 = __toESM(require("path"));
26199
+ var RUNNER_CONFIG_PATH = import_path19.default.join(CONFIG_DIR, "runner-config.json");
25817
26200
  function connectionPath(spaceSlug) {
25818
- return import_path17.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26201
+ return import_path19.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
25819
26202
  }
25820
26203
  function writeJson(filePath, value2) {
25821
26204
  writeSecureJson(filePath, value2);
@@ -25849,7 +26232,7 @@ function migrateLegacyConfigs() {
25849
26232
  for (const name of legacy) {
25850
26233
  const slug = name.slice("space-".length, -".json".length);
25851
26234
  const parsed = readSecureJson(
25852
- import_path17.default.join(CONFIG_DIR, name)
26235
+ import_path19.default.join(CONFIG_DIR, name)
25853
26236
  );
25854
26237
  if (!parsed) continue;
25855
26238
  if (!file.defaultSlug) {
@@ -25874,7 +26257,7 @@ function encryptStrayPlaintextConfigs() {
25874
26257
  return;
25875
26258
  }
25876
26259
  for (const name of names) {
25877
- const filePath = import_path17.default.join(CONFIG_DIR, name);
26260
+ const filePath = import_path19.default.join(CONFIG_DIR, name);
25878
26261
  try {
25879
26262
  const raw = import_fs9.default.readFileSync(filePath, "utf-8");
25880
26263
  if (!raw.startsWith("{")) continue;
@@ -26003,7 +26386,7 @@ var localConfigProvider = {
26003
26386
  "claude",
26004
26387
  "codex"
26005
26388
  ]) {
26006
- import_fs9.default.mkdirSync(import_path17.default.join(dir, sub), { recursive: true });
26389
+ import_fs9.default.mkdirSync(import_path19.default.join(dir, sub), { recursive: true });
26007
26390
  }
26008
26391
  return dir;
26009
26392
  }
@@ -26177,15 +26560,15 @@ async function startDaemon(config) {
26177
26560
  }
26178
26561
  );
26179
26562
  const indexCandidates = [
26180
- import_path18.default.join(__dirname, "..", "index"),
26181
- import_path18.default.join(__dirname, "..", "..", "index", "image")
26563
+ import_path20.default.join(__dirname, "..", "index"),
26564
+ import_path20.default.join(__dirname, "..", "..", "index", "image")
26182
26565
  ];
26183
26566
  const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
26184
26567
  const odIndex = new IndexService({
26185
- controlToken: (0, import_crypto7.createHash)("sha256").update(config.authToken).digest("hex"),
26568
+ controlToken: (0, import_crypto8.createHash)("sha256").update(config.authToken).digest("hex"),
26186
26569
  embedUrl: () => config.serverUrl,
26187
26570
  indexDir,
26188
- dataDir: import_path18.default.join(CONFIG_DIR, "index-data")
26571
+ dataDir: import_path20.default.join(CONFIG_DIR, "index-data")
26189
26572
  });
26190
26573
  void odIndex.ensureRunning().catch((err) => {
26191
26574
  console.error(