@factiii/runner 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -963,7 +963,7 @@ var require_command = __commonJS({
963
963
  "node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path20 = require("node:path");
966
+ var path21 = require("node:path");
967
967
  var fs15 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -1896,9 +1896,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path20.resolve(baseDir, baseName);
1899
+ const localBin = path21.resolve(baseDir, baseName);
1900
1900
  if (fs15.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path20.extname(baseName))) return void 0;
1901
+ if (sourceExt.includes(path21.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
1903
  (ext) => fs15.existsSync(`${localBin}${ext}`)
1904
1904
  );
@@ -1916,17 +1916,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path20.resolve(
1920
- path20.dirname(resolvedScriptPath),
1919
+ executableDir = path21.resolve(
1920
+ path21.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 = path20.basename(
1927
+ const legacyName = path21.basename(
1928
1928
  this._scriptPath,
1929
- path20.extname(this._scriptPath)
1929
+ path21.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(path20.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path21.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 = path20.basename(filename, path20.extname(filename));
2780
+ this._name = path21.basename(filename, path21.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(path21) {
2795
- if (path21 === void 0) return this._executableDir;
2796
- this._executableDir = path21;
2794
+ executableDir(path22) {
2795
+ if (path22 === void 0) return this._executableDir;
2796
+ this._executableDir = path22;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -8428,8 +8428,8 @@ function getErrorMap() {
8428
8428
 
8429
8429
  // ../../node_modules/zod/v3/helpers/parseUtil.js
8430
8430
  var makeIssue = (params) => {
8431
- const { data, path: path20, errorMaps, issueData } = params;
8432
- const fullPath = [...path20, ...issueData.path || []];
8431
+ const { data, path: path21, errorMaps, issueData } = params;
8432
+ const fullPath = [...path21, ...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, path20, key) {
8548
+ constructor(parent, value2, path21, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path20;
8552
+ this._path = path21;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12659,7 +12659,7 @@ var prompts_default = {
12659
12659
  "- Write ONLY those three files under .agents/skills/deploy/.",
12660
12660
  "- Do NOT git commit or git push; the harness pushes for review.",
12661
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.",
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
+ '- Secret-bearing commands: any command needing a variable declared in required-variables.json MUST be written in the skill as `factiii-secrets run --key NAME [--key NAME]... -- <command>`, naming exactly the keys THAT command reads (a wrapper script\'s keys are what the script reads, not what its command line shows), with the variables referenced unexpanded and quoted (e.g. "$DATABASE_URL"). Only the named keys are injected: a step asks for its own keys, never for every key the skill declares. 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.',
12663
12663
  "- No secret values in any file, ever."
12664
12664
  ],
12665
12665
  deployCreateRevise: [
@@ -12667,7 +12667,7 @@ var prompts_default = {
12667
12667
  "",
12668
12668
  "{{FEEDBACK}}",
12669
12669
  "",
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
+ "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 --key NAME ... -- <command>` naming exactly the keys that command reads, 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."
12671
12671
  ],
12672
12672
  deployRun: [
12673
12673
  "## Deploy run protocol",
@@ -12675,9 +12675,10 @@ var prompts_default = {
12675
12675
  "",
12676
12676
  "## Secrets (read this before anything else)",
12677
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.",
12678
+ "- To run a command that needs any declared variable, prefix it and NAME the keys that command reads: `factiii-secrets run --key NAME [--key NAME]... -- <command>`. The owner authorizes it with their password, the runner runs it with ONLY the keys you named injected, and you get the output back with the values redacted. A command that names no key gets no secrets, so always name them. Repeat --key per name (or comma-separate them), always BEFORE the `--`. 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
12679
  '- Quote the variables (`"$DATABASE_URL"`): they expand on the runner\'s side, not in your shell.',
12680
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
+ "- Name what the command actually reads. For a wrapper script, that is what the SCRIPT reads, not what appears on its command line. If you are unsure of a name, run `factiii-secrets list` - it prints the key names and whether each is set, never a value. A name that is not in the store expands empty, and the runner tells you which one it was.",
12681
12682
  "- Wrap ONLY the commands that actually need a secret. Everything else runs unwrapped and needs no authorization.",
12682
12683
  "",
12683
12684
  "- 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.",
@@ -12909,7 +12910,7 @@ ${diff.slice(0, 8e3)}`,
12909
12910
  }
12910
12911
 
12911
12912
  // ../../shared/all/helpers/board-agent-core/engine.ts
12912
- var import_path12 = require("path");
12913
+ var import_path13 = require("path");
12913
12914
 
12914
12915
  // ../../shared/all/domains/electron.ts
12915
12916
  var SETUP_LOG_PREFIX = "log ";
@@ -13018,7 +13019,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13018
13019
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13019
13020
  }
13020
13021
  async function addWorktree(target, workName, baseBranch, kind) {
13021
- const path20 = worktreeFor(target.paths, workName);
13022
+ const path21 = worktreeFor(target.paths, workName);
13022
13023
  const branch = workBranch(workName, kind);
13023
13024
  await gitInDir(
13024
13025
  target,
@@ -13028,7 +13029,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13028
13029
  baseBranch
13029
13030
  ).catch(() => {
13030
13031
  });
13031
- await removeWorktree(target, path20).catch(() => {
13032
+ await removeWorktree(target, path21).catch(() => {
13032
13033
  });
13033
13034
  await gitInDir(
13034
13035
  target,
@@ -13037,30 +13038,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13037
13038
  "add",
13038
13039
  "-B",
13039
13040
  branch,
13040
- path20,
13041
+ path21,
13041
13042
  `origin/${baseBranch}`
13042
13043
  );
13043
- return { path: path20, branch };
13044
+ return { path: path21, branch };
13044
13045
  }
13045
- async function removeWorktree(target, path20) {
13046
+ async function removeWorktree(target, path21) {
13046
13047
  await gitInDir(
13047
13048
  target,
13048
13049
  target.paths.repo,
13049
13050
  "worktree",
13050
13051
  "remove",
13051
13052
  "--force",
13052
- path20
13053
+ path21
13053
13054
  );
13054
13055
  }
13055
- async function removeWorktreeAndBranch(target, path20) {
13056
+ async function removeWorktreeAndBranch(target, path21) {
13056
13057
  const branch = await gitInDir(
13057
13058
  target,
13058
- path20,
13059
+ path21,
13059
13060
  "rev-parse",
13060
13061
  "--abbrev-ref",
13061
13062
  "HEAD"
13062
13063
  ).then((out) => out.trim()).catch(() => "");
13063
- await removeWorktree(target, path20);
13064
+ await removeWorktree(target, path21);
13064
13065
  if (branch && branch !== "HEAD") {
13065
13066
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13066
13067
  () => {
@@ -13074,8 +13075,8 @@ async function pruneWorktrees(target) {
13074
13075
  }
13075
13076
  );
13076
13077
  }
13077
- function isWorktreePath(target, path20) {
13078
- return path20.startsWith(`${target.paths.worktrees}/`);
13078
+ function isWorktreePath(target, path21) {
13079
+ return path21.startsWith(`${target.paths.worktrees}/`);
13079
13080
  }
13080
13081
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13081
13082
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13101,13 +13102,13 @@ function parseStatus(raw) {
13101
13102
  const y = line[1];
13102
13103
  const rest = line.slice(3);
13103
13104
  const arrow = rest.indexOf(" -> ");
13104
- const path20 = arrow === -1 ? rest : rest.slice(arrow + 4);
13105
+ const path21 = arrow === -1 ? rest : rest.slice(arrow + 4);
13105
13106
  if (x === "?" || y === "?") {
13106
- unstaged.push({ path: path20, code: "A" });
13107
+ unstaged.push({ path: path21, code: "A" });
13107
13108
  continue;
13108
13109
  }
13109
- if (x !== " ") staged.push({ path: path20, code: x });
13110
- if (y !== " ") unstaged.push({ path: path20, code: y });
13110
+ if (x !== " ") staged.push({ path: path21, code: x });
13111
+ if (y !== " ") unstaged.push({ path: path21, code: y });
13111
13112
  }
13112
13113
  return { staged, unstaged };
13113
13114
  }
@@ -13465,10 +13466,12 @@ function execLogin(command, opts) {
13465
13466
  let output = "";
13466
13467
  let settled = false;
13467
13468
  let timer;
13469
+ let idleTimer;
13468
13470
  const done = (code, extra = "") => {
13469
13471
  if (settled) return;
13470
13472
  settled = true;
13471
13473
  if (timer) clearTimeout(timer);
13474
+ if (idleTimer) clearTimeout(idleTimer);
13472
13475
  resolve2({ code, output: (output + extra).slice(-cap) });
13473
13476
  };
13474
13477
  let proc;
@@ -13490,9 +13493,22 @@ function execLogin(command, opts) {
13490
13493
  done(-1, `
13491
13494
  [timed out after ${Math.round(opts.timeout / 1e3)}s]`);
13492
13495
  }, opts.timeout);
13496
+ const idleMs = opts.idleTimeout;
13497
+ const bumpIdle = () => {
13498
+ if (!idleMs) return;
13499
+ if (idleTimer) clearTimeout(idleTimer);
13500
+ idleTimer = setTimeout(() => {
13501
+ killTree(proc);
13502
+ const secs = Math.round(idleMs / 1e3);
13503
+ done(-1, `
13504
+ [no output for ${secs}s - timed out]`);
13505
+ }, idleMs);
13506
+ };
13507
+ bumpIdle();
13493
13508
  const collect = (chunk) => {
13494
13509
  output += chunk.toString("utf-8");
13495
13510
  if (output.length > cap * 2) output = output.slice(-cap);
13511
+ bumpIdle();
13496
13512
  opts.onOutput?.(output);
13497
13513
  };
13498
13514
  proc.stdout.on("data", collect);
@@ -13838,6 +13854,9 @@ function createEventSink(send, spaceSlug, notify) {
13838
13854
  emitDeployEnv(payload) {
13839
13855
  send(channel("deploy-env"), payload);
13840
13856
  },
13857
+ emitSecretsAuth(payload) {
13858
+ send(channel("secrets-auth"), payload);
13859
+ },
13841
13860
  emitBareFsChange(postId, change) {
13842
13861
  send(channel(`bare-fs:${postId}`), change);
13843
13862
  },
@@ -14373,6 +14392,7 @@ var import_util5 = require("util");
14373
14392
  var import_promises2 = __toESM(require("fs/promises"));
14374
14393
  var import_net2 = __toESM(require("net"));
14375
14394
  var import_path4 = __toESM(require("path"));
14395
+ var SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14376
14396
  function secretsSocketPath(spaceDir) {
14377
14397
  return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14378
14398
  }
@@ -14400,9 +14420,23 @@ async function handleRun(request, handlers, reply) {
14400
14420
  return;
14401
14421
  }
14402
14422
  const cwd = typeof request.cwd === "string" ? request.cwd : "";
14423
+ const requested = Array.isArray(request.keys) ? request.keys : [];
14424
+ const keys = [];
14425
+ for (const entry of requested) {
14426
+ if (typeof entry !== "string" || !SECRET_KEY_PATTERN.test(entry)) {
14427
+ reply({
14428
+ type: "error",
14429
+ message: `"${String(entry)}" is not a key name.`
14430
+ });
14431
+ return;
14432
+ }
14433
+ if (!keys.includes(entry)) keys.push(entry);
14434
+ }
14403
14435
  const result = await handlers.run({
14404
14436
  command,
14405
14437
  cwd,
14438
+ keys,
14439
+ token: typeof request.token === "string" ? request.token : "",
14406
14440
  status: (text) => reply({ type: "status", text })
14407
14441
  });
14408
14442
  if ("denied" in result) {
@@ -14443,7 +14477,9 @@ async function startSecretsBridge(opts) {
14443
14477
  return;
14444
14478
  }
14445
14479
  if (request.op === "list") {
14446
- void handlers.list().then((keys) => reply({ type: "keys", keys })).catch(
14480
+ void handlers.list().then(
14481
+ (entries) => reply({ type: "keys", keys: entries.map((e) => e.key), entries })
14482
+ ).catch(
14447
14483
  (err) => reply({ type: "error", message: errorText(err) })
14448
14484
  ).finally(() => conn.end());
14449
14485
  return;
@@ -14639,6 +14675,60 @@ async function checkDeployStatus(opts) {
14639
14675
  variables
14640
14676
  };
14641
14677
  }
14678
+ var RELEASE_BRANCH_LIMIT = 25;
14679
+ var BRANCH_FIELDS = "%(refname:short)%09%(committerdate:unix)";
14680
+ async function listReleaseBranches(opts) {
14681
+ const { workspace, branch } = await ensureWorkspaceClone(opts);
14682
+ await hostGit(workspace, "fetch", "origin", "--prune");
14683
+ let counted = true;
14684
+ let raw = "";
14685
+ try {
14686
+ raw = await hostGit(
14687
+ workspace,
14688
+ "for-each-ref",
14689
+ "--sort=-committerdate",
14690
+ `--format=${BRANCH_FIELDS}%09%(ahead-behind:refs/remotes/origin/${branch})%09%(contents:subject)`,
14691
+ "refs/remotes/origin"
14692
+ );
14693
+ } catch {
14694
+ counted = false;
14695
+ raw = await hostGit(
14696
+ workspace,
14697
+ "for-each-ref",
14698
+ "--sort=-committerdate",
14699
+ `--format=${BRANCH_FIELDS}%09%(contents:subject)`,
14700
+ "refs/remotes/origin"
14701
+ );
14702
+ }
14703
+ const branches = [];
14704
+ for (const row of raw.split("\n")) {
14705
+ if (branches.length >= RELEASE_BRANCH_LIMIT) break;
14706
+ const [ref = "", unix = "", ...rest] = row.split(" ");
14707
+ if (!ref.startsWith("origin/")) continue;
14708
+ const name = ref.slice("origin/".length);
14709
+ if (name === "HEAD" || name === branch) continue;
14710
+ let ahead = counted ? Number(rest.shift()?.split(" ")[0]) || 0 : 0;
14711
+ if (!counted) {
14712
+ try {
14713
+ const count = await hostGit(
14714
+ workspace,
14715
+ "rev-list",
14716
+ "--count",
14717
+ `origin/${branch}..${ref}`
14718
+ );
14719
+ ahead = Number(count.trim()) || 0;
14720
+ } catch {
14721
+ }
14722
+ }
14723
+ branches.push({
14724
+ name,
14725
+ ahead,
14726
+ updated: Number(unix) || 0,
14727
+ subject: rest.join(" ").trim()
14728
+ });
14729
+ }
14730
+ return branches;
14731
+ }
14642
14732
  var requiredVariableSchema = external_exports.object({
14643
14733
  key: external_exports.string().min(1),
14644
14734
  title: external_exports.string().catch(""),
@@ -15025,9 +15115,9 @@ function summarize(raw, base) {
15025
15115
  }
15026
15116
  async function rootLinkState(root) {
15027
15117
  const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15028
- const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15029
- if (!stat2) return "missing";
15030
- if (!stat2.isSymbolicLink()) return "blocked";
15118
+ const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15119
+ if (!stat) return "missing";
15120
+ if (!stat.isSymbolicLink()) return "blocked";
15031
15121
  const target = await import_promises4.default.readlink(linkPath).catch(() => "");
15032
15122
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
15033
15123
  }
@@ -15366,8 +15456,8 @@ async function migrateGlobal() {
15366
15456
  const moved = [];
15367
15457
  for (const root of CLI_ROOTS) {
15368
15458
  const dir = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15369
- const stat2 = await import_promises4.default.lstat(dir).catch(() => null);
15370
- if (!stat2 || stat2.isSymbolicLink()) continue;
15459
+ const stat = await import_promises4.default.lstat(dir).catch(() => null);
15460
+ if (!stat || stat.isSymbolicLink()) continue;
15371
15461
  const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15372
15462
  for (const entry of entries) {
15373
15463
  if (entry.isFile() || entry.isSymbolicLink()) continue;
@@ -15487,9 +15577,9 @@ async function installBundledSkill(opts) {
15487
15577
  await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15488
15578
  for (const root of CLI_ROOTS) {
15489
15579
  const linkPath = import_path6.default.join(home, ...root.split("/"));
15490
- const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15491
- if (stat2?.isSymbolicLink()) continue;
15492
- if (!stat2) {
15580
+ const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15581
+ if (stat?.isSymbolicLink()) continue;
15582
+ if (!stat) {
15493
15583
  await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15494
15584
  await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15495
15585
  continue;
@@ -16047,18 +16137,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16047
16137
  await this.core.ensureSpaceWorkspace();
16048
16138
  await assertAgentSignedIn(session.provider, this.core.target());
16049
16139
  if (hasGit) {
16050
- const { path: path20 } = await addWorktree(
16140
+ const { path: path21 } = await addWorktree(
16051
16141
  this.core.target(),
16052
16142
  session.taskId,
16053
16143
  baseBranch || config.mainBranch || "main",
16054
16144
  "card"
16055
16145
  );
16056
- session.workspacePath = path20;
16146
+ session.workspacePath = path21;
16057
16147
  await setGitIdentity(
16058
16148
  this.core.target(),
16059
16149
  config.gitName,
16060
16150
  config.gitEmail,
16061
- path20
16151
+ path21
16062
16152
  );
16063
16153
  this.emitter.addLog(session, "system", "Worktree ready.");
16064
16154
  } else {
@@ -16280,18 +16370,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16280
16370
  this.emitter.addLog(session, "system", "Preparing environment...");
16281
16371
  await this.core.ensureSpaceWorkspace();
16282
16372
  await assertAgentSignedIn(session.provider, this.core.target());
16283
- const { path: path20 } = await addWorktree(
16373
+ const { path: path21 } = await addWorktree(
16284
16374
  this.core.target(),
16285
16375
  session.taskId,
16286
16376
  config.mainBranch || "main",
16287
16377
  "card"
16288
16378
  );
16289
- session.workspacePath = path20;
16379
+ session.workspacePath = path21;
16290
16380
  await setGitIdentity(
16291
16381
  this.core.target(),
16292
16382
  config.gitName,
16293
16383
  config.gitEmail,
16294
- path20
16384
+ path21
16295
16385
  );
16296
16386
  await this.putCard(session);
16297
16387
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16838,12 +16928,12 @@ var FLUSH_MS = 120;
16838
16928
  var SELF_WRITE_MS = 2e3;
16839
16929
  var dynamicImport = new Function("s", "return import(s)");
16840
16930
  var importFs = new Function("s", "return import(s)");
16841
- async function resolveReal(path20) {
16931
+ async function resolveReal(path21) {
16842
16932
  try {
16843
16933
  const { realpath } = await importFs("fs/promises");
16844
- return await realpath(path20);
16934
+ return await realpath(path21);
16845
16935
  } catch {
16846
- return path20;
16936
+ return path21;
16847
16937
  }
16848
16938
  }
16849
16939
  var loadSubscribe = null;
@@ -16957,19 +17047,19 @@ var WorkspaceWatcher = class {
16957
17047
  }
16958
17048
  record(kind, relPath) {
16959
17049
  if (this.closed) return;
16960
- const path20 = "/" + relPath;
16961
- const expiry = this.selfWrites.get(path20);
17050
+ const path21 = "/" + relPath;
17051
+ const expiry = this.selfWrites.get(path21);
16962
17052
  if (expiry !== void 0) {
16963
17053
  if (expiry > Date.now()) return;
16964
- this.selfWrites.delete(path20);
17054
+ this.selfWrites.delete(path21);
16965
17055
  }
16966
17056
  const { added, changed, removed } = this.pending;
16967
- added.delete(path20);
16968
- changed.delete(path20);
16969
- removed.delete(path20);
16970
- if (kind === "added") added.add(path20);
16971
- else if (kind === "changed") changed.add(path20);
16972
- else removed.add(path20);
17057
+ added.delete(path21);
17058
+ changed.delete(path21);
17059
+ removed.delete(path21);
17060
+ if (kind === "added") added.add(path21);
17061
+ else if (kind === "changed") changed.add(path21);
17062
+ else removed.add(path21);
16973
17063
  if (added.size + changed.size + removed.size > MAX_BATCH) {
16974
17064
  this.pending = { ...emptyPending(), resync: true };
16975
17065
  }
@@ -16988,8 +17078,8 @@ var WorkspaceWatcher = class {
16988
17078
  this.pending = emptyPending();
16989
17079
  if (!resync && !added.size && !changed.size && !removed.size) return;
16990
17080
  const now = Date.now();
16991
- for (const [path20, expiry] of this.selfWrites) {
16992
- if (expiry <= now) this.selfWrites.delete(path20);
17081
+ for (const [path21, expiry] of this.selfWrites) {
17082
+ if (expiry <= now) this.selfWrites.delete(path21);
16993
17083
  }
16994
17084
  this.emit({
16995
17085
  added: resync ? [] : [...added],
@@ -17193,8 +17283,8 @@ var TerminalModeEngine = class {
17193
17283
  /** Suppress the change event our own write is about to produce, so a save
17194
17284
  * never comes back to the editor as an outside edit. Normalized to the form
17195
17285
  * the watcher emits, since callers spell the path either way. */
17196
- expectWrite(postId, path20) {
17197
- this.watchers.get(postId)?.expectWrite(posixNormalize(path20));
17286
+ expectWrite(postId, path21) {
17287
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path21));
17198
17288
  }
17199
17289
  // ── Lifecycle / ModeEngine ──
17200
17290
  start(base, args) {
@@ -17391,18 +17481,18 @@ var TerminalModeEngine = class {
17391
17481
  this.reservedNames.values()
17392
17482
  );
17393
17483
  this.reservedNames.set(session.postId, workName);
17394
- const { path: path20 } = await addWorktree(
17484
+ const { path: path21 } = await addWorktree(
17395
17485
  this.core.target(),
17396
17486
  workName,
17397
17487
  branch,
17398
17488
  "terminal"
17399
17489
  );
17400
- session.workspacePath = path20;
17490
+ session.workspacePath = path21;
17401
17491
  await setGitIdentity(
17402
17492
  this.core.target(),
17403
17493
  config.gitName,
17404
17494
  config.gitEmail,
17405
- path20
17495
+ path21
17406
17496
  );
17407
17497
  this.emitter.addLog(session, "system", "Worktree ready.");
17408
17498
  } else {
@@ -17464,11 +17554,11 @@ var TerminalModeEngine = class {
17464
17554
  }
17465
17555
  async bareFsList({
17466
17556
  postId,
17467
- path: path20
17557
+ path: path21
17468
17558
  }) {
17469
17559
  const target = this.getBareTarget(postId);
17470
17560
  const root = this.getBareRoot(postId);
17471
- const abs = this.resolveBarePath(path20, root);
17561
+ const abs = this.resolveBarePath(path21, root);
17472
17562
  const relRoot = abs.slice(root.length) || "/";
17473
17563
  const out = await target.run([
17474
17564
  "sh",
@@ -17518,30 +17608,30 @@ var TerminalModeEngine = class {
17518
17608
  }
17519
17609
  async bareFsRead({
17520
17610
  postId,
17521
- path: path20
17611
+ path: path21
17522
17612
  }) {
17523
17613
  const target = this.getBareTarget(postId);
17524
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17614
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17525
17615
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17526
17616
  '`)).trim();
17527
17617
  return { content: b64, encoding: "base64" };
17528
17618
  }
17529
17619
  async bareFsWrite({
17530
17620
  postId,
17531
- path: path20,
17621
+ path: path21,
17532
17622
  content,
17533
17623
  encoding
17534
17624
  }) {
17535
17625
  const target = this.getBareTarget(postId);
17536
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17626
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17537
17627
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17538
17628
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17539
17629
  throw new Error("Invalid base64 payload.");
17540
17630
  }
17541
- this.expectWrite(String(postId), path20);
17631
+ this.expectWrite(String(postId), path21);
17542
17632
  await writeBase64File(target, abs, b64);
17543
17633
  const session = this.sessions.get(String(postId));
17544
- if (session) delete session.buffers[path20];
17634
+ if (session) delete session.buffers[path21];
17545
17635
  }
17546
17636
  /** Append one chunk of a dropped image to the per-space drops dir
17547
17637
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17591,39 +17681,39 @@ var TerminalModeEngine = class {
17591
17681
  }
17592
17682
  bareOpenFile({
17593
17683
  postId: rawPostId,
17594
- path: path20
17684
+ path: path21
17595
17685
  }) {
17596
17686
  const session = this.sessions.get(String(rawPostId));
17597
17687
  if (!session || session.mode !== "bare") return;
17598
- if (!session.openedFiles.includes(path20)) {
17599
- session.openedFiles.push(path20);
17688
+ if (!session.openedFiles.includes(path21)) {
17689
+ session.openedFiles.push(path21);
17600
17690
  }
17601
17691
  }
17602
17692
  bareCloseFile({
17603
17693
  postId: rawPostId,
17604
- path: path20
17694
+ path: path21
17605
17695
  }) {
17606
17696
  const session = this.sessions.get(String(rawPostId));
17607
17697
  if (!session || session.mode !== "bare") return;
17608
- session.openedFiles = session.openedFiles.filter((p) => p !== path20);
17609
- delete session.buffers[path20];
17698
+ session.openedFiles = session.openedFiles.filter((p) => p !== path21);
17699
+ delete session.buffers[path21];
17610
17700
  }
17611
17701
  bareBufferSet({
17612
17702
  postId: rawPostId,
17613
- path: path20,
17703
+ path: path21,
17614
17704
  content
17615
17705
  }) {
17616
17706
  const session = this.sessions.get(String(rawPostId));
17617
17707
  if (!session || session.mode !== "bare") return;
17618
- session.buffers[path20] = content;
17708
+ session.buffers[path21] = content;
17619
17709
  }
17620
17710
  async bareFsDelete({
17621
17711
  postId,
17622
- path: path20
17712
+ path: path21
17623
17713
  }) {
17624
17714
  const target = this.getBareTarget(postId);
17625
17715
  const root = this.getBareRoot(postId);
17626
- const abs = this.resolveBarePath(path20, root);
17716
+ const abs = this.resolveBarePath(path21, root);
17627
17717
  if (abs === root) {
17628
17718
  throw new Error("Refusing to delete the repo root.");
17629
17719
  }
@@ -17631,10 +17721,10 @@ var TerminalModeEngine = class {
17631
17721
  }
17632
17722
  async bareFsMkdir({
17633
17723
  postId,
17634
- path: path20
17724
+ path: path21
17635
17725
  }) {
17636
17726
  const target = this.getBareTarget(postId);
17637
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17727
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17638
17728
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17639
17729
  }
17640
17730
  async bareReview({
@@ -18081,13 +18171,13 @@ var ClaudeLoginFlow = class {
18081
18171
  async onClose() {
18082
18172
  if (this.settled) return;
18083
18173
  const errLine = this.output.slice(Math.max(this.submitMark, 0)).match(/(?:Login failed|OAuth error):[^\r\n]+/)?.[0];
18084
- const tail2 = errLine ? `${errLine.trim()} Try signing in again.` : this.output.trim().slice(-300);
18174
+ const tail3 = errLine ? `${errLine.trim()} Try signing in again.` : this.output.trim().slice(-300);
18085
18175
  if (this.urlRejecter) {
18086
- this.fail(tail2 || "Claude sign-in ended unexpectedly.");
18176
+ this.fail(tail3 || "Claude sign-in ended unexpectedly.");
18087
18177
  return;
18088
18178
  }
18089
18179
  this.settled = true;
18090
- await this.verifyAndComplete(tail2);
18180
+ await this.verifyAndComplete(tail3);
18091
18181
  }
18092
18182
  fail(message) {
18093
18183
  if (this.settled) return;
@@ -18217,13 +18307,13 @@ var CodexLoginFlow = class {
18217
18307
  }
18218
18308
  async onClose() {
18219
18309
  if (this.settled) return;
18220
- const tail2 = this.output.trim().slice(-300);
18310
+ const tail3 = this.output.trim().slice(-300);
18221
18311
  if (this.startRejecter) {
18222
- this.fail(tail2 || "Codex sign-in ended unexpectedly.");
18312
+ this.fail(tail3 || "Codex sign-in ended unexpectedly.");
18223
18313
  return;
18224
18314
  }
18225
18315
  this.settled = true;
18226
- await this.verifyAndComplete(tail2);
18316
+ await this.verifyAndComplete(tail3);
18227
18317
  }
18228
18318
  fail(message) {
18229
18319
  if (this.settled) return;
@@ -18316,8 +18406,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
18316
18406
  };
18317
18407
  }
18318
18408
  }
18319
- async function githubApi(token, path20) {
18320
- const res = await fetch(`${API_BASE}${path20}`, {
18409
+ async function githubApi(token, path21) {
18410
+ const res = await fetch(`${API_BASE}${path21}`, {
18321
18411
  headers: {
18322
18412
  Authorization: `Bearer ${token}`,
18323
18413
  Accept: "application/vnd.github+json",
@@ -18326,7 +18416,7 @@ async function githubApi(token, path20) {
18326
18416
  });
18327
18417
  if (!res.ok) {
18328
18418
  throw new Error(
18329
- `GitHub ${path20} failed (${res.status}): ${await res.text()}`
18419
+ `GitHub ${path21} failed (${res.status}): ${await res.text()}`
18330
18420
  );
18331
18421
  }
18332
18422
  return await res.json();
@@ -18371,11 +18461,103 @@ async function listGithubBranches(token, fullName) {
18371
18461
  }
18372
18462
 
18373
18463
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18464
+ var import_path10 = __toESM(require("path"));
18465
+
18466
+ // ../../shared/all/helpers/board-agent-core/secrets-gate.ts
18467
+ var import_crypto2 = __toESM(require("crypto"));
18468
+ var DEPLOY_ORIGIN = "Deploy run";
18469
+ var UNATTRIBUTED = "This runner did not start the session that asked, so the request was refused. `factiii-secrets` only works inside a session the runner spawned.";
18470
+ var SecretsGate = class {
18471
+ constructor() {
18472
+ /** token -> origin label. Lives as long as the session that holds it. */
18473
+ this.tokens = /* @__PURE__ */ new Map();
18474
+ this.waiting = null;
18475
+ /** Notified on appearance, replacement AND clearing, so a modal that opened
18476
+ * on one request closes when it is answered elsewhere. More than one,
18477
+ * because the space publishes the change and the deploy run separately
18478
+ * projects its own requests onto the release timeline. */
18479
+ this.listeners = /* @__PURE__ */ new Set();
18480
+ }
18481
+ subscribe(listener) {
18482
+ this.listeners.add(listener);
18483
+ return () => this.listeners.delete(listener);
18484
+ }
18485
+ onChange() {
18486
+ for (const listener of this.listeners) listener();
18487
+ }
18488
+ // ── Who is asking ──
18489
+ /** Mint a token for one session, labelled with what the owner should see on
18490
+ * the prompt - the label is most of the decision, so name the session. */
18491
+ issueToken(origin) {
18492
+ const token = import_crypto2.default.randomBytes(24).toString("hex");
18493
+ this.tokens.set(token, origin);
18494
+ return token;
18495
+ }
18496
+ /** Drop a session's token when it ends, so a leaked one stops working. */
18497
+ revokeToken(token) {
18498
+ this.tokens.delete(token);
18499
+ }
18500
+ /** The origin for a token, or null when this runner never issued it. */
18501
+ originFor(token) {
18502
+ return this.tokens.get(token) ?? null;
18503
+ }
18504
+ // ── The pending request ──
18505
+ pending() {
18506
+ return this.waiting?.request ?? null;
18507
+ }
18508
+ /** Open the gate for one command and block until the owner answers. A second
18509
+ * request denies the older one rather than dropping it: that caller is
18510
+ * blocked on the promise and would otherwise hang forever. */
18511
+ request(opts) {
18512
+ this.waiting?.resolve({ denied: true });
18513
+ return new Promise((resolve2) => {
18514
+ this.waiting = {
18515
+ request: {
18516
+ id: import_crypto2.default.randomUUID(),
18517
+ command: opts.command,
18518
+ keys: opts.keys,
18519
+ origin: opts.origin,
18520
+ error: opts.error ?? "",
18521
+ requestedAt: Date.now()
18522
+ },
18523
+ resolve: resolve2
18524
+ };
18525
+ this.onChange();
18526
+ });
18527
+ }
18528
+ /** Answer the pending request. `id` must match what the client was shown: a
18529
+ * wrong-password re-prompt replaces the request, and an answer aimed at the
18530
+ * previous one must not resolve its successor with a stale password. */
18531
+ answer(id, result) {
18532
+ const waiting = this.waiting;
18533
+ if (!waiting || waiting.request.id !== id) return { success: false };
18534
+ this.waiting = null;
18535
+ waiting.resolve(result);
18536
+ this.onChange();
18537
+ return { success: true };
18538
+ }
18539
+ /** Deny whatever is waiting, without needing to know its id. The run was
18540
+ * cancelled, or the space is going down: nothing may stay blocked. */
18541
+ cancel() {
18542
+ const waiting = this.waiting;
18543
+ if (!waiting) return;
18544
+ this.waiting = null;
18545
+ waiting.resolve({ denied: true });
18546
+ this.onChange();
18547
+ }
18548
+ destroy() {
18549
+ this.cancel();
18550
+ this.tokens.clear();
18551
+ this.listeners.clear();
18552
+ }
18553
+ };
18554
+
18555
+ // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18374
18556
  var import_path9 = __toESM(require("path"));
18375
18557
 
18376
18558
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18377
18559
  var import_child_process4 = require("child_process");
18378
- var import_crypto2 = __toESM(require("crypto"));
18560
+ var import_crypto3 = __toESM(require("crypto"));
18379
18561
  var import_promises6 = __toESM(require("fs/promises"));
18380
18562
  var import_path8 = __toESM(require("path"));
18381
18563
  var import_util6 = require("util");
@@ -18403,7 +18585,7 @@ async function assertBinaries() {
18403
18585
  }
18404
18586
  }
18405
18587
  function deriveKey(password, salt) {
18406
- return import_crypto2.default.scryptSync(password, salt, 32, {
18588
+ return import_crypto3.default.scryptSync(password, salt, 32, {
18407
18589
  N: SCRYPT_N,
18408
18590
  maxmem: 64 * 1024 * 1024
18409
18591
  });
@@ -18432,9 +18614,9 @@ async function initSecrets(configDir, password) {
18432
18614
  if (!identity || !recipient) {
18433
18615
  throw new Error("age-keygen returned no usable keypair.");
18434
18616
  }
18435
- const salt = import_crypto2.default.randomBytes(16);
18436
- const iv = import_crypto2.default.randomBytes(12);
18437
- const cipher = import_crypto2.default.createCipheriv(
18617
+ const salt = import_crypto3.default.randomBytes(16);
18618
+ const iv = import_crypto3.default.randomBytes(12);
18619
+ const cipher = import_crypto3.default.createCipheriv(
18438
18620
  "aes-256-gcm",
18439
18621
  deriveKey(password, salt),
18440
18622
  iv
@@ -18459,7 +18641,7 @@ async function initSecrets(configDir, password) {
18459
18641
  return { success: true };
18460
18642
  }
18461
18643
  function decryptIdentity(box, password) {
18462
- const decipher = import_crypto2.default.createDecipheriv(
18644
+ const decipher = import_crypto3.default.createDecipheriv(
18463
18645
  "aes-256-gcm",
18464
18646
  deriveKey(password, Buffer.from(box.salt, "base64")),
18465
18647
  Buffer.from(box.iv, "base64")
@@ -18545,6 +18727,18 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18545
18727
  await import_promises6.default.rm(tmp, { force: true });
18546
18728
  }
18547
18729
  }
18730
+ async function storedSecretKeys(spaceDir) {
18731
+ const raw = await import_promises6.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
18732
+ const keys = [];
18733
+ for (const line of raw.split("\n")) {
18734
+ const eq = line.indexOf("=");
18735
+ if (eq <= 0 || line.startsWith("#")) continue;
18736
+ const key = line.slice(0, eq);
18737
+ if (key.startsWith("sops_")) continue;
18738
+ keys.push(key);
18739
+ }
18740
+ return keys;
18741
+ }
18548
18742
  async function listSecretKeys(configDir, spaceDir, password) {
18549
18743
  const identity = await identityFor(configDir, password);
18550
18744
  return Object.keys(await readEnv(spaceDir, identity));
@@ -18572,30 +18766,10 @@ async function secretsForInjection(configDir, spaceDir, password) {
18572
18766
  return readEnv(spaceDir, identity);
18573
18767
  }
18574
18768
 
18575
- // ../../shared/all/helpers/board-agent-core/host-run.ts
18576
- var HUMAN_MARKER = "AWAITING_INPUT:";
18577
- var SECRETS_MARKER = "NEED_SECRETS:";
18578
- var COMPLETE_MARKER = "DEPLOY_COMPLETE:";
18579
- var FAILED_MARKER = "DEPLOY_FAILED:";
18580
- var MAX_MARKERLESS_TURNS = 3;
18581
- var COMMAND_TIMEOUT_MS = 45 * 6e4;
18769
+ // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18770
+ var COMMAND_IDLE_TIMEOUT_MS = 45 * 6e4;
18771
+ var COMMAND_TIMEOUT_MS = 6 * 60 * 6e4;
18582
18772
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18583
- function tailMarker(text) {
18584
- const zone = text.slice(-2e3);
18585
- let best = null;
18586
- for (const marker of [
18587
- SECRETS_MARKER,
18588
- HUMAN_MARKER,
18589
- COMPLETE_MARKER,
18590
- FAILED_MARKER
18591
- ]) {
18592
- const at = zone.lastIndexOf(marker);
18593
- if (at !== -1 && (!best || at > best.at)) best = { marker, at };
18594
- }
18595
- if (!best) return null;
18596
- const value2 = zone.slice(best.at + best.marker.length).trim();
18597
- return value2 ? { marker: best.marker, value: value2 } : null;
18598
- }
18599
18773
  function containedCwd(cwd, spaceDir) {
18600
18774
  if (!cwd) return null;
18601
18775
  const resolved = import_path9.default.resolve(cwd);
@@ -18603,45 +18777,90 @@ function containedCwd(cwd, spaceDir) {
18603
18777
  return resolved === root || resolved.startsWith(root + import_path9.default.sep) ? resolved : null;
18604
18778
  }
18605
18779
  async function authorizeAndRun(opts) {
18606
- const { command, hooks } = opts;
18607
- opts.status?.("waiting for the owner to authorize this command");
18608
- let secrets = null;
18609
- let authError = "";
18610
- while (!secrets) {
18611
- const auth = await hooks.requestAuth(command, authError);
18780
+ const { command, gate } = opts;
18781
+ const scope = opts.keys ?? opts.declared;
18782
+ opts.status?.(
18783
+ scope.length ? `waiting for the owner to authorize this command (${scope.join(", ")})` : "waiting for the owner to authorize this command (no secrets requested)"
18784
+ );
18785
+ let store = null;
18786
+ let error = "";
18787
+ while (!store) {
18788
+ const auth = await gate.request({
18789
+ command,
18790
+ keys: scope,
18791
+ origin: opts.origin,
18792
+ error
18793
+ });
18612
18794
  if ("denied" in auth) return { denied: true };
18613
18795
  try {
18614
- secrets = await secretsForInjection(
18796
+ store = await secretsForInjection(
18615
18797
  opts.configDir,
18616
18798
  opts.spaceDir,
18617
18799
  auth.password
18618
18800
  );
18619
18801
  } catch (err) {
18620
- authError = err instanceof Error ? err.message : String(err);
18621
- opts.status?.(`authorization failed: ${authError}`);
18802
+ error = err instanceof Error ? err.message : String(err);
18803
+ opts.status?.(`authorization failed: ${error}`);
18622
18804
  }
18623
18805
  }
18624
- const redact = redactor(Object.values(secrets));
18806
+ const injected = {};
18807
+ const missing = [];
18808
+ for (const key of scope) {
18809
+ if (key in store) injected[key] = store[key];
18810
+ else missing.push(key);
18811
+ }
18812
+ if (missing.length && opts.keys) {
18813
+ const note = `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not in the store, so the command sees ${missing.length === 1 ? "it" : "them"} empty.`;
18814
+ opts.status?.(note);
18815
+ opts.sendLog?.({ type: "system", content: note });
18816
+ }
18817
+ const redact = redactor(Object.values(store));
18818
+ const names = Object.keys(injected);
18625
18819
  opts.status?.("authorized, running");
18626
- hooks.sendLog({
18820
+ opts.sendLog?.({
18627
18821
  type: "system",
18628
- content: "Authorized. Running the command with secrets injected..."
18822
+ content: `Authorized. Running the command with ${names.length ? `${names.join(", ")} injected` : "no secrets injected"}...`
18629
18823
  });
18630
18824
  const res = await execLogin(command, {
18631
18825
  cwd: opts.cwd,
18632
- env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18826
+ env: { ...process.env, ...spaceEnv(opts.spaceDir), ...injected },
18633
18827
  timeout: COMMAND_TIMEOUT_MS,
18828
+ idleTimeout: COMMAND_IDLE_TIMEOUT_MS,
18634
18829
  maxBuffer: 32 * 1024 * 1024
18635
18830
  });
18636
18831
  const output = redact(res.output);
18637
- hooks.sendLog({ type: "tool", content: `$ ${command}
18832
+ opts.sendLog?.({ type: "tool", content: `$ ${command}
18638
18833
  ${tail(output)}` });
18639
18834
  return { code: res.code, output };
18640
18835
  }
18836
+
18837
+ // ../../shared/all/helpers/board-agent-core/host-run.ts
18838
+ var HUMAN_MARKER = "AWAITING_INPUT:";
18839
+ var SECRETS_MARKER = "NEED_SECRETS:";
18840
+ var COMPLETE_MARKER = "DEPLOY_COMPLETE:";
18841
+ var FAILED_MARKER = "DEPLOY_FAILED:";
18842
+ var MAX_MARKERLESS_TURNS = 3;
18843
+ var tail2 = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18844
+ function tailMarker(text) {
18845
+ const zone = text.slice(-2e3);
18846
+ let best = null;
18847
+ for (const marker of [
18848
+ SECRETS_MARKER,
18849
+ HUMAN_MARKER,
18850
+ COMPLETE_MARKER,
18851
+ FAILED_MARKER
18852
+ ]) {
18853
+ const at = zone.lastIndexOf(marker);
18854
+ if (at !== -1 && (!best || at > best.at)) best = { marker, at };
18855
+ }
18856
+ if (!best) return null;
18857
+ const value2 = zone.slice(best.at + best.marker.length).trim();
18858
+ return value2 ? { marker: best.marker, value: value2 } : null;
18859
+ }
18641
18860
  async function startDeployRun(opts) {
18642
18861
  const { hooks } = opts;
18643
- const workspace = import_path9.default.join(opts.spaceDir, "workspace");
18644
- const configDir = import_path9.default.dirname(opts.spaceDir);
18862
+ const workspace = import_path10.default.join(opts.spaceDir, "workspace");
18863
+ const configDir = import_path10.default.dirname(opts.spaceDir);
18645
18864
  const status = await checkDeployStatus(opts);
18646
18865
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18647
18866
  throw new Error(
@@ -18661,36 +18880,23 @@ async function startDeployRun(opts) {
18661
18880
  );
18662
18881
  await hostGit(workspace, "clean", "-fd");
18663
18882
  await ensureClaudeSkillLink(workspace);
18883
+ const release = (opts.releaseBranch || "").replace(/[^\w./-]/g, "");
18884
+ 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.` : "";
18885
+ if (releaseNote) {
18886
+ hooks.sendLog({
18887
+ type: "system",
18888
+ content: `Release branch: ${release}`
18889
+ });
18890
+ }
18664
18891
  const contract = prompts_default.deployRun.join("\n");
18665
18892
  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.";
18666
- let prompt2 = opts.provider === "codex" ? `Use the "deploy" skill: load it and execute it step by step.
18667
-
18668
- ${reminder}` : `/deploy
18669
-
18670
- ${reminder}`;
18893
+ const opening = opts.provider === "codex" ? 'Use the "deploy" skill: load it and execute it step by step.' : "/deploy";
18894
+ let prompt2 = [opening, releaseNote, reminder].filter(Boolean).join("\n\n");
18671
18895
  let sessionId = "";
18672
18896
  let markerless = 0;
18673
18897
  let todos = [];
18674
- const bridge = await startSecretsBridge({
18675
- spaceDir: opts.spaceDir,
18676
- handlers: {
18677
- run: ({ command, cwd, status: report }) => authorizeAndRun({
18678
- configDir,
18679
- spaceDir: opts.spaceDir,
18680
- // A skill legitimately cds into subdirectories, so the CLI's own cwd
18681
- // is the right place to run - but only while it is still inside the
18682
- // space. Anywhere else is a confused caller, not a step, and the
18683
- // workspace is the safe reading of what it meant.
18684
- cwd: containedCwd(cwd, opts.spaceDir) ?? workspace,
18685
- command,
18686
- hooks,
18687
- status: report
18688
- }),
18689
- // Names only. The contract on the default branch already declares them
18690
- // in public, so this reveals nothing the repo does not.
18691
- list: () => Promise.resolve(status.variables.map((v) => v.key))
18692
- }
18693
- });
18898
+ const declared = status.variables.map((v) => v.key);
18899
+ const deployToken = opts.gate.issueToken(DEPLOY_ORIGIN);
18694
18900
  try {
18695
18901
  for (; ; ) {
18696
18902
  if (hooks.isCancelled()) throw new Error("Cancelled by user.");
@@ -18708,7 +18914,10 @@ ${reminder}`;
18708
18914
  includePartialMessages: true,
18709
18915
  resumeSessionId: sessionId || void 0,
18710
18916
  appendSystemPrompt: sessionId ? void 0 : contract,
18711
- env: spaceEnv(opts.spaceDir),
18917
+ env: {
18918
+ ...spaceEnv(opts.spaceDir),
18919
+ FACTIII_SECRETS_TOKEN: deployToken
18920
+ },
18712
18921
  callbacks: {
18713
18922
  initialTodos: todos,
18714
18923
  onLog: (entry) => hooks.sendLog(entry),
@@ -18732,14 +18941,22 @@ ${reminder}`;
18732
18941
  spaceDir: opts.spaceDir,
18733
18942
  cwd: workspace,
18734
18943
  command,
18735
- hooks
18944
+ // The marker carries a command and nothing else, so a skill speaking
18945
+ // it cannot name its keys. null takes the declared contract - still
18946
+ // a narrowing from the whole store, and the most this protocol can
18947
+ // say. New skills name their keys and get less.
18948
+ keys: null,
18949
+ declared,
18950
+ origin: DEPLOY_ORIGIN,
18951
+ gate: opts.gate,
18952
+ sendLog: hooks.sendLog
18736
18953
  });
18737
18954
  if ("denied" in res) {
18738
18955
  prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18739
18956
  continue;
18740
18957
  }
18741
18958
  markerless = 0;
18742
- const clean = tail(res.output);
18959
+ const clean = tail2(res.output);
18743
18960
  prompt2 = `The authorized command finished (exit ${res.code}). Redacted output:
18744
18961
 
18745
18962
  ${clean}`;
@@ -18769,7 +18986,7 @@ ${clean}`;
18769
18986
  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.";
18770
18987
  }
18771
18988
  } finally {
18772
- await bridge.close().catch(() => void 0);
18989
+ opts.gate.revokeToken(deployToken);
18773
18990
  }
18774
18991
  }
18775
18992
 
@@ -18863,37 +19080,37 @@ async function getDriveRoot(accessToken) {
18863
19080
 
18864
19081
  // ../../shared/all/helpers/board-agent-core/space-core.ts
18865
19082
  var import_promises8 = require("fs/promises");
18866
- var import_path11 = __toESM(require("path"));
19083
+ var import_path12 = __toESM(require("path"));
18867
19084
 
18868
19085
  // ../../shared/all/helpers/board-agent-core/plugin.ts
18869
- var import_crypto3 = __toESM(require("crypto"));
19086
+ var import_crypto4 = __toESM(require("crypto"));
18870
19087
  var import_fs3 = require("fs");
18871
19088
  var import_promises7 = require("fs/promises");
18872
19089
  var import_os3 = __toESM(require("os"));
18873
- var import_path10 = __toESM(require("path"));
19090
+ var import_path11 = __toESM(require("path"));
18874
19091
  var MARKETPLACE_NAME = "factiii-runner";
18875
19092
  var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
18876
19093
  var SECRETS_BIN = "factiii-secrets";
18877
19094
  function candidates() {
18878
19095
  return [
18879
- import_path10.default.join(__dirname, "..", "plugin"),
18880
- import_path10.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
18881
- import_path10.default.join(process.cwd(), "apps", "runner", "plugin")
19096
+ import_path11.default.join(__dirname, "..", "plugin"),
19097
+ import_path11.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
19098
+ import_path11.default.join(process.cwd(), "apps", "runner", "plugin")
18882
19099
  ];
18883
19100
  }
18884
19101
  function pluginRoot() {
18885
19102
  for (const dir of candidates()) {
18886
- if ((0, import_fs3.existsSync)(import_path10.default.join(dir, ".claude-plugin", "marketplace.json"))) {
19103
+ if ((0, import_fs3.existsSync)(import_path11.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18887
19104
  return dir;
18888
19105
  }
18889
19106
  }
18890
19107
  return null;
18891
19108
  }
18892
19109
  function renderedPluginRoot() {
18893
- return import_path10.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
19110
+ return import_path11.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18894
19111
  }
18895
19112
  async function runnerVersion(source) {
18896
- const pkg = import_path10.default.join(source, "..", "package.json");
19113
+ const pkg = import_path11.default.join(source, "..", "package.json");
18897
19114
  const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
18898
19115
  if (!raw) return FALLBACK_VERSION;
18899
19116
  try {
@@ -18908,16 +19125,16 @@ async function treeFiles(dir) {
18908
19125
  const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
18909
19126
  const files = [];
18910
19127
  for (const entry of entries) {
18911
- const full = import_path10.default.join(dir, entry.name);
19128
+ const full = import_path11.default.join(dir, entry.name);
18912
19129
  if (entry.isDirectory()) files.push(...await treeFiles(full));
18913
19130
  else if (entry.isFile()) files.push(full);
18914
19131
  }
18915
19132
  return files.sort();
18916
19133
  }
18917
19134
  async function fingerprint(source, url2, version = "") {
18918
- const hash = import_crypto3.default.createHash("sha256").update(url2).update(version);
19135
+ const hash = import_crypto4.default.createHash("sha256").update(url2).update(version);
18919
19136
  for (const file of await treeFiles(source)) {
18920
- hash.update(import_path10.default.relative(source, file));
19137
+ hash.update(import_path11.default.relative(source, file));
18921
19138
  hash.update(await (0, import_promises7.readFile)(file));
18922
19139
  }
18923
19140
  return hash.digest("hex");
@@ -18943,10 +19160,10 @@ async function renderPluginImpl(boardMcpUrl) {
18943
19160
  const want = await fingerprint(source, boardMcpUrl, version);
18944
19161
  const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
18945
19162
  if (have === want) return dest;
18946
- await (0, import_promises7.mkdir)(import_path10.default.dirname(dest), { recursive: true });
19163
+ await (0, import_promises7.mkdir)(import_path11.default.dirname(dest), { recursive: true });
18947
19164
  await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
18948
19165
  await (0, import_promises7.writeFile)(
18949
- import_path10.default.join(dest, "factiii", ".mcp.json"),
19166
+ import_path11.default.join(dest, "factiii", ".mcp.json"),
18950
19167
  `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
18951
19168
  `
18952
19169
  );
@@ -18955,7 +19172,7 @@ async function renderPluginImpl(boardMcpUrl) {
18955
19172
  return dest;
18956
19173
  }
18957
19174
  async function stampVersion(dest, version) {
18958
- const manifest = import_path10.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
19175
+ const manifest = import_path11.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
18959
19176
  const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
18960
19177
  if (!raw) return;
18961
19178
  const parsed = JSON.parse(raw);
@@ -19181,6 +19398,17 @@ var SpaceCore = class {
19181
19398
  // Positive-only cache per provider: true sticks until sign-out, false is
19182
19399
  // re-probed (an in-terminal login can flip it).
19183
19400
  this.loggedIn = {};
19401
+ /** The space's authorization gate, and the registry of which sessions may
19402
+ * ask at all (see secrets-gate.ts). Here rather than on the engine because
19403
+ * every session type has a SpaceCore and needs a token minted for it. */
19404
+ this.gate = new SecretsGate();
19405
+ /** The live deploy run's log sink, so a command it authorizes still lands on
19406
+ * the release timeline now that the bridge is not the run's own. Set by the
19407
+ * engine for the length of a run. */
19408
+ this.deployLog = null;
19409
+ this.secretsBridge = null;
19410
+ this.bridgeStarting = null;
19411
+ this.spaceSecretsToken = "";
19184
19412
  // ── Persistent per-space workspace: clone, worktrees, redis state ──
19185
19413
  // Just a directory, so a restart needs no reattach.
19186
19414
  this.execTarget = null;
@@ -19195,10 +19423,101 @@ var SpaceCore = class {
19195
19423
  this.mcp = options.mcp;
19196
19424
  this.requestSudo = options.requestSudo;
19197
19425
  this.activity = new ActivityTracker(this.emitter);
19426
+ this.gate.subscribe(() => {
19427
+ const pending = this.gate.pending();
19428
+ if (pending) {
19429
+ this.activity.set(
19430
+ "secrets-auth",
19431
+ "awaiting-input",
19432
+ "Needs your store password",
19433
+ `${pending.origin}: ${pending.command}`
19434
+ );
19435
+ } else {
19436
+ this.activity.clear("secrets-auth");
19437
+ }
19438
+ this.emitter.emitSecretsAuth({ pending: pending !== null });
19439
+ });
19198
19440
  }
19199
19441
  setLoggedIn(provider, value2) {
19200
19442
  this.loggedIn[provider] = value2;
19201
19443
  }
19444
+ /** Serve `factiii-secrets` for this space, once. Lazy rather than
19445
+ * constructor-time: the socket lives under `state`, which does not exist
19446
+ * until the workspace is provisioned. Provisioning and reattach both call
19447
+ * it, so the socket is up before anything can reach for it. */
19448
+ async ensureSecretsBridge() {
19449
+ if (this.secretsBridge) return;
19450
+ this.bridgeStarting ??= this.startSecretsBridgeOnce().finally(() => {
19451
+ this.bridgeStarting = null;
19452
+ });
19453
+ return this.bridgeStarting;
19454
+ }
19455
+ async startSecretsBridgeOnce() {
19456
+ const spaceDir = this.spaceDir();
19457
+ this.secretsBridge = await startSecretsBridge({
19458
+ spaceDir,
19459
+ handlers: {
19460
+ run: async ({ command, cwd, keys, token, status }) => {
19461
+ const origin = token ? this.gate.originFor(token) : null;
19462
+ if (!origin) {
19463
+ status(UNATTRIBUTED);
19464
+ return { code: 3, output: "" };
19465
+ }
19466
+ return authorizeAndRun({
19467
+ configDir: import_path12.default.dirname(spaceDir),
19468
+ spaceDir,
19469
+ // A step legitimately cds into subdirectories, so the caller's own
19470
+ // cwd is right - but only inside the space. Anywhere else is a
19471
+ // confused caller, and the workspace is the safe reading.
19472
+ cwd: containedCwd(cwd, spaceDir) ?? this.target().paths.workspace,
19473
+ command,
19474
+ keys,
19475
+ // Only the legacy marker needs a declared fallback, and that never
19476
+ // arrives here - a bridge caller always names its own keys.
19477
+ declared: [],
19478
+ origin,
19479
+ gate: this.gate,
19480
+ // A deploy-origin command belongs on the release timeline; every
19481
+ // other origin has no log of its own and reads the CLI's stderr.
19482
+ sendLog: this.deployLog ?? void 0
19483
+ });
19484
+ },
19485
+ list: () => this.secretsKeyInfo()
19486
+ }
19487
+ });
19488
+ }
19489
+ /** Key NAMES and status, no password. The contract declares its keys in
19490
+ * public on the default branch, and sops leaves names in cleartext, so this
19491
+ * reveals nothing the repo and the file do not already say. */
19492
+ async secretsKeyInfo() {
19493
+ const stored = new Set(await storedSecretKeys(this.spaceDir()));
19494
+ const config = this.readConfig();
19495
+ const declared = new Set(
19496
+ config.repoUrl && config.githubToken ? await checkDeployStatus({
19497
+ spaceDir: this.spaceDir(),
19498
+ repoUrl: config.repoUrl,
19499
+ mainBranch: config.mainBranch,
19500
+ githubToken: config.githubToken
19501
+ }).then((status) => status.variables.map((v) => v.key)).catch(() => []) : []
19502
+ );
19503
+ return [.../* @__PURE__ */ new Set([...declared, ...stored])].sort().map((key) => ({
19504
+ key,
19505
+ stored: stored.has(key),
19506
+ declared: declared.has(key)
19507
+ }));
19508
+ }
19509
+ /**
19510
+ * The token every session this space spawns carries, so `factiii-secrets`
19511
+ * inside one is attributable and everything else is refused.
19512
+ *
19513
+ * One per space, not per session: `extraEnv` is the only place every spawn
19514
+ * passes through and it cannot tell which session it is building for. A run
19515
+ * that DOES know itself - the deploy run - mints its own and overrides this.
19516
+ */
19517
+ secretsToken() {
19518
+ this.spaceSecretsToken ||= this.gate.issueToken("A session on this board");
19519
+ return this.spaceSecretsToken;
19520
+ }
19202
19521
  readConfig() {
19203
19522
  return this.configProvider.readConfig(this.spaceSlug);
19204
19523
  }
@@ -19210,6 +19529,7 @@ var SpaceCore = class {
19210
19529
  for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
19211
19530
  const board = this.boardMcp();
19212
19531
  if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
19532
+ env.FACTIII_SECRETS_TOKEN = this.secretsToken();
19213
19533
  return env;
19214
19534
  });
19215
19535
  return this.execTarget;
@@ -19385,6 +19705,7 @@ var SpaceCore = class {
19385
19705
  `mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin}`
19386
19706
  );
19387
19707
  await allocateSpacePorts(this.spaceDir());
19708
+ await this.ensureSecretsBridge().catch(() => void 0);
19388
19709
  await ensureRedis(target);
19389
19710
  await Promise.all([
19390
19711
  this.provisionGitLane(status),
@@ -19481,14 +19802,14 @@ var SpaceCore = class {
19481
19802
  const source = pluginRoot();
19482
19803
  if (!source) return;
19483
19804
  const { bin } = this.target().paths;
19484
- const from = import_path11.default.join(source, "factiii", "bin", SECRETS_BIN);
19485
- const to = import_path11.default.join(bin, SECRETS_BIN);
19805
+ const from = import_path12.default.join(source, "factiii", "bin", SECRETS_BIN);
19806
+ const to = import_path12.default.join(bin, SECRETS_BIN);
19486
19807
  try {
19487
19808
  const [src, staged] = await Promise.all([
19488
- (0, import_promises8.stat)(from),
19489
- (0, import_promises8.stat)(to).catch(() => null)
19809
+ (0, import_promises8.readFile)(from),
19810
+ (0, import_promises8.readFile)(to).catch(() => null)
19490
19811
  ]);
19491
- if (staged?.size === src.size) return;
19812
+ if (staged?.equals(src)) return;
19492
19813
  await (0, import_promises8.mkdir)(bin, { recursive: true });
19493
19814
  await (0, import_promises8.copyFile)(from, to);
19494
19815
  await (0, import_promises8.chmod)(to, 493);
@@ -19547,10 +19868,12 @@ var SpaceCore = class {
19547
19868
  } catch {
19548
19869
  return false;
19549
19870
  }
19871
+ await this.ensureSecretsBridge().catch(() => void 0);
19550
19872
  await ensureRedis(target).catch(() => {
19551
19873
  });
19552
19874
  await pruneWorktrees(target).catch(() => {
19553
19875
  });
19876
+ await Promise.all([this.provisionPluginLane(), this.provisionBinLane()]);
19554
19877
  await this.provisionMcpLane();
19555
19878
  this.ready = true;
19556
19879
  return true;
@@ -19575,6 +19898,9 @@ var SpaceCore = class {
19575
19898
  destroy() {
19576
19899
  this.ready = false;
19577
19900
  this.setupPromise = null;
19901
+ this.gate.destroy();
19902
+ void this.secretsBridge?.close();
19903
+ this.secretsBridge = null;
19578
19904
  this.activity.destroy();
19579
19905
  }
19580
19906
  };
@@ -19625,6 +19951,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19625
19951
  logs: [],
19626
19952
  question: "",
19627
19953
  pendingCommand: "",
19954
+ pendingKeys: [],
19628
19955
  error: "",
19629
19956
  summary: "",
19630
19957
  startedAt: 0,
@@ -19646,6 +19973,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19646
19973
  this.emitter = options.emitter;
19647
19974
  this.claude = new ClaudeModeEngine(this.core, this.emitter);
19648
19975
  this.terminal = new TerminalModeEngine(this.core, this.emitter);
19976
+ this.gate.subscribe(() => this.projectGateOntoRun());
19649
19977
  }
19650
19978
  /** Everything the runner does for this space that isn't a card session, so
19651
19979
  * the Running jobs panel can report the whole machine. Owned by the core,
@@ -19653,6 +19981,39 @@ var BoardAgentEngine = class _BoardAgentEngine {
19653
19981
  get activity() {
19654
19982
  return this.core.activity;
19655
19983
  }
19984
+ /** The space's gate (space-core.ts). Reached through here because most of
19985
+ * what uses it - the bridge, the deploy run - lives on the engine. */
19986
+ get gate() {
19987
+ return this.core.gate;
19988
+ }
19989
+ /** Mirror the gate onto the deploy run's phase, for clients older than the
19990
+ * global modal - they know a password is needed only from `awaiting-auth`. */
19991
+ projectGateOntoRun() {
19992
+ const pending = this.gate.pending();
19993
+ const runLive = this.runState.phase === "running" || this.runState.phase === "awaiting-auth";
19994
+ if (runLive && pending?.origin === DEPLOY_ORIGIN) {
19995
+ this.setRun({
19996
+ phase: "awaiting-auth",
19997
+ pendingCommand: pending.command,
19998
+ pendingKeys: pending.keys,
19999
+ error: pending.error
20000
+ });
20001
+ } else if (this.runState.phase === "awaiting-auth") {
20002
+ this.setRun({ phase: "running", pendingCommand: "", pendingKeys: [] });
20003
+ }
20004
+ }
20005
+ // ── Secrets authorization (see secrets-gate.ts + .specs/deploy.md) ──
20006
+ /** What the global authorization modal renders. Null when nothing is
20007
+ * waiting, which is also how an answered prompt tells clients to close. */
20008
+ secretsAuthState() {
20009
+ return { pending: this.gate.pending() };
20010
+ }
20011
+ secretsAuthAnswer(payload) {
20012
+ return this.gate.answer(
20013
+ payload.id,
20014
+ payload.password === void 0 ? { denied: true } : { password: payload.password }
20015
+ );
20016
+ }
19656
20017
  /** How this host protects stored credentials. Read-only: the daemon answers
19657
20018
  * unlock and password changes itself (SECURE_ACTIONS in daemon.ts). */
19658
20019
  secureStatus() {
@@ -19713,6 +20074,20 @@ var BoardAgentEngine = class _BoardAgentEngine {
19713
20074
  githubToken: config.githubToken
19714
20075
  });
19715
20076
  }
20077
+ // What the Deploy modal's release picker offers: branches on origin that
20078
+ // could ship, newest first. See listReleaseBranches.
20079
+ async deployBranches() {
20080
+ const config = this.core.readConfig();
20081
+ if (!config.repoUrl || !config.githubToken) {
20082
+ throw new Error("Set the repository URL and GitHub token first.");
20083
+ }
20084
+ return listReleaseBranches({
20085
+ spaceDir: this.core.spaceDir(),
20086
+ repoUrl: config.repoUrl,
20087
+ mainBranch: config.mainBranch,
20088
+ githubToken: config.githubToken
20089
+ });
20090
+ }
19716
20091
  // ── Agent skills (SKILL.md) ──
19717
20092
  // Repo opts are optional here, unlike deploy: the global roots are readable
19718
20093
  // with no repo configured, and listSkills reports repoAvailable: false.
@@ -19849,7 +20224,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19849
20224
  * sit on `awaiting-input` for hours, which is exactly the state worth
19850
20225
  * seeing without opening the modal. */
19851
20226
  syncRunActivity() {
19852
- const { phase, question, pendingCommand, error } = this.runState;
20227
+ const { phase, question, pendingCommand, pendingKeys, error } = this.runState;
19853
20228
  switch (phase) {
19854
20229
  case "running":
19855
20230
  return this.activity.set("deploy-run", "working", "Deploying");
@@ -19865,7 +20240,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19865
20240
  "deploy-run",
19866
20241
  "awaiting-input",
19867
20242
  "Deploy needs authorization",
19868
- pendingCommand
20243
+ pendingKeys.length ? `${pendingCommand} (${pendingKeys.join(", ")})` : pendingCommand
19869
20244
  );
19870
20245
  case "failed":
19871
20246
  return this.activity.set("deploy-run", "error", "Deploy failed", error);
@@ -19874,7 +20249,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19874
20249
  return this.activity.clear("deploy-run");
19875
20250
  }
19876
20251
  }
19877
- deployRunStart(sendLog, provider) {
20252
+ deployRunStart(sendLog, provider, releaseBranch) {
19878
20253
  if (this.runState.phase === "running" || this.runState.phase === "awaiting-input" || this.runState.phase === "awaiting-auth") {
19879
20254
  throw new Error("A deploy run is already in progress.");
19880
20255
  }
@@ -19887,6 +20262,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19887
20262
  logs: [],
19888
20263
  question: "",
19889
20264
  pendingCommand: "",
20265
+ pendingKeys: [],
19890
20266
  error: "",
19891
20267
  summary: "",
19892
20268
  startedAt: Date.now(),
@@ -19911,12 +20287,15 @@ var BoardAgentEngine = class _BoardAgentEngine {
19911
20287
  }
19912
20288
  sendLog(entry);
19913
20289
  };
20290
+ this.core.deployLog = bufferLog;
19914
20291
  void startDeployRun({
19915
20292
  spaceDir: opts.spaceDir,
19916
20293
  repoUrl: opts.repoUrl,
19917
20294
  mainBranch: opts.mainBranch,
19918
20295
  githubToken: opts.githubToken,
19919
20296
  provider: opts.provider,
20297
+ releaseBranch,
20298
+ gate: this.gate,
19920
20299
  hooks: {
19921
20300
  sendLog: bufferLog,
19922
20301
  onTodos: (todos) => this.setRun({ todos }),
@@ -19924,19 +20303,13 @@ var BoardAgentEngine = class _BoardAgentEngine {
19924
20303
  this.runResolvers.human = resolve2;
19925
20304
  this.setRun({ phase: "awaiting-input", question });
19926
20305
  }),
19927
- requestAuth: (command, errorNote) => new Promise((resolve2) => {
19928
- this.runResolvers.auth = resolve2;
19929
- this.setRun({
19930
- phase: "awaiting-auth",
19931
- pendingCommand: command,
19932
- error: errorNote ?? ""
19933
- });
19934
- }),
19935
20306
  isCancelled: () => this.runCancelled,
19936
20307
  registerProc: (proc) => {
19937
20308
  this.runProc = proc;
19938
20309
  }
19939
20310
  }
20311
+ }).finally(() => {
20312
+ this.core.deployLog = null;
19940
20313
  }).then(({ shipped, summary }) => {
19941
20314
  if (shipped) {
19942
20315
  this.setRun({ phase: "success", summary, finishedAt: Date.now() });
@@ -19969,31 +20342,28 @@ var BoardAgentEngine = class _BoardAgentEngine {
19969
20342
  }
19970
20343
  // The password arrives RUNNER-DIRECT here and only feeds decryption; it
19971
20344
  // never enters the agent session (see .specs/deploy.md).
20345
+ //
20346
+ // Both predate the global modal, when a deploy run was the only thing that
20347
+ // could ask, and are kept so a client too old to send `secretsAuthAnswer`
20348
+ // still works.
19972
20349
  deployRunAuthorize(payload) {
19973
- const resolve2 = this.runResolvers.auth;
19974
- if (!resolve2 || this.runState.phase !== "awaiting-auth") {
19975
- throw new Error("No command is awaiting authorization.");
19976
- }
19977
- this.runResolvers.auth = void 0;
19978
- this.setRun({ phase: "running", pendingCommand: "", error: "" });
19979
- resolve2({ password: payload.password });
19980
- return { success: true };
20350
+ return this.answerPendingAuth({ password: payload.password });
19981
20351
  }
19982
20352
  deployRunDeny() {
19983
- const resolve2 = this.runResolvers.auth;
19984
- if (!resolve2 || this.runState.phase !== "awaiting-auth") {
19985
- throw new Error("No command is awaiting authorization.");
20353
+ return this.answerPendingAuth({ denied: true });
20354
+ }
20355
+ answerPendingAuth(result) {
20356
+ const pending = this.gate.pending();
20357
+ if (pending?.origin !== DEPLOY_ORIGIN) {
20358
+ throw new Error("No deploy command is awaiting authorization.");
19986
20359
  }
19987
- this.runResolvers.auth = void 0;
19988
- this.setRun({ phase: "running", pendingCommand: "" });
19989
- resolve2({ denied: true });
19990
- return { success: true };
20360
+ return this.gate.answer(pending.id, result);
19991
20361
  }
19992
20362
  deployRunCancel() {
19993
20363
  this.runCancelled = true;
19994
20364
  if (this.runProc) killTree(this.runProc, "SIGTERM");
19995
20365
  this.runResolvers.human?.("cancel");
19996
- this.runResolvers.auth?.({ denied: true });
20366
+ if (this.gate.pending()?.origin === DEPLOY_ORIGIN) this.gate.cancel();
19997
20367
  this.runResolvers = {};
19998
20368
  return { success: true };
19999
20369
  }
@@ -20010,6 +20380,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20010
20380
  logs: [],
20011
20381
  question: "",
20012
20382
  pendingCommand: "",
20383
+ pendingKeys: [],
20013
20384
  error: "",
20014
20385
  summary: "",
20015
20386
  startedAt: 0,
@@ -20157,7 +20528,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20157
20528
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
20158
20529
  secretsDirs() {
20159
20530
  const spaceDir = this.core.spaceDir();
20160
- return { configDir: (0, import_path12.dirname)(spaceDir), spaceDir };
20531
+ return { configDir: (0, import_path13.dirname)(spaceDir), spaceDir };
20161
20532
  }
20162
20533
  async deploySecretsStatus() {
20163
20534
  return {
@@ -20964,6 +21335,8 @@ ${c.content.trim() || "(no description)"}`
20964
21335
  return await this.bareWorkNames();
20965
21336
  case "deployStatus":
20966
21337
  return await this.deployStatus();
21338
+ case "deployBranches":
21339
+ return await this.deployBranches();
20967
21340
  case "skillsList":
20968
21341
  return await this.skillsList();
20969
21342
  case "skillRead":
@@ -21018,7 +21391,8 @@ ${c.content.trim() || "(no description)"}`
21018
21391
  return this.deployRunStart(
21019
21392
  askLogSender ?? (() => {
21020
21393
  }),
21021
- payload?.provider
21394
+ payload?.provider,
21395
+ payload?.branch
21022
21396
  );
21023
21397
  case "deployRunState":
21024
21398
  return this.deployRunState();
@@ -21038,6 +21412,12 @@ ${c.content.trim() || "(no description)"}`
21038
21412
  return this.deployRunDismiss();
21039
21413
  case "deployApproveSkill":
21040
21414
  return await this.deployApproveSkill();
21415
+ case "secretsAuthState":
21416
+ return this.secretsAuthState();
21417
+ case "secretsAuthAnswer":
21418
+ return this.secretsAuthAnswer(
21419
+ payload
21420
+ );
21041
21421
  case "deploySecretsStatus":
21042
21422
  return await this.deploySecretsStatus();
21043
21423
  case "deploySecretsInit":
@@ -21218,11 +21598,11 @@ ${c.content.trim() || "(no description)"}`
21218
21598
 
21219
21599
  // ../../shared/all/helpers/board-agent-core/index-service.ts
21220
21600
  var import_child_process5 = require("child_process");
21221
- var import_crypto4 = require("crypto");
21601
+ var import_crypto5 = require("crypto");
21222
21602
  var import_fs4 = require("fs");
21223
21603
  var import_promises9 = __toESM(require("fs/promises"));
21224
21604
  var import_net3 = require("net");
21225
- var import_path13 = require("path");
21605
+ var import_path14 = require("path");
21226
21606
  var import_util7 = require("util");
21227
21607
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
21228
21608
  var IndexService = class {
@@ -21236,7 +21616,7 @@ var IndexService = class {
21236
21616
  /** Per-space bearer for MCP + file, derived from the control token (the
21237
21617
  * service computes the same). */
21238
21618
  bearerToken(spaceSlug) {
21239
- return (0, import_crypto4.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21619
+ return (0, import_crypto5.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21240
21620
  }
21241
21621
  /** MCP bridge for a space. Null until the service is up (which it kicks
21242
21622
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -21281,7 +21661,7 @@ var IndexService = class {
21281
21661
  const log = await this.openLog();
21282
21662
  this.proc = (0, import_child_process5.spawn)(
21283
21663
  process.execPath,
21284
- [(0, import_path13.join)(this.opts.indexDir, "index.js")],
21664
+ [(0, import_path14.join)(this.opts.indexDir, "index.js")],
21285
21665
  {
21286
21666
  cwd: this.opts.indexDir,
21287
21667
  env: {
@@ -21310,7 +21690,7 @@ var IndexService = class {
21310
21690
  async openLog() {
21311
21691
  try {
21312
21692
  await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true });
21313
- return (0, import_fs4.openSync)((0, import_path13.join)(this.opts.dataDir, "index.log"), "a");
21693
+ return (0, import_fs4.openSync)((0, import_path14.join)(this.opts.dataDir, "index.log"), "a");
21314
21694
  } catch {
21315
21695
  return null;
21316
21696
  }
@@ -21319,10 +21699,10 @@ var IndexService = class {
21319
21699
  * platform needing node-gyp fails here rather than at query time. */
21320
21700
  async installDeps() {
21321
21701
  const dir = this.opts.indexDir;
21322
- if ((0, import_fs4.existsSync)((0, import_path13.join)(dir, "node_modules"))) return;
21702
+ if ((0, import_fs4.existsSync)((0, import_path14.join)(dir, "node_modules"))) return;
21323
21703
  await import_promises9.default.copyFile(
21324
- (0, import_path13.join)(dir, "image-package.json"),
21325
- (0, import_path13.join)(dir, "package.json")
21704
+ (0, import_path14.join)(dir, "image-package.json"),
21705
+ (0, import_path14.join)(dir, "package.json")
21326
21706
  );
21327
21707
  await execFileAsync4(
21328
21708
  "npm",
@@ -21334,7 +21714,7 @@ var IndexService = class {
21334
21714
  * CLIs' persistent MCP config, so a fresh port each start would leave every
21335
21715
  * registration pointing at a dead one. */
21336
21716
  async stablePort() {
21337
- const file = (0, import_path13.join)(this.opts.dataDir, ".port");
21717
+ const file = (0, import_path14.join)(this.opts.dataDir, ".port");
21338
21718
  const saved = Number(await import_promises9.default.readFile(file, "utf-8").catch(() => ""));
21339
21719
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
21340
21720
  return saved;
@@ -21511,22 +21891,22 @@ async function pairWithBrowser(serverUrl) {
21511
21891
  // src/config.ts
21512
21892
  var import_fs8 = __toESM(require("fs"));
21513
21893
  var import_os6 = __toESM(require("os"));
21514
- var import_path18 = __toESM(require("path"));
21894
+ var import_path19 = __toESM(require("path"));
21515
21895
 
21516
21896
  // src/secureFile.ts
21517
- var import_crypto7 = __toESM(require("crypto"));
21897
+ var import_crypto8 = __toESM(require("crypto"));
21518
21898
  var import_fs7 = __toESM(require("fs"));
21519
- var import_path17 = __toESM(require("path"));
21899
+ var import_path18 = __toESM(require("path"));
21520
21900
 
21521
21901
  // src/keychain.ts
21522
21902
  var import_child_process7 = require("child_process");
21523
- var import_crypto5 = __toESM(require("crypto"));
21903
+ var import_crypto6 = __toESM(require("crypto"));
21524
21904
  var import_fs5 = __toESM(require("fs"));
21525
21905
  var import_os4 = __toESM(require("os"));
21526
- var import_path14 = __toESM(require("path"));
21906
+ var import_path15 = __toESM(require("path"));
21527
21907
  var SERVICE = "factiii-runner";
21528
21908
  var ACCOUNT = "config-encryption-key";
21529
- var DPAPI_KEY_FILE = import_path14.default.join(
21909
+ var DPAPI_KEY_FILE = import_path15.default.join(
21530
21910
  import_os4.default.homedir(),
21531
21911
  ".factiii-runner",
21532
21912
  "config-key.dpapi"
@@ -21632,7 +22012,7 @@ function winWrite(key) {
21632
22012
  "-Command",
21633
22013
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
21634
22014
  ]);
21635
- import_fs5.default.mkdirSync(import_path14.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22015
+ import_fs5.default.mkdirSync(import_path15.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21636
22016
  import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21637
22017
  `, { mode: 384 });
21638
22018
  }
@@ -21772,7 +22152,7 @@ function resolve() {
21772
22152
  cached = { key: null, status: diagnose() };
21773
22153
  return cached;
21774
22154
  }
21775
- const fresh = import_crypto5.default.randomBytes(32);
22155
+ const fresh = import_crypto6.default.randomBytes(32);
21776
22156
  writeKey(fresh);
21777
22157
  const verify = readKey();
21778
22158
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -21794,23 +22174,23 @@ function keychainStatus() {
21794
22174
  }
21795
22175
 
21796
22176
  // src/secureStore.ts
21797
- var import_crypto6 = __toESM(require("crypto"));
22177
+ var import_crypto7 = __toESM(require("crypto"));
21798
22178
  var import_fs6 = __toESM(require("fs"));
21799
- var import_path16 = __toESM(require("path"));
22179
+ var import_path17 = __toESM(require("path"));
21800
22180
 
21801
22181
  // src/paths.ts
21802
22182
  var import_os5 = __toESM(require("os"));
21803
- var import_path15 = __toESM(require("path"));
21804
- var CONFIG_DIR = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
22183
+ var import_path16 = __toESM(require("path"));
22184
+ var CONFIG_DIR = import_path16.default.join(import_os5.default.homedir(), ".factiii-runner");
21805
22185
  function safeSlug(spaceSlug) {
21806
22186
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
21807
22187
  }
21808
22188
  function spaceDirPath(spaceSlug) {
21809
- return import_path15.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22189
+ return import_path16.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21810
22190
  }
21811
22191
 
21812
22192
  // src/secureStore.ts
21813
- var VAULT_KEY_FILE = import_path16.default.join(CONFIG_DIR, "vault-key.json");
22193
+ var VAULT_KEY_FILE = import_path17.default.join(CONFIG_DIR, "vault-key.json");
21814
22194
  var SCRYPT_N2 = 1 << 15;
21815
22195
  var MIN_PASSWORD_LENGTH = 8;
21816
22196
  var SecureStoreError = class extends Error {
@@ -21837,7 +22217,7 @@ function requireKeychainKey() {
21837
22217
  );
21838
22218
  }
21839
22219
  function passwordKey(password, salt) {
21840
- return import_crypto6.default.scryptSync(password, salt, 32, {
22220
+ return import_crypto7.default.scryptSync(password, salt, 32, {
21841
22221
  N: SCRYPT_N2,
21842
22222
  maxmem: 64 * 1024 * 1024
21843
22223
  });
@@ -21845,7 +22225,7 @@ function passwordKey(password, salt) {
21845
22225
  function wrapKey(machineKey, pwKey) {
21846
22226
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
21847
22227
  return Buffer.from(
21848
- import_crypto6.default.hkdfSync(
22228
+ import_crypto7.default.hkdfSync(
21849
22229
  "sha256",
21850
22230
  ikm,
21851
22231
  Buffer.alloc(0),
@@ -21855,8 +22235,8 @@ function wrapKey(machineKey, pwKey) {
21855
22235
  );
21856
22236
  }
21857
22237
  function seal(plaintext, key) {
21858
- const iv = import_crypto6.default.randomBytes(12);
21859
- const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
22238
+ const iv = import_crypto7.default.randomBytes(12);
22239
+ const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
21860
22240
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
21861
22241
  return {
21862
22242
  iv: iv.toString("base64"),
@@ -21866,7 +22246,7 @@ function seal(plaintext, key) {
21866
22246
  }
21867
22247
  function unseal(box, key) {
21868
22248
  try {
21869
- const decipher = import_crypto6.default.createDecipheriv(
22249
+ const decipher = import_crypto7.default.createDecipheriv(
21870
22250
  "aes-256-gcm",
21871
22251
  key,
21872
22252
  Buffer.from(box.iv, "base64")
@@ -21899,7 +22279,7 @@ function writeVaultFile(file) {
21899
22279
  }
21900
22280
  var unlockedDek = null;
21901
22281
  function createVault(machineKey) {
21902
- const dek = import_crypto6.default.randomBytes(32);
22282
+ const dek = import_crypto7.default.randomBytes(32);
21903
22283
  writeVaultFile({
21904
22284
  v: 1,
21905
22285
  passwordEnabled: false,
@@ -21994,7 +22374,7 @@ function enablePassword(password) {
21994
22374
  );
21995
22375
  }
21996
22376
  const dek = dataKey();
21997
- const salt = import_crypto6.default.randomBytes(16);
22377
+ const salt = import_crypto7.default.randomBytes(16);
21998
22378
  writeVaultFile({
21999
22379
  v: 1,
22000
22380
  passwordEnabled: true,
@@ -22022,7 +22402,7 @@ function changePassword(current, next) {
22022
22402
  "The current password is not correct."
22023
22403
  );
22024
22404
  }
22025
- const salt = import_crypto6.default.randomBytes(16);
22405
+ const salt = import_crypto7.default.randomBytes(16);
22026
22406
  writeVaultFile({
22027
22407
  v: 1,
22028
22408
  passwordEnabled: true,
@@ -22099,8 +22479,8 @@ function storeStatus() {
22099
22479
  // src/secureFile.ts
22100
22480
  var MAGIC = "FACTIII1";
22101
22481
  function encrypt(plain, key, version) {
22102
- const iv = import_crypto7.default.randomBytes(12);
22103
- const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
22482
+ const iv = import_crypto8.default.randomBytes(12);
22483
+ const cipher = import_crypto8.default.createCipheriv("aes-256-gcm", key, iv);
22104
22484
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
22105
22485
  const envelope = {
22106
22486
  v: version,
@@ -22111,7 +22491,7 @@ function encrypt(plain, key, version) {
22111
22491
  return `${MAGIC}${JSON.stringify(envelope)}`;
22112
22492
  }
22113
22493
  function decrypt(envelope, key) {
22114
- const decipher = import_crypto7.default.createDecipheriv(
22494
+ const decipher = import_crypto8.default.createDecipheriv(
22115
22495
  "aes-256-gcm",
22116
22496
  key,
22117
22497
  Buffer.from(envelope.iv, "base64")
@@ -22132,7 +22512,7 @@ function parseEnvelope(raw) {
22132
22512
  }
22133
22513
  }
22134
22514
  function atomicWrite(filePath, body) {
22135
- import_fs7.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
22515
+ import_fs7.default.mkdirSync(import_path18.default.dirname(filePath), { recursive: true });
22136
22516
  const tmp = `${filePath}.${process.pid}.tmp`;
22137
22517
  import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
22138
22518
  import_fs7.default.renameSync(tmp, filePath);
@@ -22247,8 +22627,8 @@ function writeBootJson(filePath, value2) {
22247
22627
  }
22248
22628
 
22249
22629
  // src/config.ts
22250
- var CONFIG_DIR2 = import_path18.default.join(import_os6.default.homedir(), ".factiii-runner");
22251
- var CONFIG_FILE = import_path18.default.join(CONFIG_DIR2, "config.json");
22630
+ var CONFIG_DIR2 = import_path19.default.join(import_os6.default.homedir(), ".factiii-runner");
22631
+ var CONFIG_FILE = import_path19.default.join(CONFIG_DIR2, "config.json");
22252
22632
  function readRunnerConfig() {
22253
22633
  return readBootJson(CONFIG_FILE);
22254
22634
  }
@@ -22417,10 +22797,10 @@ function makeChunkReassembler() {
22417
22797
  }
22418
22798
 
22419
22799
  // src/daemon.ts
22420
- var import_crypto8 = require("crypto");
22800
+ var import_crypto9 = require("crypto");
22421
22801
  var import_fs10 = __toESM(require("fs"));
22422
22802
  var import_node_datachannel = require("node-datachannel");
22423
- var import_path20 = __toESM(require("path"));
22803
+ var import_path21 = __toESM(require("path"));
22424
22804
 
22425
22805
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
22426
22806
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23600,12 +23980,12 @@ function parse2(str) {
23600
23980
  uri.queryKey = queryKey(uri, uri["query"]);
23601
23981
  return uri;
23602
23982
  }
23603
- function pathNames(obj, path20) {
23604
- const regx = /\/{2,9}/g, names = path20.replace(regx, "/").split("/");
23605
- if (path20.slice(0, 1) == "/" || path20.length === 0) {
23983
+ function pathNames(obj, path21) {
23984
+ const regx = /\/{2,9}/g, names = path21.replace(regx, "/").split("/");
23985
+ if (path21.slice(0, 1) == "/" || path21.length === 0) {
23606
23986
  names.splice(0, 1);
23607
23987
  }
23608
- if (path20.slice(-1) == "/") {
23988
+ if (path21.slice(-1) == "/") {
23609
23989
  names.splice(names.length - 1, 1);
23610
23990
  }
23611
23991
  return names;
@@ -24224,7 +24604,7 @@ var protocol2 = Socket.protocol;
24224
24604
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
24225
24605
  var import_debug7 = __toESM(require_src(), 1);
24226
24606
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
24227
- function url(uri, path20 = "", loc) {
24607
+ function url(uri, path21 = "", loc) {
24228
24608
  let obj = uri;
24229
24609
  loc = loc || typeof location !== "undefined" && location;
24230
24610
  if (null == uri)
@@ -24258,7 +24638,7 @@ function url(uri, path20 = "", loc) {
24258
24638
  obj.path = obj.path || "/";
24259
24639
  const ipv6 = obj.host.indexOf(":") !== -1;
24260
24640
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
24261
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path20;
24641
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path21;
24262
24642
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
24263
24643
  return obj;
24264
24644
  }
@@ -25892,8 +26272,8 @@ function lookup(uri, opts) {
25892
26272
  const parsed = url(uri, opts.path || "/socket.io");
25893
26273
  const source = parsed.source;
25894
26274
  const id = parsed.id;
25895
- const path20 = parsed.path;
25896
- const sameNamespace = cache[id] && path20 in cache[id]["nsps"];
26275
+ const path21 = parsed.path;
26276
+ const sameNamespace = cache[id] && path21 in cache[id]["nsps"];
25897
26277
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
25898
26278
  let io;
25899
26279
  if (newConnection) {
@@ -25935,8 +26315,8 @@ function cardContextPath(target, postId) {
25935
26315
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
25936
26316
  return `${target.paths.root}/.cards/${safe}.md`;
25937
26317
  }
25938
- function cardContextPrompt(path20) {
25939
- return `Read ${path20} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
26318
+ function cardContextPrompt(path21) {
26319
+ return `Read ${path21} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
25940
26320
  }
25941
26321
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
25942
26322
  var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
@@ -26101,10 +26481,10 @@ var TerminalManager = class {
26101
26481
 
26102
26482
  // src/agent-adapter.ts
26103
26483
  var import_fs9 = __toESM(require("fs"));
26104
- var import_path19 = __toESM(require("path"));
26105
- var RUNNER_CONFIG_PATH = import_path19.default.join(CONFIG_DIR, "runner-config.json");
26484
+ var import_path20 = __toESM(require("path"));
26485
+ var RUNNER_CONFIG_PATH = import_path20.default.join(CONFIG_DIR, "runner-config.json");
26106
26486
  function connectionPath(spaceSlug) {
26107
- return import_path19.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26487
+ return import_path20.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26108
26488
  }
26109
26489
  function writeJson(filePath, value2) {
26110
26490
  writeSecureJson(filePath, value2);
@@ -26138,7 +26518,7 @@ function migrateLegacyConfigs() {
26138
26518
  for (const name of legacy) {
26139
26519
  const slug = name.slice("space-".length, -".json".length);
26140
26520
  const parsed = readSecureJson(
26141
- import_path19.default.join(CONFIG_DIR, name)
26521
+ import_path20.default.join(CONFIG_DIR, name)
26142
26522
  );
26143
26523
  if (!parsed) continue;
26144
26524
  if (!file.defaultSlug) {
@@ -26163,7 +26543,7 @@ function encryptStrayPlaintextConfigs() {
26163
26543
  return;
26164
26544
  }
26165
26545
  for (const name of names) {
26166
- const filePath = import_path19.default.join(CONFIG_DIR, name);
26546
+ const filePath = import_path20.default.join(CONFIG_DIR, name);
26167
26547
  try {
26168
26548
  const raw = import_fs9.default.readFileSync(filePath, "utf-8");
26169
26549
  if (!raw.startsWith("{")) continue;
@@ -26292,7 +26672,7 @@ var localConfigProvider = {
26292
26672
  "claude",
26293
26673
  "codex"
26294
26674
  ]) {
26295
- import_fs9.default.mkdirSync(import_path19.default.join(dir, sub), { recursive: true });
26675
+ import_fs9.default.mkdirSync(import_path20.default.join(dir, sub), { recursive: true });
26296
26676
  }
26297
26677
  return dir;
26298
26678
  }
@@ -26466,15 +26846,15 @@ async function startDaemon(config) {
26466
26846
  }
26467
26847
  );
26468
26848
  const indexCandidates = [
26469
- import_path20.default.join(__dirname, "..", "index"),
26470
- import_path20.default.join(__dirname, "..", "..", "index", "image")
26849
+ import_path21.default.join(__dirname, "..", "index"),
26850
+ import_path21.default.join(__dirname, "..", "..", "index", "image")
26471
26851
  ];
26472
26852
  const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
26473
26853
  const odIndex = new IndexService({
26474
- controlToken: (0, import_crypto8.createHash)("sha256").update(config.authToken).digest("hex"),
26854
+ controlToken: (0, import_crypto9.createHash)("sha256").update(config.authToken).digest("hex"),
26475
26855
  embedUrl: () => config.serverUrl,
26476
26856
  indexDir,
26477
- dataDir: import_path20.default.join(CONFIG_DIR, "index-data")
26857
+ dataDir: import_path21.default.join(CONFIG_DIR, "index-data")
26478
26858
  });
26479
26859
  void odIndex.ensureRunning().catch((err) => {
26480
26860
  console.error(