@f-o-h/cli 0.1.10 → 0.1.12

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.
Files changed (3) hide show
  1. package/README.md +43 -6
  2. package/dist/foh.js +830 -104
  3. package/package.json +1 -1
package/dist/foh.js CHANGED
@@ -6046,7 +6046,7 @@ var require_compile = __commonJS({
6046
6046
  const schOrFunc = root.refs[ref];
6047
6047
  if (schOrFunc)
6048
6048
  return schOrFunc;
6049
- let _sch = resolve10.call(this, root, ref);
6049
+ let _sch = resolve12.call(this, root, ref);
6050
6050
  if (_sch === void 0) {
6051
6051
  const schema2 = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
6052
6052
  const { schemaId } = this.opts;
@@ -6073,7 +6073,7 @@ var require_compile = __commonJS({
6073
6073
  function sameSchemaEnv(s1, s2) {
6074
6074
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
6075
6075
  }
6076
- function resolve10(root, ref) {
6076
+ function resolve12(root, ref) {
6077
6077
  let sch;
6078
6078
  while (typeof (sch = this.refs[ref]) == "string")
6079
6079
  ref = sch;
@@ -6648,55 +6648,55 @@ var require_fast_uri = __commonJS({
6648
6648
  }
6649
6649
  return uri;
6650
6650
  }
6651
- function resolve10(baseURI, relativeURI, options) {
6651
+ function resolve12(baseURI, relativeURI, options) {
6652
6652
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
6653
6653
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
6654
6654
  schemelessOptions.skipEscape = true;
6655
6655
  return serialize(resolved, schemelessOptions);
6656
6656
  }
6657
- function resolveComponent(base, relative, options, skipNormalization) {
6657
+ function resolveComponent(base, relative2, options, skipNormalization) {
6658
6658
  const target = {};
6659
6659
  if (!skipNormalization) {
6660
6660
  base = parse3(serialize(base, options), options);
6661
- relative = parse3(serialize(relative, options), options);
6661
+ relative2 = parse3(serialize(relative2, options), options);
6662
6662
  }
6663
6663
  options = options || {};
6664
- if (!options.tolerant && relative.scheme) {
6665
- target.scheme = relative.scheme;
6666
- target.userinfo = relative.userinfo;
6667
- target.host = relative.host;
6668
- target.port = relative.port;
6669
- target.path = removeDotSegments(relative.path || "");
6670
- target.query = relative.query;
6664
+ if (!options.tolerant && relative2.scheme) {
6665
+ target.scheme = relative2.scheme;
6666
+ target.userinfo = relative2.userinfo;
6667
+ target.host = relative2.host;
6668
+ target.port = relative2.port;
6669
+ target.path = removeDotSegments(relative2.path || "");
6670
+ target.query = relative2.query;
6671
6671
  } else {
6672
- if (relative.userinfo !== void 0 || relative.host !== void 0 || relative.port !== void 0) {
6673
- target.userinfo = relative.userinfo;
6674
- target.host = relative.host;
6675
- target.port = relative.port;
6676
- target.path = removeDotSegments(relative.path || "");
6677
- target.query = relative.query;
6672
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
6673
+ target.userinfo = relative2.userinfo;
6674
+ target.host = relative2.host;
6675
+ target.port = relative2.port;
6676
+ target.path = removeDotSegments(relative2.path || "");
6677
+ target.query = relative2.query;
6678
6678
  } else {
6679
- if (!relative.path) {
6679
+ if (!relative2.path) {
6680
6680
  target.path = base.path;
6681
- if (relative.query !== void 0) {
6682
- target.query = relative.query;
6681
+ if (relative2.query !== void 0) {
6682
+ target.query = relative2.query;
6683
6683
  } else {
6684
6684
  target.query = base.query;
6685
6685
  }
6686
6686
  } else {
6687
- if (relative.path[0] === "/") {
6688
- target.path = removeDotSegments(relative.path);
6687
+ if (relative2.path[0] === "/") {
6688
+ target.path = removeDotSegments(relative2.path);
6689
6689
  } else {
6690
6690
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
6691
- target.path = "/" + relative.path;
6691
+ target.path = "/" + relative2.path;
6692
6692
  } else if (!base.path) {
6693
- target.path = relative.path;
6693
+ target.path = relative2.path;
6694
6694
  } else {
6695
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
6695
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
6696
6696
  }
6697
6697
  target.path = removeDotSegments(target.path);
6698
6698
  }
6699
- target.query = relative.query;
6699
+ target.query = relative2.query;
6700
6700
  }
6701
6701
  target.userinfo = base.userinfo;
6702
6702
  target.host = base.host;
@@ -6704,7 +6704,7 @@ var require_fast_uri = __commonJS({
6704
6704
  }
6705
6705
  target.scheme = base.scheme;
6706
6706
  }
6707
- target.fragment = relative.fragment;
6707
+ target.fragment = relative2.fragment;
6708
6708
  return target;
6709
6709
  }
6710
6710
  function equal(uriA, uriB, options) {
@@ -6875,7 +6875,7 @@ var require_fast_uri = __commonJS({
6875
6875
  var fastUri = {
6876
6876
  SCHEMES,
6877
6877
  normalize,
6878
- resolve: resolve10,
6878
+ resolve: resolve12,
6879
6879
  resolveComponent,
6880
6880
  equal,
6881
6881
  serialize,
@@ -10105,21 +10105,21 @@ async function promptLine(label, {
10105
10105
  allowEmpty = false,
10106
10106
  defaultValue
10107
10107
  } = {}) {
10108
- return await new Promise((resolve10) => {
10108
+ return await new Promise((resolve12) => {
10109
10109
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
10110
10110
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
10111
10111
  rl.question(`${label}${suffix}: `, (answer) => {
10112
10112
  rl.close();
10113
10113
  const value = String(answer ?? "").trim();
10114
10114
  if (!value && typeof defaultValue === "string") {
10115
- resolve10(defaultValue);
10115
+ resolve12(defaultValue);
10116
10116
  return;
10117
10117
  }
10118
10118
  if (!value && !allowEmpty) {
10119
- resolve10("");
10119
+ resolve12("");
10120
10120
  return;
10121
10121
  }
10122
- resolve10(value);
10122
+ resolve12(value);
10123
10123
  });
10124
10124
  });
10125
10125
  }
@@ -10127,7 +10127,7 @@ async function promptSecret(label) {
10127
10127
  if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
10128
10128
  return await promptLine(label);
10129
10129
  }
10130
- return await new Promise((resolve10) => {
10130
+ return await new Promise((resolve12) => {
10131
10131
  const stdin = process.stdin;
10132
10132
  const stdout = process.stdout;
10133
10133
  const wasRaw = Boolean(stdin.isRaw);
@@ -10141,7 +10141,7 @@ async function promptSecret(label) {
10141
10141
  const finish = () => {
10142
10142
  cleanup();
10143
10143
  stdout.write("\n");
10144
- resolve10(value);
10144
+ resolve12(value);
10145
10145
  };
10146
10146
  const onData = (chunk) => {
10147
10147
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
@@ -10150,7 +10150,7 @@ async function promptSecret(label) {
10150
10150
  cleanup();
10151
10151
  process.exitCode = 130;
10152
10152
  stdout.write("\n");
10153
- return resolve10("");
10153
+ return resolve12("");
10154
10154
  }
10155
10155
  if (char === "\r" || char === "\n") {
10156
10156
  finish();
@@ -10419,7 +10419,7 @@ async function storeAuthenticatedSession(params) {
10419
10419
  return output;
10420
10420
  }
10421
10421
  function sleep(ms) {
10422
- return new Promise((resolve10) => setTimeout(resolve10, ms));
10422
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
10423
10423
  }
10424
10424
  async function runDeviceLogin(opts) {
10425
10425
  const jsonMode = Boolean(opts.json);
@@ -10957,7 +10957,7 @@ async function pollUntil(check2, opts) {
10957
10957
  }
10958
10958
  }
10959
10959
  function sleep2(ms) {
10960
- return new Promise((resolve10) => setTimeout(resolve10, ms));
10960
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
10961
10961
  }
10962
10962
 
10963
10963
  // src/commands/compliance.ts
@@ -13995,8 +13995,8 @@ function registerAgentGuardrailCommands(agent) {
13995
13995
  try {
13996
13996
  rule = JSON.parse(opts.rule);
13997
13997
  } catch {
13998
- const { readFileSync: readFileSync10 } = await import("fs");
13999
- rule = JSON.parse(readFileSync10(opts.rule, "utf-8"));
13998
+ const { readFileSync: readFileSync12 } = await import("fs");
13999
+ rule = JSON.parse(readFileSync12(opts.rule, "utf-8"));
14000
14000
  }
14001
14001
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
14002
14002
  method: "POST",
@@ -14596,9 +14596,9 @@ function registerAgent(program3) {
14596
14596
  process.stdout.write(yaml);
14597
14597
  return;
14598
14598
  }
14599
- const { writeFileSync: writeFileSync7 } = await import("fs");
14599
+ const { writeFileSync: writeFileSync9 } = await import("fs");
14600
14600
  const outputPath = opts.output ?? "tenant.yaml";
14601
- writeFileSync7(
14601
+ writeFileSync9(
14602
14602
  outputPath,
14603
14603
  `# tenant.yaml - Front Of House agent manifest
14604
14604
  # Edit this file and run: foh plan tenant.yaml
@@ -16033,11 +16033,11 @@ function registerVoice(program3) {
16033
16033
  }
16034
16034
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
16035
16035
  const audio = Buffer.from(await res.arrayBuffer());
16036
- const { mkdirSync: mkdirSync6, writeFileSync: writeFileSync7 } = await import("fs");
16037
- const { dirname: dirname5, resolve: resolve10 } = await import("path");
16038
- const absolutePath = resolve10(outputPath);
16039
- mkdirSync6(dirname5(absolutePath), { recursive: true });
16040
- writeFileSync7(absolutePath, audio);
16036
+ const { mkdirSync: mkdirSync7, writeFileSync: writeFileSync9 } = await import("fs");
16037
+ const { dirname: dirname5, resolve: resolve12 } = await import("path");
16038
+ const absolutePath = resolve12(outputPath);
16039
+ mkdirSync7(dirname5(absolutePath), { recursive: true });
16040
+ writeFileSync9(absolutePath, audio);
16041
16041
  format({
16042
16042
  status: "ok",
16043
16043
  provider,
@@ -30518,7 +30518,7 @@ var Protocol = class {
30518
30518
  return;
30519
30519
  }
30520
30520
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
30521
- await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
30521
+ await new Promise((resolve12) => setTimeout(resolve12, pollInterval));
30522
30522
  options?.signal?.throwIfAborted();
30523
30523
  }
30524
30524
  } catch (error2) {
@@ -30535,7 +30535,7 @@ var Protocol = class {
30535
30535
  */
30536
30536
  request(request, resultSchema, options) {
30537
30537
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
30538
- return new Promise((resolve10, reject) => {
30538
+ return new Promise((resolve12, reject) => {
30539
30539
  const earlyReject = (error2) => {
30540
30540
  reject(error2);
30541
30541
  };
@@ -30613,7 +30613,7 @@ var Protocol = class {
30613
30613
  if (!parseResult.success) {
30614
30614
  reject(parseResult.error);
30615
30615
  } else {
30616
- resolve10(parseResult.data);
30616
+ resolve12(parseResult.data);
30617
30617
  }
30618
30618
  } catch (error2) {
30619
30619
  reject(error2);
@@ -30874,12 +30874,12 @@ var Protocol = class {
30874
30874
  }
30875
30875
  } catch {
30876
30876
  }
30877
- return new Promise((resolve10, reject) => {
30877
+ return new Promise((resolve12, reject) => {
30878
30878
  if (signal.aborted) {
30879
30879
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
30880
30880
  return;
30881
30881
  }
30882
- const timeoutId = setTimeout(resolve10, interval);
30882
+ const timeoutId = setTimeout(resolve12, interval);
30883
30883
  signal.addEventListener("abort", () => {
30884
30884
  clearTimeout(timeoutId);
30885
30885
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -31979,7 +31979,7 @@ var McpServer = class {
31979
31979
  let task = createTaskResult.task;
31980
31980
  const pollInterval = task.pollInterval ?? 5e3;
31981
31981
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
31982
- await new Promise((resolve10) => setTimeout(resolve10, pollInterval));
31982
+ await new Promise((resolve12) => setTimeout(resolve12, pollInterval));
31983
31983
  const updatedTask = await extra.taskStore.getTask(taskId);
31984
31984
  if (!updatedTask) {
31985
31985
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -32628,19 +32628,19 @@ var StdioServerTransport = class {
32628
32628
  this.onclose?.();
32629
32629
  }
32630
32630
  send(message) {
32631
- return new Promise((resolve10) => {
32631
+ return new Promise((resolve12) => {
32632
32632
  const json3 = serializeMessage(message);
32633
32633
  if (this._stdout.write(json3)) {
32634
- resolve10();
32634
+ resolve12();
32635
32635
  } else {
32636
- this._stdout.once("drain", resolve10);
32636
+ this._stdout.once("drain", resolve12);
32637
32637
  }
32638
32638
  });
32639
32639
  }
32640
32640
  };
32641
32641
 
32642
32642
  // src/lib/cli-version.ts
32643
- var CLI_VERSION = "0.1.10";
32643
+ var CLI_VERSION = "0.1.12";
32644
32644
 
32645
32645
  // src/commands/mcp-serve.ts
32646
32646
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -32825,7 +32825,7 @@ async function runFohCli(params) {
32825
32825
  effectiveArgv.push("--json");
32826
32826
  }
32827
32827
  const command = `foh ${effectiveArgv.join(" ")}`;
32828
- return await new Promise((resolve10) => {
32828
+ return await new Promise((resolve12) => {
32829
32829
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
32830
32830
  stdio: ["ignore", "pipe", "pipe"],
32831
32831
  env: {
@@ -32850,7 +32850,7 @@ async function runFohCli(params) {
32850
32850
  });
32851
32851
  child.once("error", (error2) => {
32852
32852
  clearTimeout(timeoutHandle);
32853
- resolve10({
32853
+ resolve12({
32854
32854
  ok: false,
32855
32855
  command,
32856
32856
  argv: effectiveArgv,
@@ -32866,7 +32866,7 @@ async function runFohCli(params) {
32866
32866
  const stderrText = finalizeBoundedText(stderrBuffer);
32867
32867
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
32868
32868
  const stdoutJson = tryParseJson(stdoutText);
32869
- resolve10({
32869
+ resolve12({
32870
32870
  ok: !timedOut && exitCode === 0,
32871
32871
  command,
32872
32872
  argv: effectiveArgv,
@@ -34775,8 +34775,8 @@ function registerSetup(program3) {
34775
34775
  }
34776
34776
  try {
34777
34777
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
34778
- const { writeFileSync: writeFileSync7 } = await import("fs");
34779
- writeFileSync7(
34778
+ const { writeFileSync: writeFileSync9 } = await import("fs");
34779
+ writeFileSync9(
34780
34780
  "tenant.yaml",
34781
34781
  `# tenant.yaml - Front Of House agent manifest
34782
34782
  # Edit this file and run: foh plan tenant.yaml
@@ -34944,8 +34944,8 @@ function registerSim(program3) {
34944
34944
  }
34945
34945
  const cert = response.certificate;
34946
34946
  if (opts.out) {
34947
- const { writeFileSync: writeFileSync7 } = await import("fs");
34948
- writeFileSync7(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
34947
+ const { writeFileSync: writeFileSync9 } = await import("fs");
34948
+ writeFileSync9(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
34949
34949
  process.stderr.write(` Certificate written to ${opts.out}
34950
34950
  `);
34951
34951
  }
@@ -34995,8 +34995,8 @@ function registerSim(program3) {
34995
34995
  });
34996
34996
  }
34997
34997
  if (opts.out) {
34998
- const { writeFileSync: writeFileSync7 } = await import("fs");
34999
- writeFileSync7(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
34998
+ const { writeFileSync: writeFileSync9 } = await import("fs");
34999
+ writeFileSync9(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35000
35000
  process.stderr.write(` Final certificate written to ${opts.out}
35001
35001
  `);
35002
35002
  }
@@ -37710,7 +37710,7 @@ async function runSelf(args, apiUrlOverride) {
37710
37710
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
37711
37711
  spawnArgs.push("--api-url", apiUrlOverride);
37712
37712
  }
37713
- return await new Promise((resolve10, reject) => {
37713
+ return await new Promise((resolve12, reject) => {
37714
37714
  const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
37715
37715
  stdio: "inherit",
37716
37716
  env: {
@@ -37720,7 +37720,7 @@ async function runSelf(args, apiUrlOverride) {
37720
37720
  }
37721
37721
  });
37722
37722
  child.once("error", reject);
37723
- child.once("close", (code) => resolve10(typeof code === "number" ? code : 1));
37723
+ child.once("close", (code) => resolve12(typeof code === "number" ? code : 1));
37724
37724
  });
37725
37725
  }
37726
37726
  function shouldUseInteractiveHome(argv) {
@@ -38098,17 +38098,17 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
38098
38098
  async function applyRepoUpdate(repoRoot) {
38099
38099
  const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
38100
38100
  if (process.platform === "win32") {
38101
- return await new Promise((resolve10, reject) => {
38101
+ return await new Promise((resolve12, reject) => {
38102
38102
  const child = (0, import_child_process3.spawn)(
38103
38103
  "powershell",
38104
38104
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
38105
38105
  { stdio: "inherit" }
38106
38106
  );
38107
38107
  child.once("error", reject);
38108
- child.once("close", (code) => resolve10(typeof code === "number" ? code : 1));
38108
+ child.once("close", (code) => resolve12(typeof code === "number" ? code : 1));
38109
38109
  });
38110
38110
  }
38111
- return await new Promise((resolve10, reject) => {
38111
+ return await new Promise((resolve12, reject) => {
38112
38112
  const child = (0, import_child_process3.spawn)(
38113
38113
  "corepack",
38114
38114
  ["pnpm", "cli:install:global"],
@@ -38118,7 +38118,7 @@ async function applyRepoUpdate(repoRoot) {
38118
38118
  }
38119
38119
  );
38120
38120
  child.once("error", reject);
38121
- child.once("close", (code) => resolve10(typeof code === "number" ? code : 1));
38121
+ child.once("close", (code) => resolve12(typeof code === "number" ? code : 1));
38122
38122
  });
38123
38123
  }
38124
38124
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -38254,18 +38254,149 @@ function registerUpdate(program3) {
38254
38254
  }
38255
38255
 
38256
38256
  // src/commands/eval.ts
38257
- var import_fs13 = require("fs");
38258
- var import_path11 = require("path");
38259
- var import_child_process4 = require("child_process");
38257
+ var import_fs15 = require("fs");
38258
+ var import_path13 = require("path");
38259
+ var import_child_process5 = require("child_process");
38260
38260
 
38261
- // src/lib/external-agent-capture.ts
38261
+ // src/lib/external-agent-artifact-safety.ts
38262
38262
  var import_fs12 = require("fs");
38263
38263
  var import_path10 = require("path");
38264
+ var DEFAULT_MAX_BYTES_PER_FILE = 5 * 1024 * 1024;
38265
+ var TEXT_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
38266
+ "codex-events.jsonl",
38267
+ "codex-exec.jsonl",
38268
+ "codex-last-message.md",
38269
+ "commands.ndjson",
38270
+ "executor-plan.json",
38271
+ "improvement-packet.json",
38272
+ "knowledge.json",
38273
+ "notes.md",
38274
+ "proof.json",
38275
+ "replay.json",
38276
+ "run.json",
38277
+ "terminal-transcript.txt"
38278
+ ]);
38279
+ var SECRET_RE2 = /\b(?:Bearer\s+)?(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat|npm_)[A-Za-z0-9_\-.]{12,}\b/gi;
38280
+ var EMAIL_RE2 = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
38281
+ var PHONE_RE2 = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
38282
+ function escapeRegExp(value) {
38283
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38284
+ }
38285
+ function pathVariants(path2) {
38286
+ const resolved = (0, import_path10.resolve)(path2);
38287
+ const slash = resolved.replace(/\\/g, "/");
38288
+ const backslash = resolved.replace(/\//g, "\\");
38289
+ return Array.from(new Set([
38290
+ resolved,
38291
+ slash,
38292
+ backslash,
38293
+ backslash.replace(/\\/g, "\\\\")
38294
+ ].filter((entry) => entry.length > 3)));
38295
+ }
38296
+ function regexForLiteralVariants(values) {
38297
+ const parts = values.map((value) => escapeRegExp(value)).filter(Boolean);
38298
+ if (parts.length === 0) return null;
38299
+ return new RegExp(parts.join("|"), "gi");
38300
+ }
38301
+ function matchesFor(pattern, text) {
38302
+ if (!pattern) return 0;
38303
+ const matches = text.match(pattern);
38304
+ return matches ? matches.length : 0;
38305
+ }
38306
+ function redactionPatterns(input) {
38307
+ return {
38308
+ secret: SECRET_RE2,
38309
+ email: EMAIL_RE2,
38310
+ phone: PHONE_RE2,
38311
+ privateRepo: input.privateRepoRoot ? regexForLiteralVariants(pathVariants(input.privateRepoRoot)) : null,
38312
+ home: input.homeDir ? regexForLiteralVariants(pathVariants(input.homeDir)) : null
38313
+ };
38314
+ }
38315
+ function redactExternalAgentArtifactText(text, input = {}) {
38316
+ const patterns = redactionPatterns(input);
38317
+ let redacted = text.replace(patterns.secret, "[redacted_secret]").replace(patterns.email, "[redacted_email]").replace(patterns.phone, "[redacted_phone]");
38318
+ if (patterns.privateRepo) redacted = redacted.replace(patterns.privateRepo, "[redacted_private_repo_path]");
38319
+ if (patterns.home) redacted = redacted.replace(patterns.home, "[redacted_home_path]");
38320
+ return redacted;
38321
+ }
38322
+ function artifactFiles(runDir) {
38323
+ if (!(0, import_fs12.existsSync)(runDir)) return [];
38324
+ return (0, import_fs12.readdirSync)(runDir).map((name) => (0, import_path10.join)(runDir, name)).filter((path2) => {
38325
+ const stat = (0, import_fs12.statSync)(path2);
38326
+ return stat.isFile() && TEXT_ARTIFACT_NAMES.has(path2.split(/[\\/]/).pop() || "");
38327
+ }).sort();
38328
+ }
38329
+ function finding(kind, file2, count) {
38330
+ if (count < 1) return null;
38331
+ return {
38332
+ kind,
38333
+ file: file2,
38334
+ count,
38335
+ fatal: kind === "secret_like_token" || kind === "private_repo_path"
38336
+ };
38337
+ }
38338
+ function scanText(input) {
38339
+ const patterns = redactionPatterns(input);
38340
+ return [
38341
+ finding("secret_like_token", input.file, matchesFor(patterns.secret, input.text)),
38342
+ finding("private_repo_path", input.file, matchesFor(patterns.privateRepo, input.text)),
38343
+ finding("local_home_path", input.file, matchesFor(patterns.home, input.text)),
38344
+ finding("email_address", input.file, matchesFor(patterns.email, input.text)),
38345
+ finding("phone_number", input.file, matchesFor(patterns.phone, input.text))
38346
+ ].filter(Boolean);
38347
+ }
38348
+ function scanExternalAgentArtifacts(options) {
38349
+ const runDir = (0, import_path10.resolve)(options.runDir);
38350
+ const privateRepoRoot = options.privateRepoRoot ? (0, import_path10.resolve)(options.privateRepoRoot) : void 0;
38351
+ const homeDir = options.homeDir || process.env.USERPROFILE || process.env.HOME;
38352
+ const maxBytes = options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
38353
+ const files = artifactFiles(runDir);
38354
+ const findings = [];
38355
+ const redactedFiles = [];
38356
+ for (const file2 of files) {
38357
+ const stat = (0, import_fs12.statSync)(file2);
38358
+ if (stat.size > maxBytes) {
38359
+ findings.push({
38360
+ kind: "artifact_too_large",
38361
+ file: file2,
38362
+ count: 1,
38363
+ fatal: true
38364
+ });
38365
+ continue;
38366
+ }
38367
+ const text = (0, import_fs12.readFileSync)(file2, "utf8");
38368
+ findings.push(...scanText({ text, file: file2, privateRepoRoot, homeDir }));
38369
+ if (options.writeRedacted) {
38370
+ const redacted = redactExternalAgentArtifactText(text, { privateRepoRoot, homeDir });
38371
+ const out = `${file2}.redacted`;
38372
+ (0, import_fs12.writeFileSync)(out, redacted, "utf8");
38373
+ redactedFiles.push(out);
38374
+ }
38375
+ }
38376
+ const blockedReasonCodes = Array.from(new Set(findings.filter((entry) => entry.fatal).map((entry) => `artifact_contains_${entry.kind}`)));
38377
+ const ok = blockedReasonCodes.length === 0;
38378
+ return {
38379
+ schema_version: "external_agent_artifact_safety_report.v1",
38380
+ ok,
38381
+ status: ok ? "pass" : "blocked",
38382
+ reason_code: ok ? "external_agent_artifacts_safe" : "external_agent_artifacts_contain_blocked_content",
38383
+ scanned_at: (/* @__PURE__ */ new Date()).toISOString(),
38384
+ run_dir: runDir,
38385
+ files_scanned: files,
38386
+ redacted_files: redactedFiles,
38387
+ findings,
38388
+ blocked_reason_codes: blockedReasonCodes
38389
+ };
38390
+ }
38391
+
38392
+ // src/lib/external-agent-capture.ts
38393
+ var import_fs13 = require("fs");
38394
+ var import_path11 = require("path");
38264
38395
  var EXTERNAL_AGENT_RUN_DIR_ENV = "FOH_EXTERNAL_AGENT_RUN_DIR";
38265
38396
  var EXTERNAL_AGENT_PROMPT_VERSION_ENV = "FOH_EXTERNAL_AGENT_PROMPT_VERSION";
38266
- var SECRET_RE2 = /\b(?:Bearer\s+)?(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat|npm_)[A-Za-z0-9_\-.]{12,}\b/gi;
38397
+ var SECRET_RE3 = /\b(?:Bearer\s+)?(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat|npm_)[A-Za-z0-9_\-.]{12,}\b/gi;
38267
38398
  function redactText(value) {
38268
- return value.replace(SECRET_RE2, "[redacted_secret]");
38399
+ return value.replace(SECRET_RE3, "[redacted_secret]");
38269
38400
  }
38270
38401
  function redactPath(value) {
38271
38402
  let redacted = redactText(value);
@@ -38279,13 +38410,13 @@ function safeJsonLine(value) {
38279
38410
  function getExternalAgentRunDir() {
38280
38411
  const raw = process.env[EXTERNAL_AGENT_RUN_DIR_ENV];
38281
38412
  if (!raw || !raw.trim()) return null;
38282
- return (0, import_path10.resolve)(raw);
38413
+ return (0, import_path11.resolve)(raw);
38283
38414
  }
38284
38415
  function recordExternalAgentCliInvocation(input) {
38285
38416
  const runDir = getExternalAgentRunDir();
38286
38417
  if (!runDir) return;
38287
38418
  try {
38288
- (0, import_fs12.mkdirSync)(runDir, { recursive: true });
38419
+ (0, import_fs13.mkdirSync)(runDir, { recursive: true });
38289
38420
  const args = input.argv.slice(2).map((arg) => redactText(String(arg)));
38290
38421
  const record2 = {
38291
38422
  schema_version: "external_agent_cli_command.v1",
@@ -38297,14 +38428,507 @@ function recordExternalAgentCliInvocation(input) {
38297
38428
  json_requested: args.includes("--json"),
38298
38429
  prompt_version: process.env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || null
38299
38430
  };
38300
- (0, import_fs12.appendFileSync)((0, import_path10.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
38431
+ (0, import_fs13.appendFileSync)((0, import_path11.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
38301
38432
  } catch {
38302
38433
  }
38303
38434
  }
38304
38435
  function readCommandRecords(runDir) {
38305
- const commandLogPath = (0, import_path10.join)(runDir, "commands.ndjson");
38306
- if (!(0, import_fs12.existsSync)(commandLogPath)) return [];
38307
- return (0, import_fs12.readFileSync)(commandLogPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
38436
+ const commandLogPath = (0, import_path11.join)(runDir, "commands.ndjson");
38437
+ if (!(0, import_fs13.existsSync)(commandLogPath)) return [];
38438
+ return (0, import_fs13.readFileSync)(commandLogPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
38439
+ }
38440
+
38441
+ // src/lib/external-agent-executor.ts
38442
+ var import_fs14 = require("fs");
38443
+ var import_os2 = require("os");
38444
+ var import_path12 = require("path");
38445
+ var import_child_process4 = require("child_process");
38446
+ var CODEX_EXECUTOR_DENIED_ENV_PREFIXES = [
38447
+ "SUPABASE_",
38448
+ "DATABASE_",
38449
+ "OPENAI_",
38450
+ "XAI_",
38451
+ "ANTHROPIC_",
38452
+ "WHATSAPP_",
38453
+ "TWILIO_",
38454
+ "STRIPE_"
38455
+ ];
38456
+ var CODEX_EXECUTOR_DENIED_ENV_NAMES = [
38457
+ "DATABASE_URL",
38458
+ "NPM_TOKEN",
38459
+ "GITHUB_TOKEN",
38460
+ "GH_TOKEN",
38461
+ "META_APP_SECRET",
38462
+ "FACEBOOK_APP_SECRET",
38463
+ "GOOGLE_CLIENT_SECRET",
38464
+ "JWT_SECRET",
38465
+ "SESSION_SECRET"
38466
+ ];
38467
+ var CHILD_ENV_ALLOWLIST = [
38468
+ "APPDATA",
38469
+ "CODEX_HOME",
38470
+ "ComSpec",
38471
+ "HOME",
38472
+ "LOCALAPPDATA",
38473
+ "PATH",
38474
+ "Path",
38475
+ "SystemRoot",
38476
+ "TEMP",
38477
+ "TMP",
38478
+ "USERPROFILE",
38479
+ "WINDIR"
38480
+ ];
38481
+ var ExternalAgentExecutorError = class extends Error {
38482
+ reasonCode;
38483
+ constructor(reasonCode, message) {
38484
+ super(message);
38485
+ this.name = "ExternalAgentExecutorError";
38486
+ this.reasonCode = reasonCode;
38487
+ }
38488
+ };
38489
+ function isDeniedEnvKey(key) {
38490
+ const upper = key.toUpperCase();
38491
+ if (CODEX_EXECUTOR_DENIED_ENV_NAMES.some((name) => upper === name)) return true;
38492
+ return CODEX_EXECUTOR_DENIED_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
38493
+ }
38494
+ function buildCodexExecutorEnv(input) {
38495
+ const source = input.sourceEnv ?? process.env;
38496
+ const env = {};
38497
+ for (const key of CHILD_ENV_ALLOWLIST) {
38498
+ const value = source[key];
38499
+ if (typeof value === "string" && value.length > 0 && !isDeniedEnvKey(key)) {
38500
+ env[key] = value;
38501
+ }
38502
+ }
38503
+ env[EXTERNAL_AGENT_RUN_DIR_ENV] = input.runDir;
38504
+ env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] = input.promptVersion;
38505
+ env.FOH_CLI_SUPPRESS_BANNER = "1";
38506
+ return env;
38507
+ }
38508
+ function normalizeForCompare(path2) {
38509
+ const resolved = (0, import_path12.resolve)(path2);
38510
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
38511
+ }
38512
+ function isPathInside(childPath, parentPath) {
38513
+ const child = normalizeForCompare(childPath);
38514
+ const parent = normalizeForCompare(parentPath);
38515
+ const rel = (0, import_path12.relative)(parent, child);
38516
+ return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path12.isAbsolute)(rel);
38517
+ }
38518
+ function requireString(value, field) {
38519
+ if (typeof value !== "string" || value.trim() === "") {
38520
+ throw new ExternalAgentExecutorError("invalid_external_agent_batch", `Batch field ${field} must be a non-empty string.`);
38521
+ }
38522
+ return value;
38523
+ }
38524
+ function readBatch(batchPath) {
38525
+ if (!(0, import_fs14.existsSync)(batchPath)) {
38526
+ throw new ExternalAgentExecutorError("external_agent_batch_not_found", `Batch file not found: ${batchPath}`);
38527
+ }
38528
+ const parsed = JSON.parse((0, import_fs14.readFileSync)(batchPath, "utf8"));
38529
+ if (parsed.schema_version !== "external_agent_batch_plan.v1") {
38530
+ throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch schema_version must be external_agent_batch_plan.v1.");
38531
+ }
38532
+ if (!Array.isArray(parsed.runs) || parsed.runs.length === 0) {
38533
+ throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch runs must be a non-empty array.");
38534
+ }
38535
+ return parsed;
38536
+ }
38537
+ function defaultRunnerProbe(command, args) {
38538
+ const result = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? (0, import_child_process4.spawnSync)(
38539
+ "powershell.exe",
38540
+ ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `& ${[command, ...args].map(quotePowerShellArg).join(" ")}`],
38541
+ { encoding: "utf8" }
38542
+ ) : (0, import_child_process4.spawnSync)(command, args, { encoding: "utf8" });
38543
+ return {
38544
+ status: typeof result.status === "number" ? result.status : null,
38545
+ stdout: String(result.stdout || ""),
38546
+ stderr: String(result.stderr || ""),
38547
+ error: result.error
38548
+ };
38549
+ }
38550
+ function quotePowerShellArg(value) {
38551
+ return `'${value.replace(/'/g, "''")}'`;
38552
+ }
38553
+ function resolveCodexProbeCommand() {
38554
+ if (process.platform !== "win32") return "codex";
38555
+ const appData = process.env.APPDATA;
38556
+ if (appData) {
38557
+ const appDataShim = (0, import_path12.join)(appData, "npm", "codex.cmd");
38558
+ if ((0, import_fs14.existsSync)(appDataShim)) return appDataShim;
38559
+ }
38560
+ return "codex.cmd";
38561
+ }
38562
+ function resolveCodexExecutionCommand() {
38563
+ return resolveCodexProbeCommand();
38564
+ }
38565
+ function validateCodexRunner(options) {
38566
+ if (options.skipRunnerProbe) {
38567
+ return { binaryChecked: false, requiredFlagsChecked: false };
38568
+ }
38569
+ const probe = options.runnerProbe ?? defaultRunnerProbe;
38570
+ const probeCommand = resolveCodexProbeCommand();
38571
+ const version2 = probe(probeCommand, ["--version"]);
38572
+ if (version2.error || version2.status !== 0) {
38573
+ throw new ExternalAgentExecutorError("external_agent_runner_binary_missing", "Codex runner probe failed: `codex --version` did not exit 0.");
38574
+ }
38575
+ const help = probe(probeCommand, ["exec", "--help"]);
38576
+ if (help.error || help.status !== 0) {
38577
+ throw new ExternalAgentExecutorError("external_agent_runner_help_unavailable", "Codex runner probe failed: `codex exec --help` did not exit 0.");
38578
+ }
38579
+ const helpText = `${help.stdout}
38580
+ ${help.stderr}`;
38581
+ const requiredFlags = [
38582
+ "--cd",
38583
+ "--skip-git-repo-check",
38584
+ "--ephemeral",
38585
+ "--ignore-rules",
38586
+ "--sandbox",
38587
+ "--full-auto",
38588
+ "--json",
38589
+ "--output-last-message"
38590
+ ];
38591
+ const missing = requiredFlags.filter((flag) => !helpText.includes(flag));
38592
+ if (missing.length > 0) {
38593
+ throw new ExternalAgentExecutorError(
38594
+ "external_agent_runner_required_flags_missing",
38595
+ `Codex runner is missing required exec flag(s): ${missing.join(", ")}`
38596
+ );
38597
+ }
38598
+ return { binaryChecked: true, requiredFlagsChecked: true };
38599
+ }
38600
+ function safeRunId(value) {
38601
+ return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
38602
+ }
38603
+ function resolveWorkspaceRoot(input) {
38604
+ if (input.workspaceRoot) return (0, import_path12.resolve)(input.workspaceRoot);
38605
+ const batchStem = (0, import_path12.basename)((0, import_path12.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
38606
+ const repoStem = (0, import_path12.basename)((0, import_path12.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
38607
+ return (0, import_path12.resolve)((0, import_os2.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
38608
+ }
38609
+ function promptVersionFromPath(promptPath) {
38610
+ const raw = (0, import_fs14.readFileSync)(promptPath, "utf8");
38611
+ if (raw.includes("Do not assume access to the private source repository")) return "blank-setup.v1";
38612
+ return "unknown";
38613
+ }
38614
+ function createExternalAgentExecutorPlan(options) {
38615
+ const runner = String(options.runner || "codex");
38616
+ if (runner !== "codex") {
38617
+ throw new ExternalAgentExecutorError("unsupported_external_agent_runner", `Unsupported runner: ${runner}`);
38618
+ }
38619
+ const batchPath = (0, import_path12.resolve)(options.batchPath);
38620
+ const batch = readBatch(batchPath);
38621
+ const runnerProbe = validateCodexRunner(options);
38622
+ const privateRepoRoot = (0, import_path12.resolve)(options.privateRepoRoot || options.cwd || process.cwd());
38623
+ const workspaceRoot = resolveWorkspaceRoot({ batchPath, workspaceRoot: options.workspaceRoot, privateRepoRoot });
38624
+ if (isPathInside(workspaceRoot, privateRepoRoot)) {
38625
+ throw new ExternalAgentExecutorError(
38626
+ "external_agent_workspace_inside_private_repo",
38627
+ `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
38628
+ );
38629
+ }
38630
+ (0, import_fs14.mkdirSync)(workspaceRoot, { recursive: true });
38631
+ const batchDir = (0, import_path12.resolve)(String(batch.batch_dir || (0, import_path12.resolve)(batchPath, "..")));
38632
+ const timeoutMinutes = Number.isFinite(options.timeoutMinutes) && Number(options.timeoutMinutes) > 0 ? Number(options.timeoutMinutes) : 30;
38633
+ const runs = batch.runs.map((run) => {
38634
+ const runId = safeRunId(requireString(run.run_id, "runs[].run_id"));
38635
+ const runDir = (0, import_path12.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
38636
+ const promptPath = (0, import_path12.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
38637
+ const workspaceDir = (0, import_path12.join)(workspaceRoot, runId);
38638
+ (0, import_fs14.mkdirSync)(workspaceDir, { recursive: true });
38639
+ (0, import_fs14.writeFileSync)(
38640
+ (0, import_path12.join)(workspaceDir, "README.md"),
38641
+ [
38642
+ "# FOH External-Agent Workspace",
38643
+ "",
38644
+ "This directory is intentionally empty.",
38645
+ "Use only public FOH docs, public API docs, and the public npm CLI package.",
38646
+ "Do not assume access to the private source repository.",
38647
+ ""
38648
+ ].join("\n"),
38649
+ "utf8"
38650
+ );
38651
+ const env = buildCodexExecutorEnv({
38652
+ sourceEnv: options.env,
38653
+ runDir,
38654
+ promptVersion: promptVersionFromPath(promptPath)
38655
+ });
38656
+ const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
38657
+ const jsonlPath = (0, import_path12.join)(runDir, "codex-exec.jsonl");
38658
+ const lastMessagePath = (0, import_path12.join)(runDir, "codex-last-message.md");
38659
+ const stderrPath = (0, import_path12.join)(runDir, "codex-stderr.txt");
38660
+ const runPath = (0, import_path12.join)(runDir, "run.json");
38661
+ const artifactSafetyPath = (0, import_path12.join)(runDir, "artifact-safety.json");
38662
+ const args = [
38663
+ "exec",
38664
+ "--cd",
38665
+ workspaceDir,
38666
+ "--skip-git-repo-check",
38667
+ "--ephemeral",
38668
+ "--ignore-rules",
38669
+ "--ignore-user-config",
38670
+ "--sandbox",
38671
+ "workspace-write",
38672
+ "--full-auto",
38673
+ "--json",
38674
+ "--output-last-message",
38675
+ lastMessagePath,
38676
+ "-"
38677
+ ];
38678
+ return {
38679
+ run_id: runId,
38680
+ model_provider: String(run.model_provider || "unknown"),
38681
+ model_name: String(run.model_name || "unknown-model"),
38682
+ prompt_version: promptVersion,
38683
+ run_dir: runDir,
38684
+ prompt_path: promptPath,
38685
+ workspace_dir: workspaceDir,
38686
+ command: "codex",
38687
+ args,
38688
+ env_keys: Object.keys(env).sort(),
38689
+ outputs: {
38690
+ jsonl: jsonlPath,
38691
+ last_message: lastMessagePath,
38692
+ stderr: stderrPath,
38693
+ run: runPath,
38694
+ artifact_safety: artifactSafetyPath
38695
+ }
38696
+ };
38697
+ });
38698
+ return {
38699
+ schema_version: "external_agent_executor_plan.v1",
38700
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
38701
+ runner,
38702
+ mode: "dry_run",
38703
+ batch_path: batchPath,
38704
+ batch_dir: batchDir,
38705
+ private_repo_root: privateRepoRoot,
38706
+ workspace_root: workspaceRoot,
38707
+ timeout_minutes: timeoutMinutes,
38708
+ safety: {
38709
+ workspace_outside_private_repo: true,
38710
+ repo_files_copied: false,
38711
+ child_env_mode: "allowlist",
38712
+ denied_env_prefixes: [...CODEX_EXECUTOR_DENIED_ENV_PREFIXES],
38713
+ denied_env_names: [...CODEX_EXECUTOR_DENIED_ENV_NAMES],
38714
+ runner_probe: {
38715
+ binary_checked: runnerProbe.binaryChecked,
38716
+ required_flags_checked: runnerProbe.requiredFlagsChecked
38717
+ }
38718
+ },
38719
+ runs
38720
+ };
38721
+ }
38722
+ function writeExternalAgentExecutorPlan(plan) {
38723
+ const path2 = (0, import_path12.join)(plan.batch_dir, "executor-plan.json");
38724
+ (0, import_fs14.mkdirSync)(plan.batch_dir, { recursive: true });
38725
+ (0, import_fs14.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
38726
+ `, "utf8");
38727
+ return path2;
38728
+ }
38729
+ function proofArtifactPasses(runDir) {
38730
+ const proofPath = (0, import_path12.join)(runDir, "proof.json");
38731
+ if (!(0, import_fs14.existsSync)(proofPath)) return false;
38732
+ try {
38733
+ const parsed = JSON.parse((0, import_fs14.readFileSync)(proofPath, "utf8"));
38734
+ return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
38735
+ } catch {
38736
+ return false;
38737
+ }
38738
+ }
38739
+ function readIfExists(path2) {
38740
+ return (0, import_fs14.existsSync)(path2) ? (0, import_fs14.readFileSync)(path2, "utf8") : "";
38741
+ }
38742
+ function classifyRun(input) {
38743
+ if (input.timedOut) return { status: "hold", reasonCode: "codex_runner_timeout" };
38744
+ if (!input.artifactSafetyOk) return { status: "fail", reasonCode: "external_agent_artifact_safety_blocked" };
38745
+ const lastMessage = readIfExists(input.run.outputs.last_message);
38746
+ const stderr = readIfExists(input.run.outputs.stderr);
38747
+ const combined = `${lastMessage}
38748
+ ${stderr}`;
38749
+ if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
38750
+ return { status: "fail", reasonCode: "private_repo_assumption_detected" };
38751
+ }
38752
+ if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
38753
+ return { status: "hold", reasonCode: "auth_browser_approval_required" };
38754
+ }
38755
+ if (input.exitCode !== 0) return { status: "hold", reasonCode: "codex_runner_nonzero_exit" };
38756
+ if (proofArtifactPasses(input.run.run_dir)) return { status: "pass", reasonCode: null };
38757
+ return { status: "hold", reasonCode: "external_agent_proof_artifact_missing" };
38758
+ }
38759
+ function buildExecutedRunArtifact(input) {
38760
+ const commands = readCommandRecords(input.run.run_dir);
38761
+ return {
38762
+ schema_version: "external_agent_run.v1",
38763
+ run_id: input.run.run_id,
38764
+ status: input.status,
38765
+ failure_reason_code: input.reasonCode,
38766
+ model_provider: input.run.model_provider,
38767
+ model_name: input.run.model_name,
38768
+ agent_shell: "codex-exec",
38769
+ workspace_type: "clean-no-repo-programmatic",
38770
+ prompt_version: input.run.prompt_version,
38771
+ prompt_path: "prompt.txt",
38772
+ started_at: input.startedAt,
38773
+ ended_at: input.endedAt,
38774
+ manual_intervention_count: 0,
38775
+ manual_interventions: [],
38776
+ environment: {
38777
+ os: process.platform,
38778
+ node_version: process.version,
38779
+ npm_version: null,
38780
+ foh_cli_version: CLI_VERSION,
38781
+ runner_exit_code: input.exitCode,
38782
+ runner_timed_out: input.timedOut,
38783
+ duration_ms: input.durationMs
38784
+ },
38785
+ public_entrypoints: [
38786
+ "https://frontofhouse.okii.uk",
38787
+ "https://frontofhouse.okii.uk/llms.txt",
38788
+ "https://frontofhouse.okii.uk/openapi.yaml",
38789
+ "npx --yes @f-o-h/cli@latest"
38790
+ ],
38791
+ commands_run: commands.map((command) => command.command),
38792
+ docs_pages_used: [],
38793
+ artifacts: {
38794
+ terminal_transcript: input.run.outputs.jsonl,
38795
+ command_log: (0, import_fs14.existsSync)((0, import_path12.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
38796
+ proof_bundle: (0, import_fs14.existsSync)((0, import_path12.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
38797
+ replay_packet: (0, import_fs14.existsSync)((0, import_path12.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
38798
+ knowledge_packet: (0, import_fs14.existsSync)((0, import_path12.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
38799
+ improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
38800
+ notes: (0, import_fs14.existsSync)((0, import_path12.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
38801
+ codex_last_message: input.run.outputs.last_message,
38802
+ codex_stderr: input.run.outputs.stderr,
38803
+ artifact_safety: input.run.outputs.artifact_safety
38804
+ },
38805
+ summary: input.status === "pass" ? "Controlled Codex external-agent run produced passing proof evidence." : `Controlled Codex external-agent run ended as ${input.status} with reason ${input.reasonCode}.`,
38806
+ next_commands: input.status === "pass" ? ["corepack pnpm eval:external-agent:runs:summary"] : [
38807
+ `foh eval external-agent scan-artifacts --run-dir ${input.run.run_dir} --private-repo-root <private_repo_root> --write-redacted --json`,
38808
+ `foh bug improve --from external-agent-run --file ${input.run.outputs.run} --out ${(0, import_path12.join)(input.run.run_dir, "improvement-packet.json")} --json`,
38809
+ "corepack pnpm eval:external-agent:runs:summary"
38810
+ ]
38811
+ };
38812
+ }
38813
+ function spawnCodex(input) {
38814
+ return new Promise((resolveRun) => {
38815
+ const started = Date.now();
38816
+ const useShell = process.platform === "win32" && input.command.toLowerCase().endsWith(".cmd");
38817
+ const child = (0, import_child_process4.spawn)(input.command, input.args, {
38818
+ cwd: input.cwd,
38819
+ env: input.env,
38820
+ shell: useShell,
38821
+ stdio: ["pipe", "pipe", "pipe"],
38822
+ windowsHide: true
38823
+ });
38824
+ const stdout = (0, import_fs14.createWriteStream)(input.stdoutPath, { flags: "w" });
38825
+ const stderr = (0, import_fs14.createWriteStream)(input.stderrPath, { flags: "w" });
38826
+ child.stdout.pipe(stdout);
38827
+ child.stderr.pipe(stderr);
38828
+ child.stdin.end(input.prompt);
38829
+ let timedOut = false;
38830
+ const timer = setTimeout(() => {
38831
+ timedOut = true;
38832
+ if (child.pid && process.platform === "win32") {
38833
+ (0, import_child_process4.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
38834
+ } else {
38835
+ child.kill("SIGKILL");
38836
+ }
38837
+ }, input.timeoutMs);
38838
+ child.on("close", (exitCode) => {
38839
+ clearTimeout(timer);
38840
+ stdout.end();
38841
+ stderr.end();
38842
+ resolveRun({
38843
+ exitCode,
38844
+ timedOut,
38845
+ durationMs: Date.now() - started
38846
+ });
38847
+ });
38848
+ child.on("error", () => {
38849
+ clearTimeout(timer);
38850
+ stdout.end();
38851
+ stderr.end();
38852
+ resolveRun({
38853
+ exitCode: null,
38854
+ timedOut,
38855
+ durationMs: Date.now() - started
38856
+ });
38857
+ });
38858
+ });
38859
+ }
38860
+ async function executeExternalAgentExecutorPlan(plan, options = {}) {
38861
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
38862
+ const results = [];
38863
+ const runnerCommand = options.runnerCommand || resolveCodexExecutionCommand();
38864
+ for (const run of plan.runs) {
38865
+ const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
38866
+ const env = buildCodexExecutorEnv({
38867
+ sourceEnv: options.env,
38868
+ runDir: run.run_dir,
38869
+ promptVersion: run.prompt_version
38870
+ });
38871
+ const spawned = await spawnCodex({
38872
+ command: runnerCommand,
38873
+ args: run.args,
38874
+ cwd: run.workspace_dir,
38875
+ env,
38876
+ prompt: (0, import_fs14.readFileSync)(run.prompt_path, "utf8"),
38877
+ stdoutPath: run.outputs.jsonl,
38878
+ stderrPath: run.outputs.stderr,
38879
+ timeoutMs: plan.timeout_minutes * 60 * 1e3
38880
+ });
38881
+ const artifactSafety = scanExternalAgentArtifacts({
38882
+ runDir: run.run_dir,
38883
+ privateRepoRoot: options.privateRepoRoot || plan.private_repo_root,
38884
+ writeRedacted: true
38885
+ });
38886
+ (0, import_fs14.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
38887
+ `, "utf8");
38888
+ const runEndedAt = (/* @__PURE__ */ new Date()).toISOString();
38889
+ const classification = classifyRun({
38890
+ run,
38891
+ exitCode: spawned.exitCode,
38892
+ timedOut: spawned.timedOut,
38893
+ artifactSafetyOk: artifactSafety.ok
38894
+ });
38895
+ const runArtifact = buildExecutedRunArtifact({
38896
+ run,
38897
+ startedAt: runStartedAt,
38898
+ endedAt: runEndedAt,
38899
+ status: classification.status,
38900
+ reasonCode: classification.reasonCode,
38901
+ exitCode: spawned.exitCode,
38902
+ timedOut: spawned.timedOut,
38903
+ durationMs: spawned.durationMs
38904
+ });
38905
+ (0, import_fs14.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
38906
+ `, "utf8");
38907
+ results.push({
38908
+ run_id: run.run_id,
38909
+ status: classification.status,
38910
+ failure_reason_code: classification.reasonCode,
38911
+ exit_code: spawned.exitCode,
38912
+ timed_out: spawned.timedOut,
38913
+ duration_ms: spawned.durationMs,
38914
+ artifacts: run.outputs
38915
+ });
38916
+ }
38917
+ const endedAt = (/* @__PURE__ */ new Date()).toISOString();
38918
+ const failed = results.some((result) => result.status === "fail");
38919
+ const held = results.some((result) => result.status === "hold");
38920
+ const status = failed ? "fail" : held ? "hold" : "pass";
38921
+ return {
38922
+ schema_version: "external_agent_execution_batch_result.v1",
38923
+ ok: status === "pass",
38924
+ status,
38925
+ reason_code: status === "pass" ? "external_agent_execution_passed" : "external_agent_execution_needs_review",
38926
+ started_at: startedAt,
38927
+ ended_at: endedAt,
38928
+ runner: plan.runner,
38929
+ run_count: results.length,
38930
+ results
38931
+ };
38308
38932
  }
38309
38933
 
38310
38934
  // src/commands/eval.ts
@@ -38326,13 +38950,13 @@ function defaultRunDir(modelName, promptVersion) {
38326
38950
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
38327
38951
  const safeModel = String(modelName || "unknown-model").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
38328
38952
  const safePrompt = String(promptVersion || DEFAULT_PROMPT_VERSION).toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
38329
- return (0, import_path11.resolve)("test-results", "external-agent-runs", date4, `${safeModel}-${safePrompt}-${stamp}`);
38953
+ return (0, import_path13.resolve)("test-results", "external-agent-runs", date4, `${safeModel}-${safePrompt}-${stamp}`);
38330
38954
  }
38331
38955
  function defaultBatchDir(promptVersion) {
38332
38956
  const date4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
38333
38957
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
38334
38958
  const safePrompt = String(promptVersion || DEFAULT_PROMPT_VERSION).toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
38335
- return (0, import_path11.resolve)("test-results", "external-agent-runs", date4, `batch-${safePrompt}-${stamp}`);
38959
+ return (0, import_path13.resolve)("test-results", "external-agent-runs", date4, `batch-${safePrompt}-${stamp}`);
38336
38960
  }
38337
38961
  function safeSlug(value) {
38338
38962
  return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
@@ -38360,14 +38984,14 @@ function inferShell(raw) {
38360
38984
  }
38361
38985
  function writePrompt(runDir, promptVersion) {
38362
38986
  const prompt = PROMPTS[promptVersion] ?? PROMPTS[DEFAULT_PROMPT_VERSION];
38363
- const path2 = (0, import_path11.join)(runDir, "prompt.txt");
38364
- (0, import_fs13.writeFileSync)(path2, `${prompt}
38987
+ const path2 = (0, import_path13.join)(runDir, "prompt.txt");
38988
+ (0, import_fs15.writeFileSync)(path2, `${prompt}
38365
38989
  `, "utf8");
38366
38990
  return path2;
38367
38991
  }
38368
38992
  function writeSession(runDir, session) {
38369
- const path2 = (0, import_path11.join)(runDir, "session.json");
38370
- (0, import_fs13.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
38993
+ const path2 = (0, import_path13.join)(runDir, "session.json");
38994
+ (0, import_fs15.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
38371
38995
  `, "utf8");
38372
38996
  return path2;
38373
38997
  }
@@ -38418,7 +39042,7 @@ function buildRunArtifact(input) {
38418
39042
  },
38419
39043
  summary: status === "pass" ? "External-agent capture session completed and was marked pass." : `External-agent capture session completed with ${commands.length} captured FOH command(s); classify and improve reason ${reasonCode}.`,
38420
39044
  next_commands: status === "pass" ? ["corepack pnpm eval:external-agent:runs:summary"] : [
38421
- `foh bug improve --from external-agent-run --file ${(0, import_path11.join)(input.runDir, "run.json")} --out ${(0, import_path11.join)(input.runDir, "improvement-packet.json")} --json`,
39045
+ `foh bug improve --from external-agent-run --file ${(0, import_path13.join)(input.runDir, "run.json")} --out ${(0, import_path13.join)(input.runDir, "improvement-packet.json")} --json`,
38422
39046
  "corepack pnpm eval:external-agent:runs:summary"
38423
39047
  ]
38424
39048
  };
@@ -38428,13 +39052,13 @@ function registerEval(program3) {
38428
39052
  const external = evalCommand.command("external-agent").description("Capture clean external coding-agent setup attempts");
38429
39053
  external.command("batch").description("Create a deterministic multi-model external-agent batch plan").option("--models <list>", "Comma-separated provider/model list", DEFAULT_BATCH_MODELS).option("--prompt-version <version>", "Prompt version", DEFAULT_PROMPT_VERSION).option("--workspace-type <type>", "Workspace type label", "clean-no-repo").option("--agent-shell <name>", "Agent shell label", "vscode-terminal").option("--out-dir <path>", "Batch output directory").option("--json", "Output as JSON").action(async (opts) => {
38430
39054
  const promptVersion = String(opts.promptVersion || DEFAULT_PROMPT_VERSION);
38431
- const batchDir = (0, import_path11.resolve)(String(opts.outDir || defaultBatchDir(promptVersion)));
39055
+ const batchDir = (0, import_path13.resolve)(String(opts.outDir || defaultBatchDir(promptVersion)));
38432
39056
  const models = parseModelList(String(opts.models || DEFAULT_BATCH_MODELS));
38433
- (0, import_fs13.mkdirSync)(batchDir, { recursive: true });
39057
+ (0, import_fs15.mkdirSync)(batchDir, { recursive: true });
38434
39058
  const runs = models.map((model, index) => {
38435
39059
  const runId = `${String(index + 1).padStart(2, "0")}-${safeSlug(model.provider)}-${safeSlug(model.name)}`;
38436
- const runDir = (0, import_path11.join)(batchDir, runId);
38437
- (0, import_fs13.mkdirSync)(runDir, { recursive: true });
39060
+ const runDir = (0, import_path13.join)(batchDir, runId);
39061
+ (0, import_fs15.mkdirSync)(runDir, { recursive: true });
38438
39062
  const promptPath = writePrompt(runDir, promptVersion);
38439
39063
  const commandArgs = [
38440
39064
  "eval",
@@ -38475,8 +39099,8 @@ function registerEval(program3) {
38475
39099
  runs,
38476
39100
  summary_command: `corepack pnpm eval:external-agent:runs:summary -- --root ${batchDir}`
38477
39101
  };
38478
- const batchPath = (0, import_path11.join)(batchDir, "batch.json");
38479
- (0, import_fs13.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
39102
+ const batchPath = (0, import_path13.join)(batchDir, "batch.json");
39103
+ (0, import_fs15.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
38480
39104
  `, "utf8");
38481
39105
  format(cliEnvelope({
38482
39106
  schemaVersion: "external_agent_batch_plan_result.v1",
@@ -38496,8 +39120,8 @@ function registerEval(program3) {
38496
39120
  external.command("run").description("Launch an instrumented shell and emit external_agent_run.v1 when it exits").option("--model-provider <name>", "Model provider label", "unknown").option("--model-name <name>", "Model name label", "unknown-model").option("--prompt-version <version>", "Prompt version", DEFAULT_PROMPT_VERSION).option("--workspace-type <type>", "Workspace type label", "clean-no-repo").option("--agent-shell <name>", "Agent shell label", "vscode-terminal").option("--out-dir <path>", "Run output directory").option("--status <status>", "Final status when not interactively classified: pass|hold|fail", "hold").option("--reason-code <code>", "Failure/hold reason code", "external_agent_run_needs_review").option("--shell <command>", "Shell command to launch for capture").option("--no-shell", "Do not launch a shell; create/finalize artifacts immediately").option("--json", "Output as JSON").action(async (opts) => {
38497
39121
  const status = normalizeStatus(opts.status);
38498
39122
  const promptVersion = String(opts.promptVersion || DEFAULT_PROMPT_VERSION);
38499
- const runDir = (0, import_path11.resolve)(String(opts.outDir || defaultRunDir(opts.modelName, promptVersion)));
38500
- (0, import_fs13.mkdirSync)(runDir, { recursive: true });
39123
+ const runDir = (0, import_path13.resolve)(String(opts.outDir || defaultRunDir(opts.modelName, promptVersion)));
39124
+ (0, import_fs15.mkdirSync)(runDir, { recursive: true });
38501
39125
  const runId = runDir.split(/[\\/]/).filter(Boolean).slice(-1)[0];
38502
39126
  const promptPath = writePrompt(runDir, promptVersion);
38503
39127
  const shell = inferShell(opts.shell);
@@ -38519,7 +39143,7 @@ function registerEval(program3) {
38519
39143
  }
38520
39144
  };
38521
39145
  writeSession(runDir, session);
38522
- (0, import_fs13.writeFileSync)((0, import_path11.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
39146
+ (0, import_fs15.writeFileSync)((0, import_path13.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
38523
39147
  let shellExitCode = null;
38524
39148
  if (opts.shell !== false) {
38525
39149
  process.stdout.write(`
@@ -38529,7 +39153,7 @@ Prompt: ${promptPath}
38529
39153
  Exit the shell to finalize run.json.
38530
39154
 
38531
39155
  `);
38532
- const result = (0, import_child_process4.spawnSync)(shell.command, shell.args, {
39156
+ const result = (0, import_child_process5.spawnSync)(shell.command, shell.args, {
38533
39157
  stdio: "inherit",
38534
39158
  env: {
38535
39159
  ...process.env,
@@ -38541,8 +39165,8 @@ Exit the shell to finalize run.json.
38541
39165
  shellExitCode = typeof result.status === "number" ? result.status : null;
38542
39166
  }
38543
39167
  const artifact = buildRunArtifact({ runDir, session, status, reasonCode: opts.reasonCode, shellExitCode });
38544
- const runPath = (0, import_path11.join)(runDir, "run.json");
38545
- (0, import_fs13.writeFileSync)(runPath, `${JSON.stringify(artifact, null, 2)}
39168
+ const runPath = (0, import_path13.join)(runDir, "run.json");
39169
+ (0, import_fs15.writeFileSync)(runPath, `${JSON.stringify(artifact, null, 2)}
38546
39170
  `, "utf8");
38547
39171
  format(cliEnvelope({
38548
39172
  schemaVersion: "external_agent_capture_result.v1",
@@ -38552,12 +39176,114 @@ Exit the shell to finalize run.json.
38552
39176
  artifacts: {
38553
39177
  run: runPath,
38554
39178
  prompt: promptPath,
38555
- commands: (0, import_path11.join)(runDir, "commands.ndjson")
39179
+ commands: (0, import_path13.join)(runDir, "commands.ndjson")
38556
39180
  },
38557
39181
  nextCommands: artifact.next_commands,
38558
39182
  extra: { run: artifact }
38559
39183
  }), { json: Boolean(opts.json) });
38560
39184
  });
39185
+ external.command("scan-artifacts").description("Scan and redact external-agent run artifacts before they are promoted into improvement loops").requiredOption("--run-dir <path>", "External-agent run artifact directory").option("--private-repo-root <path>", "Private repository root that must not appear in artifacts").option("--write-redacted", "Write .redacted copies next to scanned artifacts").option("--json", "Output as JSON").action(async (opts) => {
39186
+ const report = scanExternalAgentArtifacts({
39187
+ runDir: String(opts.runDir),
39188
+ privateRepoRoot: opts.privateRepoRoot ? String(opts.privateRepoRoot) : process.cwd(),
39189
+ writeRedacted: Boolean(opts.writeRedacted)
39190
+ });
39191
+ format(cliEnvelope({
39192
+ schemaVersion: "external_agent_artifact_safety_result.v1",
39193
+ status: report.ok ? "pass" : "blocked",
39194
+ reasonCode: report.reason_code,
39195
+ summary: report.ok ? `External-agent artifacts are safe to promote (${report.files_scanned.length} file(s) scanned).` : `External-agent artifacts contain blocked content: ${report.blocked_reason_codes.join(", ")}`,
39196
+ artifacts: {
39197
+ report,
39198
+ redacted_files: report.redacted_files
39199
+ },
39200
+ nextCommands: report.ok ? ["foh bug improve --from external-agent-run --file <run.json> --json"] : ["Do not share or promote raw artifacts. Inspect .redacted copies and fix capture redaction before live rollout."]
39201
+ }), { json: Boolean(opts.json) });
39202
+ if (!report.ok) process.exitCode = 1;
39203
+ });
39204
+ external.command("execute").description("Create a guarded dry-run executor plan for programmable external-agent runners").requiredOption("--batch <path>", "Path to external_agent_batch_plan.v1 batch.json").option("--runner <runner>", "Runner implementation", "codex").option("--workspace-root <path>", "Clean executor workspace root; must be outside the private repo").option("--private-repo-root <path>", "Private repository root to guard against").option("--timeout-minutes <minutes>", "Per-run timeout planned for future execution", "30").option("--skip-runner-probe", "Skip local runner binary/help probing").option("--dry-run", "Write the executor plan without launching the runner", true).option("--live", "Launch one controlled external-agent run after writing the guarded plan").option("--json", "Output as JSON").action(async (opts) => {
39205
+ try {
39206
+ const plan = createExternalAgentExecutorPlan({
39207
+ batchPath: String(opts.batch),
39208
+ runner: String(opts.runner || "codex"),
39209
+ workspaceRoot: opts.workspaceRoot ? String(opts.workspaceRoot) : void 0,
39210
+ privateRepoRoot: opts.privateRepoRoot ? String(opts.privateRepoRoot) : void 0,
39211
+ timeoutMinutes: Number(opts.timeoutMinutes || 30),
39212
+ skipRunnerProbe: Boolean(opts.skipRunnerProbe),
39213
+ cwd: process.cwd()
39214
+ });
39215
+ const planPath = writeExternalAgentExecutorPlan(plan);
39216
+ if (opts.live) {
39217
+ if (plan.runs.length !== 1) {
39218
+ format(cliEnvelope({
39219
+ schemaVersion: "external_agent_execution_result.v1",
39220
+ status: "blocked",
39221
+ reasonCode: "external_agent_live_runner_single_run_only",
39222
+ summary: "Live programmable runner rollout is limited to one run at a time until artifact safety is proven.",
39223
+ artifacts: { plan: planPath },
39224
+ nextCommands: [
39225
+ "Create a one-model batch and rerun with --live."
39226
+ ]
39227
+ }), { json: Boolean(opts.json) });
39228
+ process.exitCode = 1;
39229
+ return;
39230
+ }
39231
+ const result = await executeExternalAgentExecutorPlan(plan, {
39232
+ privateRepoRoot: plan.private_repo_root
39233
+ });
39234
+ const resultPath = (0, import_path13.join)(plan.batch_dir, "execution-result.json");
39235
+ (0, import_fs15.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
39236
+ `, "utf8");
39237
+ format(cliEnvelope({
39238
+ schemaVersion: "external_agent_execution_result.v1",
39239
+ status: result.status,
39240
+ reasonCode: result.reason_code,
39241
+ summary: `Controlled ${plan.runner} execution completed with ${result.status}.`,
39242
+ artifacts: {
39243
+ plan: planPath,
39244
+ result: resultPath,
39245
+ runs: result.results.map((run) => run.artifacts.run)
39246
+ },
39247
+ nextCommands: [
39248
+ ...result.results.map((run) => `foh eval external-agent scan-artifacts --run-dir ${quoteArg(plan.runs.find((item) => item.run_id === run.run_id)?.run_dir || ".")} --private-repo-root ${quoteArg(plan.private_repo_root)} --write-redacted --json`),
39249
+ "corepack pnpm eval:external-agent:runs:summary"
39250
+ ],
39251
+ extra: { result }
39252
+ }), { json: Boolean(opts.json) });
39253
+ if (!result.ok) process.exitCode = 1;
39254
+ return;
39255
+ }
39256
+ format(cliEnvelope({
39257
+ schemaVersion: "external_agent_executor_plan_result.v1",
39258
+ status: "exported",
39259
+ reasonCode: "external_agent_executor_plan_created",
39260
+ summary: `External-agent ${plan.runner} dry-run executor plan created for ${plan.runs.length} run(s).`,
39261
+ artifacts: {
39262
+ plan: planPath
39263
+ },
39264
+ nextCommands: [
39265
+ "Review executor-plan.json for workspace/env isolation before enabling any live runner.",
39266
+ ...plan.runs.map((run) => `${run.command} ${run.args.map(quoteArg).join(" ")} < ${quoteArg(run.prompt_path)} > ${quoteArg(run.outputs.jsonl)}`)
39267
+ ],
39268
+ extra: { plan }
39269
+ }), { json: Boolean(opts.json) });
39270
+ } catch (error2) {
39271
+ if (error2 instanceof ExternalAgentExecutorError) {
39272
+ format(cliEnvelope({
39273
+ schemaVersion: "external_agent_executor_plan_result.v1",
39274
+ status: "blocked",
39275
+ reasonCode: error2.reasonCode,
39276
+ summary: error2.message,
39277
+ nextCommands: [
39278
+ "Fix the executor plan input or workspace path and rerun with --dry-run."
39279
+ ]
39280
+ }), { json: Boolean(opts.json) });
39281
+ process.exitCode = 1;
39282
+ return;
39283
+ }
39284
+ throw error2;
39285
+ }
39286
+ });
38561
39287
  }
38562
39288
 
38563
39289
  // src/lib/banner.ts