@factiii/runner 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -915,18 +915,18 @@ var require_suggestSimilar = __commonJS({
915
915
  }
916
916
  return d[a.length][b.length];
917
917
  }
918
- function suggestSimilar(word, candidates) {
919
- if (!candidates || candidates.length === 0) return "";
920
- candidates = Array.from(new Set(candidates));
918
+ function suggestSimilar(word, candidates2) {
919
+ if (!candidates2 || candidates2.length === 0) return "";
920
+ candidates2 = Array.from(new Set(candidates2));
921
921
  const searchingOptions = word.startsWith("--");
922
922
  if (searchingOptions) {
923
923
  word = word.slice(2);
924
- candidates = candidates.map((candidate) => candidate.slice(2));
924
+ candidates2 = candidates2.map((candidate) => candidate.slice(2));
925
925
  }
926
926
  let similar = [];
927
927
  let bestDistance = maxDistance;
928
928
  const minSimilarity = 0.4;
929
- candidates.forEach((candidate) => {
929
+ candidates2.forEach((candidate) => {
930
930
  if (candidate.length <= 1) return;
931
931
  const distance = editDistance(word, candidate);
932
932
  const length = Math.max(word.length, candidate.length);
@@ -963,8 +963,8 @@ var require_command = __commonJS({
963
963
  "node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path17 = require("node:path");
967
- var fs14 = require("node:fs");
966
+ var path20 = require("node:path");
967
+ var fs15 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
970
970
  var { CommanderError: CommanderError2 } = require_error();
@@ -1896,11 +1896,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path17.resolve(baseDir, baseName);
1900
- if (fs14.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path17.extname(baseName))) return void 0;
1899
+ const localBin = path20.resolve(baseDir, baseName);
1900
+ if (fs15.existsSync(localBin)) return localBin;
1901
+ if (sourceExt.includes(path20.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
- (ext) => fs14.existsSync(`${localBin}${ext}`)
1903
+ (ext) => fs15.existsSync(`${localBin}${ext}`)
1904
1904
  );
1905
1905
  if (foundExt) return `${localBin}${foundExt}`;
1906
1906
  return void 0;
@@ -1912,21 +1912,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1912
1912
  if (this._scriptPath) {
1913
1913
  let resolvedScriptPath;
1914
1914
  try {
1915
- resolvedScriptPath = fs14.realpathSync(this._scriptPath);
1915
+ resolvedScriptPath = fs15.realpathSync(this._scriptPath);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path17.resolve(
1920
- path17.dirname(resolvedScriptPath),
1919
+ executableDir = path20.resolve(
1920
+ path20.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path17.basename(
1927
+ const legacyName = path20.basename(
1928
1928
  this._scriptPath,
1929
- path17.extname(this._scriptPath)
1929
+ path20.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path17.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path20.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process2.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path17.basename(filename, path17.extname(filename));
2780
+ this._name = path20.basename(filename, path20.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path18) {
2795
- if (path18 === void 0) return this._executableDir;
2796
- this._executableDir = path18;
2794
+ executableDir(path21) {
2795
+ if (path21 === void 0) return this._executableDir;
2796
+ this._executableDir = path21;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -3026,7 +3026,7 @@ var require_commander = __commonJS({
3026
3026
  // ../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3027
3027
  var require_XMLHttpRequest = __commonJS({
3028
3028
  "../../node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports2, module2) {
3029
- var fs14 = require("fs");
3029
+ var fs15 = require("fs");
3030
3030
  var Url = require("url");
3031
3031
  var spawn5 = require("child_process").spawn;
3032
3032
  module2.exports = XMLHttpRequest3;
@@ -3184,7 +3184,7 @@ var require_XMLHttpRequest = __commonJS({
3184
3184
  throw new Error("XMLHttpRequest: Only GET method is supported");
3185
3185
  }
3186
3186
  if (settings.async) {
3187
- fs14.readFile(unescape(url2.pathname), function(error, data2) {
3187
+ fs15.readFile(unescape(url2.pathname), function(error, data2) {
3188
3188
  if (error) {
3189
3189
  self.handleError(error, error.errno || -1);
3190
3190
  } else {
@@ -3196,7 +3196,7 @@ var require_XMLHttpRequest = __commonJS({
3196
3196
  });
3197
3197
  } else {
3198
3198
  try {
3199
- this.response = fs14.readFileSync(unescape(url2.pathname));
3199
+ this.response = fs15.readFileSync(unescape(url2.pathname));
3200
3200
  this.responseText = this.response.toString("utf8");
3201
3201
  this.status = 200;
3202
3202
  setState(self.DONE);
@@ -3322,15 +3322,15 @@ var require_XMLHttpRequest = __commonJS({
3322
3322
  } else {
3323
3323
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
3324
3324
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
3325
- fs14.writeFileSync(syncFile, "", "utf8");
3325
+ fs15.writeFileSync(syncFile, "", "utf8");
3326
3326
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
3327
3327
  var syncProc = spawn5(process.argv[0], ["-e", execString]);
3328
3328
  var statusText;
3329
- while (fs14.existsSync(syncFile)) {
3329
+ while (fs15.existsSync(syncFile)) {
3330
3330
  }
3331
- self.responseText = fs14.readFileSync(contentFile, "utf8");
3331
+ self.responseText = fs15.readFileSync(contentFile, "utf8");
3332
3332
  syncProc.stdin.end();
3333
- fs14.unlinkSync(contentFile);
3333
+ fs15.unlinkSync(contentFile);
3334
3334
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
3335
3335
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
3336
3336
  self.handleError(errorObj, 503);
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
3984
3984
  var require_supports_color = __commonJS({
3985
3985
  "../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
3986
3986
  "use strict";
3987
- var os6 = require("os");
3987
+ var os7 = require("os");
3988
3988
  var hasFlag = require_has_flag();
3989
3989
  var env = process.env;
3990
3990
  var forceColor;
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
4022
4022
  }
4023
4023
  const min = forceColor ? 1 : 0;
4024
4024
  if (process.platform === "win32") {
4025
- const osRelease = os6.release().split(".");
4025
+ const osRelease = os7.release().split(".");
4026
4026
  if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
4027
4027
  return Number(osRelease[2]) >= 14931 ? 3 : 2;
4028
4028
  }
@@ -6497,7 +6497,7 @@ var require_websocket = __commonJS({
6497
6497
  var EventEmitter = require("events");
6498
6498
  var https = require("https");
6499
6499
  var http = require("http");
6500
- var net2 = require("net");
6500
+ var net3 = require("net");
6501
6501
  var tls = require("tls");
6502
6502
  var { randomBytes, createHash: createHash2 } = require("crypto");
6503
6503
  var { Duplex, Readable } = require("stream");
@@ -7241,12 +7241,12 @@ var require_websocket = __commonJS({
7241
7241
  }
7242
7242
  function netConnect(options) {
7243
7243
  options.path = options.socketPath;
7244
- return net2.connect(options);
7244
+ return net3.connect(options);
7245
7245
  }
7246
7246
  function tlsConnect(options) {
7247
7247
  options.path = void 0;
7248
7248
  if (!options.servername && options.servername !== "") {
7249
- options.servername = net2.isIP(options.host) ? "" : options.host;
7249
+ options.servername = net3.isIP(options.host) ? "" : options.host;
7250
7250
  }
7251
7251
  return tls.connect(options);
7252
7252
  }
@@ -8428,8 +8428,8 @@ function getErrorMap() {
8428
8428
 
8429
8429
  // ../../node_modules/zod/v3/helpers/parseUtil.js
8430
8430
  var makeIssue = (params) => {
8431
- const { data, path: path17, errorMaps, issueData } = params;
8432
- const fullPath = [...path17, ...issueData.path || []];
8431
+ const { data, path: path20, errorMaps, issueData } = params;
8432
+ const fullPath = [...path20, ...issueData.path || []];
8433
8433
  const fullIssue = {
8434
8434
  ...issueData,
8435
8435
  path: fullPath
@@ -8545,11 +8545,11 @@ var errorUtil;
8545
8545
 
8546
8546
  // ../../node_modules/zod/v3/types.js
8547
8547
  var ParseInputLazyPath = class {
8548
- constructor(parent, value2, path17, key) {
8548
+ constructor(parent, value2, path20, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path17;
8552
+ this._path = path20;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12197,9 +12197,9 @@ function processLine(trimmed, state, cb) {
12197
12197
  if (e.type === "result" /* Result */) {
12198
12198
  if (e.is_error) {
12199
12199
  state.isError = true;
12200
- const errorText = e.result || "Claude encountered an error";
12201
- state.fullText = errorText;
12202
- cb.onLog?.({ type: "error", content: errorText });
12200
+ const errorText2 = e.result || "Claude encountered an error";
12201
+ state.fullText = errorText2;
12202
+ cb.onLog?.({ type: "error", content: errorText2 });
12203
12203
  return;
12204
12204
  }
12205
12205
  if (e.result && !state.fullText) {
@@ -12647,7 +12647,8 @@ var prompts_default = {
12647
12647
  "- $WORKSPACE - the repo checkout (the working directory), reset to the default branch at run start",
12648
12648
  "- $BACKUPS_DIR - durable backups; every backup (db dumps etc) is written here and nowhere else",
12649
12649
  "- $BUILDS_DIR - build artifacts; each build writes into $BUILDS_DIR/<commit-hash>/",
12650
- "Reference paths ONLY via these variables or relative to the working directory. NEVER hardcode absolute machine paths (like /Users/...) anywhere in the skill - it must run unchanged on any runner. Secrets live in an encrypted store you never touch: their values reach commands only via runner-side injection of the keys declared in required-variables.json.",
12650
+ "Reference paths ONLY via these variables or relative to the working directory. NEVER hardcode absolute machine paths (like /Users/...) anywhere in the skill - it must run unchanged on any runner.",
12651
+ "Secrets live in an encrypted store you never touch, and `factiii-secrets` is how a command gets them - it is on the PATH of every run. Load the `factiii-secrets` skill installed on this machine (it is not part of this repo) for exactly how it behaves, and write the skill's commands in that shape.",
12651
12652
  "",
12652
12653
  "## Produce exactly three files",
12653
12654
  "1. .agents/skills/deploy/SKILL.md - the runbook, written for a competent operator. It MUST start with YAML frontmatter (--- name: deploy / description: one line saying this is the repository's release runbook and when to run it ---) so agent CLIs discover it as a skill, followed by: named steps in ship order, the exact commands, what success looks like per step, and explicit stop-and-ask points wherever human judgment or confirmation belongs (e.g. 'confirm before the production cutover'). Plain language; no secret values or key material anywhere in the file - reference secrets ONLY as $KEY env names.",
@@ -12658,7 +12659,7 @@ var prompts_default = {
12658
12659
  "- Write ONLY those three files under .agents/skills/deploy/.",
12659
12660
  "- Do NOT git commit or git push; the harness pushes for review.",
12660
12661
  "- Cloud CLIs on this machine (aws, vercel, wrangler, gh...) may already be signed in. You may use them READ-ONLY to verify what exists (project names, repo names, regions) so the skill is accurate - but NEVER create, modify, deploy, or delete anything, and never echo a credential or token value.",
12661
- "- The harness injects required variables per-command at execution time (the owner authorizes each secret-bearing command individually). Write the skill accordingly: commands simply reference the variables; NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.",
12662
+ '- Secret-bearing commands: any command needing a variable declared in required-variables.json MUST be written in the skill as `factiii-secrets run -- <command>`, with the variables referenced unexpanded and quoted (e.g. "$DATABASE_URL"). The owner authorizes each one at run time and the runner injects the values. Wrap only the commands that need a secret. NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.',
12662
12663
  "- No secret values in any file, ever."
12663
12664
  ],
12664
12665
  deployCreateRevise: [
@@ -12666,18 +12667,18 @@ var prompts_default = {
12666
12667
  "",
12667
12668
  "{{FEEDBACK}}",
12668
12669
  "",
12669
- "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12670
+ "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, every secret-bearing command still written as `factiii-secrets run -- <command>`, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12670
12671
  ],
12671
12672
  deployRun: [
12672
12673
  "## Deploy run protocol",
12673
12674
  "You are executing this repository's deploy skill NOW, on the owner's machine, for real. Invoke it and follow it exactly - the skill is the program; do not improvise steps it doesn't contain or change any step's intent.",
12674
12675
  "",
12675
- "## The secrets model (read this before anything else)",
12676
- "Every variable declared in required-variables.json is UNSET in your shell BY DESIGN, and will stay unset for the whole run. That is the security model working, not a problem: the owner already stored the values encrypted on this machine, and the runner injects them into commands you request through the NEED_SECRETS gate below. So:",
12677
- "- NEVER check whether these variables are set (no `env`, `printenv`, `test -n`, echo checks). Their absence tells you nothing.",
12678
- "- NEVER report them as missing, ask the owner to configure them, or abort the run because your shell lacks them. Aborting for unset declared variables is ALWAYS wrong - the gate is how every one of them gets used.",
12679
- "- If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
12680
- '- To run a command that needs any declared variable, print as the LAST line of your message exactly: NEED_SECRETS: <the complete command> - then END your turn. Write the variables unexpanded in that command (e.g. "$DATABASE_URL"). The owner inspects and authorizes it with their password; the runner executes it with every declared variable injected, and your next message contains its redacted output. One command per request.',
12676
+ "## Secrets (read this before anything else)",
12677
+ "Every variable declared in required-variables.json is UNSET in your shell BY DESIGN, and will stay unset for the whole run. The values are stored encrypted on this machine, and `factiii-secrets` is how a command gets them - it is on your PATH, and the `factiii-secrets` skill describes it in full.",
12678
+ "- To run a command that needs any declared variable, prefix it: `factiii-secrets run -- <command>`. The owner authorizes it with their password, the runner runs it with every declared value injected, and you get the output back with the values redacted. It exits with the command's own code, so `&&`, `||` and `set -e` behave normally. Exit 77 means the owner declined - adapt or stop, and never retry the same command.",
12679
+ '- Quote the variables (`"$DATABASE_URL"`): they expand on the runner\'s side, not in your shell.',
12680
+ "- NEVER check whether these variables are set (no `env`, `printenv`, `test -n`, echo checks), never report them as missing, and never abort because your shell lacks them. Their absence tells you nothing. If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
12681
+ "- Wrap ONLY the commands that actually need a secret. Everything else runs unwrapped and needs no authorization.",
12681
12682
  "",
12682
12683
  "- Track the release with your todo/plan tool: one todo per step in ship order, updated as you go. This is the owner's live timeline.",
12683
12684
  "- HUMAN GATES: wherever the skill says to stop and ask (and before anything irreversible the skill flags), print as the LAST line: AWAITING_INPUT: <the question> - then END your turn. The owner's answer arrives as your next message.",
@@ -12908,7 +12909,7 @@ ${diff.slice(0, 8e3)}`,
12908
12909
  }
12909
12910
 
12910
12911
  // ../../shared/all/helpers/board-agent-core/engine.ts
12911
- var import_path9 = require("path");
12912
+ var import_path12 = require("path");
12912
12913
 
12913
12914
  // ../../shared/all/domains/electron.ts
12914
12915
  var SETUP_LOG_PREFIX = "log ";
@@ -13017,7 +13018,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13017
13018
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13018
13019
  }
13019
13020
  async function addWorktree(target, workName, baseBranch, kind) {
13020
- const path17 = worktreeFor(target.paths, workName);
13021
+ const path20 = worktreeFor(target.paths, workName);
13021
13022
  const branch = workBranch(workName, kind);
13022
13023
  await gitInDir(
13023
13024
  target,
@@ -13027,7 +13028,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13027
13028
  baseBranch
13028
13029
  ).catch(() => {
13029
13030
  });
13030
- await removeWorktree(target, path17).catch(() => {
13031
+ await removeWorktree(target, path20).catch(() => {
13031
13032
  });
13032
13033
  await gitInDir(
13033
13034
  target,
@@ -13036,30 +13037,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13036
13037
  "add",
13037
13038
  "-B",
13038
13039
  branch,
13039
- path17,
13040
+ path20,
13040
13041
  `origin/${baseBranch}`
13041
13042
  );
13042
- return { path: path17, branch };
13043
+ return { path: path20, branch };
13043
13044
  }
13044
- async function removeWorktree(target, path17) {
13045
+ async function removeWorktree(target, path20) {
13045
13046
  await gitInDir(
13046
13047
  target,
13047
13048
  target.paths.repo,
13048
13049
  "worktree",
13049
13050
  "remove",
13050
13051
  "--force",
13051
- path17
13052
+ path20
13052
13053
  );
13053
13054
  }
13054
- async function removeWorktreeAndBranch(target, path17) {
13055
+ async function removeWorktreeAndBranch(target, path20) {
13055
13056
  const branch = await gitInDir(
13056
13057
  target,
13057
- path17,
13058
+ path20,
13058
13059
  "rev-parse",
13059
13060
  "--abbrev-ref",
13060
13061
  "HEAD"
13061
13062
  ).then((out) => out.trim()).catch(() => "");
13062
- await removeWorktree(target, path17);
13063
+ await removeWorktree(target, path20);
13063
13064
  if (branch && branch !== "HEAD") {
13064
13065
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13065
13066
  () => {
@@ -13073,8 +13074,8 @@ async function pruneWorktrees(target) {
13073
13074
  }
13074
13075
  );
13075
13076
  }
13076
- function isWorktreePath(target, path17) {
13077
- return path17.startsWith(`${target.paths.worktrees}/`);
13077
+ function isWorktreePath(target, path20) {
13078
+ return path20.startsWith(`${target.paths.worktrees}/`);
13078
13079
  }
13079
13080
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13080
13081
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13100,13 +13101,13 @@ function parseStatus(raw) {
13100
13101
  const y = line[1];
13101
13102
  const rest = line.slice(3);
13102
13103
  const arrow = rest.indexOf(" -> ");
13103
- const path17 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
+ const path20 = arrow === -1 ? rest : rest.slice(arrow + 4);
13104
13105
  if (x === "?" || y === "?") {
13105
- unstaged.push({ path: path17, code: "A" });
13106
+ unstaged.push({ path: path20, code: "A" });
13106
13107
  continue;
13107
13108
  }
13108
- if (x !== " ") staged.push({ path: path17, code: x });
13109
- if (y !== " ") unstaged.push({ path: path17, code: y });
13109
+ if (x !== " ") staged.push({ path: path20, code: x });
13110
+ if (y !== " ") unstaged.push({ path: path20, code: y });
13110
13111
  }
13111
13112
  return { staged, unstaged };
13112
13113
  }
@@ -13300,16 +13301,18 @@ function cardKey(postId) {
13300
13301
  return `card:${safeId(postId)}`;
13301
13302
  }
13302
13303
  var INDEX_KEY = "cards";
13303
- function socketPath(target) {
13304
+ function redisSocketPath(target) {
13304
13305
  return `${target.paths.state}/redis.sock`;
13305
13306
  }
13306
13307
  async function redisCli(target, args) {
13307
- const out = await target.sh(`redis-cli -s ${socketPath(target)} ${args}`);
13308
+ const out = await target.sh(
13309
+ `redis-cli -s ${redisSocketPath(target)} ${args}`
13310
+ );
13308
13311
  return out.replace(/\n$/, "");
13309
13312
  }
13310
13313
  function redisCliStdin(target, args, stdin) {
13311
13314
  return target.runWithStdin(
13312
- ["redis-cli", "-s", socketPath(target), "-x", ...args],
13315
+ ["redis-cli", "-s", redisSocketPath(target), "-x", ...args],
13313
13316
  stdin,
13314
13317
  `redis-cli ${args.join(" ")}`
13315
13318
  );
@@ -13322,7 +13325,7 @@ async function ensureRedis(target) {
13322
13325
  }
13323
13326
  const stateDir = target.paths.state;
13324
13327
  await target.sh(
13325
- `mkdir -p ${stateDir} && redis-server --daemonize yes --appendonly yes --save '' --dir ${stateDir} --port 0 --unixsocket ${socketPath(target)} --unixsocketperm 700`
13328
+ `mkdir -p ${stateDir} && redis-server --daemonize yes --appendonly yes --save '' --dir ${stateDir} --port 0 --unixsocket ${redisSocketPath(target)} --unixsocketperm 700`
13326
13329
  );
13327
13330
  for (let i = 0; i < 20; i += 1) {
13328
13331
  try {
@@ -13688,6 +13691,7 @@ function toSummary(session) {
13688
13691
  pendingQuestion: session.pendingQuestion,
13689
13692
  error: session.error,
13690
13693
  startedAt: session.startedAt,
13694
+ agentState: session.agentState,
13691
13695
  targetBranch: session.targetBranch,
13692
13696
  taskTitle: session.taskTitle,
13693
13697
  todos: session.todos,
@@ -13845,13 +13849,13 @@ function createEventSink(send, spaceSlug, notify) {
13845
13849
 
13846
13850
  // ../../shared/all/helpers/card-ai/frpc.ts
13847
13851
  var import_fs2 = require("fs");
13848
- var import_promises4 = __toESM(require("fs/promises"));
13849
- var import_path6 = __toESM(require("path"));
13852
+ var import_promises5 = __toESM(require("fs/promises"));
13853
+ var import_path7 = __toESM(require("path"));
13850
13854
 
13851
13855
  // ../../shared/all/helpers/board-agent-core/host-skills.ts
13852
- var import_promises3 = __toESM(require("fs/promises"));
13856
+ var import_promises4 = __toESM(require("fs/promises"));
13853
13857
  var import_os2 = __toESM(require("os"));
13854
- var import_path5 = __toESM(require("path"));
13858
+ var import_path6 = __toESM(require("path"));
13855
13859
 
13856
13860
  // ../../shared/all/domains/posts.ts
13857
13861
  var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
@@ -14361,9 +14365,134 @@ var skillScopeSchema = external_exports.enum(["global", "repo"]);
14361
14365
 
14362
14366
  // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14363
14367
  var import_child_process3 = require("child_process");
14368
+ var import_promises3 = __toESM(require("fs/promises"));
14369
+ var import_path5 = __toESM(require("path"));
14370
+ var import_util5 = require("util");
14371
+
14372
+ // ../../shared/all/helpers/board-agent-core/secrets-bridge.ts
14364
14373
  var import_promises2 = __toESM(require("fs/promises"));
14374
+ var import_net2 = __toESM(require("net"));
14365
14375
  var import_path4 = __toESM(require("path"));
14366
- var import_util5 = require("util");
14376
+ function secretsSocketPath(spaceDir) {
14377
+ return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14378
+ }
14379
+ function redactor(values) {
14380
+ const significant = values.filter((v) => v.length >= 4);
14381
+ return (text) => {
14382
+ let out = text;
14383
+ for (const value2 of significant) {
14384
+ out = out.split(value2).join("[REDACTED]");
14385
+ }
14386
+ return out;
14387
+ };
14388
+ }
14389
+ function replier(conn) {
14390
+ return (message) => {
14391
+ if (conn.destroyed) return;
14392
+ conn.write(`${JSON.stringify(message)}
14393
+ `);
14394
+ };
14395
+ }
14396
+ async function handleRun(request, handlers, reply) {
14397
+ const command = typeof request.command === "string" ? request.command.trim() : "";
14398
+ if (!command) {
14399
+ reply({ type: "error", message: "No command was sent." });
14400
+ return;
14401
+ }
14402
+ const cwd = typeof request.cwd === "string" ? request.cwd : "";
14403
+ const result = await handlers.run({
14404
+ command,
14405
+ cwd,
14406
+ status: (text) => reply({ type: "status", text })
14407
+ });
14408
+ if ("denied" in result) {
14409
+ reply({ type: "denied" });
14410
+ return;
14411
+ }
14412
+ if (result.output) reply({ type: "output", chunk: result.output });
14413
+ reply({ type: "exit", code: result.code });
14414
+ }
14415
+ async function startSecretsBridge(opts) {
14416
+ const socketPath = secretsSocketPath(opts.spaceDir);
14417
+ await import_promises2.default.mkdir(import_path4.default.dirname(socketPath), { recursive: true });
14418
+ await import_promises2.default.rm(socketPath, { force: true });
14419
+ const { handlers } = opts;
14420
+ let queue = Promise.resolve();
14421
+ let queueDepth = 0;
14422
+ const live = /* @__PURE__ */ new Set();
14423
+ const server = import_net2.default.createServer((conn) => {
14424
+ live.add(conn);
14425
+ conn.on("close", () => live.delete(conn));
14426
+ const reply = replier(conn);
14427
+ let buffer = "";
14428
+ let handled = false;
14429
+ conn.on("error", () => {
14430
+ });
14431
+ conn.on("data", (chunk) => {
14432
+ if (handled) return;
14433
+ buffer += chunk.toString("utf-8");
14434
+ const newline = buffer.indexOf("\n");
14435
+ if (newline === -1) return;
14436
+ handled = true;
14437
+ let request;
14438
+ try {
14439
+ request = JSON.parse(buffer.slice(0, newline));
14440
+ } catch {
14441
+ reply({ type: "error", message: "Malformed request." });
14442
+ conn.end();
14443
+ return;
14444
+ }
14445
+ if (request.op === "list") {
14446
+ void handlers.list().then((keys) => reply({ type: "keys", keys })).catch(
14447
+ (err) => reply({ type: "error", message: errorText(err) })
14448
+ ).finally(() => conn.end());
14449
+ return;
14450
+ }
14451
+ if (request.op !== "run") {
14452
+ reply({ type: "error", message: "Unknown operation." });
14453
+ conn.end();
14454
+ return;
14455
+ }
14456
+ if (queueDepth > 0) {
14457
+ reply({
14458
+ type: "status",
14459
+ text: "queued behind another command awaiting authorization"
14460
+ });
14461
+ }
14462
+ queueDepth += 1;
14463
+ queue = queue.then(() => {
14464
+ if (conn.destroyed) return void 0;
14465
+ return handleRun(request, handlers, reply);
14466
+ }).catch(
14467
+ (err) => reply({ type: "error", message: errorText(err) })
14468
+ ).finally(() => {
14469
+ queueDepth -= 1;
14470
+ conn.end();
14471
+ });
14472
+ });
14473
+ });
14474
+ await new Promise((resolve2, reject) => {
14475
+ server.once("error", reject);
14476
+ server.listen(socketPath, () => {
14477
+ server.removeListener("error", reject);
14478
+ resolve2();
14479
+ });
14480
+ });
14481
+ await import_promises2.default.chmod(socketPath, 384).catch(() => void 0);
14482
+ return {
14483
+ close: async () => {
14484
+ for (const conn of live) conn.destroy();
14485
+ live.clear();
14486
+ await new Promise((resolve2) => server.close(() => resolve2()));
14487
+ await import_promises2.default.rm(socketPath, { force: true });
14488
+ }
14489
+ };
14490
+ }
14491
+ function errorText(err) {
14492
+ return err instanceof Error ? err.message : String(err);
14493
+ }
14494
+
14495
+ // ../../shared/all/helpers/board-agent-core/host-deploy.ts
14367
14496
  var execFileAsync2 = (0, import_util5.promisify)(import_child_process3.execFile);
14368
14497
  var SKILL_DIR = ".agents/skills/deploy";
14369
14498
  var SKILL_PATH = `${SKILL_DIR}/SKILL.md`;
@@ -14372,14 +14501,14 @@ var CHECKER_PATH = `${SKILL_DIR}/check-environment.sh`;
14372
14501
  var CLAUDE_SKILLS_DIR = ".claude/skills";
14373
14502
  var CLAUDE_LINK = `${CLAUDE_SKILLS_DIR}/deploy`;
14374
14503
  async function ensureClaudeSkillLink(workspace) {
14375
- const linkPath = import_path4.default.join(workspace, CLAUDE_LINK);
14504
+ const linkPath = import_path5.default.join(workspace, CLAUDE_LINK);
14376
14505
  try {
14377
- await import_promises2.default.lstat(linkPath);
14506
+ await import_promises3.default.lstat(linkPath);
14378
14507
  return;
14379
14508
  } catch {
14380
14509
  }
14381
- await import_promises2.default.mkdir(import_path4.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14382
- await import_promises2.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14510
+ await import_promises3.default.mkdir(import_path5.default.join(workspace, CLAUDE_SKILLS_DIR), { recursive: true });
14511
+ await import_promises3.default.symlink("../../.agents/skills/deploy", linkPath, "dir");
14383
14512
  }
14384
14513
  var GIT_NO_HELPER = ["-c", "credential.helper="];
14385
14514
  var GIT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
@@ -14424,7 +14553,7 @@ async function gitCatFileBatch(cwd, shas) {
14424
14553
  return out;
14425
14554
  }
14426
14555
  async function ensureWorkspaceClone(opts) {
14427
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14556
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14428
14557
  const branch = (opts.mainBranch || "main").replace(/[^\w./-]/g, "");
14429
14558
  const url2 = buildCloneUrl(opts.repoUrl, opts.githubToken);
14430
14559
  let cloned = false;
@@ -14432,14 +14561,14 @@ async function ensureWorkspaceClone(opts) {
14432
14561
  await hostGit(workspace, "rev-parse", "--git-dir");
14433
14562
  await hostGit(workspace, "remote", "set-url", "origin", url2);
14434
14563
  } catch {
14435
- const leftovers = await import_promises2.default.readdir(workspace).catch(() => []);
14564
+ const leftovers = await import_promises3.default.readdir(workspace).catch(() => []);
14436
14565
  if (leftovers.includes(".git")) {
14437
14566
  throw new Error(
14438
14567
  `${workspace} holds a git repository that cannot be read. Remove it and retry.`
14439
14568
  );
14440
14569
  }
14441
14570
  for (const name of leftovers) {
14442
- await import_promises2.default.rm(import_path4.default.join(workspace, name), { recursive: true, force: true });
14571
+ await import_promises3.default.rm(import_path5.default.join(workspace, name), { recursive: true, force: true });
14443
14572
  }
14444
14573
  await execFileAsync2(
14445
14574
  "git",
@@ -14523,6 +14652,10 @@ function spaceEnv(spaceDir) {
14523
14652
  WORKSPACE: paths.workspace,
14524
14653
  BACKUPS_DIR: paths.backups,
14525
14654
  BUILDS_DIR: paths.builds,
14655
+ // Where `factiii-secrets` finds the runner. It can also walk up for the
14656
+ // socket, but only from inside the space tree - a step that cd'd into a
14657
+ // build dir elsewhere would not find it.
14658
+ FACTIII_SECRETS_SOCK: secretsSocketPath(spaceDir),
14526
14659
  // No keychain credential helper and no interactive git prompts anywhere
14527
14660
  // in the deploy surface (agent sessions + authorized commands): auth
14528
14661
  // comes from the secrets store or the tokened URL, never the keychain.
@@ -14533,15 +14666,15 @@ function spaceEnv(spaceDir) {
14533
14666
  };
14534
14667
  }
14535
14668
  async function readContract(workspace) {
14536
- const skill = await import_promises2.default.readFile(import_path4.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14669
+ const skill = await import_promises3.default.readFile(import_path5.default.join(workspace, SKILL_PATH), "utf-8").catch(() => {
14537
14670
  throw new Error("The creator wrote no SKILL.md.");
14538
14671
  });
14539
- const checker = await import_promises2.default.readFile(import_path4.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14672
+ const checker = await import_promises3.default.readFile(import_path5.default.join(workspace, CHECKER_PATH), "utf-8").catch(() => {
14540
14673
  throw new Error("The creator wrote no check-environment.sh.");
14541
14674
  });
14542
14675
  try {
14543
14676
  const raw = JSON.parse(
14544
- await import_promises2.default.readFile(import_path4.default.join(workspace, VARIABLES_PATH), "utf-8")
14677
+ await import_promises3.default.readFile(import_path5.default.join(workspace, VARIABLES_PATH), "utf-8")
14545
14678
  );
14546
14679
  return {
14547
14680
  skill,
@@ -14558,7 +14691,7 @@ async function generateDeploySkill(opts) {
14558
14691
  throw new Error("Describe your deployment process first.");
14559
14692
  }
14560
14693
  const status = await checkDeployStatus(opts);
14561
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14694
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14562
14695
  sendLog({
14563
14696
  type: "system",
14564
14697
  content: `Resetting workspace to origin/${status.branch}...`
@@ -14583,13 +14716,12 @@ async function generateDeploySkill(opts) {
14583
14716
  prompt: buildPrompt(prompts_default.deployCreate, {
14584
14717
  INSTRUCTIONS: instructions
14585
14718
  }),
14586
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14719
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch,Skill",
14587
14720
  // auto: the per-action classifier blocks escalations/unrecognized-infra
14588
14721
  // actions - a host session with no human reviewing each command should
14589
14722
  // not run bypassPermissions. (Codex maps this to a workspace-write
14590
14723
  // sandbox; see agent.ts.)
14591
14724
  permissionMode: "auto",
14592
- model: "claude-opus-4-6",
14593
14725
  includePartialMessages: true,
14594
14726
  env: spaceEnv(opts.spaceDir),
14595
14727
  callbacks: { onLog: (entry) => sendLog(entry) }
@@ -14602,7 +14734,7 @@ async function generateDeploySkill(opts) {
14602
14734
  async function reviseDeploySkill(opts) {
14603
14735
  const { feedback, sendLog } = opts;
14604
14736
  if (!feedback.trim()) throw new Error("Corrections are empty.");
14605
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14737
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14606
14738
  sendLog({ type: "system", content: "Applying your corrections..." });
14607
14739
  const { result } = await spawnAgent(
14608
14740
  opts.provider,
@@ -14612,9 +14744,8 @@ async function reviseDeploySkill(opts) {
14612
14744
  prompt: buildPrompt(prompts_default.deployCreateRevise, {
14613
14745
  FEEDBACK: feedback
14614
14746
  }),
14615
- allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch",
14747
+ allowedTools: "Read,Grep,Glob,Bash,Edit,Write,WebSearch,WebFetch,Skill",
14616
14748
  permissionMode: "auto",
14617
- model: "claude-opus-4-6",
14618
14749
  includePartialMessages: true,
14619
14750
  resumeSessionId: opts.resumeSessionId || void 0,
14620
14751
  env: spaceEnv(opts.spaceDir),
@@ -14631,7 +14762,7 @@ async function loadSkillDraft(opts) {
14631
14762
  throw new Error("Not a deploy-skill branch.");
14632
14763
  }
14633
14764
  await checkDeployStatus(opts);
14634
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14765
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14635
14766
  await hostGit(
14636
14767
  workspace,
14637
14768
  "fetch",
@@ -14643,7 +14774,7 @@ async function loadSkillDraft(opts) {
14643
14774
  return readContract(workspace);
14644
14775
  }
14645
14776
  async function approveDeploySkill(opts) {
14646
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14777
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14647
14778
  await readContract(workspace);
14648
14779
  await ensureClaudeSkillLink(workspace);
14649
14780
  if (opts.gitName)
@@ -14686,19 +14817,19 @@ async function runEnvironmentCheck(opts) {
14686
14817
  "No environment checker on the default branch. Generate the deploy skill first."
14687
14818
  );
14688
14819
  }
14689
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14820
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14690
14821
  const script = await hostGit(
14691
14822
  workspace,
14692
14823
  "cat-file",
14693
14824
  "-p",
14694
14825
  `origin/${status.branch}:${CHECKER_PATH}`
14695
14826
  );
14696
- const tmp = import_path4.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14697
- await import_promises2.default.writeFile(tmp, script, { mode: 448 });
14827
+ const tmp = import_path5.default.join(opts.spaceDir, `.env-check.${process.pid}.sh`);
14828
+ await import_promises3.default.writeFile(tmp, script, { mode: 448 });
14698
14829
  let ok = true;
14699
14830
  let output = "";
14700
14831
  try {
14701
- const scriptArg = tmp.split(import_path4.default.sep).join("/");
14832
+ const scriptArg = tmp.split(import_path5.default.sep).join("/");
14702
14833
  const res = await execLogin(`bash ${JSON.stringify(scriptArg)}`, {
14703
14834
  cwd: workspace,
14704
14835
  env: { ...process.env, ...spaceEnv(opts.spaceDir) },
@@ -14707,7 +14838,7 @@ async function runEnvironmentCheck(opts) {
14707
14838
  output = res.output;
14708
14839
  if (res.code !== 0) ok = false;
14709
14840
  } finally {
14710
- await import_promises2.default.rm(tmp, { force: true });
14841
+ await import_promises3.default.rm(tmp, { force: true });
14711
14842
  }
14712
14843
  const checks = [];
14713
14844
  for (const line of output.split("\n")) {
@@ -14729,7 +14860,7 @@ async function fixEnvironment(opts) {
14729
14860
  const before = await runEnvironmentCheck(opts);
14730
14861
  if (before.ok) return before;
14731
14862
  const status = await checkDeployStatus(opts);
14732
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14863
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14733
14864
  const script = await hostGit(
14734
14865
  workspace,
14735
14866
  "cat-file",
@@ -14754,7 +14885,6 @@ async function fixEnvironment(opts) {
14754
14885
  // auto: installs are in-scope of the owner's explicit "fix environment"
14755
14886
  // request; the classifier still blocks anything beyond it.
14756
14887
  permissionMode: "auto",
14757
- model: "claude-opus-4-6",
14758
14888
  includePartialMessages: true,
14759
14889
  env: spaceEnv(opts.spaceDir),
14760
14890
  callbacks: { onLog: (entry) => sendLog(entry) }
@@ -14765,7 +14895,7 @@ async function fixEnvironment(opts) {
14765
14895
  return runEnvironmentCheck(opts);
14766
14896
  }
14767
14897
  async function discardDeploySkill(opts) {
14768
- const workspace = import_path4.default.join(opts.spaceDir, "workspace");
14898
+ const workspace = import_path5.default.join(opts.spaceDir, "workspace");
14769
14899
  await hostGit(workspace, "checkout", "--", ".").catch(() => null);
14770
14900
  await hostGit(workspace, "clean", "-fd");
14771
14901
  return { success: true };
@@ -14884,7 +15014,7 @@ var AGENTS_ROOT = ".agents/skills";
14884
15014
  var CLAUDE_ROOT = ".claude/skills";
14885
15015
  var CLI_ROOTS = [CLAUDE_ROOT, ".codex/skills"];
14886
15016
  function globalAgentsRoot() {
14887
- return import_path5.default.join(import_os2.default.homedir(), ".agents", "skills");
15017
+ return import_path6.default.join(import_os2.default.homedir(), ".agents", "skills");
14888
15018
  }
14889
15019
  var AGENTS_LINK_TARGET = "../.agents/skills";
14890
15020
  var BRANCH_PREFIX = "skill/";
@@ -14894,11 +15024,11 @@ function summarize(raw, base) {
14894
15024
  return { ...base, description: frontmatter.description, errors };
14895
15025
  }
14896
15026
  async function rootLinkState(root) {
14897
- const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
14898
- const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
14899
- if (!stat) return "missing";
14900
- if (!stat.isSymbolicLink()) return "blocked";
14901
- const target = await import_promises3.default.readlink(linkPath).catch(() => "");
15027
+ 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";
15031
+ const target = await import_promises4.default.readlink(linkPath).catch(() => "");
14902
15032
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
14903
15033
  }
14904
15034
  async function globalLinkState() {
@@ -14908,12 +15038,12 @@ async function globalLinkState() {
14908
15038
  }
14909
15039
  async function listGlobal() {
14910
15040
  const root = globalAgentsRoot();
14911
- const entries = await import_promises3.default.readdir(root, { withFileTypes: true }).catch(() => []);
15041
+ const entries = await import_promises4.default.readdir(root, { withFileTypes: true }).catch(() => []);
14912
15042
  const out = [];
14913
15043
  for (const entry of entries) {
14914
15044
  if (entry.isFile()) continue;
14915
- const dir = import_path5.default.join(root, entry.name);
14916
- const raw = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
15045
+ const dir = import_path6.default.join(root, entry.name);
15046
+ const raw = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8").catch(() => "");
14917
15047
  if (!raw) continue;
14918
15048
  out.push(
14919
15049
  summarize(raw, {
@@ -15082,7 +15212,7 @@ async function ensureSkillsWorktree(spaceDir, branch) {
15082
15212
  } catch {
15083
15213
  await hostGit(repo, "worktree", "prune").catch(() => {
15084
15214
  });
15085
- await import_promises3.default.rm(worktree, { recursive: true, force: true });
15215
+ await import_promises4.default.rm(worktree, { recursive: true, force: true });
15086
15216
  await hostGit(
15087
15217
  repo,
15088
15218
  "worktree",
@@ -15165,11 +15295,11 @@ async function bundleFilesOnDisk(dir) {
15165
15295
  const out = [];
15166
15296
  const walk = async (current, prefix, depth) => {
15167
15297
  if (depth > 3 || out.length >= 200) return;
15168
- const entries = await import_promises3.default.readdir(current, { withFileTypes: true }).catch(() => []);
15298
+ const entries = await import_promises4.default.readdir(current, { withFileTypes: true }).catch(() => []);
15169
15299
  for (const entry of entries) {
15170
15300
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
15171
15301
  if (entry.isDirectory()) {
15172
- await walk(import_path5.default.join(current, entry.name), rel, depth + 1);
15302
+ await walk(import_path6.default.join(current, entry.name), rel, depth + 1);
15173
15303
  } else if (rel !== "SKILL.md") {
15174
15304
  out.push(rel);
15175
15305
  }
@@ -15181,7 +15311,7 @@ async function bundleFilesOnDisk(dir) {
15181
15311
  async function readSkillImpl(opts) {
15182
15312
  if (opts.scope === "global") {
15183
15313
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15184
- const content2 = await import_promises3.default.readFile(import_path5.default.join(dir, "SKILL.md"), "utf-8");
15314
+ const content2 = await import_promises4.default.readFile(import_path6.default.join(dir, "SKILL.md"), "utf-8");
15185
15315
  const { frontmatter: frontmatter2, errors: errors2 } = parseSkill(content2);
15186
15316
  return {
15187
15317
  name: opts.name,
@@ -15216,55 +15346,55 @@ async function readSkillImpl(opts) {
15216
15346
  };
15217
15347
  }
15218
15348
  async function moveIntoAgents(from, to) {
15219
- if (await import_promises3.default.access(to).then(
15349
+ if (await import_promises4.default.access(to).then(
15220
15350
  () => true,
15221
15351
  () => false
15222
15352
  )) {
15223
15353
  throw new Error(
15224
- `${import_path5.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15354
+ `${import_path6.default.basename(to)} exists in both roots. Remove one copy, then migrate.`
15225
15355
  );
15226
15356
  }
15227
- await import_promises3.default.mkdir(import_path5.default.dirname(to), { recursive: true });
15357
+ await import_promises4.default.mkdir(import_path6.default.dirname(to), { recursive: true });
15228
15358
  try {
15229
- await import_promises3.default.rename(from, to);
15359
+ await import_promises4.default.rename(from, to);
15230
15360
  } catch {
15231
- await import_promises3.default.cp(from, to, { recursive: true });
15232
- await import_promises3.default.rm(from, { recursive: true, force: true });
15361
+ await import_promises4.default.cp(from, to, { recursive: true });
15362
+ await import_promises4.default.rm(from, { recursive: true, force: true });
15233
15363
  }
15234
15364
  }
15235
15365
  async function migrateGlobal() {
15236
15366
  const moved = [];
15237
15367
  for (const root of CLI_ROOTS) {
15238
- const dir = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15239
- const stat = await import_promises3.default.lstat(dir).catch(() => null);
15240
- if (!stat || stat.isSymbolicLink()) continue;
15241
- const entries = await import_promises3.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15368
+ 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;
15371
+ const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15242
15372
  for (const entry of entries) {
15243
15373
  if (entry.isFile() || entry.isSymbolicLink()) continue;
15244
- const src = import_path5.default.join(dir, entry.name);
15245
- if (!await import_promises3.default.access(import_path5.default.join(src, "SKILL.md")).then(
15374
+ const src = import_path6.default.join(dir, entry.name);
15375
+ if (!await import_promises4.default.access(import_path6.default.join(src, "SKILL.md")).then(
15246
15376
  () => true,
15247
15377
  () => false
15248
15378
  )) {
15249
15379
  continue;
15250
15380
  }
15251
- await moveIntoAgents(src, import_path5.default.join(globalAgentsRoot(), entry.name));
15381
+ await moveIntoAgents(src, import_path6.default.join(globalAgentsRoot(), entry.name));
15252
15382
  moved.push(entry.name);
15253
15383
  }
15254
- await import_promises3.default.rmdir(dir).catch(() => {
15384
+ await import_promises4.default.rmdir(dir).catch(() => {
15255
15385
  });
15256
15386
  }
15257
15387
  for (const root of CLI_ROOTS) {
15258
- const linkPath = import_path5.default.join(import_os2.default.homedir(), ...root.split("/"));
15259
- const after = await import_promises3.default.lstat(linkPath).catch(() => null);
15260
- if (after?.isSymbolicLink()) await import_promises3.default.rm(linkPath, { force: true });
15388
+ const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15389
+ const after = await import_promises4.default.lstat(linkPath).catch(() => null);
15390
+ if (after?.isSymbolicLink()) await import_promises4.default.rm(linkPath, { force: true });
15261
15391
  else if (after) {
15262
15392
  throw new Error(
15263
15393
  `~/${root} still has files that are not skills. Empty it, then migrate.`
15264
15394
  );
15265
15395
  }
15266
- await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15267
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15396
+ await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15397
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15268
15398
  }
15269
15399
  return moved;
15270
15400
  }
@@ -15298,13 +15428,13 @@ async function migrateSkillsImpl(opts) {
15298
15428
  `${name} exists in both ${AGENTS_ROOT} and ${stray}. Remove one copy, then migrate.`
15299
15429
  );
15300
15430
  }
15301
- await import_promises3.default.mkdir(import_path5.default.join(workspace, AGENTS_ROOT), { recursive: true });
15431
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, AGENTS_ROOT), { recursive: true });
15302
15432
  await hostGit(workspace, "mv", stray, dest);
15303
15433
  moved.push(name);
15304
15434
  }
15305
- const linkPath = import_path5.default.join(workspace, CLAUDE_ROOT);
15306
- await import_promises3.default.rm(linkPath, { recursive: true, force: true });
15307
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15435
+ const linkPath = import_path6.default.join(workspace, CLAUDE_ROOT);
15436
+ await import_promises4.default.rm(linkPath, { recursive: true, force: true });
15437
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir");
15308
15438
  await hostGit(workspace, "add", "-A");
15309
15439
  const dirty = await hostGit(workspace, "status", "--porcelain");
15310
15440
  if (!dirty.trim()) return { branch: "", moved: [] };
@@ -15330,16 +15460,16 @@ function assertWritable(name, content) {
15330
15460
  }
15331
15461
  async function writeSkillFile(opts) {
15332
15462
  const dir = resolveSkillDir(opts.root, opts.name);
15333
- const file = import_path5.default.join(dir, "SKILL.md");
15463
+ const file = import_path6.default.join(dir, "SKILL.md");
15334
15464
  if (opts.keepExisting) {
15335
- const exists = await import_promises3.default.access(file).then(
15465
+ const exists = await import_promises4.default.access(file).then(
15336
15466
  () => true,
15337
15467
  () => false
15338
15468
  );
15339
15469
  if (exists) return;
15340
15470
  }
15341
- await import_promises3.default.mkdir(dir, { recursive: true });
15342
- await import_promises3.default.writeFile(file, opts.content, "utf-8");
15471
+ await import_promises4.default.mkdir(dir, { recursive: true });
15472
+ await import_promises4.default.writeFile(file, opts.content, "utf-8");
15343
15473
  }
15344
15474
  async function writeGlobalSkill(opts) {
15345
15475
  assertWritable(opts.name, opts.content);
@@ -15354,14 +15484,14 @@ async function installBundledSkill(opts) {
15354
15484
  content: opts.content,
15355
15485
  keepExisting: true
15356
15486
  });
15357
- await write(import_path5.default.join(home, ...AGENTS_ROOT.split("/")));
15487
+ await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15358
15488
  for (const root of CLI_ROOTS) {
15359
- const linkPath = import_path5.default.join(home, ...root.split("/"));
15360
- const stat = await import_promises3.default.lstat(linkPath).catch(() => null);
15361
- if (stat?.isSymbolicLink()) continue;
15362
- if (!stat) {
15363
- await import_promises3.default.mkdir(import_path5.default.dirname(linkPath), { recursive: true });
15364
- await import_promises3.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15489
+ 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) {
15493
+ await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15494
+ await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15365
15495
  continue;
15366
15496
  }
15367
15497
  await write(linkPath);
@@ -15369,7 +15499,7 @@ async function installBundledSkill(opts) {
15369
15499
  }
15370
15500
  async function deleteGlobalSkill(opts) {
15371
15501
  const dir = resolveSkillDir(globalAgentsRoot(), opts.name);
15372
- await import_promises3.default.rm(dir, { recursive: true, force: true });
15502
+ await import_promises4.default.rm(dir, { recursive: true, force: true });
15373
15503
  }
15374
15504
  async function proposeRepoSkillImpl(opts) {
15375
15505
  assertWritable(opts.name, opts.content);
@@ -15407,9 +15537,9 @@ async function proposeRepoSkillImpl(opts) {
15407
15537
  if (opts.gitEmail) {
15408
15538
  await hostGit(workspace, "config", "user.email", opts.gitEmail);
15409
15539
  }
15410
- await import_promises3.default.mkdir(import_path5.default.join(workspace, dir), { recursive: true });
15411
- await import_promises3.default.writeFile(
15412
- import_path5.default.join(workspace, dir, "SKILL.md"),
15540
+ await import_promises4.default.mkdir(import_path6.default.join(workspace, dir), { recursive: true });
15541
+ await import_promises4.default.writeFile(
15542
+ import_path6.default.join(workspace, dir, "SKILL.md"),
15413
15543
  opts.content,
15414
15544
  "utf-8"
15415
15545
  );
@@ -15516,18 +15646,18 @@ function vendoredBinaryName() {
15516
15646
  async function installFrpcBinary(target) {
15517
15647
  const p = frpcPaths(target);
15518
15648
  const binaryName = vendoredBinaryName();
15519
- const src = import_path6.default.join(__dirname, "..", "vendor", binaryName);
15649
+ const src = import_path7.default.join(__dirname, "..", "vendor", binaryName);
15520
15650
  if (!(0, import_fs2.existsSync)(src)) {
15521
15651
  throw new Error(
15522
15652
  `Vendored frpc missing (${src}). Run apps/runner/scripts/fetch-frpc.sh.`
15523
15653
  );
15524
15654
  }
15525
- const staged = await import_promises4.default.stat(p.binary).catch(() => null);
15526
- const source = await import_promises4.default.stat(src);
15655
+ const staged = await import_promises5.default.stat(p.binary).catch(() => null);
15656
+ const source = await import_promises5.default.stat(src);
15527
15657
  if (staged?.size === source.size) return;
15528
- await import_promises4.default.mkdir(p.binDir, { recursive: true });
15529
- await import_promises4.default.copyFile(src, p.binary);
15530
- await import_promises4.default.chmod(p.binary, 493);
15658
+ await import_promises5.default.mkdir(p.binDir, { recursive: true });
15659
+ await import_promises5.default.copyFile(src, p.binary);
15660
+ await import_promises5.default.chmod(p.binary, 493);
15531
15661
  }
15532
15662
  var EXPOSE_SKILL_NAME = "share-a-port";
15533
15663
  var FENCE = "```";
@@ -15917,18 +16047,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
15917
16047
  await this.core.ensureSpaceWorkspace();
15918
16048
  await assertAgentSignedIn(session.provider, this.core.target());
15919
16049
  if (hasGit) {
15920
- const { path: path17 } = await addWorktree(
16050
+ const { path: path20 } = await addWorktree(
15921
16051
  this.core.target(),
15922
16052
  session.taskId,
15923
16053
  baseBranch || config.mainBranch || "main",
15924
16054
  "card"
15925
16055
  );
15926
- session.workspacePath = path17;
16056
+ session.workspacePath = path20;
15927
16057
  await setGitIdentity(
15928
16058
  this.core.target(),
15929
16059
  config.gitName,
15930
16060
  config.gitEmail,
15931
- path17
16061
+ path20
15932
16062
  );
15933
16063
  this.emitter.addLog(session, "system", "Worktree ready.");
15934
16064
  } else {
@@ -16150,18 +16280,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16150
16280
  this.emitter.addLog(session, "system", "Preparing environment...");
16151
16281
  await this.core.ensureSpaceWorkspace();
16152
16282
  await assertAgentSignedIn(session.provider, this.core.target());
16153
- const { path: path17 } = await addWorktree(
16283
+ const { path: path20 } = await addWorktree(
16154
16284
  this.core.target(),
16155
16285
  session.taskId,
16156
16286
  config.mainBranch || "main",
16157
16287
  "card"
16158
16288
  );
16159
- session.workspacePath = path17;
16289
+ session.workspacePath = path20;
16160
16290
  await setGitIdentity(
16161
16291
  this.core.target(),
16162
16292
  config.gitName,
16163
16293
  config.gitEmail,
16164
- path17
16294
+ path20
16165
16295
  );
16166
16296
  await this.putCard(session);
16167
16297
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16456,6 +16586,50 @@ async function startTmuxSession(opts) {
16456
16586
  };
16457
16587
  }
16458
16588
 
16589
+ // ../../shared/all/helpers/terminal-ai/agentState.ts
16590
+ var STATES = /* @__PURE__ */ new Set(["working", "idle", "blocked"]);
16591
+ var AGENT_STATE_CHANNEL = "agent-state";
16592
+ function agentStateKey(paneId) {
16593
+ return `agent-state:${paneId}`;
16594
+ }
16595
+ function parseAgentMessage(payload) {
16596
+ const at = payload.indexOf(" ");
16597
+ if (at <= 0) return null;
16598
+ const state = parseAgentState(payload.slice(at + 1));
16599
+ return state ? { paneId: payload.slice(0, at), state } : null;
16600
+ }
16601
+ function postIdForSessionName(sessionName, postIds) {
16602
+ for (const postId of postIds) {
16603
+ const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
16604
+ if (sessionName.startsWith(prefix)) return postId;
16605
+ }
16606
+ return null;
16607
+ }
16608
+ function parseAgentState(text) {
16609
+ const word = text.trim();
16610
+ return STATES.has(word) ? word : null;
16611
+ }
16612
+ function drainStateMessages(buffer) {
16613
+ const lines = buffer.split("\n");
16614
+ const partial = lines.pop() ?? "";
16615
+ const states = [];
16616
+ let i = 0;
16617
+ let consumed = 0;
16618
+ while (i < lines.length) {
16619
+ if (lines[i] !== "message") {
16620
+ i += 1;
16621
+ consumed = i;
16622
+ continue;
16623
+ }
16624
+ if (i + 2 >= lines.length) break;
16625
+ const parsed = parseAgentMessage(lines[i + 2]);
16626
+ if (parsed) states.push(parsed);
16627
+ i += 3;
16628
+ consumed = i;
16629
+ }
16630
+ return { states, rest: [...lines.slice(consumed), partial].join("\n") };
16631
+ }
16632
+
16459
16633
  // ../../shared/all/helpers/terminal-ai/processes.ts
16460
16634
  var PS_FORMAT = "pid=,ppid=,pcpu=,rss=,etime=,args=";
16461
16635
  var PS_FIELDS = 5;
@@ -16501,6 +16675,45 @@ function descendantsOf(rows, roots) {
16501
16675
  }
16502
16676
  return found.sort((a, b) => a.pid - b.pid);
16503
16677
  }
16678
+ function panesByPid(raw) {
16679
+ const found = /* @__PURE__ */ new Map();
16680
+ for (const line of raw.split("\n")) {
16681
+ const trimmed = line.trim();
16682
+ const pid = Number(trimmed.split(/\s+/)[0]);
16683
+ if (!Number.isInteger(pid) || pid <= 0) continue;
16684
+ const match = /(?:^|\s)TMUX_PANE=(%\d+)(?:\s|$)/.exec(trimmed);
16685
+ if (match) found.set(pid, match[1]);
16686
+ }
16687
+ return found;
16688
+ }
16689
+ async function processesForCard(panes, table, lookupPanes) {
16690
+ const mine = /* @__PURE__ */ new Map();
16691
+ for (const pane of panes) {
16692
+ for (const row of processesForPane(table, pane.pid)) {
16693
+ mine.set(row.pid, { tabId: pane.tabId, row });
16694
+ }
16695
+ }
16696
+ const shells = new Set(panes.map((pane) => pane.pid));
16697
+ const rest = table.filter((row) => row.pid > 1 && !mine.has(row.pid) && !shells.has(row.pid)).map((row) => row.pid);
16698
+ if (rest.length === 0) return mine;
16699
+ const paneOf = await lookupPanes(rest);
16700
+ const tabByPane = new Map(panes.map((pane) => [pane.paneId, pane.tabId]));
16701
+ const adopted = /* @__PURE__ */ new Map();
16702
+ for (const row of table) {
16703
+ const tabId = tabByPane.get(paneOf.get(row.pid) ?? "");
16704
+ if (!tabId || mine.has(row.pid)) continue;
16705
+ mine.set(row.pid, { tabId, row });
16706
+ const roots = adopted.get(tabId);
16707
+ if (roots) roots.push(row.pid);
16708
+ else adopted.set(tabId, [row.pid]);
16709
+ }
16710
+ for (const [tabId, roots] of adopted) {
16711
+ for (const row of descendantsOf(table, roots)) {
16712
+ if (!mine.has(row.pid)) mine.set(row.pid, { tabId, row });
16713
+ }
16714
+ }
16715
+ return mine;
16716
+ }
16504
16717
  function commandOf(args) {
16505
16718
  const first = args.trim().split(/\s+/)[0] ?? "";
16506
16719
  const base = first.split("/").pop() ?? first;
@@ -16625,12 +16838,12 @@ var FLUSH_MS = 120;
16625
16838
  var SELF_WRITE_MS = 2e3;
16626
16839
  var dynamicImport = new Function("s", "return import(s)");
16627
16840
  var importFs = new Function("s", "return import(s)");
16628
- async function resolveReal(path17) {
16841
+ async function resolveReal(path20) {
16629
16842
  try {
16630
16843
  const { realpath } = await importFs("fs/promises");
16631
- return await realpath(path17);
16844
+ return await realpath(path20);
16632
16845
  } catch {
16633
- return path17;
16846
+ return path20;
16634
16847
  }
16635
16848
  }
16636
16849
  var loadSubscribe = null;
@@ -16744,19 +16957,19 @@ var WorkspaceWatcher = class {
16744
16957
  }
16745
16958
  record(kind, relPath) {
16746
16959
  if (this.closed) return;
16747
- const path17 = "/" + relPath;
16748
- const expiry = this.selfWrites.get(path17);
16960
+ const path20 = "/" + relPath;
16961
+ const expiry = this.selfWrites.get(path20);
16749
16962
  if (expiry !== void 0) {
16750
16963
  if (expiry > Date.now()) return;
16751
- this.selfWrites.delete(path17);
16964
+ this.selfWrites.delete(path20);
16752
16965
  }
16753
16966
  const { added, changed, removed } = this.pending;
16754
- added.delete(path17);
16755
- changed.delete(path17);
16756
- removed.delete(path17);
16757
- if (kind === "added") added.add(path17);
16758
- else if (kind === "changed") changed.add(path17);
16759
- else removed.add(path17);
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);
16760
16973
  if (added.size + changed.size + removed.size > MAX_BATCH) {
16761
16974
  this.pending = { ...emptyPending(), resync: true };
16762
16975
  }
@@ -16775,8 +16988,8 @@ var WorkspaceWatcher = class {
16775
16988
  this.pending = emptyPending();
16776
16989
  if (!resync && !added.size && !changed.size && !removed.size) return;
16777
16990
  const now = Date.now();
16778
- for (const [path17, expiry] of this.selfWrites) {
16779
- if (expiry <= now) this.selfWrites.delete(path17);
16991
+ for (const [path20, expiry] of this.selfWrites) {
16992
+ if (expiry <= now) this.selfWrites.delete(path20);
16780
16993
  }
16781
16994
  this.emit({
16782
16995
  added: resync ? [] : [...added],
@@ -16822,6 +17035,7 @@ async function writeBase64File(target, abs, b64, opts = {}) {
16822
17035
  `Write ${abs}`
16823
17036
  );
16824
17037
  }
17038
+ var PANE_REREAD_MS = 1e3;
16825
17039
  var TerminalModeEngine = class {
16826
17040
  constructor(core, emitter) {
16827
17041
  this.core = core;
@@ -16832,12 +17046,117 @@ var TerminalModeEngine = class {
16832
17046
  /** Worktree name each session claimed, keyed by postId. Covers the window
16833
17047
  * `claimWorkName`'s disk scan cannot: a name taken but not yet created. */
16834
17048
  this.reservedNames = /* @__PURE__ */ new Map();
17049
+ // ── agent activity ──
17050
+ /** Long-lived `redis-cli subscribe` on the space's own socket. One for every
17051
+ * session: the channel is space-wide and the pane rides in the payload, so
17052
+ * a session starting does not mean re-subscribing. */
17053
+ this.stateSub = null;
17054
+ /** Applies states one at a time; see the comment at the push site. */
17055
+ this.stateQueue = Promise.resolve();
17056
+ /** pane id -> tmux session name, refreshed only when a pane we have not seen
17057
+ * turns up. Panes outlive individual messages, so re-reading the whole map
17058
+ * per message would be a tmux call per hook. */
17059
+ this.paneSessions = /* @__PURE__ */ new Map();
17060
+ this.paneReadAt = 0;
17061
+ }
17062
+ /** Idempotent, like startWatching: re-entering a session must not stack
17063
+ * subscribers. */
17064
+ async startStateSub() {
17065
+ if (this.stateSub) return;
17066
+ const target = this.core.target();
17067
+ const child = await target.spawn([
17068
+ "redis-cli",
17069
+ "-s",
17070
+ redisSocketPath(target),
17071
+ "subscribe",
17072
+ AGENT_STATE_CHANNEL
17073
+ ]).catch(() => null);
17074
+ if (!child) return;
17075
+ this.stateSub = child;
17076
+ let buffer = "";
17077
+ child.stdout?.on("data", (chunk) => {
17078
+ buffer += String(chunk);
17079
+ const { states, rest } = drainStateMessages(buffer);
17080
+ buffer = rest;
17081
+ for (const { paneId, state } of states) {
17082
+ this.stateQueue = this.stateQueue.then(() => this.applyPaneState(paneId, state)).catch(() => {
17083
+ });
17084
+ }
17085
+ });
17086
+ child.once("exit", () => {
17087
+ if (this.stateSub === child) this.stateSub = null;
17088
+ });
17089
+ void this.hydrateAgentStates();
17090
+ }
17091
+ /** Read the last reported state for every live session's pane.
17092
+ *
17093
+ * Subscribing only catches what happens next. After a runner restart the
17094
+ * sessions come back but no hook has fired yet, so an idle session would
17095
+ * show as working until someone typed in it — which for an idle session may
17096
+ * be never. This is what the hook's SET alongside its PUBLISH is for. */
17097
+ async hydrateAgentStates() {
17098
+ if (this.sessions.size === 0) return;
17099
+ await this.refreshPanes();
17100
+ const target = this.core.target();
17101
+ for (const [paneId, name] of this.paneSessions) {
17102
+ const postId = postIdForSessionName(name, this.sessions.keys());
17103
+ const session = postId ? this.sessions.get(postId) : null;
17104
+ if (!session || session.agentState) continue;
17105
+ const raw = await target.sh(
17106
+ `redis-cli -s ${redisSocketPath(target)} get '${agentStateKey(paneId)}'`
17107
+ ).catch(() => "");
17108
+ const state = parseAgentState(raw);
17109
+ if (!state) continue;
17110
+ session.agentState = state;
17111
+ this.emitter.emitSessionUpdate(session);
17112
+ }
17113
+ }
17114
+ /** Called after a session is removed, never before: every stopWatching call
17115
+ * site runs while the session is still in the map, so keying this off
17116
+ * stopWatching would leave the subscriber running for the life of the
17117
+ * process. */
17118
+ releaseStateSub() {
17119
+ if (this.sessions.size > 0) return;
17120
+ this.stateSub?.kill();
17121
+ this.stateSub = null;
17122
+ }
17123
+ async applyPaneState(paneId, state) {
17124
+ let name = this.paneSessions.get(paneId);
17125
+ if (!name) {
17126
+ await this.refreshPanes();
17127
+ name = this.paneSessions.get(paneId);
17128
+ }
17129
+ if (!name) return;
17130
+ const postId = postIdForSessionName(name, this.sessions.keys());
17131
+ if (!postId) return;
17132
+ const session = this.sessions.get(postId);
17133
+ if (!session || session.agentState === state) return;
17134
+ session.agentState = state;
17135
+ this.emitter.emitSessionUpdate(session);
17136
+ }
17137
+ /** Rate-limited: a pane that will never resolve — one whose session has
17138
+ * since closed — would otherwise re-list on every message it sent, which is
17139
+ * a tmux subprocess per hook. */
17140
+ async refreshPanes() {
17141
+ const now = Date.now();
17142
+ if (now - this.paneReadAt < PANE_REREAD_MS) return;
17143
+ this.paneReadAt = now;
17144
+ const target = this.core.target();
17145
+ const out = await target.sh(
17146
+ `${tmuxCli(target)} list-panes -a -F '#{pane_id}|#{session_name}' 2>/dev/null || true`
17147
+ ).catch(() => "");
17148
+ this.paneSessions.clear();
17149
+ for (const raw of out.split("\n")) {
17150
+ const [pane, name] = raw.trim().split("|");
17151
+ if (pane && name) this.paneSessions.set(pane, name);
17152
+ }
16835
17153
  }
16836
17154
  // ── working-tree watching ──
16837
17155
  /** Start watching a session's tree. Idempotent: re-entering a session (the
16838
17156
  * modal reopening, a hydrate after restart) must not stack watchers. */
16839
17157
  startWatching(session) {
16840
17158
  const postId = session.postId;
17159
+ void this.startStateSub();
16841
17160
  if (this.watchers.has(postId)) return;
16842
17161
  const paths = this.core.target().paths;
16843
17162
  const root = session.workspacePath || paths.root;
@@ -16874,8 +17193,8 @@ var TerminalModeEngine = class {
16874
17193
  /** Suppress the change event our own write is about to produce, so a save
16875
17194
  * never comes back to the editor as an outside edit. Normalized to the form
16876
17195
  * the watcher emits, since callers spell the path either way. */
16877
- expectWrite(postId, path17) {
16878
- this.watchers.get(postId)?.expectWrite(posixNormalize(path17));
17196
+ expectWrite(postId, path20) {
17197
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path20));
16879
17198
  }
16880
17199
  // ── Lifecycle / ModeEngine ──
16881
17200
  start(base, args) {
@@ -16921,6 +17240,7 @@ var TerminalModeEngine = class {
16921
17240
  this.stopWatching(postId);
16922
17241
  this.reservedNames.delete(postId);
16923
17242
  this.sessions.delete(postId);
17243
+ this.releaseStateSub();
16924
17244
  }
16925
17245
  async kill(postId) {
16926
17246
  let session = this.sessions.get(postId);
@@ -16938,6 +17258,7 @@ var TerminalModeEngine = class {
16938
17258
  const removedPostId = session.postId;
16939
17259
  this.reservedNames.delete(removedPostId);
16940
17260
  this.sessions.delete(removedPostId);
17261
+ this.releaseStateSub();
16941
17262
  this.emitter.emitSessionRemoved(removedPostId);
16942
17263
  }
16943
17264
  destroy() {
@@ -17070,18 +17391,18 @@ var TerminalModeEngine = class {
17070
17391
  this.reservedNames.values()
17071
17392
  );
17072
17393
  this.reservedNames.set(session.postId, workName);
17073
- const { path: path17 } = await addWorktree(
17394
+ const { path: path20 } = await addWorktree(
17074
17395
  this.core.target(),
17075
17396
  workName,
17076
17397
  branch,
17077
17398
  "terminal"
17078
17399
  );
17079
- session.workspacePath = path17;
17400
+ session.workspacePath = path20;
17080
17401
  await setGitIdentity(
17081
17402
  this.core.target(),
17082
17403
  config.gitName,
17083
17404
  config.gitEmail,
17084
- path17
17405
+ path20
17085
17406
  );
17086
17407
  this.emitter.addLog(session, "system", "Worktree ready.");
17087
17408
  } else {
@@ -17143,11 +17464,11 @@ var TerminalModeEngine = class {
17143
17464
  }
17144
17465
  async bareFsList({
17145
17466
  postId,
17146
- path: path17
17467
+ path: path20
17147
17468
  }) {
17148
17469
  const target = this.getBareTarget(postId);
17149
17470
  const root = this.getBareRoot(postId);
17150
- const abs = this.resolveBarePath(path17, root);
17471
+ const abs = this.resolveBarePath(path20, root);
17151
17472
  const relRoot = abs.slice(root.length) || "/";
17152
17473
  const out = await target.run([
17153
17474
  "sh",
@@ -17197,30 +17518,30 @@ var TerminalModeEngine = class {
17197
17518
  }
17198
17519
  async bareFsRead({
17199
17520
  postId,
17200
- path: path17
17521
+ path: path20
17201
17522
  }) {
17202
17523
  const target = this.getBareTarget(postId);
17203
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17524
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17204
17525
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17205
17526
  '`)).trim();
17206
17527
  return { content: b64, encoding: "base64" };
17207
17528
  }
17208
17529
  async bareFsWrite({
17209
17530
  postId,
17210
- path: path17,
17531
+ path: path20,
17211
17532
  content,
17212
17533
  encoding
17213
17534
  }) {
17214
17535
  const target = this.getBareTarget(postId);
17215
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17536
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17216
17537
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17217
17538
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17218
17539
  throw new Error("Invalid base64 payload.");
17219
17540
  }
17220
- this.expectWrite(String(postId), path17);
17541
+ this.expectWrite(String(postId), path20);
17221
17542
  await writeBase64File(target, abs, b64);
17222
17543
  const session = this.sessions.get(String(postId));
17223
- if (session) delete session.buffers[path17];
17544
+ if (session) delete session.buffers[path20];
17224
17545
  }
17225
17546
  /** Append one chunk of a dropped image to the per-space drops dir
17226
17547
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17270,39 +17591,39 @@ var TerminalModeEngine = class {
17270
17591
  }
17271
17592
  bareOpenFile({
17272
17593
  postId: rawPostId,
17273
- path: path17
17594
+ path: path20
17274
17595
  }) {
17275
17596
  const session = this.sessions.get(String(rawPostId));
17276
17597
  if (!session || session.mode !== "bare") return;
17277
- if (!session.openedFiles.includes(path17)) {
17278
- session.openedFiles.push(path17);
17598
+ if (!session.openedFiles.includes(path20)) {
17599
+ session.openedFiles.push(path20);
17279
17600
  }
17280
17601
  }
17281
17602
  bareCloseFile({
17282
17603
  postId: rawPostId,
17283
- path: path17
17604
+ path: path20
17284
17605
  }) {
17285
17606
  const session = this.sessions.get(String(rawPostId));
17286
17607
  if (!session || session.mode !== "bare") return;
17287
- session.openedFiles = session.openedFiles.filter((p) => p !== path17);
17288
- delete session.buffers[path17];
17608
+ session.openedFiles = session.openedFiles.filter((p) => p !== path20);
17609
+ delete session.buffers[path20];
17289
17610
  }
17290
17611
  bareBufferSet({
17291
17612
  postId: rawPostId,
17292
- path: path17,
17613
+ path: path20,
17293
17614
  content
17294
17615
  }) {
17295
17616
  const session = this.sessions.get(String(rawPostId));
17296
17617
  if (!session || session.mode !== "bare") return;
17297
- session.buffers[path17] = content;
17618
+ session.buffers[path20] = content;
17298
17619
  }
17299
17620
  async bareFsDelete({
17300
17621
  postId,
17301
- path: path17
17622
+ path: path20
17302
17623
  }) {
17303
17624
  const target = this.getBareTarget(postId);
17304
17625
  const root = this.getBareRoot(postId);
17305
- const abs = this.resolveBarePath(path17, root);
17626
+ const abs = this.resolveBarePath(path20, root);
17306
17627
  if (abs === root) {
17307
17628
  throw new Error("Refusing to delete the repo root.");
17308
17629
  }
@@ -17310,10 +17631,10 @@ var TerminalModeEngine = class {
17310
17631
  }
17311
17632
  async bareFsMkdir({
17312
17633
  postId,
17313
- path: path17
17634
+ path: path20
17314
17635
  }) {
17315
17636
  const target = this.getBareTarget(postId);
17316
- const abs = this.resolveBarePath(path17, this.getBareRoot(postId));
17637
+ const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17317
17638
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17318
17639
  }
17319
17640
  async bareReview({
@@ -17456,53 +17777,63 @@ var TerminalModeEngine = class {
17456
17777
  return [];
17457
17778
  }
17458
17779
  }
17459
- /** Pane pids for this card's tabs, keyed by tab id. The pane's own shell is
17460
- * the root of everything the user started in that tab. */
17780
+ /** This card's panes: the pid its tree hangs off, and the pane id its
17781
+ * processes carry in their environment. */
17461
17782
  async panePids(target, postId) {
17462
17783
  const prefix = `terminal-${postId.replace(/[^a-zA-Z0-9._-]/g, "_")}-`;
17463
17784
  const stdout = await target.sh(
17464
- `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_pid}' 2>/dev/null || true`
17785
+ `${tmuxCli(target)} list-panes -a -F '#{session_name}|#{pane_pid}|#{pane_id}' 2>/dev/null || true`
17465
17786
  ).catch(() => "");
17466
17787
  const panes = [];
17467
17788
  for (const raw of stdout.split("\n")) {
17468
17789
  const line = raw.trim();
17469
17790
  if (!line.startsWith(prefix)) continue;
17470
- const rest = line.slice(prefix.length);
17471
- const sep = rest.indexOf("|");
17472
- if (sep === -1) continue;
17473
- const pid = Number(rest.slice(sep + 1));
17474
- if (!Number.isInteger(pid) || pid <= 0) continue;
17475
- panes.push({ tabId: rest.slice(0, sep), pid });
17791
+ const [tabId, rawPid, paneId] = line.slice(prefix.length).split("|");
17792
+ const pid = Number(rawPid);
17793
+ if (!tabId || !paneId || !Number.isInteger(pid) || pid <= 0) continue;
17794
+ panes.push({ tabId, pid, paneId });
17476
17795
  }
17477
17796
  return panes;
17478
17797
  }
17798
+ /**
17799
+ * Every process this card is running, by pid. Shared with bareKillProcess,
17800
+ * so stopping a process accepts exactly the set the panel shows.
17801
+ *
17802
+ * The `-p <pids>` on the environment call is required, not a saving: macOS
17803
+ * omits environments from an unscoped `ps -axeww` — see panesByPid.
17804
+ */
17805
+ async cardProcesses(target, postId) {
17806
+ const panes = await this.panePids(target, postId);
17807
+ if (panes.length === 0) return /* @__PURE__ */ new Map();
17808
+ const table = parsePsTable(
17809
+ await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17810
+ );
17811
+ return processesForCard(
17812
+ panes,
17813
+ table,
17814
+ async (pids) => panesByPid(
17815
+ await target.run(["ps", "eww", "-o", "pid=,command=", "-p", pids.join(",")]).catch(() => "")
17816
+ )
17817
+ );
17818
+ }
17479
17819
  async bareListProcesses({
17480
17820
  postId: rawPostId
17481
17821
  }) {
17482
17822
  const postId = String(rawPostId);
17483
17823
  const target = this.getBareTarget(postId);
17484
- const panes = await this.panePids(target, postId);
17485
- if (panes.length === 0) return [];
17486
- const table = parsePsTable(
17487
- await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17488
- );
17489
- const found = [];
17490
- for (const pane of panes) {
17491
- for (const row of processesForPane(table, pane.pid)) {
17492
- found.push({
17493
- pid: row.pid,
17494
- tabId: pane.tabId,
17495
- command: commandOf(row.args),
17496
- args: row.args,
17497
- cpu: row.cpu,
17498
- memoryKb: row.rssKb,
17499
- elapsed: row.elapsed,
17500
- ports: [],
17501
- connections: 0
17502
- });
17503
- }
17504
- }
17505
- if (found.length === 0) return [];
17824
+ const mine = await this.cardProcesses(target, postId);
17825
+ if (mine.size === 0) return [];
17826
+ const found = Array.from(mine.values(), ({ tabId, row }) => ({
17827
+ pid: row.pid,
17828
+ tabId,
17829
+ command: commandOf(row.args),
17830
+ args: row.args,
17831
+ cpu: row.cpu,
17832
+ memoryKb: row.rssKb,
17833
+ elapsed: row.elapsed,
17834
+ ports: [],
17835
+ connections: 0
17836
+ }));
17506
17837
  const sockets = await socketsFor(
17507
17838
  target,
17508
17839
  found.map((p) => p.pid)
@@ -17525,14 +17856,8 @@ var TerminalModeEngine = class {
17525
17856
  if (!Number.isInteger(pid) || pid <= 1) {
17526
17857
  throw new Error("Not a process this card can stop.");
17527
17858
  }
17528
- const panes = await this.panePids(target, postId);
17529
- const table = parsePsTable(
17530
- await target.run(["ps", "-axo", PS_FORMAT]).catch(() => "")
17531
- );
17532
- const mine = panes.some(
17533
- (pane) => processesForPane(table, pane.pid).some((row) => row.pid === pid)
17534
- );
17535
- if (!mine) {
17859
+ const mine = await this.cardProcesses(target, postId);
17860
+ if (!mine.has(pid)) {
17536
17861
  throw new Error("That process is not running in this card.");
17537
17862
  }
17538
17863
  await target.run(["kill", "-TERM", String(pid)]);
@@ -17991,8 +18316,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
17991
18316
  };
17992
18317
  }
17993
18318
  }
17994
- async function githubApi(token, path17) {
17995
- const res = await fetch(`${API_BASE}${path17}`, {
18319
+ async function githubApi(token, path20) {
18320
+ const res = await fetch(`${API_BASE}${path20}`, {
17996
18321
  headers: {
17997
18322
  Authorization: `Bearer ${token}`,
17998
18323
  Accept: "application/vnd.github+json",
@@ -18001,7 +18326,7 @@ async function githubApi(token, path17) {
18001
18326
  });
18002
18327
  if (!res.ok) {
18003
18328
  throw new Error(
18004
- `GitHub ${path17} failed (${res.status}): ${await res.text()}`
18329
+ `GitHub ${path20} failed (${res.status}): ${await res.text()}`
18005
18330
  );
18006
18331
  }
18007
18332
  return await res.json();
@@ -18046,13 +18371,13 @@ async function listGithubBranches(token, fullName) {
18046
18371
  }
18047
18372
 
18048
18373
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18049
- var import_path8 = __toESM(require("path"));
18374
+ var import_path9 = __toESM(require("path"));
18050
18375
 
18051
18376
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18052
18377
  var import_child_process4 = require("child_process");
18053
18378
  var import_crypto2 = __toESM(require("crypto"));
18054
- var import_promises5 = __toESM(require("fs/promises"));
18055
- var import_path7 = __toESM(require("path"));
18379
+ var import_promises6 = __toESM(require("fs/promises"));
18380
+ var import_path8 = __toESM(require("path"));
18056
18381
  var import_util6 = require("util");
18057
18382
  var execFileAsync3 = (0, import_util6.promisify)(import_child_process4.execFile);
18058
18383
  var IDENTITY_FILE = "age-identity.enc";
@@ -18085,7 +18410,7 @@ function deriveKey(password, salt) {
18085
18410
  }
18086
18411
  async function secretsInitialized(configDir) {
18087
18412
  try {
18088
- await import_promises5.default.access(import_path7.default.join(configDir, IDENTITY_FILE));
18413
+ await import_promises6.default.access(import_path8.default.join(configDir, IDENTITY_FILE));
18089
18414
  return true;
18090
18415
  } catch {
18091
18416
  return false;
@@ -18124,10 +18449,10 @@ async function initSecrets(configDir, password) {
18124
18449
  tag: cipher.getAuthTag().toString("base64"),
18125
18450
  data: data.toString("base64")
18126
18451
  };
18127
- await import_promises5.default.writeFile(import_path7.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18452
+ await import_promises6.default.writeFile(import_path8.default.join(configDir, IDENTITY_FILE), JSON.stringify(box), {
18128
18453
  mode: 384
18129
18454
  });
18130
- await import_promises5.default.writeFile(import_path7.default.join(configDir, RECIPIENT_FILE), `${recipient}
18455
+ await import_promises6.default.writeFile(import_path8.default.join(configDir, RECIPIENT_FILE), `${recipient}
18131
18456
  `, {
18132
18457
  mode: 384
18133
18458
  });
@@ -18150,17 +18475,17 @@ function decryptIdentity(box, password) {
18150
18475
  }
18151
18476
  }
18152
18477
  function envPath(spaceDir) {
18153
- return import_path7.default.join(spaceDir, "secrets", ENV_FILE);
18478
+ return import_path8.default.join(spaceDir, "secrets", ENV_FILE);
18154
18479
  }
18155
18480
  async function identityFor(configDir, password) {
18156
18481
  const box = JSON.parse(
18157
- await import_promises5.default.readFile(import_path7.default.join(configDir, IDENTITY_FILE), "utf-8")
18482
+ await import_promises6.default.readFile(import_path8.default.join(configDir, IDENTITY_FILE), "utf-8")
18158
18483
  );
18159
18484
  return decryptIdentity(box, password);
18160
18485
  }
18161
18486
  async function readEnv(spaceDir, identity) {
18162
18487
  try {
18163
- await import_promises5.default.access(envPath(spaceDir));
18488
+ await import_promises6.default.access(envPath(spaceDir));
18164
18489
  } catch {
18165
18490
  return {};
18166
18491
  }
@@ -18189,14 +18514,14 @@ async function readEnv(spaceDir, identity) {
18189
18514
  return values;
18190
18515
  }
18191
18516
  async function writeEnv(configDir, spaceDir, identity, values) {
18192
- const recipient = (await import_promises5.default.readFile(import_path7.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18517
+ const recipient = (await import_promises6.default.readFile(import_path8.default.join(configDir, RECIPIENT_FILE), "utf-8")).trim();
18193
18518
  const plain = `${Object.entries(values).map(([k, v]) => `${k}=${v}`).join("\n")}
18194
18519
  `;
18195
18520
  const tmp = envPath(spaceDir).replace(
18196
18521
  /\.enc\.env$/,
18197
18522
  `.${process.pid}.tmp.env`
18198
18523
  );
18199
- await import_promises5.default.writeFile(tmp, plain, { mode: 384 });
18524
+ await import_promises6.default.writeFile(tmp, plain, { mode: 384 });
18200
18525
  try {
18201
18526
  const { stdout } = await execFileAsync3(
18202
18527
  "sops",
@@ -18215,9 +18540,9 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18215
18540
  maxBuffer: 4 * 1024 * 1024
18216
18541
  }
18217
18542
  );
18218
- await import_promises5.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18543
+ await import_promises6.default.writeFile(envPath(spaceDir), stdout, { mode: 384 });
18219
18544
  } finally {
18220
- await import_promises5.default.rm(tmp, { force: true });
18545
+ await import_promises6.default.rm(tmp, { force: true });
18221
18546
  }
18222
18547
  }
18223
18548
  async function listSecretKeys(configDir, spaceDir, password) {
@@ -18255,16 +18580,6 @@ var FAILED_MARKER = "DEPLOY_FAILED:";
18255
18580
  var MAX_MARKERLESS_TURNS = 3;
18256
18581
  var COMMAND_TIMEOUT_MS = 45 * 6e4;
18257
18582
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18258
- function redactor(values) {
18259
- const significant = values.filter((v) => v.length >= 4);
18260
- return (text) => {
18261
- let out = text;
18262
- for (const value2 of significant) {
18263
- out = out.split(value2).join("[REDACTED]");
18264
- }
18265
- return out;
18266
- };
18267
- }
18268
18583
  function tailMarker(text) {
18269
18584
  const zone = text.slice(-2e3);
18270
18585
  let best = null;
@@ -18281,10 +18596,52 @@ function tailMarker(text) {
18281
18596
  const value2 = zone.slice(best.at + best.marker.length).trim();
18282
18597
  return value2 ? { marker: best.marker, value: value2 } : null;
18283
18598
  }
18599
+ function containedCwd(cwd, spaceDir) {
18600
+ if (!cwd) return null;
18601
+ const resolved = import_path9.default.resolve(cwd);
18602
+ const root = import_path9.default.resolve(spaceDir);
18603
+ return resolved === root || resolved.startsWith(root + import_path9.default.sep) ? resolved : null;
18604
+ }
18605
+ 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);
18612
+ if ("denied" in auth) return { denied: true };
18613
+ try {
18614
+ secrets = await secretsForInjection(
18615
+ opts.configDir,
18616
+ opts.spaceDir,
18617
+ auth.password
18618
+ );
18619
+ } catch (err) {
18620
+ authError = err instanceof Error ? err.message : String(err);
18621
+ opts.status?.(`authorization failed: ${authError}`);
18622
+ }
18623
+ }
18624
+ const redact = redactor(Object.values(secrets));
18625
+ opts.status?.("authorized, running");
18626
+ hooks.sendLog({
18627
+ type: "system",
18628
+ content: "Authorized. Running the command with secrets injected..."
18629
+ });
18630
+ const res = await execLogin(command, {
18631
+ cwd: opts.cwd,
18632
+ env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18633
+ timeout: COMMAND_TIMEOUT_MS,
18634
+ maxBuffer: 32 * 1024 * 1024
18635
+ });
18636
+ const output = redact(res.output);
18637
+ hooks.sendLog({ type: "tool", content: `$ ${command}
18638
+ ${tail(output)}` });
18639
+ return { code: res.code, output };
18640
+ }
18284
18641
  async function startDeployRun(opts) {
18285
18642
  const { hooks } = opts;
18286
- const workspace = import_path8.default.join(opts.spaceDir, "workspace");
18287
- const configDir = import_path8.default.dirname(opts.spaceDir);
18643
+ const workspace = import_path9.default.join(opts.spaceDir, "workspace");
18644
+ const configDir = import_path9.default.dirname(opts.spaceDir);
18288
18645
  const status = await checkDeployStatus(opts);
18289
18646
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18290
18647
  throw new Error(
@@ -18305,7 +18662,7 @@ async function startDeployRun(opts) {
18305
18662
  await hostGit(workspace, "clean", "-fd");
18306
18663
  await ensureClaudeSkillLink(workspace);
18307
18664
  const contract = prompts_default.deployRun.join("\n");
18308
- const reminder = "Reminder: every declared deploy variable is intentionally unset in your shell. Never check for them or abort over them - route any command that needs one through NEED_SECRETS and the runner injects the values.";
18665
+ 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.";
18309
18666
  let prompt2 = opts.provider === "codex" ? `Use the "deploy" skill: load it and execute it step by step.
18310
18667
 
18311
18668
  ${reminder}` : `/deploy
@@ -18314,109 +18671,105 @@ ${reminder}`;
18314
18671
  let sessionId = "";
18315
18672
  let markerless = 0;
18316
18673
  let todos = [];
18317
- for (; ; ) {
18318
- if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18319
- const { proc, result } = await spawnAgent(
18320
- opts.provider,
18321
- hostTarget(opts.spaceDir),
18322
- {
18323
- cwd: workspace,
18324
- prompt: prompt2,
18325
- allowedTools: "Bash,Read,Grep,Glob,TodoWrite,WebSearch,WebFetch",
18326
- // The run's guardrails are structural (merge-gated skill, per-command
18327
- // authorization, runner-side injection); the auto classifier fights
18328
- // legitimate infrastructure commands, so the session itself runs open.
18329
- permissionMode: "bypassPermissions",
18330
- model: "claude-opus-4-6",
18331
- includePartialMessages: true,
18332
- resumeSessionId: sessionId || void 0,
18333
- appendSystemPrompt: sessionId ? void 0 : contract,
18334
- env: spaceEnv(opts.spaceDir),
18335
- callbacks: {
18336
- initialTodos: todos,
18337
- onLog: (entry) => hooks.sendLog(entry),
18338
- onTodos: (updated) => {
18339
- todos = updated;
18340
- hooks.onTodos(updated);
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
+ });
18694
+ try {
18695
+ for (; ; ) {
18696
+ if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18697
+ const { proc, result } = await spawnAgent(
18698
+ opts.provider,
18699
+ hostTarget(opts.spaceDir),
18700
+ {
18701
+ cwd: workspace,
18702
+ prompt: prompt2,
18703
+ allowedTools: "Bash,Read,Grep,Glob,TodoWrite,WebSearch,WebFetch,Skill",
18704
+ // The run's guardrails are structural (merge-gated skill, per-command
18705
+ // authorization, runner-side injection); the auto classifier fights
18706
+ // legitimate infrastructure commands, so the session itself runs open.
18707
+ permissionMode: "bypassPermissions",
18708
+ includePartialMessages: true,
18709
+ resumeSessionId: sessionId || void 0,
18710
+ appendSystemPrompt: sessionId ? void 0 : contract,
18711
+ env: spaceEnv(opts.spaceDir),
18712
+ callbacks: {
18713
+ initialTodos: todos,
18714
+ onLog: (entry) => hooks.sendLog(entry),
18715
+ onTodos: (updated) => {
18716
+ todos = updated;
18717
+ hooks.onTodos(updated);
18718
+ }
18341
18719
  }
18342
18720
  }
18343
- }
18344
- );
18345
- hooks.registerProc(proc);
18346
- const { text, sessionId: sid } = await result;
18347
- hooks.registerProc(null);
18348
- sessionId = sid || sessionId;
18349
- if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18350
- const found = tailMarker(text);
18351
- if (found?.marker === SECRETS_MARKER) {
18352
- const command = found.value.split("\n")[0].trim();
18353
- let secrets = null;
18354
- let denied = false;
18355
- let authError = "";
18356
- while (!secrets && !denied) {
18357
- const auth = await hooks.requestAuth(command, authError);
18358
- if ("denied" in auth) {
18359
- denied = true;
18360
- break;
18361
- }
18362
- try {
18363
- secrets = await secretsForInjection(
18364
- configDir,
18365
- opts.spaceDir,
18366
- auth.password
18367
- );
18368
- } catch (err) {
18369
- authError = err instanceof Error ? err.message : String(err);
18721
+ );
18722
+ hooks.registerProc(proc);
18723
+ const { text, sessionId: sid } = await result;
18724
+ hooks.registerProc(null);
18725
+ sessionId = sid || sessionId;
18726
+ if (hooks.isCancelled()) throw new Error("Cancelled by user.");
18727
+ const found = tailMarker(text);
18728
+ if (found?.marker === SECRETS_MARKER) {
18729
+ const command = found.value.split("\n")[0].trim();
18730
+ const res = await authorizeAndRun({
18731
+ configDir,
18732
+ spaceDir: opts.spaceDir,
18733
+ cwd: workspace,
18734
+ command,
18735
+ hooks
18736
+ });
18737
+ if ("denied" in res) {
18738
+ prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18739
+ continue;
18370
18740
  }
18741
+ markerless = 0;
18742
+ const clean = tail(res.output);
18743
+ prompt2 = `The authorized command finished (exit ${res.code}). Redacted output:
18744
+
18745
+ ${clean}`;
18746
+ continue;
18371
18747
  }
18372
- if (denied || !secrets) {
18373
- prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18748
+ if (found?.marker === HUMAN_MARKER) {
18749
+ markerless = 0;
18750
+ prompt2 = await hooks.requestHuman(found.value);
18374
18751
  continue;
18375
18752
  }
18376
- const redact = redactor(Object.values(secrets));
18753
+ if (found?.marker === COMPLETE_MARKER) {
18754
+ return { shipped: true, summary: found.value };
18755
+ }
18756
+ if (found?.marker === FAILED_MARKER) {
18757
+ return { shipped: false, summary: found.value };
18758
+ }
18759
+ markerless += 1;
18760
+ if (markerless >= MAX_MARKERLESS_TURNS) {
18761
+ throw new Error(
18762
+ "The run ended without declaring completion (no DEPLOY_COMPLETE after repeated turns)."
18763
+ );
18764
+ }
18377
18765
  hooks.sendLog({
18378
18766
  type: "system",
18379
- content: `Authorized. Running the command with secrets injected...`
18380
- });
18381
- const res = await execLogin(command, {
18382
- cwd: workspace,
18383
- env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18384
- timeout: COMMAND_TIMEOUT_MS,
18385
- maxBuffer: 32 * 1024 * 1024
18767
+ content: "Turn ended without a protocol marker - resuming the run..."
18386
18768
  });
18387
- const output = res.output;
18388
- const exitNote = `exit ${res.code}`;
18389
- markerless = 0;
18390
- const clean = redact(tail(output));
18391
- hooks.sendLog({ type: "tool", content: `$ ${command}
18392
- ${clean}` });
18393
- prompt2 = `The authorized command finished (${exitNote}). Redacted output:
18394
-
18395
- ${clean}`;
18396
- continue;
18397
- }
18398
- if (found?.marker === HUMAN_MARKER) {
18399
- markerless = 0;
18400
- prompt2 = await hooks.requestHuman(found.value);
18401
- continue;
18402
- }
18403
- if (found?.marker === COMPLETE_MARKER) {
18404
- return { shipped: true, summary: found.value };
18405
- }
18406
- if (found?.marker === FAILED_MARKER) {
18407
- return { shipped: false, summary: found.value };
18769
+ 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.";
18408
18770
  }
18409
- markerless += 1;
18410
- if (markerless >= MAX_MARKERLESS_TURNS) {
18411
- throw new Error(
18412
- "The run ended without declaring completion (no DEPLOY_COMPLETE after repeated turns)."
18413
- );
18414
- }
18415
- hooks.sendLog({
18416
- type: "system",
18417
- content: "Turn ended without a protocol marker - resuming the run..."
18418
- });
18419
- 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.";
18771
+ } finally {
18772
+ await bridge.close().catch(() => void 0);
18420
18773
  }
18421
18774
  }
18422
18775
 
@@ -18508,6 +18861,125 @@ async function getDriveRoot(accessToken) {
18508
18861
  };
18509
18862
  }
18510
18863
 
18864
+ // ../../shared/all/helpers/board-agent-core/space-core.ts
18865
+ var import_promises8 = require("fs/promises");
18866
+ var import_path11 = __toESM(require("path"));
18867
+
18868
+ // ../../shared/all/helpers/board-agent-core/plugin.ts
18869
+ var import_crypto3 = __toESM(require("crypto"));
18870
+ var import_fs3 = require("fs");
18871
+ var import_promises7 = require("fs/promises");
18872
+ var import_os3 = __toESM(require("os"));
18873
+ var import_path10 = __toESM(require("path"));
18874
+ var MARKETPLACE_NAME = "factiii-runner";
18875
+ var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
18876
+ var SECRETS_BIN = "factiii-secrets";
18877
+ function candidates() {
18878
+ 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")
18882
+ ];
18883
+ }
18884
+ function pluginRoot() {
18885
+ for (const dir of candidates()) {
18886
+ if ((0, import_fs3.existsSync)(import_path10.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18887
+ return dir;
18888
+ }
18889
+ }
18890
+ return null;
18891
+ }
18892
+ function renderedPluginRoot() {
18893
+ return import_path10.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18894
+ }
18895
+ async function runnerVersion(source) {
18896
+ const pkg = import_path10.default.join(source, "..", "package.json");
18897
+ const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
18898
+ if (!raw) return FALLBACK_VERSION;
18899
+ try {
18900
+ const { version } = JSON.parse(raw);
18901
+ return version || FALLBACK_VERSION;
18902
+ } catch {
18903
+ return FALLBACK_VERSION;
18904
+ }
18905
+ }
18906
+ var FALLBACK_VERSION = "0.0.0-unknown";
18907
+ async function treeFiles(dir) {
18908
+ const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
18909
+ const files = [];
18910
+ for (const entry of entries) {
18911
+ const full = import_path10.default.join(dir, entry.name);
18912
+ if (entry.isDirectory()) files.push(...await treeFiles(full));
18913
+ else if (entry.isFile()) files.push(full);
18914
+ }
18915
+ return files.sort();
18916
+ }
18917
+ async function fingerprint(source, url2, version = "") {
18918
+ const hash = import_crypto3.default.createHash("sha256").update(url2).update(version);
18919
+ for (const file of await treeFiles(source)) {
18920
+ hash.update(import_path10.default.relative(source, file));
18921
+ hash.update(await (0, import_promises7.readFile)(file));
18922
+ }
18923
+ return hash.digest("hex");
18924
+ }
18925
+ function stampPath(dest) {
18926
+ return `${dest}.stamp`;
18927
+ }
18928
+ var inFlight = /* @__PURE__ */ new Map();
18929
+ function renderPlugin(boardMcpUrl) {
18930
+ const running = inFlight.get(boardMcpUrl);
18931
+ if (running) return running;
18932
+ const started = renderPluginImpl(boardMcpUrl).finally(
18933
+ () => inFlight.delete(boardMcpUrl)
18934
+ );
18935
+ inFlight.set(boardMcpUrl, started);
18936
+ return started;
18937
+ }
18938
+ async function renderPluginImpl(boardMcpUrl) {
18939
+ const source = pluginRoot();
18940
+ if (!source) return null;
18941
+ const dest = renderedPluginRoot();
18942
+ const version = await runnerVersion(source);
18943
+ const want = await fingerprint(source, boardMcpUrl, version);
18944
+ const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
18945
+ if (have === want) return dest;
18946
+ await (0, import_promises7.mkdir)(import_path10.default.dirname(dest), { recursive: true });
18947
+ await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
18948
+ await (0, import_promises7.writeFile)(
18949
+ import_path10.default.join(dest, "factiii", ".mcp.json"),
18950
+ `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
18951
+ `
18952
+ );
18953
+ await stampVersion(dest, version);
18954
+ await (0, import_promises7.writeFile)(stampPath(dest), want);
18955
+ return dest;
18956
+ }
18957
+ async function stampVersion(dest, version) {
18958
+ const manifest = import_path10.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
18959
+ const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
18960
+ if (!raw) return;
18961
+ const parsed = JSON.parse(raw);
18962
+ await (0, import_promises7.writeFile)(
18963
+ manifest,
18964
+ `${JSON.stringify({ ...parsed, version }, null, 2)}
18965
+ `
18966
+ );
18967
+ }
18968
+ function boardMcpConfig(url2) {
18969
+ return {
18970
+ mcpServers: {
18971
+ "factiii-board": {
18972
+ type: "http",
18973
+ url: url2,
18974
+ bearer_token_env_var: BOARD_MCP_TOKEN_ENV,
18975
+ // Written literally, for claude to expand at connect time. Not a
18976
+ // template string: the `${…}` has to survive into the file.
18977
+ headers: { Authorization: "Bearer ${" + BOARD_MCP_TOKEN_ENV + "}" }
18978
+ }
18979
+ }
18980
+ };
18981
+ }
18982
+
18511
18983
  // ../../shared/all/helpers/board-agent-core/toolchain.ts
18512
18984
  var TOOLS = [
18513
18985
  { bin: "git" },
@@ -18736,6 +19208,8 @@ var SpaceCore = class {
18736
19208
  this.execTarget ??= hostTarget(this.spaceDir(), () => {
18737
19209
  const env = {};
18738
19210
  for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
19211
+ const board = this.boardMcp();
19212
+ if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
18739
19213
  return env;
18740
19214
  });
18741
19215
  return this.execTarget;
@@ -18765,10 +19239,6 @@ var SpaceCore = class {
18765
19239
  enabled: this.isOneDriveConnected()
18766
19240
  });
18767
19241
  }
18768
- const board = this.boardMcp();
18769
- if (board) {
18770
- lanes.push({ mcp: board, tokenEnv: BOARD_MCP_TOKEN_ENV, enabled: true });
18771
- }
18772
19242
  return lanes;
18773
19243
  }
18774
19244
  isReady() {
@@ -18919,6 +19389,8 @@ var SpaceCore = class {
18919
19389
  await Promise.all([
18920
19390
  this.provisionGitLane(status),
18921
19391
  this.provisionMcpLane(),
19392
+ this.provisionPluginLane(),
19393
+ this.provisionBinLane(),
18922
19394
  // Covers a host signed in outside the one-click button, which would
18923
19395
  // otherwise meet the wizard the first time it opens a terminal.
18924
19396
  markOnboardingComplete(target).catch(() => {
@@ -18961,17 +19433,89 @@ var SpaceCore = class {
18961
19433
  target.paths.repo
18962
19434
  );
18963
19435
  }
19436
+ /** Install the factiii plugin into both CLIs: the hooks behind the session
19437
+ * status dot, the skills this workspace provides, and the board MCP.
19438
+ *
19439
+ * A plugin rather than hand-written config because it is additive — unlike
19440
+ * pointing CLAUDE_CONFIG_DIR or CODEX_HOME at our own directory, which
19441
+ * would hide the user's own settings, skills, MCP entries and auth.
19442
+ *
19443
+ * Both CLIs read the same manifest, so one marketplace serves both, and
19444
+ * adding a marketplace or plugin that is already there is a no-op in each —
19445
+ * which is what makes this safe on every ensure. */
19446
+ async provisionPluginLane() {
19447
+ const board = this.boardMcp();
19448
+ const root = board ? await renderPlugin(board.url).catch(() => null) : null;
19449
+ if (!root) return;
19450
+ const q = `'${root}'`;
19451
+ const cmd = [
19452
+ `claude plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19453
+ `claude plugin install ${PLUGIN_ID} --scope user >/dev/null 2>&1 || true`,
19454
+ // Both CLIs install by COPYING the plugin into a cache keyed by its
19455
+ // version, and that copy is what loads. claude's `install` refuses once
19456
+ // something is installed ("already installed"), so without these two an
19457
+ // upgrade renders new files into a directory nothing reads again. Codex
19458
+ // needs no equivalent: its `add` re-installs from a local marketplace.
19459
+ `claude plugin marketplace update ${MARKETPLACE_NAME} >/dev/null 2>&1 || true`,
19460
+ `claude plugin update ${PLUGIN_ID} >/dev/null 2>&1 || true`,
19461
+ `codex plugin marketplace add ${q} >/dev/null 2>&1 || true`,
19462
+ `codex plugin add ${PLUGIN_ID} >/dev/null 2>&1 || true`
19463
+ ].join("; ");
19464
+ try {
19465
+ await this.target().sh(cmd);
19466
+ } catch {
19467
+ }
19468
+ }
19469
+ /** Stage the plugin's `factiii-secrets` onto the agent's PATH.
19470
+ *
19471
+ * A skill can only teach a command that exists. `paths.bin` is already the
19472
+ * PATH prefix `agentEnv()` hands every spawn (it is how `factiii-expose`
19473
+ * resolves), but nothing put anything there for a deploy run - so the
19474
+ * skill would name a command the shell could not find.
19475
+ *
19476
+ * Copied rather than symlinked: an `npm i -g @factiii/runner` upgrade
19477
+ * replaces the package directory, and a link into the old one dangles.
19478
+ * Re-copied whenever the sizes differ, which is what carries an upgrade
19479
+ * through without re-copying on every ensure. */
19480
+ async provisionBinLane() {
19481
+ const source = pluginRoot();
19482
+ if (!source) return;
19483
+ 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);
19486
+ try {
19487
+ const [src, staged] = await Promise.all([
19488
+ (0, import_promises8.stat)(from),
19489
+ (0, import_promises8.stat)(to).catch(() => null)
19490
+ ]);
19491
+ if (staged?.size === src.size) return;
19492
+ await (0, import_promises8.mkdir)(bin, { recursive: true });
19493
+ await (0, import_promises8.copyFile)(from, to);
19494
+ await (0, import_promises8.chmod)(to, 493);
19495
+ } catch {
19496
+ }
19497
+ }
18964
19498
  // Register this space's MCPs with both CLIs, user-scoped, and clear the ones
18965
19499
  // it cannot currently offer. Registering regardless left every space holding
18966
19500
  // an entry it never uses, which the CLI then fails to handshake with on every
18967
19501
  // run. Removal is unconditional so a disconnect cleans itself up.
19502
+ //
19503
+ // No early return on an empty lane list: since the board lane moved into the
19504
+ // plugin, a host with no OneDrive has nothing to add and still has the old
19505
+ // board entries to drop.
18968
19506
  async provisionMcpLane() {
18969
19507
  const lanes = this.mcpLanes();
18970
- if (!lanes.length) return;
19508
+ const board = this.boardMcp();
19509
+ const boardCodexTable = board?.name.replace(/-/g, "_");
18971
19510
  const cmd = [
18972
19511
  // Pre-rename entries, when the name was the same for every space.
18973
19512
  `claude mcp remove factiii-onedrive --scope user >/dev/null 2>&1 || true`,
18974
19513
  `codex mcp remove factiii_onedrive >/dev/null 2>&1 || true`,
19514
+ // The board lane moved into the plugin; drop what the old path wrote.
19515
+ ...board ? [
19516
+ `claude mcp remove ${board.name} --scope user >/dev/null 2>&1 || true`,
19517
+ `codex mcp remove ${boardCodexTable} >/dev/null 2>&1 || true`
19518
+ ] : [],
18975
19519
  ...lanes.flatMap(({ mcp, tokenEnv, enabled }) => {
18976
19520
  const codexTable = mcp.name.replace(/-/g, "_");
18977
19521
  const claudeJson = JSON.stringify({
@@ -19051,6 +19595,10 @@ var BoardAgentEngine = class _BoardAgentEngine {
19051
19595
  // In-flight creator session, memory-only (the repo is the source of truth
19052
19596
  // once merged). Survives modal reopen via deployCreatorState.
19053
19597
  this.skillCreatorSessionId = null;
19598
+ // Which CLI produced that session. A resume id belongs to ONE provider, so
19599
+ // switching the picker between drafting and revising has to start fresh
19600
+ // rather than hand a claude session id to codex.
19601
+ this.skillCreatorProvider = null;
19054
19602
  // Set when the draft was resumed from a pushed deploy-skill branch:
19055
19603
  // approve commits onto that branch instead of cutting a new one.
19056
19604
  this.skillDraftBranch = null;
@@ -19071,6 +19619,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19071
19619
  // ── Deploy run (host executor; see host-run.ts + .specs/deploy.md) ──
19072
19620
  // Memory-only: reopened clients reattach, a restart loses the run and says so.
19073
19621
  this.runState = {
19622
+ provider: "claude",
19074
19623
  phase: "idle",
19075
19624
  todos: [],
19076
19625
  logs: [],
@@ -19214,6 +19763,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19214
19763
  });
19215
19764
  const draft = await loadSkillDraft({ ...opts, branch: payload.branch });
19216
19765
  this.skillCreatorSessionId = null;
19766
+ this.skillCreatorProvider = null;
19217
19767
  this.skillDraftBranch = payload.branch;
19218
19768
  this.creatorState = { phase: "review", logs: [], draft, error: "" };
19219
19769
  return draft;
@@ -19259,8 +19809,8 @@ var BoardAgentEngine = class _BoardAgentEngine {
19259
19809
  throw err;
19260
19810
  }
19261
19811
  }
19262
- deployFixEnvironment(sendLog) {
19263
- const opts = this.deployCreatorOpts(sendLog);
19812
+ deployFixEnvironment(sendLog, provider) {
19813
+ const opts = this.deployCreatorOpts(sendLog, provider);
19264
19814
  return this.runEnvPass(
19265
19815
  "fixing",
19266
19816
  () => fixEnvironment({
@@ -19324,13 +19874,14 @@ var BoardAgentEngine = class _BoardAgentEngine {
19324
19874
  return this.activity.clear("deploy-run");
19325
19875
  }
19326
19876
  }
19327
- deployRunStart(sendLog) {
19877
+ deployRunStart(sendLog, provider) {
19328
19878
  if (this.runState.phase === "running" || this.runState.phase === "awaiting-input" || this.runState.phase === "awaiting-auth") {
19329
19879
  throw new Error("A deploy run is already in progress.");
19330
19880
  }
19331
- const opts = this.deployCreatorOpts(sendLog);
19881
+ const opts = this.deployCreatorOpts(sendLog, provider);
19332
19882
  this.runCancelled = false;
19333
19883
  this.runState = {
19884
+ provider: opts.provider,
19334
19885
  phase: "running",
19335
19886
  todos: [],
19336
19887
  logs: [],
@@ -19451,6 +20002,9 @@ var BoardAgentEngine = class _BoardAgentEngine {
19451
20002
  throw new Error("A deploy run is still in progress.");
19452
20003
  }
19453
20004
  this.runState = {
20005
+ // Unread while idle - the next run sets its own. Carried rather than
20006
+ // reset so nothing has to invent a provider that means nothing yet.
20007
+ provider: this.runState.provider,
19454
20008
  phase: "idle",
19455
20009
  todos: [],
19456
20010
  logs: [],
@@ -19549,7 +20103,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19549
20103
  throw err;
19550
20104
  }
19551
20105
  }
19552
- deployCreatorOpts(sendLog) {
20106
+ deployCreatorOpts(sendLog, provider) {
19553
20107
  const config = this.core.readConfig();
19554
20108
  if (!config.repoUrl || !config.githubToken) {
19555
20109
  throw new Error("Set the repository URL and GitHub token first.");
@@ -19559,36 +20113,51 @@ var BoardAgentEngine = class _BoardAgentEngine {
19559
20113
  repoUrl: config.repoUrl,
19560
20114
  mainBranch: config.mainBranch,
19561
20115
  githubToken: config.githubToken,
19562
- provider: this.core.agentProvider(),
20116
+ // resolveProvider coerces anything unknown to claude, so it can only be
20117
+ // asked about a value that is actually there.
20118
+ provider: provider ? resolveProvider(provider) : this.core.agentProvider(),
19563
20119
  sendLog
19564
20120
  };
19565
20121
  }
19566
20122
  deployCreateSkill(payload, sendLog) {
19567
20123
  this.skillDraftBranch = null;
19568
20124
  return this.runCreatorPass(async () => {
20125
+ const opts = this.deployCreatorOpts(
20126
+ this.creatorLogger(sendLog),
20127
+ payload.provider
20128
+ );
19569
20129
  const { sessionId, ...draft } = await generateDeploySkill({
19570
- ...this.deployCreatorOpts(this.creatorLogger(sendLog)),
20130
+ ...opts,
19571
20131
  instructions: payload.instructions
19572
20132
  });
19573
20133
  this.skillCreatorSessionId = sessionId || null;
20134
+ this.skillCreatorProvider = opts.provider;
19574
20135
  return draft;
19575
20136
  });
19576
20137
  }
19577
20138
  deployReviseSkill(payload, sendLog) {
19578
20139
  return this.runCreatorPass(async () => {
20140
+ const opts = this.deployCreatorOpts(
20141
+ this.creatorLogger(sendLog),
20142
+ payload.provider
20143
+ );
20144
+ const resumable = this.skillCreatorProvider === opts.provider;
19579
20145
  const { sessionId, ...draft } = await reviseDeploySkill({
19580
- ...this.deployCreatorOpts(this.creatorLogger(sendLog)),
20146
+ ...opts,
19581
20147
  feedback: payload.feedback,
19582
- resumeSessionId: this.skillCreatorSessionId ?? void 0
20148
+ // A different CLI than drafted this: its session id means nothing to
20149
+ // the new one, so the revision starts from the files on disk.
20150
+ resumeSessionId: resumable ? this.skillCreatorSessionId ?? void 0 : void 0
19583
20151
  });
19584
20152
  this.skillCreatorSessionId = sessionId || this.skillCreatorSessionId;
20153
+ this.skillCreatorProvider = opts.provider;
19585
20154
  return draft;
19586
20155
  });
19587
20156
  }
19588
20157
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
19589
20158
  secretsDirs() {
19590
20159
  const spaceDir = this.core.spaceDir();
19591
- return { configDir: (0, import_path9.dirname)(spaceDir), spaceDir };
20160
+ return { configDir: (0, import_path12.dirname)(spaceDir), spaceDir };
19592
20161
  }
19593
20162
  async deploySecretsStatus() {
19594
20163
  return {
@@ -19622,6 +20191,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19622
20191
  const config = this.core.readConfig();
19623
20192
  const onBranch = this.skillDraftBranch ?? void 0;
19624
20193
  this.skillCreatorSessionId = null;
20194
+ this.skillCreatorProvider = null;
19625
20195
  this.skillDraftBranch = null;
19626
20196
  this.creatorState = { phase: "idle", logs: [], draft: null, error: "" };
19627
20197
  this.activity.clear("deploy-skill");
@@ -19634,6 +20204,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19634
20204
  }
19635
20205
  deployDiscardSkill() {
19636
20206
  this.skillCreatorSessionId = null;
20207
+ this.skillCreatorProvider = null;
19637
20208
  this.skillDraftBranch = null;
19638
20209
  this.creatorState = { phase: "idle", logs: [], draft: null, error: "" };
19639
20210
  this.activity.clear("deploy-skill");
@@ -20438,14 +21009,16 @@ ${c.content.trim() || "(no description)"}`
20438
21009
  case "deployFixEnvironment":
20439
21010
  return await this.deployFixEnvironment(
20440
21011
  askLogSender ?? (() => {
20441
- })
21012
+ }),
21013
+ payload?.provider
20442
21014
  );
20443
21015
  case "deployEnvState":
20444
21016
  return this.deployEnvState();
20445
21017
  case "deployRunStart":
20446
21018
  return this.deployRunStart(
20447
21019
  askLogSender ?? (() => {
20448
- })
21020
+ }),
21021
+ payload?.provider
20449
21022
  );
20450
21023
  case "deployRunState":
20451
21024
  return this.deployRunState();
@@ -20645,11 +21218,11 @@ ${c.content.trim() || "(no description)"}`
20645
21218
 
20646
21219
  // ../../shared/all/helpers/board-agent-core/index-service.ts
20647
21220
  var import_child_process5 = require("child_process");
20648
- var import_crypto3 = require("crypto");
20649
- var import_fs3 = require("fs");
20650
- var import_promises6 = __toESM(require("fs/promises"));
20651
- var import_net2 = require("net");
20652
- var import_path10 = require("path");
21221
+ var import_crypto4 = require("crypto");
21222
+ var import_fs4 = require("fs");
21223
+ var import_promises9 = __toESM(require("fs/promises"));
21224
+ var import_net3 = require("net");
21225
+ var import_path13 = require("path");
20653
21226
  var import_util7 = require("util");
20654
21227
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
20655
21228
  var IndexService = class {
@@ -20663,7 +21236,7 @@ var IndexService = class {
20663
21236
  /** Per-space bearer for MCP + file, derived from the control token (the
20664
21237
  * service computes the same). */
20665
21238
  bearerToken(spaceSlug) {
20666
- return (0, import_crypto3.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21239
+ return (0, import_crypto4.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
20667
21240
  }
20668
21241
  /** MCP bridge for a space. Null until the service is up (which it kicks
20669
21242
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -20708,7 +21281,7 @@ var IndexService = class {
20708
21281
  const log = await this.openLog();
20709
21282
  this.proc = (0, import_child_process5.spawn)(
20710
21283
  process.execPath,
20711
- [(0, import_path10.join)(this.opts.indexDir, "index.js")],
21284
+ [(0, import_path13.join)(this.opts.indexDir, "index.js")],
20712
21285
  {
20713
21286
  cwd: this.opts.indexDir,
20714
21287
  env: {
@@ -20736,8 +21309,8 @@ var IndexService = class {
20736
21309
  * opened (a missing log must never stop the service from starting). */
20737
21310
  async openLog() {
20738
21311
  try {
20739
- await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true });
20740
- return (0, import_fs3.openSync)((0, import_path10.join)(this.opts.dataDir, "index.log"), "a");
21312
+ 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");
20741
21314
  } catch {
20742
21315
  return null;
20743
21316
  }
@@ -20746,10 +21319,10 @@ var IndexService = class {
20746
21319
  * platform needing node-gyp fails here rather than at query time. */
20747
21320
  async installDeps() {
20748
21321
  const dir = this.opts.indexDir;
20749
- if ((0, import_fs3.existsSync)((0, import_path10.join)(dir, "node_modules"))) return;
20750
- await import_promises6.default.copyFile(
20751
- (0, import_path10.join)(dir, "image-package.json"),
20752
- (0, import_path10.join)(dir, "package.json")
21322
+ if ((0, import_fs4.existsSync)((0, import_path13.join)(dir, "node_modules"))) return;
21323
+ await import_promises9.default.copyFile(
21324
+ (0, import_path13.join)(dir, "image-package.json"),
21325
+ (0, import_path13.join)(dir, "package.json")
20753
21326
  );
20754
21327
  await execFileAsync4(
20755
21328
  "npm",
@@ -20761,15 +21334,15 @@ var IndexService = class {
20761
21334
  * CLIs' persistent MCP config, so a fresh port each start would leave every
20762
21335
  * registration pointing at a dead one. */
20763
21336
  async stablePort() {
20764
- const file = (0, import_path10.join)(this.opts.dataDir, ".port");
20765
- const saved = Number(await import_promises6.default.readFile(file, "utf-8").catch(() => ""));
21337
+ const file = (0, import_path13.join)(this.opts.dataDir, ".port");
21338
+ const saved = Number(await import_promises9.default.readFile(file, "utf-8").catch(() => ""));
20766
21339
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
20767
21340
  return saved;
20768
21341
  }
20769
21342
  const port = await freePort();
20770
- await import_promises6.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
21343
+ await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true }).catch(() => {
20771
21344
  });
20772
- await import_promises6.default.writeFile(file, String(port), "utf-8").catch(() => {
21345
+ await import_promises9.default.writeFile(file, String(port), "utf-8").catch(() => {
20773
21346
  });
20774
21347
  return port;
20775
21348
  }
@@ -20863,14 +21436,14 @@ var IndexService = class {
20863
21436
  };
20864
21437
  function isFree(port) {
20865
21438
  return new Promise((resolve2) => {
20866
- const srv = (0, import_net2.createServer)();
21439
+ const srv = (0, import_net3.createServer)();
20867
21440
  srv.once("error", () => resolve2(false));
20868
21441
  srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
20869
21442
  });
20870
21443
  }
20871
21444
  function freePort() {
20872
21445
  return new Promise((resolve2, reject) => {
20873
- const srv = (0, import_net2.createServer)();
21446
+ const srv = (0, import_net3.createServer)();
20874
21447
  srv.on("error", reject);
20875
21448
  srv.listen(0, "127.0.0.1", () => {
20876
21449
  const addr = srv.address();
@@ -20936,25 +21509,25 @@ async function pairWithBrowser(serverUrl) {
20936
21509
  }
20937
21510
 
20938
21511
  // src/config.ts
20939
- var import_fs7 = __toESM(require("fs"));
20940
- var import_os5 = __toESM(require("os"));
20941
- var import_path15 = __toESM(require("path"));
21512
+ var import_fs8 = __toESM(require("fs"));
21513
+ var import_os6 = __toESM(require("os"));
21514
+ var import_path18 = __toESM(require("path"));
20942
21515
 
20943
21516
  // src/secureFile.ts
20944
- var import_crypto6 = __toESM(require("crypto"));
20945
- var import_fs6 = __toESM(require("fs"));
20946
- var import_path14 = __toESM(require("path"));
21517
+ var import_crypto7 = __toESM(require("crypto"));
21518
+ var import_fs7 = __toESM(require("fs"));
21519
+ var import_path17 = __toESM(require("path"));
20947
21520
 
20948
21521
  // src/keychain.ts
20949
21522
  var import_child_process7 = require("child_process");
20950
- var import_crypto4 = __toESM(require("crypto"));
20951
- var import_fs4 = __toESM(require("fs"));
20952
- var import_os3 = __toESM(require("os"));
20953
- var import_path11 = __toESM(require("path"));
21523
+ var import_crypto5 = __toESM(require("crypto"));
21524
+ var import_fs5 = __toESM(require("fs"));
21525
+ var import_os4 = __toESM(require("os"));
21526
+ var import_path14 = __toESM(require("path"));
20954
21527
  var SERVICE = "factiii-runner";
20955
21528
  var ACCOUNT = "config-encryption-key";
20956
- var DPAPI_KEY_FILE = import_path11.default.join(
20957
- import_os3.default.homedir(),
21529
+ var DPAPI_KEY_FILE = import_path14.default.join(
21530
+ import_os4.default.homedir(),
20958
21531
  ".factiii-runner",
20959
21532
  "config-key.dpapi"
20960
21533
  );
@@ -21034,7 +21607,7 @@ function linuxWrite(key) {
21034
21607
  function winRead() {
21035
21608
  let wrapped;
21036
21609
  try {
21037
- wrapped = import_fs4.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21610
+ wrapped = import_fs5.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
21038
21611
  } catch {
21039
21612
  return { state: "missing" };
21040
21613
  }
@@ -21059,8 +21632,8 @@ function winWrite(key) {
21059
21632
  "-Command",
21060
21633
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
21061
21634
  ]);
21062
- import_fs4.default.mkdirSync(import_path11.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21063
- import_fs4.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21635
+ import_fs5.default.mkdirSync(import_path14.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21636
+ import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21064
21637
  `, { mode: 384 });
21065
21638
  }
21066
21639
  function keychainBackend() {
@@ -21199,7 +21772,7 @@ function resolve() {
21199
21772
  cached = { key: null, status: diagnose() };
21200
21773
  return cached;
21201
21774
  }
21202
- const fresh = import_crypto4.default.randomBytes(32);
21775
+ const fresh = import_crypto5.default.randomBytes(32);
21203
21776
  writeKey(fresh);
21204
21777
  const verify = readKey();
21205
21778
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -21221,23 +21794,23 @@ function keychainStatus() {
21221
21794
  }
21222
21795
 
21223
21796
  // src/secureStore.ts
21224
- var import_crypto5 = __toESM(require("crypto"));
21225
- var import_fs5 = __toESM(require("fs"));
21226
- var import_path13 = __toESM(require("path"));
21797
+ var import_crypto6 = __toESM(require("crypto"));
21798
+ var import_fs6 = __toESM(require("fs"));
21799
+ var import_path16 = __toESM(require("path"));
21227
21800
 
21228
21801
  // src/paths.ts
21229
- var import_os4 = __toESM(require("os"));
21230
- var import_path12 = __toESM(require("path"));
21231
- var CONFIG_DIR = import_path12.default.join(import_os4.default.homedir(), ".factiii-runner");
21802
+ 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");
21232
21805
  function safeSlug(spaceSlug) {
21233
21806
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
21234
21807
  }
21235
21808
  function spaceDirPath(spaceSlug) {
21236
- return import_path12.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21809
+ return import_path15.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21237
21810
  }
21238
21811
 
21239
21812
  // src/secureStore.ts
21240
- var VAULT_KEY_FILE = import_path13.default.join(CONFIG_DIR, "vault-key.json");
21813
+ var VAULT_KEY_FILE = import_path16.default.join(CONFIG_DIR, "vault-key.json");
21241
21814
  var SCRYPT_N2 = 1 << 15;
21242
21815
  var MIN_PASSWORD_LENGTH = 8;
21243
21816
  var SecureStoreError = class extends Error {
@@ -21264,7 +21837,7 @@ function requireKeychainKey() {
21264
21837
  );
21265
21838
  }
21266
21839
  function passwordKey(password, salt) {
21267
- return import_crypto5.default.scryptSync(password, salt, 32, {
21840
+ return import_crypto6.default.scryptSync(password, salt, 32, {
21268
21841
  N: SCRYPT_N2,
21269
21842
  maxmem: 64 * 1024 * 1024
21270
21843
  });
@@ -21272,7 +21845,7 @@ function passwordKey(password, salt) {
21272
21845
  function wrapKey(machineKey, pwKey) {
21273
21846
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
21274
21847
  return Buffer.from(
21275
- import_crypto5.default.hkdfSync(
21848
+ import_crypto6.default.hkdfSync(
21276
21849
  "sha256",
21277
21850
  ikm,
21278
21851
  Buffer.alloc(0),
@@ -21282,8 +21855,8 @@ function wrapKey(machineKey, pwKey) {
21282
21855
  );
21283
21856
  }
21284
21857
  function seal(plaintext, key) {
21285
- const iv = import_crypto5.default.randomBytes(12);
21286
- const cipher = import_crypto5.default.createCipheriv("aes-256-gcm", key, iv);
21858
+ const iv = import_crypto6.default.randomBytes(12);
21859
+ const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
21287
21860
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
21288
21861
  return {
21289
21862
  iv: iv.toString("base64"),
@@ -21293,7 +21866,7 @@ function seal(plaintext, key) {
21293
21866
  }
21294
21867
  function unseal(box, key) {
21295
21868
  try {
21296
- const decipher = import_crypto5.default.createDecipheriv(
21869
+ const decipher = import_crypto6.default.createDecipheriv(
21297
21870
  "aes-256-gcm",
21298
21871
  key,
21299
21872
  Buffer.from(box.iv, "base64")
@@ -21310,7 +21883,7 @@ function unseal(box, key) {
21310
21883
  function readVaultFile() {
21311
21884
  try {
21312
21885
  const parsed = JSON.parse(
21313
- import_fs5.default.readFileSync(VAULT_KEY_FILE, "utf-8")
21886
+ import_fs6.default.readFileSync(VAULT_KEY_FILE, "utf-8")
21314
21887
  );
21315
21888
  return parsed.v === 1 ? parsed : null;
21316
21889
  } catch {
@@ -21318,15 +21891,15 @@ function readVaultFile() {
21318
21891
  }
21319
21892
  }
21320
21893
  function writeVaultFile(file) {
21321
- import_fs5.default.mkdirSync(CONFIG_DIR, { recursive: true });
21894
+ import_fs6.default.mkdirSync(CONFIG_DIR, { recursive: true });
21322
21895
  const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
21323
- import_fs5.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
21896
+ import_fs6.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
21324
21897
  `, { mode: 384 });
21325
- import_fs5.default.renameSync(tmp, VAULT_KEY_FILE);
21898
+ import_fs6.default.renameSync(tmp, VAULT_KEY_FILE);
21326
21899
  }
21327
21900
  var unlockedDek = null;
21328
21901
  function createVault(machineKey) {
21329
- const dek = import_crypto5.default.randomBytes(32);
21902
+ const dek = import_crypto6.default.randomBytes(32);
21330
21903
  writeVaultFile({
21331
21904
  v: 1,
21332
21905
  passwordEnabled: false,
@@ -21421,7 +21994,7 @@ function enablePassword(password) {
21421
21994
  );
21422
21995
  }
21423
21996
  const dek = dataKey();
21424
- const salt = import_crypto5.default.randomBytes(16);
21997
+ const salt = import_crypto6.default.randomBytes(16);
21425
21998
  writeVaultFile({
21426
21999
  v: 1,
21427
22000
  passwordEnabled: true,
@@ -21449,7 +22022,7 @@ function changePassword(current, next) {
21449
22022
  "The current password is not correct."
21450
22023
  );
21451
22024
  }
21452
- const salt = import_crypto5.default.randomBytes(16);
22025
+ const salt = import_crypto6.default.randomBytes(16);
21453
22026
  writeVaultFile({
21454
22027
  v: 1,
21455
22028
  passwordEnabled: true,
@@ -21526,8 +22099,8 @@ function storeStatus() {
21526
22099
  // src/secureFile.ts
21527
22100
  var MAGIC = "FACTIII1";
21528
22101
  function encrypt(plain, key, version) {
21529
- const iv = import_crypto6.default.randomBytes(12);
21530
- const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
22102
+ const iv = import_crypto7.default.randomBytes(12);
22103
+ const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
21531
22104
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
21532
22105
  const envelope = {
21533
22106
  v: version,
@@ -21538,7 +22111,7 @@ function encrypt(plain, key, version) {
21538
22111
  return `${MAGIC}${JSON.stringify(envelope)}`;
21539
22112
  }
21540
22113
  function decrypt(envelope, key) {
21541
- const decipher = import_crypto6.default.createDecipheriv(
22114
+ const decipher = import_crypto7.default.createDecipheriv(
21542
22115
  "aes-256-gcm",
21543
22116
  key,
21544
22117
  Buffer.from(envelope.iv, "base64")
@@ -21559,15 +22132,15 @@ function parseEnvelope(raw) {
21559
22132
  }
21560
22133
  }
21561
22134
  function atomicWrite(filePath, body) {
21562
- import_fs6.default.mkdirSync(import_path14.default.dirname(filePath), { recursive: true });
22135
+ import_fs7.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
21563
22136
  const tmp = `${filePath}.${process.pid}.tmp`;
21564
- import_fs6.default.writeFileSync(tmp, body, { mode: 384 });
21565
- import_fs6.default.renameSync(tmp, filePath);
22137
+ import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
22138
+ import_fs7.default.renameSync(tmp, filePath);
21566
22139
  }
21567
22140
  function readSecureBuffer(filePath) {
21568
22141
  let raw;
21569
22142
  try {
21570
- raw = import_fs6.default.readFileSync(filePath, "utf-8");
22143
+ raw = import_fs7.default.readFileSync(filePath, "utf-8");
21571
22144
  } catch {
21572
22145
  return null;
21573
22146
  }
@@ -21624,7 +22197,7 @@ function writeSecureJson(filePath, value2) {
21624
22197
  function readBootJson(filePath) {
21625
22198
  let raw;
21626
22199
  try {
21627
- raw = import_fs6.default.readFileSync(filePath, "utf-8");
22200
+ raw = import_fs7.default.readFileSync(filePath, "utf-8");
21628
22201
  } catch {
21629
22202
  return null;
21630
22203
  }
@@ -21674,8 +22247,8 @@ function writeBootJson(filePath, value2) {
21674
22247
  }
21675
22248
 
21676
22249
  // src/config.ts
21677
- var CONFIG_DIR2 = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
21678
- var CONFIG_FILE = import_path15.default.join(CONFIG_DIR2, "config.json");
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");
21679
22252
  function readRunnerConfig() {
21680
22253
  return readBootJson(CONFIG_FILE);
21681
22254
  }
@@ -21683,7 +22256,7 @@ function writeRunnerConfig(config) {
21683
22256
  writeBootJson(CONFIG_FILE, config);
21684
22257
  }
21685
22258
  function configExists() {
21686
- return import_fs7.default.existsSync(CONFIG_FILE);
22259
+ return import_fs8.default.existsSync(CONFIG_FILE);
21687
22260
  }
21688
22261
 
21689
22262
  // src/promptPassword.ts
@@ -21844,10 +22417,10 @@ function makeChunkReassembler() {
21844
22417
  }
21845
22418
 
21846
22419
  // src/daemon.ts
21847
- var import_crypto7 = require("crypto");
21848
- var import_fs9 = __toESM(require("fs"));
22420
+ var import_crypto8 = require("crypto");
22421
+ var import_fs10 = __toESM(require("fs"));
21849
22422
  var import_node_datachannel = require("node-datachannel");
21850
- var import_path17 = __toESM(require("path"));
22423
+ var import_path20 = __toESM(require("path"));
21851
22424
 
21852
22425
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
21853
22426
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23027,12 +23600,12 @@ function parse2(str) {
23027
23600
  uri.queryKey = queryKey(uri, uri["query"]);
23028
23601
  return uri;
23029
23602
  }
23030
- function pathNames(obj, path17) {
23031
- const regx = /\/{2,9}/g, names = path17.replace(regx, "/").split("/");
23032
- if (path17.slice(0, 1) == "/" || path17.length === 0) {
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) {
23033
23606
  names.splice(0, 1);
23034
23607
  }
23035
- if (path17.slice(-1) == "/") {
23608
+ if (path20.slice(-1) == "/") {
23036
23609
  names.splice(names.length - 1, 1);
23037
23610
  }
23038
23611
  return names;
@@ -23651,7 +24224,7 @@ var protocol2 = Socket.protocol;
23651
24224
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
23652
24225
  var import_debug7 = __toESM(require_src(), 1);
23653
24226
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
23654
- function url(uri, path17 = "", loc) {
24227
+ function url(uri, path20 = "", loc) {
23655
24228
  let obj = uri;
23656
24229
  loc = loc || typeof location !== "undefined" && location;
23657
24230
  if (null == uri)
@@ -23685,7 +24258,7 @@ function url(uri, path17 = "", loc) {
23685
24258
  obj.path = obj.path || "/";
23686
24259
  const ipv6 = obj.host.indexOf(":") !== -1;
23687
24260
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
23688
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path17;
24261
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path20;
23689
24262
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
23690
24263
  return obj;
23691
24264
  }
@@ -25319,8 +25892,8 @@ function lookup(uri, opts) {
25319
25892
  const parsed = url(uri, opts.path || "/socket.io");
25320
25893
  const source = parsed.source;
25321
25894
  const id = parsed.id;
25322
- const path17 = parsed.path;
25323
- const sameNamespace = cache[id] && path17 in cache[id]["nsps"];
25895
+ const path20 = parsed.path;
25896
+ const sameNamespace = cache[id] && path20 in cache[id]["nsps"];
25324
25897
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
25325
25898
  let io;
25326
25899
  if (newConnection) {
@@ -25362,8 +25935,8 @@ function cardContextPath(target, postId) {
25362
25935
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
25363
25936
  return `${target.paths.root}/.cards/${safe}.md`;
25364
25937
  }
25365
- function cardContextPrompt(path17) {
25366
- return `Read ${path17} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
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.`;
25367
25940
  }
25368
25941
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
25369
25942
  var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
@@ -25527,11 +26100,11 @@ var TerminalManager = class {
25527
26100
  };
25528
26101
 
25529
26102
  // src/agent-adapter.ts
25530
- var import_fs8 = __toESM(require("fs"));
25531
- var import_path16 = __toESM(require("path"));
25532
- var RUNNER_CONFIG_PATH = import_path16.default.join(CONFIG_DIR, "runner-config.json");
26103
+ 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");
25533
26106
  function connectionPath(spaceSlug) {
25534
- return import_path16.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26107
+ return import_path19.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
25535
26108
  }
25536
26109
  function writeJson(filePath, value2) {
25537
26110
  writeSecureJson(filePath, value2);
@@ -25558,14 +26131,14 @@ function migrateLegacyConfigs() {
25558
26131
  const file = { boards: {} };
25559
26132
  let legacy = [];
25560
26133
  try {
25561
- legacy = import_fs8.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
26134
+ legacy = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
25562
26135
  } catch {
25563
26136
  return file;
25564
26137
  }
25565
26138
  for (const name of legacy) {
25566
26139
  const slug = name.slice("space-".length, -".json".length);
25567
26140
  const parsed = readSecureJson(
25568
- import_path16.default.join(CONFIG_DIR, name)
26141
+ import_path19.default.join(CONFIG_DIR, name)
25569
26142
  );
25570
26143
  if (!parsed) continue;
25571
26144
  if (!file.defaultSlug) {
@@ -25585,14 +26158,14 @@ function encryptStrayPlaintextConfigs() {
25585
26158
  const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
25586
26159
  let names = [];
25587
26160
  try {
25588
- names = import_fs8.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
26161
+ names = import_fs9.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
25589
26162
  } catch {
25590
26163
  return;
25591
26164
  }
25592
26165
  for (const name of names) {
25593
- const filePath = import_path16.default.join(CONFIG_DIR, name);
26166
+ const filePath = import_path19.default.join(CONFIG_DIR, name);
25594
26167
  try {
25595
- const raw = import_fs8.default.readFileSync(filePath, "utf-8");
26168
+ const raw = import_fs9.default.readFileSync(filePath, "utf-8");
25596
26169
  if (!raw.startsWith("{")) continue;
25597
26170
  writeSecureJson(filePath, JSON.parse(raw));
25598
26171
  } catch {
@@ -25623,7 +26196,7 @@ async function getConfiguredConnections() {
25623
26196
  }
25624
26197
  if (await isSignedIn(hostTarget(CONFIG_DIR), "claude")) kinds.add("claude");
25625
26198
  try {
25626
- const hasOneDrive = import_fs8.default.readdirSync(CONFIG_DIR).some((f) => f.endsWith(".onedrive.json"));
26199
+ const hasOneDrive = import_fs9.default.readdirSync(CONFIG_DIR).some((f) => f.endsWith(".onedrive.json"));
25627
26200
  if (hasOneDrive) kinds.add("onedrive");
25628
26201
  } catch {
25629
26202
  }
@@ -25694,7 +26267,7 @@ var localConfigProvider = {
25694
26267
  writeOneDriveConnection(spaceSlug, connection) {
25695
26268
  const filePath = connectionPath(spaceSlug);
25696
26269
  if (!connection) {
25697
- import_fs8.default.rmSync(filePath, { force: true });
26270
+ import_fs9.default.rmSync(filePath, { force: true });
25698
26271
  return;
25699
26272
  }
25700
26273
  writeJson(filePath, connection);
@@ -25719,7 +26292,7 @@ var localConfigProvider = {
25719
26292
  "claude",
25720
26293
  "codex"
25721
26294
  ]) {
25722
- import_fs8.default.mkdirSync(import_path16.default.join(dir, sub), { recursive: true });
26295
+ import_fs9.default.mkdirSync(import_path19.default.join(dir, sub), { recursive: true });
25723
26296
  }
25724
26297
  return dir;
25725
26298
  }
@@ -25893,15 +26466,15 @@ async function startDaemon(config) {
25893
26466
  }
25894
26467
  );
25895
26468
  const indexCandidates = [
25896
- import_path17.default.join(__dirname, "..", "index"),
25897
- import_path17.default.join(__dirname, "..", "..", "index", "image")
26469
+ import_path20.default.join(__dirname, "..", "index"),
26470
+ import_path20.default.join(__dirname, "..", "..", "index", "image")
25898
26471
  ];
25899
- const indexDir = indexCandidates.find((p) => import_fs9.default.existsSync(p)) ?? indexCandidates[0];
26472
+ const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
25900
26473
  const odIndex = new IndexService({
25901
- controlToken: (0, import_crypto7.createHash)("sha256").update(config.authToken).digest("hex"),
26474
+ controlToken: (0, import_crypto8.createHash)("sha256").update(config.authToken).digest("hex"),
25902
26475
  embedUrl: () => config.serverUrl,
25903
26476
  indexDir,
25904
- dataDir: import_path17.default.join(CONFIG_DIR, "index-data")
26477
+ dataDir: import_path20.default.join(CONFIG_DIR, "index-data")
25905
26478
  });
25906
26479
  void odIndex.ensureRunning().catch((err) => {
25907
26480
  console.error(