@f-o-h/cli 0.1.82 → 0.1.84

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 (2) hide show
  1. package/dist/foh.js +1953 -998
  2. 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 = resolve14.call(this, root, ref);
6049
+ let _sch = resolve15.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 resolve14(root, ref) {
6076
+ function resolve15(root, ref) {
6077
6077
  let sch;
6078
6078
  while (typeof (sch = this.refs[ref]) == "string")
6079
6079
  ref = sch;
@@ -6648,7 +6648,7 @@ var require_fast_uri = __commonJS({
6648
6648
  }
6649
6649
  return uri;
6650
6650
  }
6651
- function resolve14(baseURI, relativeURI, options) {
6651
+ function resolve15(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;
@@ -6875,7 +6875,7 @@ var require_fast_uri = __commonJS({
6875
6875
  var fastUri = {
6876
6876
  SCHEMES,
6877
6877
  normalize,
6878
- resolve: resolve14,
6878
+ resolve: resolve15,
6879
6879
  resolveComponent,
6880
6880
  equal,
6881
6881
  serialize,
@@ -10183,21 +10183,21 @@ async function promptLine(label, {
10183
10183
  allowEmpty = false,
10184
10184
  defaultValue
10185
10185
  } = {}) {
10186
- return await new Promise((resolve14) => {
10186
+ return await new Promise((resolve15) => {
10187
10187
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
10188
10188
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
10189
10189
  rl.question(`${label}${suffix}: `, (answer) => {
10190
10190
  rl.close();
10191
10191
  const value = String(answer ?? "").trim();
10192
10192
  if (!value && typeof defaultValue === "string") {
10193
- resolve14(defaultValue);
10193
+ resolve15(defaultValue);
10194
10194
  return;
10195
10195
  }
10196
10196
  if (!value && !allowEmpty) {
10197
- resolve14("");
10197
+ resolve15("");
10198
10198
  return;
10199
10199
  }
10200
- resolve14(value);
10200
+ resolve15(value);
10201
10201
  });
10202
10202
  });
10203
10203
  }
@@ -10205,7 +10205,7 @@ async function promptSecret(label) {
10205
10205
  if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
10206
10206
  return await promptLine(label);
10207
10207
  }
10208
- return await new Promise((resolve14) => {
10208
+ return await new Promise((resolve15) => {
10209
10209
  const stdin = process.stdin;
10210
10210
  const stdout = process.stdout;
10211
10211
  const wasRaw = Boolean(stdin.isRaw);
@@ -10219,7 +10219,7 @@ async function promptSecret(label) {
10219
10219
  const finish = () => {
10220
10220
  cleanup();
10221
10221
  stdout.write("\n");
10222
- resolve14(value);
10222
+ resolve15(value);
10223
10223
  };
10224
10224
  const onData = (chunk) => {
10225
10225
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
@@ -10228,7 +10228,7 @@ async function promptSecret(label) {
10228
10228
  cleanup();
10229
10229
  process.exitCode = 130;
10230
10230
  stdout.write("\n");
10231
- return resolve14("");
10231
+ return resolve15("");
10232
10232
  }
10233
10233
  if (char === "\r" || char === "\n") {
10234
10234
  finish();
@@ -10256,6 +10256,79 @@ async function promptSecret(label) {
10256
10256
  });
10257
10257
  }
10258
10258
 
10259
+ // src/tui/wizard.ts
10260
+ function resolveDefaultValue(defaultValue, state) {
10261
+ if (typeof defaultValue === "function") return defaultValue(state);
10262
+ return defaultValue;
10263
+ }
10264
+ async function readPromptValue(prompt, state) {
10265
+ const kind = prompt.kind ?? "line";
10266
+ const defaultValue = resolveDefaultValue(prompt.defaultValue, state);
10267
+ if (kind === "confirm") {
10268
+ const answer = await promptLine(prompt.label, { allowEmpty: true, defaultValue });
10269
+ return isAffirmative(answer, prompt.confirmDefault ?? false);
10270
+ }
10271
+ if (kind === "secret") {
10272
+ return (await promptSecret(prompt.label)).trim();
10273
+ }
10274
+ return await promptLine(prompt.label, {
10275
+ allowEmpty: prompt.allowEmpty ?? false,
10276
+ defaultValue
10277
+ });
10278
+ }
10279
+ async function runWizardSession(options) {
10280
+ const state = { ...options.initialState };
10281
+ if (!options.enabled) return state;
10282
+ ensureInteractive(options.step, options.remediation);
10283
+ if (process.stdin.isTTY && process.stdout.isTTY) {
10284
+ if (options.title) {
10285
+ process.stdout.write(`
10286
+ ${options.title}
10287
+ `);
10288
+ process.stdout.write(`${"-".repeat(options.title.length)}
10289
+ `);
10290
+ }
10291
+ if (options.subtitle) process.stdout.write(`${options.subtitle}
10292
+ `);
10293
+ if (Array.isArray(options.notes) && options.notes.length > 0) {
10294
+ for (const note of options.notes) {
10295
+ process.stdout.write(`${note}
10296
+ `);
10297
+ }
10298
+ }
10299
+ if (options.title || options.subtitle || (options.notes?.length ?? 0) > 0) {
10300
+ process.stdout.write("\n");
10301
+ }
10302
+ }
10303
+ for (const prompt of options.prompts) {
10304
+ if (typeof prompt.shouldPrompt === "function" && !prompt.shouldPrompt(state)) continue;
10305
+ while (true) {
10306
+ const resolved = await readPromptValue(prompt, state);
10307
+ if (typeof resolved === "boolean") {
10308
+ state[prompt.key] = resolved;
10309
+ prompt.onResolved?.(resolved, state);
10310
+ break;
10311
+ }
10312
+ const normalized = prompt.normalize ? prompt.normalize(resolved, state) : String(resolved).trim();
10313
+ if (prompt.required && !normalized) {
10314
+ process.stdout.write(`${prompt.label} is required.
10315
+ `);
10316
+ continue;
10317
+ }
10318
+ const validationError = prompt.validate?.(normalized, state) ?? null;
10319
+ if (validationError) {
10320
+ process.stdout.write(`${validationError}
10321
+ `);
10322
+ continue;
10323
+ }
10324
+ state[prompt.key] = normalized;
10325
+ prompt.onResolved?.(normalized, state);
10326
+ break;
10327
+ }
10328
+ }
10329
+ return state;
10330
+ }
10331
+
10259
10332
  // src/lib/console-url.ts
10260
10333
  var DEFAULT_FOH_CONSOLE_URL = "https://app.frontofhouse.okii.uk";
10261
10334
  function normalizeBaseUrl(value) {
@@ -10393,26 +10466,35 @@ function emitBrowserSignupLink(opts) {
10393
10466
  }, { json: opts.json ?? false });
10394
10467
  }
10395
10468
  async function resolveLoginInputs(opts) {
10396
- let email3 = String(opts.email ?? "").trim();
10397
- let password = String(opts.password ?? "").trim();
10398
- const needsPrompts = !email3 || !password;
10399
- if (needsPrompts) {
10400
- ensureInteractive("auth.login", "Pass --email and --password when running non-interactively.");
10401
- }
10402
- if ((opts.wizard || needsPrompts) && process.stdout.isTTY && process.stdin.isTTY && !opts.json) {
10403
- process.stdout.write("\nAuth Login Wizard\n");
10404
- process.stdout.write("-----------------\n");
10405
- process.stdout.write("Step 1/2: Enter account credentials.\n");
10406
- }
10407
- while (!email3) {
10408
- email3 = await promptLine("Email");
10409
- if (!email3) process.stdout.write("Email is required.\n");
10410
- }
10411
- while (!password) {
10412
- password = (await promptSecret("Password")).trim();
10413
- if (!password) process.stdout.write("Password is required.\n");
10414
- }
10415
- if (!email3 || !password) {
10469
+ const initial = {
10470
+ email: String(opts.email ?? "").trim(),
10471
+ password: String(opts.password ?? "").trim()
10472
+ };
10473
+ const needsPrompts = !initial.email || !initial.password;
10474
+ const resolved = await runWizardSession({
10475
+ step: "auth.login",
10476
+ remediation: "Pass --email and --password when running non-interactively.",
10477
+ title: !opts.json ? "Auth Login Wizard" : void 0,
10478
+ subtitle: !opts.json ? "Step 1/2: Enter account credentials." : void 0,
10479
+ enabled: needsPrompts,
10480
+ initialState: initial,
10481
+ prompts: [
10482
+ {
10483
+ key: "email",
10484
+ label: "Email",
10485
+ required: true,
10486
+ shouldPrompt: (state) => !String(state.email ?? "").trim()
10487
+ },
10488
+ {
10489
+ key: "password",
10490
+ label: "Password",
10491
+ kind: "secret",
10492
+ required: true,
10493
+ shouldPrompt: (state) => !String(state.password ?? "").trim()
10494
+ }
10495
+ ]
10496
+ });
10497
+ if (!resolved.email || !resolved.password) {
10416
10498
  throw new FohError({
10417
10499
  step: "auth.login",
10418
10500
  error: "Email and password are required",
@@ -10422,7 +10504,7 @@ async function resolveLoginInputs(opts) {
10422
10504
  if ((opts.wizard || needsPrompts) && process.stdout.isTTY && process.stdin.isTTY && !opts.json) {
10423
10505
  process.stdout.write("Step 2/2: Requesting service token from API.\n");
10424
10506
  }
10425
- return { email: email3, password };
10507
+ return { email: String(resolved.email), password: String(resolved.password) };
10426
10508
  }
10427
10509
  async function maybeSelectDefaultOrg(orgs, jsonMode) {
10428
10510
  if (jsonMode) return void 0;
@@ -10501,7 +10583,7 @@ async function storeAuthenticatedSession(params) {
10501
10583
  return output;
10502
10584
  }
10503
10585
  function sleep(ms) {
10504
- return new Promise((resolve14) => setTimeout(resolve14, ms));
10586
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
10505
10587
  }
10506
10588
  function hasExplicitTimeoutFlag(argv = process.argv) {
10507
10589
  return argv.some((arg) => arg === "--timeout-seconds" || arg.startsWith("--timeout-seconds="));
@@ -11059,7 +11141,7 @@ async function pollUntil(check2, opts) {
11059
11141
  }
11060
11142
  }
11061
11143
  function sleep2(ms) {
11062
- return new Promise((resolve14) => setTimeout(resolve14, ms));
11144
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
11063
11145
  }
11064
11146
 
11065
11147
  // src/commands/compliance.ts
@@ -14183,8 +14265,8 @@ function registerAgentGuardrailCommands(agent) {
14183
14265
  try {
14184
14266
  rule = JSON.parse(opts.rule);
14185
14267
  } catch {
14186
- const { readFileSync: readFileSync17 } = await import("fs");
14187
- rule = JSON.parse(readFileSync17(opts.rule, "utf-8"));
14268
+ const { readFileSync: readFileSync19 } = await import("fs");
14269
+ rule = JSON.parse(readFileSync19(opts.rule, "utf-8"));
14188
14270
  }
14189
14271
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
14190
14272
  method: "POST",
@@ -14680,8 +14762,8 @@ function registerAgent(program3) {
14680
14762
  });
14681
14763
  format(data, { json: opts.json ?? false });
14682
14764
  }));
14683
- agent.command("rollback").description("Roll back an agent to a previous version").requiredOption("--agent <id>", "Agent ID").option("--version <versionId>", "Target version ID (defaults to previous)").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14684
- const body = opts.version ? { version: Number(opts.version) } : {};
14765
+ agent.command("rollback").description("Roll back an agent to a previous version").requiredOption("--agent <id>", "Agent ID").option("--target-version <n>", "Target policy version number (defaults to previous)").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14766
+ const body = opts.targetVersion ? { version: Number(opts.targetVersion) } : {};
14685
14767
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/rollback`, {
14686
14768
  method: "POST",
14687
14769
  body: JSON.stringify(body),
@@ -14753,9 +14835,9 @@ function registerAgent(program3) {
14753
14835
  process.stdout.write(yaml);
14754
14836
  return;
14755
14837
  }
14756
- const { writeFileSync: writeFileSync14 } = await import("fs");
14838
+ const { writeFileSync: writeFileSync15 } = await import("fs");
14757
14839
  const outputPath = opts.output ?? "tenant.yaml";
14758
- writeFileSync14(
14840
+ writeFileSync15(
14759
14841
  outputPath,
14760
14842
  `# tenant.yaml - Front Of House agent manifest
14761
14843
  # Edit this file and run: foh plan tenant.yaml
@@ -14779,8 +14861,8 @@ ${yaml}`,
14779
14861
  format(data, { json: opts.json ?? false });
14780
14862
  }));
14781
14863
  agent.command("chat").description("Interactive REPL \u2014 send messages to an agent (human use only)").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").action(async (opts) => withCommandErrorHandling(async () => {
14782
- const { createInterface: createInterface2 } = await import("readline");
14783
- const rl = createInterface2({ input: process.stdin, output: process.stdout, terminal: true });
14864
+ const { createInterface: createInterface3 } = await import("readline");
14865
+ const rl = createInterface3({ input: process.stdin, output: process.stdout, terminal: true });
14784
14866
  const ask = (prompt) => new Promise((res) => rl.question(prompt, res));
14785
14867
  console.log("Type your message and press Enter. Ctrl+C to exit.\n");
14786
14868
  while (true) {
@@ -15286,6 +15368,9 @@ function registerInstagramChannelCommands(instagram, addCommonOptions) {
15286
15368
  }));
15287
15369
  }
15288
15370
 
15371
+ // src/commands/channel-whatsapp.ts
15372
+ var import_node_fs2 = require("node:fs");
15373
+
15289
15374
  // src/commands/channel-whatsapp-helpers.ts
15290
15375
  function parsePositiveNumber(value, fallback) {
15291
15376
  if (value === void 0 || value === null || String(value).trim() === "") return fallback;
@@ -15299,82 +15384,6 @@ function parsePositiveNumber(value, fallback) {
15299
15384
  }
15300
15385
  return parsed;
15301
15386
  }
15302
- function readCapabilityFlag(capabilities, key) {
15303
- if (!capabilities || typeof capabilities !== "object") return false;
15304
- const direct = capabilities[key];
15305
- if (typeof direct === "boolean") return direct;
15306
- const lower = capabilities[key.toLowerCase()];
15307
- if (typeof lower === "boolean") return lower;
15308
- const upper = capabilities[key.toUpperCase()];
15309
- if (typeof upper === "boolean") return upper;
15310
- return false;
15311
- }
15312
- function resolveWhatsAppJourney(params) {
15313
- const readinessCode = String(params.verificationReadinessCode || "").trim();
15314
- const selectedMethod = String(params.selectedOtpMethod || "").trim().toLowerCase();
15315
- const recordingUrl = String(params.verificationReadiness?.recent_voice_call?.recording_url || "").trim();
15316
- if (params.telephonyReady === false || readinessCode === "twilio_number_not_configured") {
15317
- return {
15318
- stage: "provision_number",
15319
- reason_code: readinessCode || "telephony_step_incomplete",
15320
- explanation: "A dedicated Twilio phone number is required before WhatsApp verification can start.",
15321
- do_this_now: params.commands.provisionBuy,
15322
- after_this: params.commands.verificationReadiness
15323
- };
15324
- }
15325
- if (readinessCode === "verification_voice_call_required") {
15326
- return {
15327
- stage: "verify_number_voice",
15328
- reason_code: readinessCode,
15329
- explanation: recordingUrl ? `Voice OTP path is active. Latest call recording is available for OTP recovery: ${recordingUrl}` : "Voice OTP path is active. Complete Meta phone-call verification first.",
15330
- do_this_now: params.commands.verificationReadinessVoice,
15331
- after_this: params.commands.connect
15332
- };
15333
- }
15334
- if (readinessCode === "otp_method_sms_not_supported") {
15335
- return {
15336
- stage: "verify_number_voice",
15337
- reason_code: readinessCode,
15338
- explanation: "The current number is voice-only, so SMS verification is not supported.",
15339
- do_this_now: params.commands.verificationReadinessVoice,
15340
- after_this: params.commands.connect
15341
- };
15342
- }
15343
- if (readinessCode === "otp_method_voice_not_supported") {
15344
- return {
15345
- stage: "verify_number_sms",
15346
- reason_code: readinessCode,
15347
- explanation: "The current number does not support voice verification, so SMS must be used.",
15348
- do_this_now: params.commands.verificationReadinessSms,
15349
- after_this: params.commands.connect
15350
- };
15351
- }
15352
- if (readinessCode === "verification_code_not_found") {
15353
- return {
15354
- stage: selectedMethod === "voice" ? "verify_number_voice" : "verify_number_sms",
15355
- reason_code: readinessCode,
15356
- explanation: selectedMethod === "voice" ? "Waiting for an inbound verification call from Meta." : "Waiting for an inbound SMS verification code from Meta.",
15357
- do_this_now: selectedMethod === "voice" ? params.commands.verificationReadinessVoice : params.commands.verificationReadiness,
15358
- after_this: params.commands.connect
15359
- };
15360
- }
15361
- if (!params.whatsappConfigured || !params.whatsappReady || readinessCode === "verification_code_detected") {
15362
- return {
15363
- stage: "connect_channel",
15364
- reason_code: readinessCode || "whatsapp_channel_not_ready",
15365
- explanation: "Phone verification is ready; now connect WhatsApp channel credentials.",
15366
- do_this_now: params.commands.connect,
15367
- after_this: params.commands.proof
15368
- };
15369
- }
15370
- return {
15371
- stage: "proof",
15372
- reason_code: readinessCode || "ready_for_proof",
15373
- explanation: "Channel is configured and ready for deterministic closure checks.",
15374
- do_this_now: params.commands.proof,
15375
- after_this: null
15376
- };
15377
- }
15378
15387
  function withNumberedSteps(steps) {
15379
15388
  return steps.filter((step) => step.trim().length > 0).map((step, index) => `${index + 1}. ${step}`).join(" ");
15380
15389
  }
@@ -15596,85 +15605,6 @@ function buildWebhookUrl(apiBaseUrl) {
15596
15605
  function generateVerifyToken() {
15597
15606
  return `${WHATSAPP_VERIFY_TOKEN_PREFIX}-${(0, import_node_crypto.randomBytes)(12).toString("hex")}`;
15598
15607
  }
15599
- function readStepStatus(steps, id) {
15600
- const matched = Array.isArray(steps) ? steps.find((step) => String(step?.id || "").trim() === id) : null;
15601
- if (!matched) return null;
15602
- return matched.status === true;
15603
- }
15604
- async function resolveWhatsAppSetupInputs(opts) {
15605
- let phoneNumberId = String(opts.phoneNumberId ?? "").trim();
15606
- let accessToken = String(opts.accessToken ?? "").trim();
15607
- let verifyToken = String(opts.verifyToken ?? "").trim();
15608
- let verifyTokenGenerated = false;
15609
- let appSecret = String(opts.appSecret ?? "").trim();
15610
- const canGenerateVerifyToken = Boolean(opts.generateVerifyToken);
15611
- let audioEnabled = parseBooleanOption({
15612
- value: opts.audioEnabled,
15613
- fallback: true,
15614
- optionName: "--audio-enabled",
15615
- step: "channel.whatsapp.setup"
15616
- });
15617
- const needsPrompts = !phoneNumberId || !accessToken || !verifyToken && !canGenerateVerifyToken || !appSecret;
15618
- if (opts.wizard || needsPrompts) {
15619
- ensureInteractive(
15620
- "channel.whatsapp.setup",
15621
- "Pass --phone-number-id, --access-token, --verify-token and --app-secret when running non-interactively."
15622
- );
15623
- }
15624
- if ((opts.wizard || needsPrompts) && process.stdin.isTTY && process.stdout.isTTY && !opts.json) {
15625
- process.stdout.write("\nWhatsApp Setup Wizard\n");
15626
- process.stdout.write("---------------------\n");
15627
- process.stdout.write("This flow configures the channel and runs readiness checks before completion.\n");
15628
- process.stdout.write("Use a verified WhatsApp Business sender for production traffic; Meta test numbers are sandbox-only.\n");
15629
- process.stdout.write("\nRequired Meta values:\n");
15630
- process.stdout.write(" - Phone Number ID (verified production sender for production; sandbox test sender only for temporary tests)\n");
15631
- process.stdout.write(" - Access Token (temporary/system-user token)\n");
15632
- process.stdout.write(" - Verify Token (your own secret string; can auto-generate)\n");
15633
- process.stdout.write(" - App Secret (Meta App -> Settings -> Basic)\n\n");
15634
- }
15635
- while (!phoneNumberId) {
15636
- phoneNumberId = (await promptLine("Meta Phone Number ID")).trim();
15637
- if (!phoneNumberId) process.stdout.write("Phone Number ID is required.\n");
15638
- }
15639
- while (!accessToken) {
15640
- accessToken = (await promptSecret("Meta Access Token")).trim();
15641
- if (!accessToken) process.stdout.write("Access Token is required.\n");
15642
- }
15643
- if (!verifyToken && opts.generateVerifyToken) {
15644
- verifyToken = generateVerifyToken();
15645
- verifyTokenGenerated = true;
15646
- }
15647
- if (!verifyToken && process.stdin.isTTY && process.stdout.isTTY && !opts.json) {
15648
- const answer = await promptLine("Generate webhook verify token automatically? [Y/n]", { allowEmpty: true });
15649
- if (isAffirmative(answer, true)) {
15650
- verifyToken = generateVerifyToken();
15651
- verifyTokenGenerated = true;
15652
- process.stdout.write(`Generated Verify Token: ${verifyToken}
15653
- `);
15654
- process.stdout.write("Copy this token into Meta webhook settings exactly.\n");
15655
- }
15656
- }
15657
- while (!verifyToken) {
15658
- verifyToken = (await promptSecret("Webhook Verify Token")).trim();
15659
- if (!verifyToken) process.stdout.write("Verify Token is required.\n");
15660
- }
15661
- while (!appSecret) {
15662
- appSecret = (await promptSecret("Meta App Secret")).trim();
15663
- if (!appSecret) process.stdout.write("App Secret is required.\n");
15664
- }
15665
- if (opts.audioEnabled === void 0 && process.stdin.isTTY && process.stdout.isTTY && !opts.json) {
15666
- const answer = await promptLine("Enable inbound WhatsApp audio fallback auto-reply? [Y/n]", { allowEmpty: true });
15667
- audioEnabled = isAffirmative(answer, true);
15668
- }
15669
- return {
15670
- phoneNumberId,
15671
- accessToken,
15672
- verifyToken,
15673
- verifyTokenGenerated,
15674
- appSecret,
15675
- audioEnabled
15676
- };
15677
- }
15678
15608
  async function runWebhookChallengeCheck({
15679
15609
  apiBaseUrl,
15680
15610
  verifyToken
@@ -15728,277 +15658,409 @@ function assertProofPass(strict, reasons) {
15728
15658
  }
15729
15659
 
15730
15660
  // src/commands/channel-whatsapp.ts
15731
- function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
15732
- addCommonOptions(
15733
- whatsapp.command("start").description("Assess WhatsApp onboarding readiness and print fastest setup path")
15734
- ).option("--country <cc>", "ISO country code used for Twilio number recommendations", "GB").option("--area-code <code>", "Optional area code preference for number suggestions").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Optional verify token to include in generated commands").option("--generate-verify-token", "Generate verify token for the onboarding plan output").action(async (opts) => withCommandErrorHandling(async () => {
15735
- const resolvedOrg = resolveRequiredOrgId(opts.org, {
15736
- apiUrl: opts.apiUrl,
15737
- step: "channel.whatsapp.start",
15738
- remediation: "Run: foh org use --org <id> to set a default, or pass --org <id>"
15661
+ async function runWhatsAppOnboardingSession(params) {
15662
+ return await apiFetch("/v1/console/channels/whatsapp/onboarding-session", {
15663
+ method: "POST",
15664
+ body: JSON.stringify({
15665
+ accessToken: params.accessToken,
15666
+ wabaId: params.wabaId,
15667
+ phoneNumberId: params.phoneNumberId,
15668
+ verifyToken: params.verifyToken,
15669
+ appSecret: params.appSecret,
15670
+ agentId: params.agentId,
15671
+ businessSlug: params.businessSlug,
15672
+ audioEnabled: params.audioEnabled,
15673
+ dryRun: params.dryRun
15674
+ }),
15675
+ orgId: params.orgId,
15676
+ apiUrlOverride: params.apiUrl
15677
+ });
15678
+ }
15679
+ function emitLegacyCommandNotice({
15680
+ command,
15681
+ canonical,
15682
+ jsonMode
15683
+ }) {
15684
+ if (!jsonMode) {
15685
+ process.stderr.write(
15686
+ `[deprecated] foh channel whatsapp ${command} is a compatibility wrapper.
15687
+ Use: ${canonical}
15688
+ `
15689
+ );
15690
+ }
15691
+ return {
15692
+ command,
15693
+ canonical,
15694
+ status: "deprecated_compat_wrapper"
15695
+ };
15696
+ }
15697
+ function parseBatchManifest(manifestPathRaw) {
15698
+ const manifestPath = String(manifestPathRaw || "").trim();
15699
+ if (!manifestPath) {
15700
+ throw new FohError({
15701
+ step: "channel.whatsapp.batch",
15702
+ error: "--manifest is required",
15703
+ remediation: "Provide a JSON manifest path with a businesses array."
15739
15704
  });
15740
- const country = String(opts.country || "GB").trim().toUpperCase();
15741
- const areaCode = String(opts.areaCode || "").trim();
15742
- const creds = loadCredentials(opts.apiUrl);
15743
- const apiBaseUrl = creds.apiUrl;
15744
- const webhookUrl = buildWebhookUrl(apiBaseUrl);
15745
- const verifyToken = String(opts.verifyToken || "").trim() || (opts.generateVerifyToken ? generateVerifyToken() : "<verify_token>");
15746
- const generatedVerifyToken = Boolean(opts.generateVerifyToken && !opts.verifyToken);
15747
- let onboarding = null;
15748
- try {
15749
- onboarding = await apiFetch(`/v1/console/org/${resolvedOrg}/onboarding`, {
15750
- orgId: resolvedOrg,
15751
- apiUrlOverride: opts.apiUrl
15752
- });
15753
- } catch (error2) {
15754
- if (!(error2 instanceof FohError)) throw error2;
15705
+ }
15706
+ try {
15707
+ const raw = (0, import_node_fs2.readFileSync)(manifestPath, "utf8");
15708
+ const parsed = JSON.parse(raw);
15709
+ if (!Array.isArray(parsed.businesses) || parsed.businesses.length === 0) {
15710
+ throw new Error("manifest_missing_businesses");
15755
15711
  }
15756
- let whatsappStatus = null;
15757
- let whatsappConfigured = true;
15758
- try {
15759
- whatsappStatus = await apiFetch("/v1/console/channels/whatsapp/status", {
15760
- orgId: resolvedOrg,
15761
- apiUrlOverride: opts.apiUrl
15762
- });
15763
- } catch (error2) {
15764
- if (!(error2 instanceof FohError)) throw error2;
15765
- const reasonCode = String(error2.detail?.code || "").trim();
15766
- if (error2.statusCode === 404 && reasonCode === "whatsapp_channel_not_configured") {
15767
- whatsappConfigured = false;
15768
- } else {
15769
- throw error2;
15712
+ return parsed;
15713
+ } catch (error2) {
15714
+ throw new FohError({
15715
+ step: "channel.whatsapp.batch",
15716
+ error: "Failed to read --manifest JSON or businesses array is empty.",
15717
+ remediation: 'Provide a JSON file: { "businesses": [{ "businessSlug": "...", "accessToken": "..." }] }',
15718
+ detail: { cause: error2 instanceof Error ? error2.message : String(error2) }
15719
+ });
15720
+ }
15721
+ }
15722
+ function candidateLabel(candidate, index) {
15723
+ const phoneNumberId = String(candidate.phone_number_id || "").trim() || "<missing-phone-id>";
15724
+ const display = String(candidate.display_phone_number || "").trim();
15725
+ const verified = String(candidate.verified_name || "").trim();
15726
+ const wabaId = String(candidate.waba_id || "").trim();
15727
+ const displayPart = display ? ` ${display}` : "";
15728
+ const verifiedPart = verified ? ` (${verified})` : "";
15729
+ const wabaPart = wabaId ? ` [waba:${wabaId}]` : "";
15730
+ return `${index + 1}. ${phoneNumberId}${displayPart}${verifiedPart}${wabaPart}`;
15731
+ }
15732
+ async function runWhatsAppOnboardingWizard(opts) {
15733
+ const wizardState = await runWizardSession({
15734
+ step: "channel.whatsapp.onboard",
15735
+ remediation: "Pass --access-token (and optionally --waba-id / --phone-number-id) when running non-interactively.",
15736
+ title: "WhatsApp Onboarding Wizard",
15737
+ subtitle: "Paste your Meta access token first. The wizard will auto-recover from discovery blockers.",
15738
+ enabled: true,
15739
+ initialState: {
15740
+ accessToken: String(opts.accessToken || "").trim(),
15741
+ appSecret: String(opts.appSecret || "").trim(),
15742
+ provideAppSecret: false
15743
+ },
15744
+ prompts: [
15745
+ {
15746
+ key: "accessToken",
15747
+ label: "Meta Access Token",
15748
+ kind: "secret",
15749
+ required: true,
15750
+ shouldPrompt: (state) => !String(state.accessToken ?? "").trim()
15751
+ },
15752
+ {
15753
+ key: "provideAppSecret",
15754
+ label: "Provide Meta App Secret now for signature-ready closure? [Y/n]",
15755
+ kind: "confirm",
15756
+ confirmDefault: true,
15757
+ shouldPrompt: (state) => !String(state.appSecret ?? "").trim()
15758
+ },
15759
+ {
15760
+ key: "appSecret",
15761
+ label: "Meta App Secret",
15762
+ kind: "secret",
15763
+ required: true,
15764
+ shouldPrompt: (state) => !String(state.appSecret ?? "").trim() && state.provideAppSecret === true
15770
15765
  }
15771
- }
15772
- let availableNumbers = [];
15773
- let numberLookupWarning = null;
15774
- try {
15775
- const qs = new URLSearchParams({ country });
15776
- if (areaCode) qs.set("area_code", areaCode);
15777
- const availability = await apiFetch(`/v1/console/org/twilio/available-numbers?${qs.toString()}`, {
15778
- orgId: resolvedOrg,
15779
- apiUrlOverride: opts.apiUrl
15780
- });
15781
- availableNumbers = Array.isArray(availability.numbers) ? availability.numbers : [];
15782
- } catch (error2) {
15783
- if (!(error2 instanceof FohError)) throw error2;
15784
- numberLookupWarning = error2.error || "Twilio number lookup unavailable for this org context.";
15785
- }
15786
- const smsVoiceCandidates = availableNumbers.filter((entry) => {
15787
- const capabilities = entry.capabilities || {};
15788
- return readCapabilityFlag(capabilities, "sms") && readCapabilityFlag(capabilities, "voice");
15789
- }).slice(0, 3).map((entry) => entry.number).filter((value) => typeof value === "string" && value.trim().length > 0);
15790
- const voiceOnlyCandidates = availableNumbers.filter((entry) => {
15791
- const capabilities = entry.capabilities || {};
15792
- return readCapabilityFlag(capabilities, "voice") && !readCapabilityFlag(capabilities, "sms");
15793
- }).slice(0, 3).map((entry) => entry.number).filter((value) => typeof value === "string" && value.trim().length > 0);
15794
- const telephonyReady = readStepStatus(onboarding?.steps, "telephony");
15795
- const whatsappReady = Boolean(whatsappStatus?.channel?.ready);
15796
- const areaSegment = areaCode ? ` --area-code ${areaCode}` : "";
15797
- const provisionBuyCommand = `foh provision buy --country ${country}${areaSegment} --org ${resolvedOrg}`;
15798
- const listNumbersCommand = `foh provision numbers --country ${country}${areaSegment} --org ${resolvedOrg}`;
15799
- const verificationReadinessCommand = `foh provision whatsapp-verification --lookback-minutes 60 --org ${resolvedOrg}`;
15800
- const verificationReadinessVoiceCommand = `foh provision whatsapp-verification --lookback-minutes 60 --otp-method voice --org ${resolvedOrg}`;
15801
- const verificationReadinessSmsCommand = `foh provision whatsapp-verification --lookback-minutes 60 --otp-method sms --org ${resolvedOrg}`;
15802
- const agentIdSegment = opts.agentId ? ` --agent-id ${opts.agentId}` : "";
15803
- const connectCommand = `foh channel whatsapp connect${agentIdSegment} --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token ${verifyToken} --app-secret <meta_app_secret> --org ${resolvedOrg}`;
15804
- const setupCommand = `foh channel whatsapp setup --wizard${agentIdSegment} --verify-token ${verifyToken} --org ${resolvedOrg}`;
15805
- const proofCommand = `foh channel whatsapp proof --strict --verify-token ${verifyToken} --org ${resolvedOrg}`;
15806
- const liveProofCommand = "corepack pnpm ops:whatsapp:proof:live";
15807
- let verificationReadiness = null;
15808
- let verificationReadinessCode = null;
15766
+ ]
15767
+ });
15768
+ let accessToken = String(wizardState.accessToken || "").trim();
15769
+ let wabaId = String(opts.wabaId || "").trim();
15770
+ let phoneNumberId = String(opts.phoneNumberId || "").trim();
15771
+ let verifyToken = String(opts.verifyToken || "").trim();
15772
+ let appSecret = String(wizardState.appSecret || "").trim();
15773
+ if (!accessToken) {
15774
+ throw new FohError({
15775
+ step: "channel.whatsapp.onboard",
15776
+ error: "Meta access token is required.",
15777
+ remediation: "Run onboarding again and provide a valid token."
15778
+ });
15779
+ }
15780
+ const audioEnabled = parseBooleanOption({
15781
+ value: opts.audioEnabled,
15782
+ fallback: true,
15783
+ optionName: "--audio-enabled",
15784
+ step: "channel.whatsapp.onboard"
15785
+ });
15786
+ const dryRun = Boolean(opts.dryRun);
15787
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
15809
15788
  try {
15810
- verificationReadiness = await apiFetch("/v1/console/channels/whatsapp/verification-readiness?lookback_minutes=60&otp_method=auto", {
15811
- orgId: resolvedOrg,
15812
- apiUrlOverride: opts.apiUrl
15789
+ return await runWhatsAppOnboardingSession({
15790
+ orgId: opts.org,
15791
+ apiUrl: opts.apiUrl,
15792
+ accessToken,
15793
+ wabaId: wabaId || void 0,
15794
+ phoneNumberId: phoneNumberId || void 0,
15795
+ verifyToken: verifyToken || void 0,
15796
+ appSecret: appSecret || void 0,
15797
+ agentId: opts.agentId,
15798
+ businessSlug: opts.businessSlug,
15799
+ audioEnabled,
15800
+ dryRun
15813
15801
  });
15814
- verificationReadinessCode = typeof verificationReadiness.code === "string" ? verificationReadiness.code : null;
15815
15802
  } catch (error2) {
15816
15803
  if (!(error2 instanceof FohError)) throw error2;
15817
- verificationReadinessCode = String(error2.detail?.code || "").trim() || null;
15818
- }
15819
- const selectedOtpMethod = String(verificationReadiness?.verification?.selected_method || "").trim().toLowerCase();
15820
- const verificationPrimaryStep = selectedOtpMethod === "voice" ? `In Meta number setup, request Phone call verification, then confirm readiness with: ${verificationReadinessVoiceCommand}` : `In Meta number setup, request SMS verification, then detect OTP with: ${verificationReadinessCommand}`;
15821
- const verificationFallbackStep = selectedOtpMethod === "sms" ? `If SMS OTP does not arrive, switch to voice OTP path: ${verificationReadinessVoiceCommand}` : "";
15822
- const nextSteps = dedupeSteps([
15823
- telephonyReady === false ? `Provision a dedicated business number first: ${provisionBuyCommand}` : "",
15824
- verificationPrimaryStep,
15825
- verificationFallbackStep,
15826
- !whatsappConfigured || !whatsappReady ? `Connect WhatsApp credentials: ${connectCommand}` : "",
15827
- `In Meta webhook settings, set callback URL=${webhookUrl} and verify token=${verifyToken}.`,
15828
- `Validate deterministic readiness: ${proofCommand}`,
15829
- `Capture live-provider evidence before release: ${liveProofCommand}`
15830
- ]);
15831
- const notes = dedupeSteps([
15832
- smsVoiceCandidates.length === 0 && availableNumbers.length > 0 ? "No SMS+voice candidates found in current Twilio list response; voice-only numbers may still work if Meta verification uses phone call." : "",
15833
- numberLookupWarning ? `Twilio number suggestion lookup warning: ${numberLookupWarning}` : ""
15834
- ]);
15835
- const journey = resolveWhatsAppJourney({
15836
- telephonyReady,
15837
- whatsappConfigured,
15838
- whatsappReady,
15839
- verificationReadinessCode,
15840
- selectedOtpMethod,
15841
- verificationReadiness,
15842
- commands: {
15843
- provisionBuy: provisionBuyCommand,
15844
- verificationReadiness: verificationReadinessCommand,
15845
- verificationReadinessVoice: verificationReadinessVoiceCommand,
15846
- verificationReadinessSms: verificationReadinessSmsCommand,
15847
- connect: connectCommand,
15848
- proof: proofCommand
15804
+ const detail = error2.detail || {};
15805
+ const code = String(detail.code || "").trim();
15806
+ const discovery = detail.discovery || {};
15807
+ const candidates = Array.isArray(discovery.candidates) ? discovery.candidates : [];
15808
+ if (code === "whatsapp_multi_candidate_ambiguous" && candidates.length > 0) {
15809
+ process.stdout.write("\nMultiple senders found. Choose one:\n");
15810
+ for (const [index, candidate] of candidates.entries()) {
15811
+ process.stdout.write(` ${candidateLabel(candidate, index)}
15812
+ `);
15813
+ }
15814
+ const pick2 = (await promptLine("Select number (1..n) or paste phone_number_id")).trim();
15815
+ const pickIndex = Number.parseInt(pick2, 10);
15816
+ if (Number.isFinite(pickIndex) && pickIndex >= 1 && pickIndex <= candidates.length) {
15817
+ phoneNumberId = String(candidates[pickIndex - 1]?.phone_number_id || "").trim();
15818
+ } else {
15819
+ phoneNumberId = pick2;
15820
+ }
15821
+ if (phoneNumberId) continue;
15822
+ }
15823
+ if (!wabaId) {
15824
+ const reasonCode = String(discovery.reason_code || code).trim();
15825
+ if (reasonCode === "whatsapp_discovery_failed" || reasonCode === "whatsapp_phone_discovery_blocked" || reasonCode === "whatsapp_no_phone_numbers_found") {
15826
+ const promptedWaba = (await promptLine("Paste WABA ID (optional, press Enter to skip)", { allowEmpty: true })).trim();
15827
+ if (promptedWaba) {
15828
+ wabaId = promptedWaba;
15829
+ continue;
15830
+ }
15831
+ }
15832
+ }
15833
+ if (!phoneNumberId) {
15834
+ const promptedPhone = (await promptLine("Paste phone_number_id directly (optional, press Enter to stop)", { allowEmpty: true })).trim();
15835
+ if (promptedPhone) {
15836
+ phoneNumberId = promptedPhone;
15837
+ continue;
15838
+ }
15849
15839
  }
15840
+ throw error2;
15841
+ }
15842
+ }
15843
+ throw new FohError({
15844
+ step: "channel.whatsapp.onboard",
15845
+ error: "Unable to complete WhatsApp onboarding after guided retries.",
15846
+ remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp start` for deterministic next steps."
15847
+ });
15848
+ }
15849
+ function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
15850
+ addCommonOptions(
15851
+ whatsapp.command("start").description("[Deprecated wrapper] Start onboarding using the canonical session flow")
15852
+ ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
15853
+ const accessToken = String(opts.accessToken || "").trim();
15854
+ const useWizard = Boolean(opts.wizard) || !accessToken;
15855
+ const legacy = emitLegacyCommandNotice({
15856
+ command: "start",
15857
+ canonical: "foh channel whatsapp onboard --wizard",
15858
+ jsonMode: Boolean(opts.json)
15859
+ });
15860
+ const data = useWizard ? await runWhatsAppOnboardingWizard({
15861
+ ...opts,
15862
+ dryRun: true
15863
+ }) : await runWhatsAppOnboardingSession({
15864
+ orgId: opts.org,
15865
+ apiUrl: opts.apiUrl,
15866
+ accessToken,
15867
+ wabaId: String(opts.wabaId || "").trim() || void 0,
15868
+ phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
15869
+ verifyToken: String(opts.verifyToken || "").trim() || void 0,
15870
+ appSecret: String(opts.appSecret || "").trim() || void 0,
15871
+ agentId: opts.agentId,
15872
+ businessSlug: opts.businessSlug,
15873
+ audioEnabled: parseBooleanOption({
15874
+ value: opts.audioEnabled,
15875
+ fallback: true,
15876
+ optionName: "--audio-enabled",
15877
+ step: "channel.whatsapp.start"
15878
+ }),
15879
+ dryRun: true
15850
15880
  });
15851
15881
  format({
15852
- ok: true,
15853
- channel: "whatsapp",
15854
- org_id: resolvedOrg,
15855
- webhook_callback_url: webhookUrl,
15856
- generated_verify_token: generatedVerifyToken ? verifyToken : null,
15857
- readiness: {
15858
- telephony_step_ready: telephonyReady,
15859
- whatsapp_channel_configured: whatsappConfigured,
15860
- whatsapp_channel_ready: whatsappReady
15861
- },
15862
- market_scan: {
15863
- country,
15864
- area_code: areaCode || null,
15865
- candidate_count: availableNumbers.length,
15866
- sms_voice_candidates: smsVoiceCandidates,
15867
- voice_only_candidates: voiceOnlyCandidates
15868
- },
15869
- sender_model: WHATSAPP_SENDER_MODEL,
15870
- commands: {
15871
- list_numbers: listNumbersCommand,
15872
- provision_buy: provisionBuyCommand,
15873
- verification_readiness: verificationReadinessCommand,
15874
- verification_readiness_sms: verificationReadinessSmsCommand,
15875
- verification_readiness_voice: verificationReadinessVoiceCommand,
15876
- connect: connectCommand,
15877
- setup_wizard: setupCommand,
15878
- proof: proofCommand,
15879
- live_proof: liveProofCommand
15880
- },
15881
- verification_readiness: verificationReadiness || { code: verificationReadinessCode || "verification_readiness_unavailable" },
15882
- journey,
15883
- next_steps: nextSteps,
15884
- notes
15882
+ ...data,
15883
+ legacy_wrapper: legacy,
15884
+ next_steps: dedupeSteps([
15885
+ "Run canonical onboarding apply flow: foh channel whatsapp onboard --wizard",
15886
+ "Run deterministic closure: foh channel whatsapp proof --strict",
15887
+ "Capture live provider evidence: corepack pnpm ops:whatsapp:proof:live"
15888
+ ])
15885
15889
  }, { json: opts.json ?? false });
15886
15890
  }));
15887
15891
  addCommonOptions(
15888
- whatsapp.command("setup").description("Run guided WhatsApp setup and readiness checks")
15889
- ).option("--phone-number-id <id>", "Meta WhatsApp phone number id").option("--access-token <token>", "Meta access token").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)").option("--generate-verify-token", "Generate webhook verify token automatically when missing").option("--wizard", "Run guided setup wizard prompts").action(async (opts) => withCommandErrorHandling(async () => {
15890
- const inputs = await resolveWhatsAppSetupInputs(opts);
15891
- const creds = loadCredentials(opts.apiUrl);
15892
- const apiBaseUrl = creds.apiUrl;
15893
- const webhookUrl = buildWebhookUrl(apiBaseUrl);
15894
- const connect = await apiFetch("/v1/console/channels/whatsapp/connect", {
15895
- method: "POST",
15896
- body: JSON.stringify({
15897
- phoneNumberId: inputs.phoneNumberId,
15898
- accessToken: inputs.accessToken,
15899
- agentId: opts.agentId,
15900
- verifyToken: inputs.verifyToken,
15901
- appSecret: inputs.appSecret,
15902
- audioEnabled: inputs.audioEnabled
15903
- }),
15904
- orgId: opts.org,
15905
- apiUrlOverride: opts.apiUrl
15906
- });
15907
- const checks = await runWhatsAppReadinessChecks({
15892
+ whatsapp.command("onboard").description("Run one-session WhatsApp onboarding (discover -> bind -> verify -> prove)")
15893
+ ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--dry-run", "Run discovery/binding preflight without writing channel config").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
15894
+ const accessToken = String(opts.accessToken || "").trim();
15895
+ const useWizard = Boolean(opts.wizard) || !accessToken;
15896
+ const data = useWizard ? await runWhatsAppOnboardingWizard(opts) : await runWhatsAppOnboardingSession({
15908
15897
  orgId: opts.org,
15909
- apiUrlOverride: opts.apiUrl,
15910
- verifyToken: inputs.verifyToken
15911
- });
15912
- const webhook = await runWebhookChallengeCheck({
15913
- apiBaseUrl,
15914
- verifyToken: inputs.verifyToken
15898
+ apiUrl: opts.apiUrl,
15899
+ accessToken,
15900
+ wabaId: String(opts.wabaId || "").trim() || void 0,
15901
+ phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
15902
+ verifyToken: String(opts.verifyToken || "").trim() || void 0,
15903
+ appSecret: String(opts.appSecret || "").trim() || void 0,
15904
+ agentId: opts.agentId,
15905
+ businessSlug: opts.businessSlug,
15906
+ audioEnabled: parseBooleanOption({
15907
+ value: opts.audioEnabled,
15908
+ fallback: true,
15909
+ optionName: "--audio-enabled",
15910
+ step: "channel.whatsapp.onboard"
15911
+ }),
15912
+ dryRun: Boolean(opts.dryRun)
15915
15913
  });
15916
- if (!webhook.ok) {
15917
- throw new FohError({
15918
- step: "channel.whatsapp.setup",
15919
- error: "Webhook verification challenge did not pass.",
15920
- remediation: `Set callback URL=${webhookUrl}, verify token exactly, subscribe message events, then retry.`
15921
- });
15914
+ format(data, { json: opts.json ?? false });
15915
+ }));
15916
+ addCommonOptions(
15917
+ whatsapp.command("batch").description("Run manifest-driven WhatsApp onboarding across multiple businesses")
15918
+ ).requiredOption("--manifest <path>", "Path to batch manifest JSON").option("--apply", "Apply writes (default is dry-run preflight)").option("--strict", "Exit non-zero when any business fails").action(async (opts) => withCommandErrorHandling(async () => {
15919
+ const manifest = parseBatchManifest(String(opts.manifest || ""));
15920
+ const applyMode = Boolean(opts.apply);
15921
+ const rows = manifest.businesses || [];
15922
+ const results = [];
15923
+ const failureTaxonomy = {};
15924
+ for (const [index, row] of rows.entries()) {
15925
+ const entry = row || {};
15926
+ const accessToken = String(entry.accessToken || "").trim();
15927
+ if (!accessToken) {
15928
+ const code = "whatsapp_access_token_required";
15929
+ failureTaxonomy[code] = (failureTaxonomy[code] || 0) + 1;
15930
+ results.push({
15931
+ index,
15932
+ business_slug: String(entry.businessSlug || "").trim() || null,
15933
+ ok: false,
15934
+ code,
15935
+ reason_codes: [code]
15936
+ });
15937
+ continue;
15938
+ }
15939
+ try {
15940
+ const response = await runWhatsAppOnboardingSession({
15941
+ orgId: opts.org,
15942
+ apiUrl: opts.apiUrl,
15943
+ accessToken,
15944
+ wabaId: String(entry.wabaId || "").trim() || void 0,
15945
+ phoneNumberId: String(entry.phoneNumberId || "").trim() || void 0,
15946
+ verifyToken: String(entry.verifyToken || "").trim() || void 0,
15947
+ appSecret: String(entry.appSecret || "").trim() || void 0,
15948
+ agentId: String(entry.agentId || "").trim() || void 0,
15949
+ businessSlug: String(entry.businessSlug || "").trim() || void 0,
15950
+ audioEnabled: parseBooleanOption({
15951
+ value: entry.audioEnabled,
15952
+ fallback: true,
15953
+ optionName: "batch.audioEnabled",
15954
+ step: "channel.whatsapp.batch"
15955
+ }),
15956
+ dryRun: entry.dryRun === void 0 ? !applyMode : Boolean(entry.dryRun)
15957
+ });
15958
+ results.push({
15959
+ index,
15960
+ business_slug: String(entry.businessSlug || "").trim() || null,
15961
+ ok: true,
15962
+ code: String(response.code || "whatsapp_onboarding_session_ready"),
15963
+ selected_candidate: response.selected_candidate || null
15964
+ });
15965
+ } catch (error2) {
15966
+ if (!(error2 instanceof FohError)) throw error2;
15967
+ const detail = error2.detail || {};
15968
+ const code = String(detail.code || error2.reasonCode || "whatsapp_batch_onboarding_failed").trim();
15969
+ failureTaxonomy[code] = (failureTaxonomy[code] || 0) + 1;
15970
+ results.push({
15971
+ index,
15972
+ business_slug: String(entry.businessSlug || "").trim() || null,
15973
+ ok: false,
15974
+ code,
15975
+ reason_codes: Array.isArray(detail.reason_codes) ? detail.reason_codes : [code]
15976
+ });
15977
+ }
15922
15978
  }
15923
- const readinessReasons = collectReadinessReasons({
15924
- checks,
15925
- webhookCheck: webhook
15926
- });
15927
- if (readinessReasons.length > 0) {
15928
- const steps = buildReasonedNextSteps({
15929
- reasons: readinessReasons,
15930
- webhookUrl,
15931
- verifyToken: inputs.verifyToken,
15932
- includeLiveStep: false,
15933
- liveArtifactPath: "test-results/whatsapp-live-proof.latest.json"
15934
- });
15935
- throw new FohError({
15936
- step: "channel.whatsapp.setup",
15937
- error: `Readiness checks failed: ${readinessReasons.join(", ")}`,
15938
- remediation: withNumberedSteps(steps)
15939
- });
15979
+ const successCount = results.filter((entry) => entry.ok === true).length;
15980
+ const failureCount = results.length - successCount;
15981
+ if (Boolean(opts.strict) && failureCount > 0) {
15982
+ process.exitCode = 1;
15940
15983
  }
15941
- const setupCommands = {
15942
- verify: `foh channel whatsapp proof --strict --verify-token ${inputs.verifyToken}`,
15943
- live: "corepack pnpm ops:whatsapp:proof:live"
15944
- };
15945
15984
  format({
15946
- ok: true,
15985
+ ok: failureCount === 0,
15947
15986
  channel: "whatsapp",
15948
- status: "ready",
15949
- checks: {
15950
- connect: connect.ok ?? true,
15951
- status_ready: Boolean(checks.status.channel?.ready),
15952
- verify: Boolean(checks.verify.ok),
15953
- dry_run_test: Boolean(checks.test.ok),
15954
- webhook_verification: webhook.ok
15955
- },
15956
- channel_state: {
15957
- channel_id: checks.status.channel?.channel_id ?? null,
15958
- phone_number_id: checks.status.channel?.phone_number_id ?? null,
15959
- ready: Boolean(checks.status.channel?.ready),
15960
- verify_token_configured: Boolean(checks.status.channel?.verify_token_configured),
15961
- app_secret_configured: Boolean(checks.status.channel?.app_secret_configured),
15962
- audio_enabled: Boolean(checks.status.channel?.audio_enabled)
15963
- },
15964
- sender_model: WHATSAPP_SENDER_MODEL,
15965
- generated_verify_token: inputs.verifyTokenGenerated ? inputs.verifyToken : null,
15966
- meta_next_steps: [
15967
- `Meta callback URL: ${webhookUrl}`,
15968
- "Meta verify token: use the same value configured in this setup flow.",
15969
- "Subscribe webhook events for WhatsApp messages.",
15970
- "Use Meta test sender IDs only for sandbox testing; switch FOH channel config to a verified WhatsApp Business phone_number_id before production traffic.",
15971
- "Set app to live mode when Meta requirements are complete."
15972
- ],
15973
- operator_next_steps: [
15974
- `Run \`${setupCommands.verify}\` for a deterministic closure check.`,
15975
- "Run `corepack pnpm ops:whatsapp:proof:live` to capture production live-proof artifact."
15976
- ],
15977
- setup_commands: setupCommands
15987
+ workflow: "batch_onboarding_session",
15988
+ apply_mode: applyMode,
15989
+ total: results.length,
15990
+ success_count: successCount,
15991
+ failure_count: failureCount,
15992
+ failure_taxonomy: failureTaxonomy,
15993
+ results
15978
15994
  }, { json: opts.json ?? false });
15979
15995
  }));
15980
15996
  addCommonOptions(
15981
- whatsapp.command("connect").description("Connect WhatsApp channel credentials")
15982
- ).requiredOption("--phone-number-id <id>", "Meta WhatsApp phone number id").requiredOption("--access-token <token>", "Meta access token").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").action(async (opts) => withCommandErrorHandling(async () => {
15983
- const data = await apiFetch("/v1/console/channels/whatsapp/connect", {
15984
- method: "POST",
15985
- body: JSON.stringify({
15986
- phoneNumberId: opts.phoneNumberId,
15987
- accessToken: opts.accessToken,
15988
- agentId: opts.agentId,
15989
- verifyToken: opts.verifyToken,
15990
- appSecret: opts.appSecret,
15991
- audioEnabled: parseBooleanOption({
15992
- value: opts.audioEnabled,
15993
- fallback: true,
15994
- optionName: "--audio-enabled",
15995
- step: "channel.whatsapp.connect"
15996
- })
15997
+ whatsapp.command("setup").description("[Deprecated wrapper] Use canonical onboarding session flow")
15998
+ ).option("--phone-number-id <id>", "Meta WhatsApp phone number id").option("--access-token <token>", "Meta access token").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)").option("--generate-verify-token", "Generate webhook verify token automatically when missing").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--wizard", "Run guided setup wizard prompts").action(async (opts) => withCommandErrorHandling(async () => {
15999
+ const legacy = emitLegacyCommandNotice({
16000
+ command: "setup",
16001
+ canonical: "foh channel whatsapp onboard --wizard",
16002
+ jsonMode: Boolean(opts.json)
16003
+ });
16004
+ const useWizard = Boolean(opts.wizard) || !String(opts.accessToken || "").trim();
16005
+ const generatedVerifyToken = String(opts.verifyToken || "").trim() || (Boolean(opts.generateVerifyToken) ? generateVerifyToken() : "");
16006
+ const data = useWizard ? await runWhatsAppOnboardingWizard({
16007
+ ...opts,
16008
+ verifyToken: generatedVerifyToken || void 0,
16009
+ dryRun: false
16010
+ }) : await runWhatsAppOnboardingSession({
16011
+ orgId: opts.org,
16012
+ apiUrl: opts.apiUrl,
16013
+ accessToken: String(opts.accessToken || "").trim(),
16014
+ wabaId: void 0,
16015
+ phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16016
+ verifyToken: generatedVerifyToken || void 0,
16017
+ appSecret: String(opts.appSecret || "").trim() || void 0,
16018
+ agentId: opts.agentId,
16019
+ businessSlug: String(opts.businessSlug || "").trim() || void 0,
16020
+ audioEnabled: parseBooleanOption({
16021
+ value: opts.audioEnabled,
16022
+ fallback: true,
16023
+ optionName: "--audio-enabled",
16024
+ step: "channel.whatsapp.setup"
15997
16025
  }),
16026
+ dryRun: false
16027
+ });
16028
+ format({
16029
+ ...data,
16030
+ legacy_wrapper: legacy,
16031
+ generated_verify_token: generatedVerifyToken && !opts.verifyToken ? generatedVerifyToken : null
16032
+ }, { json: opts.json ?? false });
16033
+ }));
16034
+ addCommonOptions(
16035
+ whatsapp.command("connect").description("[Deprecated wrapper] Use canonical onboarding session flow")
16036
+ ).requiredOption("--phone-number-id <id>", "Meta WhatsApp phone number id").requiredOption("--access-token <token>", "Meta access token").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").action(async (opts) => withCommandErrorHandling(async () => {
16037
+ const legacy = emitLegacyCommandNotice({
16038
+ command: "connect",
16039
+ canonical: "foh channel whatsapp onboard --access-token <token> --phone-number-id <id>",
16040
+ jsonMode: Boolean(opts.json)
16041
+ });
16042
+ const data = await runWhatsAppOnboardingSession({
15998
16043
  orgId: opts.org,
15999
- apiUrlOverride: opts.apiUrl
16044
+ apiUrl: opts.apiUrl,
16045
+ accessToken: String(opts.accessToken || "").trim(),
16046
+ wabaId: void 0,
16047
+ phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16048
+ verifyToken: String(opts.verifyToken || "").trim() || void 0,
16049
+ appSecret: String(opts.appSecret || "").trim() || void 0,
16050
+ agentId: opts.agentId,
16051
+ businessSlug: String(opts.businessSlug || "").trim() || void 0,
16052
+ audioEnabled: parseBooleanOption({
16053
+ value: opts.audioEnabled,
16054
+ fallback: true,
16055
+ optionName: "--audio-enabled",
16056
+ step: "channel.whatsapp.connect"
16057
+ }),
16058
+ dryRun: false
16000
16059
  });
16001
- format(data, { json: opts.json ?? false });
16060
+ format({
16061
+ ...data,
16062
+ legacy_wrapper: legacy
16063
+ }, { json: opts.json ?? false });
16002
16064
  }));
16003
16065
  addCommonOptions(
16004
16066
  whatsapp.command("status").description("Get WhatsApp channel status")
@@ -16057,10 +16119,11 @@ function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
16057
16119
  ],
16058
16120
  sender_model: WHATSAPP_SENDER_MODEL,
16059
16121
  commands: {
16060
- onboarding_start: `foh channel whatsapp start --generate-verify-token${agentIdSegment}${orgSegment}`,
16122
+ onboarding_start: `foh channel whatsapp onboard --wizard${agentIdSegment}${orgSegment}`,
16061
16123
  verification_readiness: `foh provision whatsapp-verification --lookback-minutes 60${orgSegment}`,
16062
16124
  verification_readiness_voice: `foh provision whatsapp-verification --lookback-minutes 60 --otp-method voice${orgSegment}`,
16063
- connect: `foh channel whatsapp connect${agentIdSegment} --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token ${verifyToken} --app-secret <meta_app_secret>${orgSegment}`,
16125
+ onboard: `foh channel whatsapp onboard${agentIdSegment} --access-token <meta_access_token> --verify-token ${verifyToken} --app-secret <meta_app_secret>${orgSegment}`,
16126
+ batch: `foh channel whatsapp batch --manifest ./whatsapp-batch.manifest.json --apply${orgSegment}`,
16064
16127
  verify: `foh channel whatsapp proof --strict --verify-token ${verifyToken}${orgSegment}`,
16065
16128
  live_proof: "corepack pnpm ops:whatsapp:proof:live",
16066
16129
  release_gate: "corepack pnpm ops:channel:proof:gate:strict"
@@ -16375,11 +16438,11 @@ function registerVoice(program3) {
16375
16438
  }
16376
16439
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
16377
16440
  const audio = Buffer.from(await res.arrayBuffer());
16378
- const { mkdirSync: mkdirSync9, writeFileSync: writeFileSync14 } = await import("fs");
16379
- const { dirname: dirname12, resolve: resolve14 } = await import("path");
16380
- const absolutePath = resolve14(outputPath);
16381
- mkdirSync9(dirname12(absolutePath), { recursive: true });
16382
- writeFileSync14(absolutePath, audio);
16441
+ const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync15 } = await import("fs");
16442
+ const { dirname: dirname13, resolve: resolve15 } = await import("path");
16443
+ const absolutePath = resolve15(outputPath);
16444
+ mkdirSync10(dirname13(absolutePath), { recursive: true });
16445
+ writeFileSync15(absolutePath, audio);
16383
16446
  format({
16384
16447
  status: "ok",
16385
16448
  provider,
@@ -30870,7 +30933,7 @@ var Protocol = class {
30870
30933
  return;
30871
30934
  }
30872
30935
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
30873
- await new Promise((resolve14) => setTimeout(resolve14, pollInterval));
30936
+ await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
30874
30937
  options?.signal?.throwIfAborted();
30875
30938
  }
30876
30939
  } catch (error2) {
@@ -30887,7 +30950,7 @@ var Protocol = class {
30887
30950
  */
30888
30951
  request(request, resultSchema, options) {
30889
30952
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
30890
- return new Promise((resolve14, reject) => {
30953
+ return new Promise((resolve15, reject) => {
30891
30954
  const earlyReject = (error2) => {
30892
30955
  reject(error2);
30893
30956
  };
@@ -30965,7 +31028,7 @@ var Protocol = class {
30965
31028
  if (!parseResult.success) {
30966
31029
  reject(parseResult.error);
30967
31030
  } else {
30968
- resolve14(parseResult.data);
31031
+ resolve15(parseResult.data);
30969
31032
  }
30970
31033
  } catch (error2) {
30971
31034
  reject(error2);
@@ -31226,12 +31289,12 @@ var Protocol = class {
31226
31289
  }
31227
31290
  } catch {
31228
31291
  }
31229
- return new Promise((resolve14, reject) => {
31292
+ return new Promise((resolve15, reject) => {
31230
31293
  if (signal.aborted) {
31231
31294
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
31232
31295
  return;
31233
31296
  }
31234
- const timeoutId = setTimeout(resolve14, interval);
31297
+ const timeoutId = setTimeout(resolve15, interval);
31235
31298
  signal.addEventListener("abort", () => {
31236
31299
  clearTimeout(timeoutId);
31237
31300
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -32331,7 +32394,7 @@ var McpServer = class {
32331
32394
  let task = createTaskResult.task;
32332
32395
  const pollInterval = task.pollInterval ?? 5e3;
32333
32396
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
32334
- await new Promise((resolve14) => setTimeout(resolve14, pollInterval));
32397
+ await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
32335
32398
  const updatedTask = await extra.taskStore.getTask(taskId);
32336
32399
  if (!updatedTask) {
32337
32400
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -32980,19 +33043,19 @@ var StdioServerTransport = class {
32980
33043
  this.onclose?.();
32981
33044
  }
32982
33045
  send(message) {
32983
- return new Promise((resolve14) => {
33046
+ return new Promise((resolve15) => {
32984
33047
  const json3 = serializeMessage(message);
32985
33048
  if (this._stdout.write(json3)) {
32986
- resolve14();
33049
+ resolve15();
32987
33050
  } else {
32988
- this._stdout.once("drain", resolve14);
33051
+ this._stdout.once("drain", resolve15);
32989
33052
  }
32990
33053
  });
32991
33054
  }
32992
33055
  };
32993
33056
 
32994
33057
  // src/lib/cli-version.ts
32995
- var CLI_VERSION = "0.1.82";
33058
+ var CLI_VERSION = "0.1.83";
32996
33059
 
32997
33060
  // src/commands/mcp-serve.ts
32998
33061
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -33177,7 +33240,7 @@ async function runFohCli(params) {
33177
33240
  effectiveArgv.push("--json");
33178
33241
  }
33179
33242
  const command = `foh ${effectiveArgv.join(" ")}`;
33180
- return await new Promise((resolve14) => {
33243
+ return await new Promise((resolve15) => {
33181
33244
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
33182
33245
  stdio: ["ignore", "pipe", "pipe"],
33183
33246
  env: {
@@ -33202,7 +33265,7 @@ async function runFohCli(params) {
33202
33265
  });
33203
33266
  child.once("error", (error2) => {
33204
33267
  clearTimeout(timeoutHandle);
33205
- resolve14({
33268
+ resolve15({
33206
33269
  ok: false,
33207
33270
  command,
33208
33271
  argv: effectiveArgv,
@@ -33218,7 +33281,7 @@ async function runFohCli(params) {
33218
33281
  const stderrText = finalizeBoundedText(stderrBuffer);
33219
33282
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
33220
33283
  const stdoutJson = tryParseJson(stdoutText);
33221
- resolve14({
33284
+ resolve15({
33222
33285
  ok: !timedOut && exitCode === 0,
33223
33286
  command,
33224
33287
  argv: effectiveArgv,
@@ -35388,8 +35451,8 @@ function registerSetup(program3) {
35388
35451
  }
35389
35452
  try {
35390
35453
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
35391
- const { writeFileSync: writeFileSync14 } = await import("fs");
35392
- writeFileSync14(
35454
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35455
+ writeFileSync15(
35393
35456
  "tenant.yaml",
35394
35457
  `# tenant.yaml - Front Of House agent manifest
35395
35458
  # Edit this file and run: foh plan tenant.yaml
@@ -35559,8 +35622,8 @@ function registerSim(program3) {
35559
35622
  }
35560
35623
  const cert = response.certificate;
35561
35624
  if (opts.out) {
35562
- const { writeFileSync: writeFileSync14 } = await import("fs");
35563
- writeFileSync14(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35625
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35626
+ writeFileSync15(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35564
35627
  process.stderr.write(` Certificate written to ${opts.out}
35565
35628
  `);
35566
35629
  }
@@ -35610,8 +35673,8 @@ function registerSim(program3) {
35610
35673
  });
35611
35674
  }
35612
35675
  if (opts.out) {
35613
- const { writeFileSync: writeFileSync14 } = await import("fs");
35614
- writeFileSync14(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35676
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35677
+ writeFileSync15(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35615
35678
  process.stderr.write(` Final certificate written to ${opts.out}
35616
35679
  `);
35617
35680
  }
@@ -35647,7 +35710,7 @@ ${passIcon} Certification loop summary
35647
35710
  }
35648
35711
 
35649
35712
  // src/commands/certify.ts
35650
- var import_node_fs2 = require("node:fs");
35713
+ var import_node_fs3 = require("node:fs");
35651
35714
  function normalizeProfile(raw) {
35652
35715
  const value = String(raw || "release").trim().toLowerCase();
35653
35716
  if (value === "smoke" || value === "release" || value === "stress") return value;
@@ -35725,7 +35788,7 @@ function registerCertify(program3) {
35725
35788
  next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh certify run --agent ${opts.agent} --profile ${profile} --json`]
35726
35789
  };
35727
35790
  if (opts.out) {
35728
- (0, import_node_fs2.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35791
+ (0, import_node_fs3.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35729
35792
  }
35730
35793
  if (opts.json ?? false) {
35731
35794
  format(result, { json: true });
@@ -36675,6 +36738,22 @@ function registerOpsAgencySetupCommands(reporting) {
36675
36738
  });
36676
36739
  format(data, { json: opts.json ?? false });
36677
36740
  }));
36741
+ reporting.command("agency-setup-workflow").description("Run the one-shot any-agency setup workflow: dossier, launch packet, evidence packet, proof commands, and no-overclaim status").requiredOption("--agency-name <name>", "Estate agency trading name").option("--source-url <url>", "Official branch/contact/source URL").option("--branch-location <value>", "Branch/location this setup represents").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <value>", "Target exposure mode", "customer_owned_voice_trial").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36742
+ const body = {
36743
+ agency_name: String(opts.agencyName),
36744
+ requested_tool_surface: parseCsvOption(opts.tools) ?? [],
36745
+ target_exposure_mode: String(opts.targetMode)
36746
+ };
36747
+ if (opts.sourceUrl) body.source_url = String(opts.sourceUrl);
36748
+ if (opts.branchLocation) body.branch_location = String(opts.branchLocation);
36749
+ const data = await apiFetch("/v1/console/agency-setup/workflow", {
36750
+ method: "POST",
36751
+ body: JSON.stringify(body),
36752
+ orgId: opts.org,
36753
+ apiUrlOverride: opts.apiUrl
36754
+ });
36755
+ format(data, { json: opts.json ?? false });
36756
+ }));
36678
36757
  }
36679
36758
 
36680
36759
  // src/commands/ops-diagnostics.ts
@@ -36929,6 +37008,16 @@ function registerOpsReportingCommands(reporting) {
36929
37008
  });
36930
37009
  format(data, { json: opts.json ?? false });
36931
37010
  }));
37011
+ reporting.command("customer-live-status").description("Show one operator-grade customer-live status answer with actions, proof refs, and next commands").option("--environment <value>", "Environment filter").option("--agency-name <name>", "Agency name for setup-preview fallback when no release packet exists").option("--source-url <url>", "Official source URL for setup-preview fallback").option("--branch-location <value>", "Branch/location for setup-preview fallback").option("--tools <csv>", "Requested tool surface for setup-preview fallback").option("--target-mode <mode>", "Target exposure mode for setup-preview fallback").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
37012
+ const params = new URLSearchParams();
37013
+ if (opts.environment) params.set("environment", String(opts.environment));
37014
+ appendSetupPreviewContext(params, opts);
37015
+ const data = await apiFetch(withQuery("/v1/console/customer-live-status", params), {
37016
+ orgId: opts.org,
37017
+ apiUrlOverride: opts.apiUrl
37018
+ });
37019
+ format(data, { json: opts.json ?? false });
37020
+ }));
36932
37021
  reporting.command("release-scorecard").description("Show deterministic release scorecard for an agent").requiredOption("--agent <id>", "Agent ID").option("--limit <n>", "Number of runs to include", "10").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36933
37022
  const limit = Math.max(1, Math.min(100, Number(opts.limit || 10) || 10));
36934
37023
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/release-scorecard?limit=${limit}`, {
@@ -37560,41 +37649,80 @@ function defaultImprovementArtifactPath() {
37560
37649
  }
37561
37650
  async function resolveBugReportWizardInputs(opts) {
37562
37651
  if (!opts.wizard) return opts;
37563
- ensureInteractive("bug.report", "Run with --out and --command in non-interactive mode, or use --wizard in a TTY.");
37564
- process.stdout.write("\nBug Report Wizard\n");
37565
- process.stdout.write("-----------------\n");
37566
- const resolved = { ...opts };
37567
- if (!String(resolved.out ?? "").trim()) {
37568
- resolved.out = await promptLine("Output file path", { defaultValue: defaultArtifactPath() });
37569
- }
37570
- if (!String(resolved.command ?? "").trim()) {
37571
- resolved.command = await promptLine("Failing command");
37572
- }
37573
- if (!String(resolved.requestMethod ?? "").trim()) {
37574
- resolved.requestMethod = await promptLine("Request method", { defaultValue: "GET" });
37575
- }
37576
- const hasRequestLocation = String(resolved.requestUrl ?? "").trim() || String(resolved.requestPath ?? "").trim();
37577
- if (!hasRequestLocation) {
37578
- const requestLocation = await promptLine("Request URL or path (for example /v1/console/agents)");
37579
- if (requestLocation.startsWith("http://") || requestLocation.startsWith("https://")) {
37580
- resolved.requestUrl = requestLocation;
37581
- } else {
37582
- resolved.requestPath = requestLocation;
37583
- }
37584
- }
37585
- if (!String(resolved.responseStatus ?? "").trim()) {
37586
- resolved.responseStatus = await promptLine("Response status", { defaultValue: "500" });
37587
- }
37588
- if (!String(resolved.severity ?? "").trim()) {
37589
- resolved.severity = await promptLine("Severity", { defaultValue: "medium" });
37590
- }
37591
- if (!String(resolved.source ?? "").trim()) {
37592
- resolved.source = await promptLine("Source", { defaultValue: "cli" });
37593
- }
37594
- if (resolved.submit === void 0) {
37595
- const submitAnswer = await promptLine("Submit packet to FOH API now? [y/N]", { allowEmpty: true });
37596
- resolved.submit = isAffirmative(submitAnswer, false);
37597
- }
37652
+ const resolved = await runWizardSession({
37653
+ step: "bug.report",
37654
+ remediation: "Run with --out and --command in non-interactive mode, or use --wizard in a TTY.",
37655
+ title: "Bug Report Wizard",
37656
+ initialState: { ...opts },
37657
+ enabled: true,
37658
+ prompts: [
37659
+ {
37660
+ key: "out",
37661
+ label: "Output file path",
37662
+ defaultValue: () => defaultArtifactPath(),
37663
+ required: true,
37664
+ shouldPrompt: (state) => !String(state.out ?? "").trim()
37665
+ },
37666
+ {
37667
+ key: "command",
37668
+ label: "Failing command",
37669
+ required: true,
37670
+ shouldPrompt: (state) => !String(state.command ?? "").trim()
37671
+ },
37672
+ {
37673
+ key: "requestMethod",
37674
+ label: "Request method",
37675
+ defaultValue: "GET",
37676
+ required: true,
37677
+ shouldPrompt: (state) => !String(state.requestMethod ?? "").trim()
37678
+ },
37679
+ {
37680
+ key: "requestLocation",
37681
+ label: "Request URL or path (for example /v1/console/agents)",
37682
+ required: true,
37683
+ shouldPrompt: (state) => !String(state.requestUrl ?? "").trim() && !String(state.requestPath ?? "").trim(),
37684
+ onResolved: (value, state) => {
37685
+ const location = String(value ?? "").trim();
37686
+ if (location.startsWith("http://") || location.startsWith("https://")) {
37687
+ state.requestUrl = location;
37688
+ state.requestPath = "";
37689
+ } else {
37690
+ state.requestPath = location;
37691
+ state.requestUrl = "";
37692
+ }
37693
+ }
37694
+ },
37695
+ {
37696
+ key: "responseStatus",
37697
+ label: "Response status",
37698
+ defaultValue: "500",
37699
+ required: true,
37700
+ shouldPrompt: (state) => !String(state.responseStatus ?? "").trim()
37701
+ },
37702
+ {
37703
+ key: "severity",
37704
+ label: "Severity",
37705
+ defaultValue: "medium",
37706
+ required: true,
37707
+ shouldPrompt: (state) => !String(state.severity ?? "").trim()
37708
+ },
37709
+ {
37710
+ key: "source",
37711
+ label: "Source",
37712
+ defaultValue: "cli",
37713
+ required: true,
37714
+ shouldPrompt: (state) => !String(state.source ?? "").trim()
37715
+ },
37716
+ {
37717
+ key: "submit",
37718
+ label: "Submit packet to FOH API now? [y/N]",
37719
+ kind: "confirm",
37720
+ confirmDefault: false,
37721
+ shouldPrompt: (state) => state.submit === void 0
37722
+ }
37723
+ ]
37724
+ });
37725
+ delete resolved.requestLocation;
37598
37726
  return resolved;
37599
37727
  }
37600
37728
  function ensureBugReportRequiredInputs(opts) {
@@ -37790,7 +37918,7 @@ function registerBug(program3) {
37790
37918
 
37791
37919
  // src/lib/proof-cache.ts
37792
37920
  var import_node_crypto2 = require("node:crypto");
37793
- var import_node_fs3 = require("node:fs");
37921
+ var import_node_fs4 = require("node:fs");
37794
37922
  var import_node_path = require("node:path");
37795
37923
  var DEFAULT_MAX_AGE_MS = 15 * 60 * 1e3;
37796
37924
  var DEFAULT_WAIT_MS = 180 * 1e3;
@@ -37814,7 +37942,7 @@ function publicPath(filePath) {
37814
37942
  }
37815
37943
  function readFreshCache(filePath, maxAgeMs) {
37816
37944
  try {
37817
- const payload = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
37945
+ const payload = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
37818
37946
  const createdAt = Date.parse(String(payload.created_at || ""));
37819
37947
  if (!Number.isFinite(createdAt)) return null;
37820
37948
  const ageMs = Date.now() - createdAt;
@@ -37826,8 +37954,8 @@ function readFreshCache(filePath, maxAgeMs) {
37826
37954
  }
37827
37955
  }
37828
37956
  function writeCache(filePath, value) {
37829
- (0, import_node_fs3.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37830
- (0, import_node_fs3.writeFileSync)(filePath, `${JSON.stringify({
37957
+ (0, import_node_fs4.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37958
+ (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify({
37831
37959
  schema_version: "foh_proof_cache_entry.v1",
37832
37960
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
37833
37961
  value
@@ -37862,7 +37990,7 @@ async function withProofCache(options, run) {
37862
37990
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
37863
37991
  const waitMs = options.waitMs ?? Number(process.env.FOH_PROOF_CACHE_WAIT_MS || DEFAULT_WAIT_MS);
37864
37992
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
37865
- (0, import_node_fs3.mkdirSync)(resolvedDir, { recursive: true });
37993
+ (0, import_node_fs4.mkdirSync)(resolvedDir, { recursive: true });
37866
37994
  const existing = readFreshCache(cachePath, maxAgeMs);
37867
37995
  if (existing) {
37868
37996
  return {
@@ -37881,7 +38009,7 @@ async function withProofCache(options, run) {
37881
38009
  }
37882
38010
  let lockOwner = false;
37883
38011
  try {
37884
- (0, import_node_fs3.mkdirSync)(lockPath);
38012
+ (0, import_node_fs4.mkdirSync)(lockPath);
37885
38013
  lockOwner = true;
37886
38014
  } catch {
37887
38015
  const started = Date.now();
@@ -37922,7 +38050,7 @@ async function withProofCache(options, run) {
37922
38050
  }
37923
38051
  };
37924
38052
  } finally {
37925
- if (lockOwner) (0, import_node_fs3.rmSync)(lockPath, { recursive: true, force: true });
38053
+ if (lockOwner) (0, import_node_fs4.rmSync)(lockPath, { recursive: true, force: true });
37926
38054
  }
37927
38055
  }
37928
38056
 
@@ -38412,7 +38540,582 @@ function registerProve(program3) {
38412
38540
  }));
38413
38541
  }
38414
38542
 
38415
- // src/tui/command-palette.ts
38543
+ // src/commands/interactive.ts
38544
+ var import_readline2 = require("readline");
38545
+ var import_child_process2 = require("child_process");
38546
+ var import_fs11 = require("fs");
38547
+ var import_os2 = require("os");
38548
+ var import_path9 = require("path");
38549
+
38550
+ // src/commands/command-graph.ts
38551
+ var COMMAND_SURFACE_DEFINITIONS = [
38552
+ {
38553
+ id: "home",
38554
+ commandPath: ["home"],
38555
+ label: "home",
38556
+ descriptionFallback: "Open onboarding home",
38557
+ mutatesState: "read",
38558
+ shellSlash: "/home",
38559
+ includeInSuggestions: false,
38560
+ homeContexts: []
38561
+ },
38562
+ {
38563
+ id: "start",
38564
+ commandPath: ["home"],
38565
+ invokeArgs: ["start"],
38566
+ label: "start",
38567
+ descriptionFallback: "Run guided onboarding start",
38568
+ mutatesState: "read",
38569
+ shellSlash: "/start",
38570
+ includeInSuggestions: false,
38571
+ homeContexts: []
38572
+ },
38573
+ {
38574
+ id: "auth_login",
38575
+ commandPath: ["auth", "login"],
38576
+ label: "login now (recommended)",
38577
+ descriptionFallback: "Run interactive login flow.",
38578
+ mutatesState: "write",
38579
+ examples: ["foh auth login --wizard"],
38580
+ homeContexts: ["unauthenticated"],
38581
+ homeAliases: ["start", "login", "auth", "auth login"],
38582
+ includeInSuggestions: true
38583
+ },
38584
+ {
38585
+ id: "auth_login_help",
38586
+ commandPath: ["auth", "login"],
38587
+ invokeArgs: ["auth", "login", "--help"],
38588
+ label: "auth login help",
38589
+ descriptionFallback: "Show auth login examples.",
38590
+ mutatesState: "read",
38591
+ homeContexts: ["unauthenticated"],
38592
+ includeInSuggestions: true
38593
+ },
38594
+ {
38595
+ id: "auth_login_wizard",
38596
+ commandPath: ["auth", "login"],
38597
+ invokeArgs: ["auth", "login", "--wizard"],
38598
+ label: "auth login wizard",
38599
+ descriptionFallback: "Run login wizard",
38600
+ mutatesState: "write",
38601
+ shellSlash: "/login",
38602
+ includeInSuggestions: true
38603
+ },
38604
+ {
38605
+ id: "auth_whoami",
38606
+ commandPath: ["auth", "whoami"],
38607
+ label: "auth whoami",
38608
+ descriptionFallback: "Show current auth context",
38609
+ requiresAuth: true,
38610
+ mutatesState: "read",
38611
+ homeContexts: ["authenticated_no_org"],
38612
+ homeAliases: ["whoami"],
38613
+ shellSlash: "/auth",
38614
+ includeInSuggestions: true
38615
+ },
38616
+ {
38617
+ id: "org_list",
38618
+ commandPath: ["org", "list"],
38619
+ label: "list my orgs",
38620
+ descriptionFallback: "View org memberships.",
38621
+ requiresAuth: true,
38622
+ mutatesState: "read",
38623
+ homeContexts: ["authenticated_no_org"],
38624
+ homeAliases: ["org"],
38625
+ shellSlash: "/orgs",
38626
+ includeInSuggestions: true
38627
+ },
38628
+ {
38629
+ id: "org_use_help",
38630
+ commandPath: ["org", "use"],
38631
+ invokeArgs: ["org", "use", "--help"],
38632
+ label: "org use help",
38633
+ descriptionFallback: "Set default org guidance.",
38634
+ requiresAuth: true,
38635
+ mutatesState: "read",
38636
+ homeContexts: ["authenticated_no_org"],
38637
+ includeInSuggestions: true
38638
+ },
38639
+ {
38640
+ id: "tenant_status",
38641
+ commandPath: ["tenant", "status"],
38642
+ label: "tenant status",
38643
+ descriptionFallback: "Check tenant status and readiness.",
38644
+ requiresAuth: true,
38645
+ requiresOrg: true,
38646
+ mutatesState: "read",
38647
+ homeContexts: ["authenticated_with_org"],
38648
+ homeAliases: ["tenant"],
38649
+ shellSlash: "/status",
38650
+ includeInSuggestions: true
38651
+ },
38652
+ {
38653
+ id: "templates_list",
38654
+ commandPath: ["templates", "list"],
38655
+ label: "template list",
38656
+ descriptionFallback: "List templates.",
38657
+ requiresAuth: true,
38658
+ requiresOrg: true,
38659
+ mutatesState: "read",
38660
+ homeContexts: ["authenticated_with_org"],
38661
+ homeAliases: ["templates", "template list"],
38662
+ shellSlash: "/templates",
38663
+ includeInSuggestions: true
38664
+ },
38665
+ {
38666
+ id: "agent_list_all",
38667
+ commandPath: ["agent", "list"],
38668
+ invokeArgs: ["agent", "list", "--segment", "all"],
38669
+ label: "agent list",
38670
+ descriptionFallback: "List all agents.",
38671
+ requiresAuth: true,
38672
+ requiresOrg: true,
38673
+ mutatesState: "read",
38674
+ homeContexts: ["authenticated_with_org"],
38675
+ homeAliases: ["agent"],
38676
+ shellSlash: "/agent",
38677
+ includeInSuggestions: true
38678
+ },
38679
+ {
38680
+ id: "setup_help",
38681
+ commandPath: ["setup"],
38682
+ invokeArgs: ["setup", "--help"],
38683
+ label: "setup help",
38684
+ descriptionFallback: "Show setup flow options.",
38685
+ mutatesState: "read",
38686
+ homeContexts: ["authenticated_with_org"],
38687
+ homeAliases: ["setup"],
38688
+ shellSlash: "/setup",
38689
+ includeInSuggestions: true
38690
+ },
38691
+ {
38692
+ id: "bug_report_help",
38693
+ commandPath: ["bug", "report"],
38694
+ invokeArgs: ["bug", "report", "--help"],
38695
+ label: "bug report help",
38696
+ descriptionFallback: "Show bug-report guidance.",
38697
+ mutatesState: "read",
38698
+ homeContexts: ["authenticated_with_org"],
38699
+ homeAliases: ["bug report", "bug report help"],
38700
+ includeInSuggestions: false
38701
+ },
38702
+ {
38703
+ id: "bug_list",
38704
+ commandPath: ["bug", "list"],
38705
+ label: "bug list",
38706
+ descriptionFallback: "List centralized bug reports.",
38707
+ requiresAuth: true,
38708
+ requiresOrg: true,
38709
+ mutatesState: "read",
38710
+ homeContexts: ["authenticated_with_org"],
38711
+ homeAliases: ["bug list"],
38712
+ includeInSuggestions: false
38713
+ },
38714
+ {
38715
+ id: "interactive_shell",
38716
+ commandPath: ["interactive"],
38717
+ label: "interactive shell",
38718
+ descriptionFallback: "Open slash-command shell.",
38719
+ mutatesState: "read",
38720
+ homeContexts: ["unauthenticated", "authenticated_no_org", "authenticated_with_org"],
38721
+ homeAliases: ["interactive", "interactive shell", "shell"],
38722
+ includeInSuggestions: false
38723
+ },
38724
+ {
38725
+ id: "full_help",
38726
+ commandPath: ["home"],
38727
+ invokeArgs: ["--help"],
38728
+ label: "full help",
38729
+ descriptionFallback: "Show all CLI commands.",
38730
+ mutatesState: "read",
38731
+ homeContexts: ["unauthenticated", "authenticated_no_org"],
38732
+ homeAliases: ["full help"],
38733
+ includeInSuggestions: false
38734
+ },
38735
+ {
38736
+ id: "whatsapp_start",
38737
+ commandPath: ["channel", "whatsapp", "start"],
38738
+ label: "whatsapp start",
38739
+ descriptionFallback: "WhatsApp readiness path",
38740
+ mutatesState: "write",
38741
+ shellSlash: "/whatsapp",
38742
+ includeInSuggestions: true
38743
+ },
38744
+ {
38745
+ id: "voice_help",
38746
+ commandPath: ["voice"],
38747
+ invokeArgs: ["voice", "--help"],
38748
+ label: "voice help",
38749
+ descriptionFallback: "Voice command help",
38750
+ mutatesState: "read",
38751
+ shellSlash: "/voice",
38752
+ includeInSuggestions: true
38753
+ },
38754
+ {
38755
+ id: "widget_help",
38756
+ commandPath: ["widget"],
38757
+ invokeArgs: ["widget", "--help"],
38758
+ label: "widget help",
38759
+ descriptionFallback: "Widget command help",
38760
+ mutatesState: "read",
38761
+ shellSlash: "/widget",
38762
+ includeInSuggestions: true
38763
+ },
38764
+ {
38765
+ id: "diag_debug",
38766
+ commandPath: ["diag"],
38767
+ invokeArgs: ["debug"],
38768
+ label: "debug",
38769
+ descriptionFallback: "Run diagnostics",
38770
+ mutatesState: "read",
38771
+ shellSlash: "/debug",
38772
+ includeInSuggestions: true
38773
+ }
38774
+ ];
38775
+ var SHELL_BUILTIN_COMMANDS = [
38776
+ { slash: "/help", description: "Show shell help" },
38777
+ { slash: "/?", description: "Show keymap help" },
38778
+ { slash: "/keys", description: "Show keymap help" },
38779
+ { slash: "/commands", description: "List available slash commands" },
38780
+ { slash: "/again", description: "Replay last successful command" },
38781
+ { slash: "/retry", description: "Replay last failed command" },
38782
+ { slash: "/editlast", description: "Prefill last command for editing" },
38783
+ { slash: "/snippet", description: "Manage named command snippets" },
38784
+ { slash: "/clear", description: "Clear the terminal output" },
38785
+ { slash: "/exit", description: "Exit shell" },
38786
+ { slash: "/quit", description: "Exit shell" }
38787
+ ];
38788
+ var HOME_QUICK_ACTION_ORDER = {
38789
+ unauthenticated: ["auth_login", "interactive_shell", "full_help", "auth_login_help"],
38790
+ authenticated_no_org: ["org_list", "interactive_shell", "org_use_help", "auth_whoami", "full_help"],
38791
+ authenticated_with_org: [
38792
+ "tenant_status",
38793
+ "interactive_shell",
38794
+ "templates_list",
38795
+ "agent_list_all",
38796
+ "setup_help",
38797
+ "bug_report_help",
38798
+ "bug_list"
38799
+ ]
38800
+ };
38801
+ var commandPathIndex = /* @__PURE__ */ new Map();
38802
+ function contextFromState(state) {
38803
+ if (!state.authenticated) return "unauthenticated";
38804
+ if (!state.orgId) return "authenticated_no_org";
38805
+ return "authenticated_with_org";
38806
+ }
38807
+ function commandKey(parts) {
38808
+ return parts.join(" ");
38809
+ }
38810
+ function normalizeCommandToken(value) {
38811
+ return String(value ?? "").trim().toLowerCase();
38812
+ }
38813
+ function readCommandDescription(command) {
38814
+ try {
38815
+ return (command.description() ?? "").trim();
38816
+ } catch {
38817
+ return "";
38818
+ }
38819
+ }
38820
+ function collectCommandPaths(program3) {
38821
+ const paths = /* @__PURE__ */ new Map();
38822
+ const walk = (parentPath, scope) => {
38823
+ for (const child of scope.commands) {
38824
+ const childName = normalizeCommandToken(child.name());
38825
+ if (!childName) continue;
38826
+ const nextPath = [...parentPath, childName];
38827
+ const canonicalPathKey = commandKey(nextPath);
38828
+ const description = readCommandDescription(child);
38829
+ paths.set(canonicalPathKey, { pathKey: canonicalPathKey, description });
38830
+ const aliases = child.aliases().map((alias) => normalizeCommandToken(alias)).filter(Boolean);
38831
+ for (const alias of aliases) {
38832
+ const aliasPath = [...parentPath, alias];
38833
+ paths.set(commandKey(aliasPath), { pathKey: canonicalPathKey, description });
38834
+ }
38835
+ walk(nextPath, child);
38836
+ }
38837
+ };
38838
+ walk([], program3);
38839
+ return paths;
38840
+ }
38841
+ function resolveDescription(definition) {
38842
+ const record2 = commandPathIndex.get(commandKey(definition.commandPath));
38843
+ if (record2 && record2.description) return record2.description;
38844
+ return definition.descriptionFallback;
38845
+ }
38846
+ function buildEntry(definition) {
38847
+ return {
38848
+ id: definition.id,
38849
+ label: definition.label,
38850
+ description: resolveDescription(definition),
38851
+ args: [...definition.invokeArgs ?? definition.commandPath],
38852
+ commandPath: [...definition.commandPath],
38853
+ requiresAuth: definition.requiresAuth === true,
38854
+ requiresOrg: definition.requiresOrg === true,
38855
+ mutatesState: definition.mutatesState,
38856
+ examples: [...definition.examples ?? []],
38857
+ homeContexts: [...definition.homeContexts ?? []],
38858
+ homeAliases: [...definition.homeAliases ?? []],
38859
+ shellSlash: definition.shellSlash,
38860
+ includeInSuggestions: definition.includeInSuggestions !== false
38861
+ };
38862
+ }
38863
+ function initializeCommandGraph(program3) {
38864
+ commandPathIndex = collectCommandPaths(program3);
38865
+ const missing = COMMAND_SURFACE_DEFINITIONS.filter((definition) => !commandPathIndex.has(commandKey(definition.commandPath))).map((definition) => definition.id);
38866
+ if (missing.length > 0) {
38867
+ throw new Error(`command_graph_definition_missing_paths:${missing.join(",")}`);
38868
+ }
38869
+ }
38870
+ function getCommandGraphEntries() {
38871
+ return COMMAND_SURFACE_DEFINITIONS.map(buildEntry);
38872
+ }
38873
+ function getHomeQuickActions(state) {
38874
+ const context = contextFromState(state);
38875
+ const rank = new Map(HOME_QUICK_ACTION_ORDER[context].map((id, index) => [id, index]));
38876
+ return getCommandGraphEntries().filter((entry) => entry.homeContexts.includes(context)).sort((left, right) => {
38877
+ const leftRank = rank.get(left.id) ?? Number.MAX_SAFE_INTEGER;
38878
+ const rightRank = rank.get(right.id) ?? Number.MAX_SAFE_INTEGER;
38879
+ if (leftRank !== rightRank) return leftRank - rightRank;
38880
+ return left.label.localeCompare(right.label);
38881
+ }).map((entry) => ({
38882
+ label: entry.label,
38883
+ description: entry.description,
38884
+ args: [...entry.args],
38885
+ aliases: [...entry.homeAliases]
38886
+ }));
38887
+ }
38888
+ function getHomePaletteCommands(state) {
38889
+ return getCommandGraphEntries().filter((entry) => {
38890
+ if (entry.requiresAuth && !state.authenticated) return false;
38891
+ if (entry.requiresOrg && (!state.authenticated || !state.orgId)) return false;
38892
+ if (entry.homeContexts.length > 0) return true;
38893
+ return Boolean(entry.shellSlash || entry.includeInSuggestions);
38894
+ }).map((entry) => ({
38895
+ label: entry.args.join(" "),
38896
+ description: entry.description,
38897
+ args: [...entry.args],
38898
+ requiresAuth: entry.requiresAuth || void 0,
38899
+ requiresOrg: entry.requiresOrg || void 0,
38900
+ mutatesState: entry.mutatesState,
38901
+ examples: [...entry.examples]
38902
+ }));
38903
+ }
38904
+ function getInteractiveSlashCommands() {
38905
+ const generated = getCommandGraphEntries().filter((entry) => typeof entry.shellSlash === "string").map((entry) => ({
38906
+ slash: entry.shellSlash,
38907
+ description: entry.description,
38908
+ args: [...entry.args]
38909
+ })).sort((left, right) => left.slash.localeCompare(right.slash));
38910
+ return [...SHELL_BUILTIN_COMMANDS, ...generated];
38911
+ }
38912
+ function getInteractiveCommandSuggestions() {
38913
+ return getCommandGraphEntries().filter((entry) => entry.includeInSuggestions).map((entry) => entry.args.join(" ")).filter((value, index, values) => values.indexOf(value) === index);
38914
+ }
38915
+ function normalizeArgs(args) {
38916
+ return args.map((token) => normalizeCommandToken(token)).filter(Boolean);
38917
+ }
38918
+ function startsWithTokens(input, expected) {
38919
+ if (expected.length === 0 || input.length < expected.length) return false;
38920
+ for (let index = 0; index < expected.length; index += 1) {
38921
+ if (input[index] !== expected[index]) return false;
38922
+ }
38923
+ return true;
38924
+ }
38925
+ function getCommandGraphEntryForArgs(args) {
38926
+ const normalizedArgs = normalizeArgs(args);
38927
+ if (normalizedArgs.length === 0) return void 0;
38928
+ const entries = getCommandGraphEntries().map((entry) => ({
38929
+ entry,
38930
+ normalizedEntryArgs: normalizeArgs(entry.args),
38931
+ normalizedCommandPath: normalizeArgs(entry.commandPath),
38932
+ entryArgsMatch: 0,
38933
+ commandPathMatch: 0
38934
+ })).map((candidate) => ({
38935
+ ...candidate,
38936
+ entryArgsMatch: startsWithTokens(normalizedArgs, candidate.normalizedEntryArgs) ? candidate.normalizedEntryArgs.length : 0,
38937
+ commandPathMatch: startsWithTokens(normalizedArgs, candidate.normalizedCommandPath) ? candidate.normalizedCommandPath.length : 0
38938
+ })).filter(({ entryArgsMatch, commandPathMatch }) => entryArgsMatch > 0 || commandPathMatch > 0).sort((left, right) => {
38939
+ if (left.entryArgsMatch !== right.entryArgsMatch) return right.entryArgsMatch - left.entryArgsMatch;
38940
+ if (left.commandPathMatch !== right.commandPathMatch) return right.commandPathMatch - left.commandPathMatch;
38941
+ return left.entry.id.localeCompare(right.entry.id);
38942
+ });
38943
+ return entries[0]?.entry;
38944
+ }
38945
+ function classifyCommandMutation(args) {
38946
+ return getCommandGraphEntryForArgs(args)?.mutatesState ?? "read";
38947
+ }
38948
+
38949
+ // src/commands/interactive.ts
38950
+ var SLASH_COMMANDS = getInteractiveSlashCommands();
38951
+ var COMMAND_SUGGESTIONS = getInteractiveCommandSuggestions();
38952
+ function nowIso2() {
38953
+ return (/* @__PURE__ */ new Date()).toISOString();
38954
+ }
38955
+ function interactiveSessionArtifactPath() {
38956
+ const override = String(process.env.FOH_CLI_INTERACTIVE_SESSION_OUT ?? "").trim();
38957
+ if (override.length > 0) return (0, import_path9.resolve)(override);
38958
+ return (0, import_path9.resolve)("test-results/interactive-shell-session.latest.json");
38959
+ }
38960
+ function createInteractiveSessionArtifact() {
38961
+ const seed = `${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`;
38962
+ return {
38963
+ schema_version: "foh_cli_interactive_session.v1",
38964
+ session_id: `shell_${seed}`,
38965
+ started_at: nowIso2(),
38966
+ status: "idle",
38967
+ command_count: 0,
38968
+ failure_count: 0,
38969
+ events: []
38970
+ };
38971
+ }
38972
+ function detectTerminalCapabilities() {
38973
+ const stdinTty = Boolean(process.stdin.isTTY);
38974
+ const stdoutTty = Boolean(process.stdout.isTTY);
38975
+ const supportsRawMode = stdinTty && typeof process.stdin.setRawMode === "function";
38976
+ const term = String(process.env.TERM ?? "").trim().toLowerCase();
38977
+ const supportsAnsiClear = stdoutTty && term !== "dumb";
38978
+ return {
38979
+ stdinTty,
38980
+ stdoutTty,
38981
+ supportsRawMode,
38982
+ supportsLinePrefill: supportsRawMode,
38983
+ supportsAnsiClear
38984
+ };
38985
+ }
38986
+ function recordInteractiveShellEvent(artifact, event) {
38987
+ artifact.events.push({
38988
+ at: nowIso2(),
38989
+ ...event
38990
+ });
38991
+ if (event.type === "command_started") {
38992
+ artifact.status = "running";
38993
+ artifact.command_count += 1;
38994
+ artifact.last_command = event.command;
38995
+ } else if (event.type === "command_succeeded") {
38996
+ artifact.status = "idle";
38997
+ artifact.last_command = event.command;
38998
+ } else if (event.type === "command_failed" || event.type === "shell_error") {
38999
+ artifact.status = "failed";
39000
+ artifact.failure_count += 1;
39001
+ artifact.last_error = event.reason;
39002
+ artifact.last_command = event.command;
39003
+ } else if (event.type === "command_blocked") {
39004
+ artifact.status = "idle";
39005
+ artifact.last_error = event.reason;
39006
+ artifact.last_command = event.command;
39007
+ } else if (event.type === "shell_closed") {
39008
+ artifact.status = artifact.status === "failed" ? "failed" : "closed";
39009
+ artifact.completed_at = nowIso2();
39010
+ }
39011
+ }
39012
+ function flushInteractiveSessionArtifact(artifact) {
39013
+ const outputPath = interactiveSessionArtifactPath();
39014
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39015
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify(artifact, null, 2), "utf8");
39016
+ }
39017
+ function interactiveMemoryPath() {
39018
+ const override = String(process.env.FOH_CLI_INTERACTIVE_MEMORY_OUT ?? "").trim();
39019
+ if (override.length > 0) return (0, import_path9.resolve)(override);
39020
+ return (0, import_path9.resolve)((0, import_os2.homedir)(), ".foh", "interactive-shell-memory.json");
39021
+ }
39022
+ function loadInteractiveShellMemory() {
39023
+ const outputPath = interactiveMemoryPath();
39024
+ if (!(0, import_fs11.existsSync)(outputPath)) {
39025
+ return {
39026
+ schema_version: "foh_cli_interactive_memory.v1",
39027
+ updated_at: nowIso2(),
39028
+ history: [],
39029
+ snippets: {}
39030
+ };
39031
+ }
39032
+ try {
39033
+ const raw = JSON.parse((0, import_fs11.readFileSync)(outputPath, "utf8"));
39034
+ return {
39035
+ schema_version: "foh_cli_interactive_memory.v1",
39036
+ updated_at: String(raw.updated_at ?? nowIso2()),
39037
+ history: Array.isArray(raw.history) ? raw.history.map((entry) => String(entry)).filter(Boolean) : [],
39038
+ snippets: raw && typeof raw.snippets === "object" && raw.snippets !== null ? Object.fromEntries(
39039
+ Object.entries(raw.snippets).map(([key, value]) => [String(key), String(value)])
39040
+ ) : {}
39041
+ };
39042
+ } catch {
39043
+ return {
39044
+ schema_version: "foh_cli_interactive_memory.v1",
39045
+ updated_at: nowIso2(),
39046
+ history: [],
39047
+ snippets: {}
39048
+ };
39049
+ }
39050
+ }
39051
+ function saveInteractiveShellMemory(memory) {
39052
+ const outputPath = interactiveMemoryPath();
39053
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39054
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify({ ...memory, updated_at: nowIso2() }, null, 2), "utf8");
39055
+ }
39056
+ function interactiveSessionReportPath() {
39057
+ const override = String(process.env.FOH_CLI_INTERACTIVE_SESSION_REPORT_OUT ?? "").trim();
39058
+ if (override.length > 0) return (0, import_path9.resolve)(override);
39059
+ return (0, import_path9.resolve)("test-results/interactive-cli-session-report.latest.json");
39060
+ }
39061
+ function median(values) {
39062
+ if (values.length === 0) return null;
39063
+ const sorted = [...values].sort((a, b) => a - b);
39064
+ const middle = Math.floor(sorted.length / 2);
39065
+ if (sorted.length % 2 === 1) return sorted[middle];
39066
+ return Math.round((sorted[middle - 1] + sorted[middle]) / 2);
39067
+ }
39068
+ function buildInteractiveSessionReport(artifact) {
39069
+ const commandStarts = [];
39070
+ const successDurations = [];
39071
+ const errorFamilies = {};
39072
+ let completedCommands = 0;
39073
+ let deadEndCount = 0;
39074
+ for (const event of artifact.events) {
39075
+ if (event.type === "command_started") {
39076
+ commandStarts.push(Date.parse(event.at));
39077
+ continue;
39078
+ }
39079
+ if (event.type === "command_succeeded") {
39080
+ completedCommands += 1;
39081
+ const startedAt = commandStarts.shift();
39082
+ if (startedAt !== void 0 && Number.isFinite(startedAt)) {
39083
+ const duration3 = Math.max(0, Date.parse(event.at) - startedAt);
39084
+ successDurations.push(duration3);
39085
+ }
39086
+ continue;
39087
+ }
39088
+ if (event.type === "command_failed" || event.type === "command_blocked" || event.type === "shell_error") {
39089
+ deadEndCount += 1;
39090
+ const key = String(event.reason ?? event.type).trim() || event.type;
39091
+ errorFamilies[key] = (errorFamilies[key] ?? 0) + 1;
39092
+ if (event.type === "command_failed") {
39093
+ commandStarts.shift();
39094
+ }
39095
+ }
39096
+ }
39097
+ const totalCommands = Math.max(artifact.command_count, 0);
39098
+ const completionRate = totalCommands > 0 ? Number((completedCommands / totalCommands).toFixed(4)) : 0;
39099
+ const topRemediationLoops = Object.entries(errorFamilies).sort((left, right) => right[1] - left[1]).slice(0, 5).map(([reason, count]) => ({ reason, count }));
39100
+ return {
39101
+ schema_version: "interactive_cli_session_report.v1",
39102
+ generated_at: nowIso2(),
39103
+ session_id: artifact.session_id,
39104
+ command_count: totalCommands,
39105
+ completion_rate: completionRate,
39106
+ dead_end_count: deadEndCount,
39107
+ command_error_families: errorFamilies,
39108
+ median_time_to_success_ms: median(successDurations),
39109
+ top_remediation_loops: topRemediationLoops,
39110
+ terminal_capabilities: artifact.terminal_capabilities
39111
+ };
39112
+ }
39113
+ function flushInteractiveSessionReport(artifact) {
39114
+ const outputPath = interactiveSessionReportPath();
39115
+ const report = buildInteractiveSessionReport(artifact);
39116
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39117
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify(report, null, 2), "utf8");
39118
+ }
38416
39119
  function tokenizeInput(value) {
38417
39120
  const tokens = [];
38418
39121
  const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
@@ -38423,193 +39126,391 @@ function tokenizeInput(value) {
38423
39126
  }
38424
39127
  return tokens;
38425
39128
  }
38426
- function normalizeTypedCommandInput(raw) {
38427
- const normalized = String(raw ?? "").trim();
38428
- if (!normalized) return [];
38429
- const withoutLeadingSlash = normalized.startsWith("/") ? normalized.slice(1) : normalized;
38430
- const tokens = tokenizeInput(withoutLeadingSlash);
38431
- if (tokens.length > 0 && tokens[0].toLowerCase() === "foh") {
38432
- tokens.shift();
39129
+ async function runSelf(args, apiUrlOverride) {
39130
+ const spawnArgs = [...args];
39131
+ if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
39132
+ spawnArgs.push("--api-url", apiUrlOverride);
38433
39133
  }
38434
- return tokens;
38435
- }
38436
- function filterPaletteCommands(commands, query) {
38437
- const normalized = String(query ?? "").trim().toLowerCase();
38438
- if (!normalized) return commands;
38439
- return commands.filter((entry) => {
38440
- const commandText = `foh ${entry.args.join(" ")}`.toLowerCase();
38441
- return entry.label.toLowerCase().includes(normalized) || commandText.includes(normalized);
39134
+ return await new Promise((resolve15, reject) => {
39135
+ const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39136
+ stdio: "inherit",
39137
+ env: {
39138
+ ...process.env,
39139
+ FOH_CLI_NO_HOME: "1",
39140
+ FOH_CLI_SUPPRESS_BANNER: "1"
39141
+ }
39142
+ });
39143
+ child.once("error", reject);
39144
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
38442
39145
  });
38443
39146
  }
38444
- function shouldStartPaletteFromHome(char) {
38445
- if (typeof char !== "string" || char.length !== 1) return false;
38446
- if (/\s/.test(char)) return false;
38447
- if (char === "/") return false;
38448
- return /^[0-9A-Za-z._:-]$/.test(char);
38449
- }
38450
-
38451
- // src/commands/home-render.ts
38452
- var SLASH_PALETTE_COMMANDS = [
38453
- { label: "auth login", description: "Authenticate the CLI session", args: ["auth", "login"] },
38454
- { label: "auth whoami", description: "Show current auth and org context", args: ["auth", "whoami"], requiresAuth: true },
38455
- { label: "org list", description: "List org memberships", args: ["org", "list"], requiresAuth: true },
38456
- { label: "org use --help", description: "See how to set default org", args: ["org", "use", "--help"], requiresAuth: true },
38457
- { label: "tenant status", description: "Check tenant readiness", args: ["tenant", "status"], requiresAuth: true, requiresOrg: true },
38458
- { label: "templates list", description: "List available templates", args: ["templates", "list"], requiresAuth: true, requiresOrg: true },
38459
- { label: "agent list --segment all", description: "List agents", args: ["agent", "list", "--segment", "all"], requiresAuth: true, requiresOrg: true },
38460
- { label: "tests run-all --help", description: "See test workflow command usage", args: ["tests", "run-all", "--help"], requiresAuth: true, requiresOrg: true },
38461
- { label: "sim certify --help", description: "See simulation certification options", args: ["sim", "certify", "--help"], requiresAuth: true, requiresOrg: true },
38462
- { label: "setup --help", description: "Show setup command help", args: ["setup", "--help"] },
38463
- { label: "bug report --help", description: "Show bug report capture help", args: ["bug", "report", "--help"] },
38464
- { label: "bug list", description: "List submitted bug reports", args: ["bug", "list"], requiresAuth: true, requiresOrg: true },
38465
- { label: "--help", description: "Show full CLI help", args: ["--help"] }
38466
- ];
38467
- function getQuickActions(state) {
38468
- if (!state.authenticated) {
38469
- return [
38470
- { label: "login now (recommended)", description: "Run interactive login flow.", args: ["auth", "login"] },
38471
- { label: "full help", description: "Show all CLI commands.", args: ["--help"] },
38472
- { label: "auth login help", description: "Show auth login examples.", args: ["auth", "login", "--help"] }
38473
- ];
38474
- }
38475
- if (!state.orgId) {
38476
- return [
38477
- { label: "list my orgs", description: "View org memberships.", args: ["org", "list"] },
38478
- { label: "org use help", description: "Set default org guidance.", args: ["org", "use", "--help"] },
38479
- { label: "auth whoami", description: "Inspect current auth context.", args: ["auth", "whoami"] },
38480
- { label: "full help", description: "Show all CLI commands.", args: ["--help"] }
38481
- ];
39147
+ function resolveInteractiveState(apiUrlOverride) {
39148
+ try {
39149
+ const creds = loadCredentials(apiUrlOverride);
39150
+ return {
39151
+ authenticated: true,
39152
+ orgId: creds.orgId
39153
+ };
39154
+ } catch {
39155
+ return { authenticated: false };
38482
39156
  }
39157
+ }
39158
+ function nextRecommendedCommand(state) {
39159
+ const next = getHomeQuickActions(state)[0];
39160
+ if (!next) return "foh --help";
39161
+ return `foh ${next.args.join(" ")}`;
39162
+ }
39163
+ function keymapHelpText() {
38483
39164
  return [
38484
- { label: "tenant status", description: "Check tenant status and readiness.", args: ["tenant", "status"] },
38485
- { label: "template list", description: "List templates.", args: ["templates", "list"] },
38486
- { label: "agent list", description: "List all agents.", args: ["agent", "list", "--segment", "all"] },
38487
- { label: "setup help", description: "Show setup flow options.", args: ["setup", "--help"] },
38488
- { label: "bug report help", description: "Show bug-report guidance.", args: ["bug", "report", "--help"] },
38489
- { label: "bug list", description: "List centralized bug reports.", args: ["bug", "list"] }
38490
- ];
39165
+ "",
39166
+ "Shell Keymap",
39167
+ "-----------",
39168
+ " Up / Down Recall command history",
39169
+ " Tab Complete ranked slash/command suggestions",
39170
+ " Enter Run current input",
39171
+ " Esc Cancel palette-style slash entry",
39172
+ " Ctrl+C Exit shell safely",
39173
+ "",
39174
+ "Fallback: if your terminal does not forward these keys, run equivalent slash commands (/help, /commands, /exit).",
39175
+ ""
39176
+ ].join("\n");
38491
39177
  }
38492
- function getPaletteCommands(state) {
38493
- return SLASH_PALETTE_COMMANDS.filter((entry) => {
38494
- if (entry.requiresAuth && !state.authenticated) return false;
38495
- if (entry.requiresOrg && (!state.authenticated || !state.orgId)) return false;
38496
- return true;
38497
- });
39178
+ function shellHelpText(state) {
39179
+ const slashLines = SLASH_COMMANDS.map((entry) => ` ${entry.slash.padEnd(12)} ${entry.description}`);
39180
+ return [
39181
+ "",
39182
+ "FOH Interactive Shell",
39183
+ "---------------------",
39184
+ "Use slash commands or type any normal `foh` command without the `foh` prefix.",
39185
+ "Keys: Up/Down history, Tab completion, Ctrl+C or /exit to leave. Run /keys for full mapping.",
39186
+ "",
39187
+ "Slash commands:",
39188
+ ...slashLines,
39189
+ "",
39190
+ `Recommended next command: ${nextRecommendedCommand(state)}`,
39191
+ ""
39192
+ ].join("\n");
38498
39193
  }
38499
- function filterHomePaletteCommands(state, query) {
38500
- return filterPaletteCommands(getPaletteCommands(state), query);
39194
+ function printPrompt(rl) {
39195
+ rl.setPrompt("foh> ");
39196
+ rl.prompt();
38501
39197
  }
38502
- function renderInteractiveHomeScreen({
38503
- state,
38504
- actions,
38505
- selectedActionIndex,
38506
- paletteOpen,
38507
- paletteQuery,
38508
- paletteSuggestions,
38509
- selectedPaletteIndex,
38510
- notice,
38511
- isBusy
38512
- }) {
38513
- process.stdout.write("\x1B[2J\x1B[H");
38514
- process.stdout.write("Front Of House CLI Home\n");
38515
- process.stdout.write("=======================\n\n");
38516
- if (!state.authenticated) {
38517
- process.stdout.write("Status: Not authenticated\n");
38518
- } else {
38519
- process.stdout.write("Status: Authenticated\n");
38520
- process.stdout.write(`API: ${state.apiUrl}
38521
- `);
38522
- process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
38523
- `);
39198
+ function safePrintPrompt(rl) {
39199
+ if (rl.closed) return;
39200
+ printPrompt(rl);
39201
+ }
39202
+ function prefillCommand(rl, command) {
39203
+ rl.write(null, { ctrl: true, name: "u" });
39204
+ rl.write(command);
39205
+ }
39206
+ function resolveSlashCommand(input, state, terminal, context) {
39207
+ const normalized = input.trim().toLowerCase();
39208
+ if (normalized === "/exit" || normalized === "/quit") return { quit: true, args: null };
39209
+ if (normalized === "/?" || normalized === "/keys") return { quit: false, args: [], message: keymapHelpText() };
39210
+ if (normalized === "/help") return { quit: false, args: [], message: shellHelpText(state) };
39211
+ if (normalized === "/commands") {
39212
+ const rows = SLASH_COMMANDS.map((entry) => ` ${entry.slash.padEnd(12)} ${entry.description}`);
39213
+ return {
39214
+ quit: false,
39215
+ args: [],
39216
+ message: `
39217
+ Slash commands:
39218
+ ${rows.join("\n")}
39219
+ `
39220
+ };
38524
39221
  }
38525
- process.stdout.write("\nQuick actions (Up/Down, j/k, Enter, 1-9):\n");
38526
- actions.forEach((action, index) => {
38527
- const isSelected = !paletteOpen && index === selectedActionIndex;
38528
- const cursor = isSelected ? ">" : " ";
38529
- process.stdout.write(` ${cursor} [${index + 1}] ${action.label}
38530
- `);
38531
- process.stdout.write(` ${action.description}
38532
- `);
38533
- });
38534
- process.stdout.write("\nShortcuts: [/] palette [h] redraw [q] exit [Ctrl+C] exit\n");
38535
- process.stdout.write("Tip: start typing any command (for example auth, help, org list) to search instantly.\n");
38536
- if (paletteOpen) {
38537
- process.stdout.write("\nCommand palette\n");
38538
- process.stdout.write("---------------\n");
38539
- process.stdout.write(`/${paletteQuery}
38540
- `);
38541
- if (paletteSuggestions.length === 0) {
38542
- process.stdout.write(" No matches. Press Enter to run typed command, or Esc to close.\n");
38543
- } else {
38544
- paletteSuggestions.slice(0, 8).forEach((entry, index) => {
38545
- const isSelected = index === selectedPaletteIndex;
38546
- const cursor = isSelected ? ">" : " ";
38547
- process.stdout.write(` ${cursor} ${entry.label}
38548
- `);
38549
- process.stdout.write(` foh ${entry.args.join(" ")}
38550
- `);
38551
- });
39222
+ if (normalized === "/clear") {
39223
+ if (terminal.supportsAnsiClear) return { quit: false, args: [], message: "\x1B[2J\x1B[H" };
39224
+ return {
39225
+ quit: false,
39226
+ args: [],
39227
+ message: "\n\n\n\n\nTerminal does not support ANSI clear. Screen reset skipped.\n"
39228
+ };
39229
+ }
39230
+ if (normalized === "/again") {
39231
+ if (context.lastSuccessfulArgs && context.lastSuccessfulArgs.length > 0) {
39232
+ return { quit: false, args: [...context.lastSuccessfulArgs] };
38552
39233
  }
38553
- } else {
38554
- process.stdout.write('\nPress "/" to open command palette.\n');
39234
+ return { quit: false, args: [], message: "No successful command in current session yet.\n" };
38555
39235
  }
38556
- if (isBusy) {
38557
- process.stdout.write("\nRunning command...\n");
38558
- } else if (notice) {
38559
- process.stdout.write(`
38560
- ${notice}
38561
- `);
39236
+ if (normalized === "/retry") {
39237
+ if (context.lastFailedArgs && context.lastFailedArgs.length > 0) {
39238
+ return { quit: false, args: [...context.lastFailedArgs] };
39239
+ }
39240
+ return { quit: false, args: [], message: "No failed command in current session yet.\n" };
38562
39241
  }
39242
+ if (normalized === "/editlast") {
39243
+ const candidate = context.lastFailedArgs ?? context.lastSuccessfulArgs;
39244
+ if (!candidate || candidate.length === 0) {
39245
+ return { quit: false, args: [], message: "No previous command to prefill.\n" };
39246
+ }
39247
+ return {
39248
+ quit: false,
39249
+ args: [],
39250
+ prefill: candidate.join(" "),
39251
+ message: terminal.supportsLinePrefill ? "Prefilled last command. Edit and press Enter to run.\n" : `Terminal cannot prefill input. Replay manually: foh ${candidate.join(" ")}
39252
+ `
39253
+ };
39254
+ }
39255
+ if (normalized.startsWith("/run ")) {
39256
+ const raw = input.trim().slice("/run ".length).trim();
39257
+ const args = tokenizeInput(raw);
39258
+ return { quit: false, args: args.length > 0 ? args : null };
39259
+ }
39260
+ if (normalized === "/snippet list") {
39261
+ const rows = Object.entries(context.snippets).sort((left, right) => left[0].localeCompare(right[0])).map(([name, command]) => ` ${name.padEnd(16)} ${command}`);
39262
+ if (rows.length === 0) return { quit: false, args: [], message: "No snippets saved.\n" };
39263
+ return { quit: false, args: [], message: `
39264
+ Saved snippets:
39265
+ ${rows.join("\n")}
39266
+ ` };
39267
+ }
39268
+ if (normalized.startsWith("/snippet save ")) {
39269
+ const parts = tokenizeInput(input.trim());
39270
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39271
+ const command = parts.slice(3).join(" ").trim();
39272
+ if (!name || !command) {
39273
+ return { quit: false, args: [], message: "Usage: /snippet save <name> <command>\n" };
39274
+ }
39275
+ const normalizedCommand = command.toLowerCase().startsWith("foh ") ? command.slice(4).trim() : command;
39276
+ context.snippets[name] = normalizedCommand;
39277
+ return { quit: false, args: [], message: `Saved snippet "${name}": ${normalizedCommand}
39278
+ ` };
39279
+ }
39280
+ if (normalized.startsWith("/snippet run ")) {
39281
+ const parts = tokenizeInput(input.trim());
39282
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39283
+ const command = context.snippets[name];
39284
+ if (!command) return { quit: false, args: [], message: `Snippet "${name}" not found.
39285
+ ` };
39286
+ return { quit: false, args: tokenizeInput(command) };
39287
+ }
39288
+ if (normalized.startsWith("/snippet edit ")) {
39289
+ const parts = tokenizeInput(input.trim());
39290
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39291
+ const command = context.snippets[name];
39292
+ if (!command) return { quit: false, args: [], message: `Snippet "${name}" not found.
39293
+ ` };
39294
+ return {
39295
+ quit: false,
39296
+ args: [],
39297
+ prefill: command,
39298
+ message: terminal.supportsLinePrefill ? `Prefilled snippet "${name}".
39299
+ ` : `Terminal cannot prefill input. Snippet "${name}": ${command}
39300
+ `
39301
+ };
39302
+ }
39303
+ const matched = SLASH_COMMANDS.find((entry) => entry.slash.toLowerCase() === normalized);
39304
+ if (matched?.args && matched.args.length > 0) {
39305
+ return { quit: false, args: [...matched.args] };
39306
+ }
39307
+ if (normalized.startsWith("/")) {
39308
+ return { quit: false, args: [], message: `Unknown slash command: ${input.trim()}
39309
+ Use /help.
39310
+ ` };
39311
+ }
39312
+ return { quit: false, args: null };
38563
39313
  }
38564
- function printHome(state) {
38565
- process.stdout.write("\nFront Of House CLI Home\n");
38566
- process.stdout.write("-----------------------\n");
38567
- if (!state.authenticated) {
38568
- process.stdout.write("Status: Not authenticated\n\n");
38569
- process.stdout.write("Fast path:\n");
38570
- process.stdout.write(" 1) Press [1] to run interactive login\n");
38571
- process.stdout.write(" 2) If multiple orgs are found, select one as default\n");
38572
- process.stdout.write(" 3) Run tenant setup and ops commands\n\n");
38573
- process.stdout.write("Quick options:\n");
38574
- process.stdout.write(" [1] login now (recommended)\n");
38575
- process.stdout.write(" [2] full help\n");
38576
- process.stdout.write(" [3] auth login help\n");
38577
- process.stdout.write(" [h] redraw home\n");
38578
- process.stdout.write(" [q] exit\n");
38579
- process.stdout.write(" Type any foh command (example: auth whoami)\n");
38580
- return;
39314
+ function normalizeCommandInput(input) {
39315
+ const trimmed = input.trim();
39316
+ if (!trimmed) return [];
39317
+ const withoutPrefix = trimmed.toLowerCase().startsWith("foh ") ? trimmed.slice(4).trim() : trimmed;
39318
+ return tokenizeInput(withoutPrefix);
39319
+ }
39320
+ function hasYesFlag(args) {
39321
+ return args.some((token) => token === "--yes" || token === "-y");
39322
+ }
39323
+ async function confirmMutationIfNeeded(rl, args) {
39324
+ const mutationState = classifyCommandMutation(args);
39325
+ if (mutationState === "read" || hasYesFlag(args)) {
39326
+ return { approved: true };
38581
39327
  }
38582
- if (!state.orgId) {
38583
- process.stdout.write(`Status: Authenticated
38584
- API: ${state.apiUrl}
39328
+ const commandEntry = getCommandGraphEntryForArgs(args);
39329
+ const label = commandEntry ? `foh ${commandEntry.args.join(" ")}` : `foh ${args.join(" ")}`;
39330
+ const warning = mutationState === "high_risk" ? "high-risk" : "write";
39331
+ const approved = await new Promise((resolve15) => {
39332
+ rl.question(`Confirm ${warning} command ${label}? [y/N]: `, (answer) => {
39333
+ const normalized = answer.trim().toLowerCase();
39334
+ resolve15(normalized === "y" || normalized === "yes");
39335
+ });
39336
+ });
39337
+ if (approved) return { approved: true };
39338
+ return {
39339
+ approved: false,
39340
+ message: `Command blocked (interactive_confirmation_required): foh ${args.join(" ")}`
39341
+ };
39342
+ }
39343
+ function commandSuggestionsForState(state) {
39344
+ const contextual = getHomePaletteCommands(state).map((entry) => entry.args.join(" "));
39345
+ const merged = [...contextual, ...COMMAND_SUGGESTIONS];
39346
+ return merged.filter((entry, index, values) => values.indexOf(entry) === index);
39347
+ }
39348
+ function fuzzyScore(query, candidate) {
39349
+ const normalizedQuery = query.trim().toLowerCase();
39350
+ const normalizedCandidate = candidate.toLowerCase();
39351
+ if (!normalizedQuery) return 0;
39352
+ if (normalizedCandidate.startsWith(normalizedQuery)) return 0;
39353
+ const includesAt = normalizedCandidate.indexOf(normalizedQuery);
39354
+ if (includesAt >= 0) return 100 + includesAt;
39355
+ let queryIndex = 0;
39356
+ let gaps = 0;
39357
+ for (let i = 0; i < normalizedCandidate.length && queryIndex < normalizedQuery.length; i += 1) {
39358
+ if (normalizedCandidate[i] === normalizedQuery[queryIndex]) {
39359
+ queryIndex += 1;
39360
+ } else if (queryIndex > 0) {
39361
+ gaps += 1;
39362
+ }
39363
+ }
39364
+ if (queryIndex !== normalizedQuery.length) return Number.POSITIVE_INFINITY;
39365
+ return 200 + gaps;
39366
+ }
39367
+ function rankMatches(query, pool) {
39368
+ const scored = pool.map((candidate) => ({ candidate, score: fuzzyScore(query, candidate) })).filter((entry) => Number.isFinite(entry.score)).sort((left, right) => {
39369
+ if (left.score !== right.score) return left.score - right.score;
39370
+ return left.candidate.localeCompare(right.candidate);
39371
+ });
39372
+ return scored.map((entry) => entry.candidate);
39373
+ }
39374
+ function buildCompleter(apiUrlOverride) {
39375
+ const slashLabels = SLASH_COMMANDS.map((entry) => entry.slash);
39376
+ return (line) => {
39377
+ const raw = String(line ?? "");
39378
+ const lower = raw.toLowerCase();
39379
+ const state = resolveInteractiveState(apiUrlOverride);
39380
+ const commandLabels = commandSuggestionsForState(state);
39381
+ const pool = lower.trim().startsWith("/") ? slashLabels : commandLabels;
39382
+ const hits = rankMatches(lower, pool);
39383
+ return [hits.length > 0 ? hits : pool, raw];
39384
+ };
39385
+ }
39386
+ function registerInteractive(program3) {
39387
+ program3.command("interactive").alias("shell").description("Open interactive FOH shell with slash commands and key support").option("--api-url <url>", "Internal API base URL override (operators only)").action(async (opts) => {
39388
+ const sessionArtifact = createInteractiveSessionArtifact();
39389
+ const terminalCapabilities = detectTerminalCapabilities();
39390
+ sessionArtifact.terminal_capabilities = {
39391
+ stdin_tty: terminalCapabilities.stdinTty,
39392
+ stdout_tty: terminalCapabilities.stdoutTty,
39393
+ supports_raw_mode: terminalCapabilities.supportsRawMode,
39394
+ supports_line_prefill: terminalCapabilities.supportsLinePrefill,
39395
+ supports_ansi_clear: terminalCapabilities.supportsAnsiClear
39396
+ };
39397
+ const memory = loadInteractiveShellMemory();
39398
+ recordInteractiveShellEvent(sessionArtifact, { type: "shell_started" });
39399
+ ensureInteractive(
39400
+ "interactive.shell",
39401
+ "Run this in a TTY terminal. For automation use normal non-interactive commands with --json."
39402
+ );
39403
+ const rl = (0, import_readline2.createInterface)({
39404
+ input: process.stdin,
39405
+ output: process.stdout,
39406
+ terminal: true,
39407
+ historySize: 500,
39408
+ removeHistoryDuplicates: true,
39409
+ history: memory.history.slice(0, 500),
39410
+ completer: buildCompleter(opts.apiUrl)
39411
+ });
39412
+ process.stdout.write(shellHelpText(resolveInteractiveState(opts.apiUrl)));
39413
+ printPrompt(rl);
39414
+ let busy = false;
39415
+ let lastSuccessfulArgs = null;
39416
+ let lastFailedArgs = null;
39417
+ rl.on("line", (line) => {
39418
+ void (async () => {
39419
+ if (busy) return;
39420
+ const raw = String(line ?? "");
39421
+ const slash = resolveSlashCommand(raw, resolveInteractiveState(opts.apiUrl), terminalCapabilities, {
39422
+ lastSuccessfulArgs,
39423
+ lastFailedArgs,
39424
+ snippets: memory.snippets
39425
+ });
39426
+ if (slash.quit) {
39427
+ rl.close();
39428
+ return;
39429
+ }
39430
+ if (slash.message) {
39431
+ process.stdout.write(`${slash.message}${slash.message.endsWith("\n") ? "" : "\n"}`);
39432
+ }
39433
+ if (slash.prefill) {
39434
+ if (terminalCapabilities.supportsLinePrefill) {
39435
+ prefillCommand(rl, slash.prefill);
39436
+ }
39437
+ return;
39438
+ }
39439
+ const commandArgs = slash.args ?? normalizeCommandInput(raw);
39440
+ if (commandArgs.length === 0) {
39441
+ safePrintPrompt(rl);
39442
+ return;
39443
+ }
39444
+ const confirmation = await confirmMutationIfNeeded(rl, commandArgs);
39445
+ if (!confirmation.approved) {
39446
+ if (confirmation.message) {
39447
+ process.stdout.write(`${confirmation.message}
38585
39448
  `);
38586
- process.stdout.write("Default org: (not set)\n\n");
38587
- process.stdout.write("Quick options:\n");
38588
- process.stdout.write(" [1] list my orgs\n");
38589
- process.stdout.write(" [2] org use help\n");
38590
- process.stdout.write(" [3] auth whoami\n");
38591
- process.stdout.write(" [4] full help\n");
38592
- process.stdout.write(" [h] redraw home\n");
38593
- process.stdout.write(" [q] exit\n");
38594
- process.stdout.write(" Type any foh command (example: org use --org <id>)\n");
38595
- return;
38596
- }
38597
- process.stdout.write(`Status: Authenticated
38598
- API: ${state.apiUrl}
39449
+ recordInteractiveShellEvent(sessionArtifact, {
39450
+ type: "command_blocked",
39451
+ command: `foh ${commandArgs.join(" ")}`,
39452
+ reason: "interactive_confirmation_required"
39453
+ });
39454
+ }
39455
+ printPrompt(rl);
39456
+ return;
39457
+ }
39458
+ recordInteractiveShellEvent(sessionArtifact, {
39459
+ type: "command_started",
39460
+ command: `foh ${commandArgs.join(" ")}`
39461
+ });
39462
+ busy = true;
39463
+ try {
39464
+ const code = await runSelf(commandArgs, opts.apiUrl);
39465
+ if (code !== 0) {
39466
+ lastFailedArgs = [...commandArgs];
39467
+ process.stdout.write(`Command exited with code ${code}: foh ${commandArgs.join(" ")}
38599
39468
  `);
38600
- process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
38601
-
39469
+ recordInteractiveShellEvent(sessionArtifact, {
39470
+ type: "command_failed",
39471
+ command: `foh ${commandArgs.join(" ")}`,
39472
+ exit_code: code,
39473
+ reason: "non_zero_exit"
39474
+ });
39475
+ } else {
39476
+ lastSuccessfulArgs = [...commandArgs];
39477
+ recordInteractiveShellEvent(sessionArtifact, {
39478
+ type: "command_succeeded",
39479
+ command: `foh ${commandArgs.join(" ")}`,
39480
+ exit_code: code
39481
+ });
39482
+ }
39483
+ } finally {
39484
+ busy = false;
39485
+ safePrintPrompt(rl);
39486
+ }
39487
+ })().catch((error2) => {
39488
+ const message = error2 instanceof Error ? error2.message : String(error2);
39489
+ process.stdout.write(`Shell error: ${message}
38602
39490
  `);
38603
- process.stdout.write("Quick options:\n");
38604
- process.stdout.write(" [1] tenant status\n");
38605
- process.stdout.write(" [2] template list\n");
38606
- process.stdout.write(" [3] agent list\n");
38607
- process.stdout.write(" [4] setup help\n");
38608
- process.stdout.write(" [5] bug report help\n");
38609
- process.stdout.write(" [6] bug list\n");
38610
- process.stdout.write(" [h] redraw home\n");
38611
- process.stdout.write(" [q] exit\n");
38612
- process.stdout.write(" Type any foh command (example: agent --help)\n");
39491
+ recordInteractiveShellEvent(sessionArtifact, {
39492
+ type: "shell_error",
39493
+ reason: message
39494
+ });
39495
+ busy = false;
39496
+ safePrintPrompt(rl);
39497
+ });
39498
+ });
39499
+ rl.on("SIGINT", () => {
39500
+ rl.close();
39501
+ });
39502
+ await new Promise((resolve15) => {
39503
+ rl.on("close", () => {
39504
+ process.stdout.write("\n");
39505
+ memory.history = rl.history.slice(0, 500);
39506
+ recordInteractiveShellEvent(sessionArtifact, { type: "shell_closed" });
39507
+ flushInteractiveSessionArtifact(sessionArtifact);
39508
+ flushInteractiveSessionReport(sessionArtifact);
39509
+ saveInteractiveShellMemory(memory);
39510
+ resolve15();
39511
+ });
39512
+ });
39513
+ });
38613
39514
  }
38614
39515
 
38615
39516
  // src/commands/home-actions.ts
@@ -38625,65 +39526,6 @@ async function promptChoice(prompt) {
38625
39526
  return await promptLine(`
38626
39527
  ${prompt}`, { allowEmpty: true });
38627
39528
  }
38628
- function resolveSelection(input, state) {
38629
- const normalized = input.trim().toLowerCase();
38630
- if (!normalized || normalized === "q" || normalized === "quit" || normalized === "exit") {
38631
- return { kind: "exit" };
38632
- }
38633
- if (normalized === "h" || normalized === "home" || normalized === "?") {
38634
- return { kind: "redraw" };
38635
- }
38636
- if (normalized === "help" || normalized === "--help") {
38637
- return { kind: "run", args: ["--help"] };
38638
- }
38639
- if (!state.authenticated) {
38640
- if (normalized === "1" || normalized === "start" || normalized === "login" || normalized === "auth" || normalized === "auth login") {
38641
- return { kind: "run", args: ["auth", "login"] };
38642
- }
38643
- if (normalized === "2" || normalized === "full help") {
38644
- return { kind: "run", args: ["--help"] };
38645
- }
38646
- if (normalized === "3" || normalized === "auth login help") {
38647
- return { kind: "run", args: ["auth", "login", "--help"] };
38648
- }
38649
- } else if (!state.orgId) {
38650
- if (normalized === "1" || normalized === "org" || normalized === "org list") {
38651
- return { kind: "run", args: ["org", "list"] };
38652
- }
38653
- if (normalized === "2" || normalized === "org use help") {
38654
- return { kind: "run", args: ["org", "use", "--help"] };
38655
- }
38656
- if (normalized === "3" || normalized === "auth whoami" || normalized === "whoami") {
38657
- return { kind: "run", args: ["auth", "whoami"] };
38658
- }
38659
- if (normalized === "4" || normalized === "full help") {
38660
- return { kind: "run", args: ["--help"] };
38661
- }
38662
- } else {
38663
- if (normalized === "1" || normalized === "tenant" || normalized === "tenant status") {
38664
- return { kind: "run", args: ["tenant", "status"] };
38665
- }
38666
- if (normalized === "2" || normalized === "templates" || normalized === "template list" || normalized === "templates list") {
38667
- return { kind: "run", args: ["templates", "list"] };
38668
- }
38669
- if (normalized === "3" || normalized === "agent" || normalized === "agent list") {
38670
- return { kind: "run", args: ["agent", "list", "--segment", "all"] };
38671
- }
38672
- if (normalized === "4" || normalized === "setup" || normalized === "setup help") {
38673
- return { kind: "run", args: ["setup", "--help"] };
38674
- }
38675
- if (normalized === "5" || normalized === "bug report" || normalized === "bug report help") {
38676
- return { kind: "run", args: ["bug", "report", "--help"] };
38677
- }
38678
- if (normalized === "6" || normalized === "bug list") {
38679
- return { kind: "run", args: ["bug", "list"] };
38680
- }
38681
- }
38682
- const tokens = tokenizeInput(input);
38683
- if (tokens.length > 0 && tokens[0].toLowerCase() === "foh") tokens.shift();
38684
- if (tokens.length > 0) return { kind: "run", args: tokens };
38685
- return { kind: "invalid" };
38686
- }
38687
39529
  async function runGuidedStart(apiUrlOverride, executeCommand) {
38688
39530
  process.stdout.write("\nGuided Start\n");
38689
39531
  process.stdout.write("------------\n");
@@ -38798,13 +39640,153 @@ async function chooseDefaultOrg(orgs) {
38798
39640
  `);
38799
39641
  }
38800
39642
  }
38801
- function getHomeQuickActions(state) {
38802
- return getQuickActions(state);
39643
+ function getHomeQuickActions2(state) {
39644
+ return getHomeQuickActions(state);
39645
+ }
39646
+
39647
+ // src/tui/command-palette.ts
39648
+ function tokenizeInput2(value) {
39649
+ const tokens = [];
39650
+ const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
39651
+ let match = matcher.exec(value);
39652
+ while (match) {
39653
+ tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
39654
+ match = matcher.exec(value);
39655
+ }
39656
+ return tokens;
39657
+ }
39658
+ function normalizeTypedCommandInput(raw) {
39659
+ const normalized = String(raw ?? "").trim();
39660
+ if (!normalized) return [];
39661
+ const withoutLeadingSlash = normalized.startsWith("/") ? normalized.slice(1) : normalized;
39662
+ const tokens = tokenizeInput2(withoutLeadingSlash);
39663
+ if (tokens.length > 0 && tokens[0].toLowerCase() === "foh") {
39664
+ tokens.shift();
39665
+ }
39666
+ return tokens;
39667
+ }
39668
+ function filterPaletteCommands(commands, query) {
39669
+ const normalized = String(query ?? "").trim().toLowerCase();
39670
+ if (!normalized) return commands;
39671
+ return commands.filter((entry) => {
39672
+ const commandText = `foh ${entry.args.join(" ")}`.toLowerCase();
39673
+ return entry.label.toLowerCase().includes(normalized) || commandText.includes(normalized);
39674
+ });
39675
+ }
39676
+ function shouldStartPaletteFromHome(char) {
39677
+ if (typeof char !== "string" || char.length !== 1) return false;
39678
+ if (/\s/.test(char)) return false;
39679
+ if (char === "/") return false;
39680
+ return /^[0-9A-Za-z._:-]$/.test(char);
39681
+ }
39682
+
39683
+ // src/commands/home-render.ts
39684
+ function getQuickActions(state) {
39685
+ return getHomeQuickActions(state);
39686
+ }
39687
+ function getPaletteCommands(state) {
39688
+ return getHomePaletteCommands(state);
39689
+ }
39690
+ function filterHomePaletteCommands(state, query) {
39691
+ const commands = getPaletteCommands(state);
39692
+ const commandIndex = new Map(commands.map((entry) => [entry.args.join(" "), entry]));
39693
+ const filtered = filterPaletteCommands(commands, query);
39694
+ return filtered.map((entry) => commandIndex.get(entry.args.join(" "))).filter((entry) => entry !== void 0);
39695
+ }
39696
+ function renderInteractiveHomeScreen({
39697
+ state,
39698
+ actions,
39699
+ selectedActionIndex,
39700
+ paletteOpen,
39701
+ paletteQuery,
39702
+ paletteSuggestions,
39703
+ selectedPaletteIndex,
39704
+ notice,
39705
+ isBusy
39706
+ }) {
39707
+ process.stdout.write("\x1B[2J\x1B[H");
39708
+ process.stdout.write("Front Of House CLI Home\n");
39709
+ process.stdout.write("=======================\n\n");
39710
+ if (!state.authenticated) {
39711
+ process.stdout.write("Status: Not authenticated\n");
39712
+ } else {
39713
+ process.stdout.write("Status: Authenticated\n");
39714
+ process.stdout.write(`API: ${state.apiUrl}
39715
+ `);
39716
+ process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
39717
+ `);
39718
+ }
39719
+ process.stdout.write("\nQuick actions (Up/Down, j/k, Enter, 1-9):\n");
39720
+ actions.forEach((action, index) => {
39721
+ const isSelected = !paletteOpen && index === selectedActionIndex;
39722
+ const cursor = isSelected ? ">" : " ";
39723
+ process.stdout.write(` ${cursor} [${index + 1}] ${action.label}
39724
+ `);
39725
+ process.stdout.write(` ${action.description}
39726
+ `);
39727
+ });
39728
+ process.stdout.write("\nShortcuts: [/] palette [h] redraw [q] exit [Ctrl+C] exit\n");
39729
+ process.stdout.write("Tip: start typing any command (for example auth, help, org list) to search instantly.\n");
39730
+ if (paletteOpen) {
39731
+ process.stdout.write("\nCommand palette\n");
39732
+ process.stdout.write("---------------\n");
39733
+ process.stdout.write(`/${paletteQuery}
39734
+ `);
39735
+ if (paletteSuggestions.length === 0) {
39736
+ process.stdout.write(" No matches. Press Enter to run typed command, or Esc to close.\n");
39737
+ } else {
39738
+ paletteSuggestions.slice(0, 8).forEach((entry, index) => {
39739
+ const isSelected = index === selectedPaletteIndex;
39740
+ const cursor = isSelected ? ">" : " ";
39741
+ process.stdout.write(` ${cursor} ${entry.label}
39742
+ `);
39743
+ process.stdout.write(` foh ${entry.args.join(" ")}
39744
+ `);
39745
+ });
39746
+ }
39747
+ } else {
39748
+ process.stdout.write('\nPress "/" to open command palette.\n');
39749
+ }
39750
+ if (isBusy) {
39751
+ process.stdout.write("\nRunning command...\n");
39752
+ } else if (notice) {
39753
+ process.stdout.write(`
39754
+ ${notice}
39755
+ `);
39756
+ }
39757
+ }
39758
+ function printHome(state) {
39759
+ process.stdout.write("\nFront Of House CLI Home\n");
39760
+ process.stdout.write("-----------------------\n");
39761
+ if (!state.authenticated) {
39762
+ process.stdout.write("Status: Not authenticated\n\n");
39763
+ } else {
39764
+ process.stdout.write(`Status: Authenticated
39765
+ API: ${state.apiUrl}
39766
+ `);
39767
+ process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
39768
+
39769
+ `);
39770
+ }
39771
+ const actions = getQuickActions(state);
39772
+ if (actions.length > 0) {
39773
+ process.stdout.write("Quick options:\n");
39774
+ actions.forEach((action, index) => {
39775
+ process.stdout.write(` [${index + 1}] ${action.label}
39776
+ `);
39777
+ });
39778
+ process.stdout.write(" [h] redraw home\n");
39779
+ process.stdout.write(" [q] exit\n");
39780
+ }
39781
+ const exampleAction = actions.find((entry) => entry.args.length > 0);
39782
+ const example = exampleAction ? `Type any foh command (example: ${exampleAction.args.join(" ")})` : "Type any foh command (example: --help)";
39783
+ process.stdout.write(` ${example}
39784
+ `);
38803
39785
  }
38804
39786
 
38805
39787
  // src/commands/home-interaction.ts
38806
- var import_child_process2 = require("child_process");
38807
- var import_readline2 = require("readline");
39788
+ var import_child_process3 = require("child_process");
39789
+ var import_readline3 = require("readline");
38808
39790
 
38809
39791
  // src/tui/keypress.ts
38810
39792
  function normalizeInputChar(inputChar) {
@@ -38852,13 +39834,13 @@ function resolveNumericSelection(inputChar, length) {
38852
39834
  }
38853
39835
 
38854
39836
  // src/commands/home-interaction.ts
38855
- async function runSelf(args, apiUrlOverride) {
39837
+ async function runSelf2(args, apiUrlOverride) {
38856
39838
  const spawnArgs = [...args];
38857
39839
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
38858
39840
  spawnArgs.push("--api-url", apiUrlOverride);
38859
39841
  }
38860
- return await new Promise((resolve14, reject) => {
38861
- const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39842
+ return await new Promise((resolve15, reject) => {
39843
+ const child = (0, import_child_process3.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
38862
39844
  stdio: "inherit",
38863
39845
  env: {
38864
39846
  ...process.env,
@@ -38867,7 +39849,7 @@ async function runSelf(args, apiUrlOverride) {
38867
39849
  }
38868
39850
  });
38869
39851
  child.once("error", reject);
38870
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
39852
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
38871
39853
  });
38872
39854
  }
38873
39855
  function shouldUseInteractiveHome(argv) {
@@ -38880,7 +39862,7 @@ function numericSelection(inputChar, actionCount) {
38880
39862
  return resolveNumericSelection(inputChar, actionCount);
38881
39863
  }
38882
39864
  function renderHome(state, selectedActionIndex, paletteOpen, paletteQuery, paletteSuggestions, selectedPaletteIndex, notice, isBusy) {
38883
- const actions = getHomeQuickActions(state);
39865
+ const actions = getHomeQuickActions2(state);
38884
39866
  renderInteractiveHomeScreen({
38885
39867
  state,
38886
39868
  actions,
@@ -38893,31 +39875,6 @@ function renderHome(state, selectedActionIndex, paletteOpen, paletteQuery, palet
38893
39875
  isBusy
38894
39876
  });
38895
39877
  }
38896
- async function runLegacyHomeLoop(initialState, apiUrlOverride, runCommand, promptChoice2) {
38897
- let state = initialState;
38898
- printHome(state);
38899
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
38900
- while (true) {
38901
- const selected = await promptChoice2("Select option/key/command: ");
38902
- const resolved = resolveSelection(selected, state);
38903
- if (resolved.kind === "exit") return;
38904
- if (resolved.kind === "redraw") {
38905
- printHome(state);
38906
- continue;
38907
- }
38908
- if (resolved.kind === "invalid") {
38909
- process.stdout.write("Unknown selection. Use keys shown above, or type a foh command.\n");
38910
- continue;
38911
- }
38912
- const code = await runCommand(resolved.args, apiUrlOverride);
38913
- if (code !== 0) {
38914
- process.stdout.write(`Command exited with code ${code}.
38915
- `);
38916
- }
38917
- state = resolveHomeState(apiUrlOverride);
38918
- printHome(state);
38919
- }
38920
- }
38921
39878
  async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38922
39879
  const stateAccessor = () => resolveHomeState(apiUrlOverride);
38923
39880
  let state = stateAccessor();
@@ -38929,7 +39886,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38929
39886
  let notice = null;
38930
39887
  let isBusy = false;
38931
39888
  let finished = false;
38932
- let actions = getHomeQuickActions(state);
39889
+ let actions = getHomeQuickActions2(state);
38933
39890
  const stdin = process.stdin;
38934
39891
  const clampPaletteIndex = () => {
38935
39892
  if (paletteSuggestions.length === 0) {
@@ -38942,7 +39899,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38942
39899
  }
38943
39900
  };
38944
39901
  const refreshDerivedState = () => {
38945
- actions = getHomeQuickActions(state);
39902
+ actions = getHomeQuickActions2(state);
38946
39903
  if (actions.length === 0) {
38947
39904
  selectedActionIndex = 0;
38948
39905
  } else if (selectedActionIndex >= actions.length) {
@@ -39106,7 +40063,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
39106
40063
  if (stdin.isTTY) stdin.setRawMode(false);
39107
40064
  };
39108
40065
  const attachInput = () => {
39109
- (0, import_readline2.emitKeypressEvents)(stdin);
40066
+ (0, import_readline3.emitKeypressEvents)(stdin);
39110
40067
  stdin.on("keypress", onKeypress);
39111
40068
  if (stdin.isTTY) stdin.setRawMode(true);
39112
40069
  stdin.resume();
@@ -39129,18 +40086,14 @@ function registerHome(program3) {
39129
40086
  try {
39130
40087
  const explicitStart = process.argv.slice(2).some((token) => token.toLowerCase() === "start");
39131
40088
  if (explicitStart) {
39132
- await runGuidedStart(opts.apiUrl, runSelf);
40089
+ await runGuidedStart(opts.apiUrl, runSelf2);
39133
40090
  }
39134
40091
  const state = resolveHomeState(opts.apiUrl);
39135
40092
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
39136
40093
  printHome(state);
39137
40094
  return;
39138
40095
  }
39139
- if (process.env.FOH_CLI_HOME_LEGACY === "1") {
39140
- await runLegacyHomeLoop(state, opts.apiUrl, runSelf, (prompt) => promptChoice(prompt));
39141
- return;
39142
- }
39143
- await runKeyDrivenHomeLoop(opts.apiUrl, runSelf);
40096
+ await runKeyDrivenHomeLoop(opts.apiUrl, runSelf2);
39144
40097
  } catch (e) {
39145
40098
  if (e instanceof FohError) {
39146
40099
  formatError(e);
@@ -39157,9 +40110,9 @@ function maybeDefaultToHome(argv = process.argv) {
39157
40110
  }
39158
40111
 
39159
40112
  // src/lib/update.ts
39160
- var import_fs11 = require("fs");
39161
- var import_path9 = require("path");
39162
- var import_child_process3 = require("child_process");
40113
+ var import_fs12 = require("fs");
40114
+ var import_path10 = require("path");
40115
+ var import_child_process4 = require("child_process");
39163
40116
  var import_crypto5 = require("crypto");
39164
40117
  function parseSemver(version2) {
39165
40118
  const trimmed = String(version2 ?? "").trim();
@@ -39179,7 +40132,7 @@ function compareSemver(a, b) {
39179
40132
  }
39180
40133
  function readPackageJsonVersion(path2) {
39181
40134
  try {
39182
- const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
40135
+ const raw = (0, import_fs12.readFileSync)(path2, "utf-8");
39183
40136
  const parsed = JSON.parse(raw);
39184
40137
  const version2 = String(parsed.version ?? "").trim();
39185
40138
  return version2 || void 0;
@@ -39188,13 +40141,13 @@ function readPackageJsonVersion(path2) {
39188
40141
  }
39189
40142
  }
39190
40143
  function findRepoRoot(startCwd = process.cwd()) {
39191
- let current = (0, import_path9.resolve)(startCwd);
40144
+ let current = (0, import_path10.resolve)(startCwd);
39192
40145
  while (true) {
39193
- const rootPackageJsonPath = (0, import_path9.join)(current, "package.json");
39194
- const cliPackageJsonPath = (0, import_path9.join)(current, "packages", "cli", "package.json");
39195
- if ((0, import_fs11.existsSync)(rootPackageJsonPath) && (0, import_fs11.existsSync)(cliPackageJsonPath)) {
40146
+ const rootPackageJsonPath = (0, import_path10.join)(current, "package.json");
40147
+ const cliPackageJsonPath = (0, import_path10.join)(current, "packages", "cli", "package.json");
40148
+ if ((0, import_fs12.existsSync)(rootPackageJsonPath) && (0, import_fs12.existsSync)(cliPackageJsonPath)) {
39196
40149
  try {
39197
- const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
40150
+ const raw = (0, import_fs12.readFileSync)(rootPackageJsonPath, "utf-8");
39198
40151
  const parsed = JSON.parse(raw);
39199
40152
  if (String(parsed.name ?? "").trim() === "front-of-house") {
39200
40153
  return current;
@@ -39202,7 +40155,7 @@ function findRepoRoot(startCwd = process.cwd()) {
39202
40155
  } catch {
39203
40156
  }
39204
40157
  }
39205
- const parent = (0, import_path9.dirname)(current);
40158
+ const parent = (0, import_path10.dirname)(current);
39206
40159
  if (parent === current) return void 0;
39207
40160
  current = parent;
39208
40161
  }
@@ -39216,7 +40169,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
39216
40169
  remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
39217
40170
  };
39218
40171
  }
39219
- const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
40172
+ const cliPackageJsonPath = (0, import_path10.join)(repoRoot, "packages", "cli", "package.json");
39220
40173
  const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
39221
40174
  if (!latestVersion) {
39222
40175
  return {
@@ -39243,20 +40196,20 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
39243
40196
  };
39244
40197
  }
39245
40198
  async function applyRepoUpdate(repoRoot) {
39246
- const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
40199
+ const scriptPath = (0, import_path10.join)(repoRoot, "scripts", "Install-FohCli.ps1");
39247
40200
  if (process.platform === "win32") {
39248
- return await new Promise((resolve14, reject) => {
39249
- const child = (0, import_child_process3.spawn)(
40201
+ return await new Promise((resolve15, reject) => {
40202
+ const child = (0, import_child_process4.spawn)(
39250
40203
  "powershell",
39251
40204
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
39252
40205
  { stdio: "inherit" }
39253
40206
  );
39254
40207
  child.once("error", reject);
39255
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
40208
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
39256
40209
  });
39257
40210
  }
39258
- return await new Promise((resolve14, reject) => {
39259
- const child = (0, import_child_process3.spawn)(
40211
+ return await new Promise((resolve15, reject) => {
40212
+ const child = (0, import_child_process4.spawn)(
39260
40213
  "corepack",
39261
40214
  ["pnpm", "cli:install:global"],
39262
40215
  {
@@ -39265,7 +40218,7 @@ async function applyRepoUpdate(repoRoot) {
39265
40218
  }
39266
40219
  );
39267
40220
  child.once("error", reject);
39268
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
40221
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
39269
40222
  });
39270
40223
  }
39271
40224
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -39279,7 +40232,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
39279
40232
  }
39280
40233
  function hashFileSha256(filePath) {
39281
40234
  try {
39282
- const bytes = (0, import_fs11.readFileSync)(filePath);
40235
+ const bytes = (0, import_fs12.readFileSync)(filePath);
39283
40236
  return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
39284
40237
  } catch {
39285
40238
  return void 0;
@@ -39289,10 +40242,10 @@ function verifyCliArtifactIntegrity(params = {}) {
39289
40242
  const cwd = params.cwd ?? process.cwd();
39290
40243
  const argv = params.argv ?? process.argv;
39291
40244
  const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
39292
- const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
40245
+ const runtimePath = (0, import_path10.resolve)(String(argv[1] || ""));
39293
40246
  const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
39294
40247
  const warnings = [];
39295
- if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
40248
+ if (!runtimePath || !(0, import_fs12.existsSync)(runtimePath)) {
39296
40249
  warnings.push("runtime_path_unreadable");
39297
40250
  }
39298
40251
  if (!runtimeHash) {
@@ -39310,8 +40263,8 @@ function verifyCliArtifactIntegrity(params = {}) {
39310
40263
  let repoDistHash;
39311
40264
  let runtimeMatchesRepoDist;
39312
40265
  if (repoRoot) {
39313
- repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
39314
- if ((0, import_fs11.existsSync)(repoDistPath)) {
40266
+ repoDistPath = (0, import_path10.join)(repoRoot, "packages", "cli", "dist", "foh.js");
40267
+ if ((0, import_fs12.existsSync)(repoDistPath)) {
39315
40268
  repoDistHash = hashFileSha256(repoDistPath);
39316
40269
  if (runtimeHash && repoDistHash) {
39317
40270
  runtimeMatchesRepoDist = runtimeHash === repoDistHash;
@@ -39401,13 +40354,13 @@ function registerUpdate(program3) {
39401
40354
  }
39402
40355
 
39403
40356
  // src/commands/eval.ts
39404
- var import_fs20 = require("fs");
39405
- var import_path19 = require("path");
39406
- var import_child_process6 = require("child_process");
40357
+ var import_fs21 = require("fs");
40358
+ var import_path20 = require("path");
40359
+ var import_child_process7 = require("child_process");
39407
40360
 
39408
40361
  // src/lib/external-agent-artifact-safety.ts
39409
- var import_fs12 = require("fs");
39410
- var import_path10 = require("path");
40362
+ var import_fs13 = require("fs");
40363
+ var import_path11 = require("path");
39411
40364
  var DEFAULT_MAX_BYTES_PER_FILE = 5 * 1024 * 1024;
39412
40365
  var TEXT_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
39413
40366
  "codex-events.jsonl",
@@ -39431,7 +40384,7 @@ function escapeRegExp(value) {
39431
40384
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39432
40385
  }
39433
40386
  function pathVariants(path2) {
39434
- const resolved = (0, import_path10.resolve)(path2);
40387
+ const resolved = (0, import_path11.resolve)(path2);
39435
40388
  const slash = resolved.replace(/\\/g, "/");
39436
40389
  const backslash = resolved.replace(/\//g, "\\");
39437
40390
  return Array.from(new Set([
@@ -39474,9 +40427,9 @@ function redactExternalAgentSecretText(text) {
39474
40427
  return text.replace(SECRET_QUERY_PARAM_RE, "$1[redacted_secret]").replace(SECRET_RE2, "[redacted_secret]");
39475
40428
  }
39476
40429
  function artifactFiles(runDir) {
39477
- if (!(0, import_fs12.existsSync)(runDir)) return [];
39478
- return (0, import_fs12.readdirSync)(runDir).map((name) => (0, import_path10.join)(runDir, name)).filter((path2) => {
39479
- const stat = (0, import_fs12.statSync)(path2);
40430
+ if (!(0, import_fs13.existsSync)(runDir)) return [];
40431
+ return (0, import_fs13.readdirSync)(runDir).map((name) => (0, import_path11.join)(runDir, name)).filter((path2) => {
40432
+ const stat = (0, import_fs13.statSync)(path2);
39480
40433
  const name = path2.split(/[\\/]/).pop() || "";
39481
40434
  if (name.endsWith(".redacted")) return false;
39482
40435
  return stat.isFile() && (TEXT_ARTIFACT_NAMES.has(name) || name.startsWith("command-output-cmd_"));
@@ -39502,15 +40455,15 @@ function scanText(input) {
39502
40455
  ].filter(Boolean);
39503
40456
  }
39504
40457
  function scanExternalAgentArtifacts(options) {
39505
- const runDir = (0, import_path10.resolve)(options.runDir);
39506
- const privateRepoRoot = options.privateRepoRoot ? (0, import_path10.resolve)(options.privateRepoRoot) : void 0;
40458
+ const runDir = (0, import_path11.resolve)(options.runDir);
40459
+ const privateRepoRoot = options.privateRepoRoot ? (0, import_path11.resolve)(options.privateRepoRoot) : void 0;
39507
40460
  const homeDir = options.homeDir || process.env.USERPROFILE || process.env.HOME;
39508
40461
  const maxBytes = options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
39509
40462
  const files = artifactFiles(runDir);
39510
40463
  const findings = [];
39511
40464
  const redactedFiles = [];
39512
40465
  for (const file2 of files) {
39513
- const stat = (0, import_fs12.statSync)(file2);
40466
+ const stat = (0, import_fs13.statSync)(file2);
39514
40467
  if (stat.size > maxBytes) {
39515
40468
  findings.push({
39516
40469
  kind: "artifact_too_large",
@@ -39520,12 +40473,12 @@ function scanExternalAgentArtifacts(options) {
39520
40473
  });
39521
40474
  continue;
39522
40475
  }
39523
- const text = (0, import_fs12.readFileSync)(file2, "utf8");
40476
+ const text = (0, import_fs13.readFileSync)(file2, "utf8");
39524
40477
  findings.push(...scanText({ text, file: file2, privateRepoRoot, homeDir }));
39525
40478
  if (options.writeRedacted) {
39526
40479
  const redacted = redactExternalAgentArtifactText(text, { privateRepoRoot, homeDir });
39527
40480
  const out = `${file2}.redacted`;
39528
- (0, import_fs12.writeFileSync)(out, redacted, "utf8");
40481
+ (0, import_fs13.writeFileSync)(out, redacted, "utf8");
39529
40482
  redactedFiles.push(out);
39530
40483
  }
39531
40484
  }
@@ -39546,8 +40499,8 @@ function scanExternalAgentArtifacts(options) {
39546
40499
  }
39547
40500
 
39548
40501
  // src/lib/external-agent-capture.ts
39549
- var import_fs13 = require("fs");
39550
- var import_path11 = require("path");
40502
+ var import_fs14 = require("fs");
40503
+ var import_path12 = require("path");
39551
40504
  var EXTERNAL_AGENT_RUN_DIR_ENV = "FOH_EXTERNAL_AGENT_RUN_DIR";
39552
40505
  var EXTERNAL_AGENT_PROMPT_VERSION_ENV = "FOH_EXTERNAL_AGENT_PROMPT_VERSION";
39553
40506
  var SECRET_RE3 = /\b(?:Bearer\s+)?(?:sk|pk|xai|whsec|EAAN|ghp|gho|github_pat|npm_)[A-Za-z0-9_\-.]{12,}\b/gi;
@@ -39566,7 +40519,7 @@ function safeJsonLine(value) {
39566
40519
  function getExternalAgentRunDir() {
39567
40520
  const raw = process.env[EXTERNAL_AGENT_RUN_DIR_ENV];
39568
40521
  if (!raw || !raw.trim()) return null;
39569
- return (0, import_path11.resolve)(raw);
40522
+ return (0, import_path12.resolve)(raw);
39570
40523
  }
39571
40524
  function commandIdFor(input) {
39572
40525
  const seed = `${input.startedAt}:${input.argv.join("\0")}:${process.pid}`;
@@ -39617,7 +40570,7 @@ function recordExternalAgentCliInvocation(input) {
39617
40570
  const runDir = getExternalAgentRunDir();
39618
40571
  if (!runDir) return null;
39619
40572
  try {
39620
- (0, import_fs13.mkdirSync)(runDir, { recursive: true });
40573
+ (0, import_fs14.mkdirSync)(runDir, { recursive: true });
39621
40574
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
39622
40575
  const args = input.argv.slice(2).map((arg) => redactText(String(arg)));
39623
40576
  const commandId = commandIdFor({ startedAt, argv: args });
@@ -39636,7 +40589,7 @@ function recordExternalAgentCliInvocation(input) {
39636
40589
  prompt_version: promptVersion,
39637
40590
  started_at: startedAt
39638
40591
  };
39639
- (0, import_fs13.appendFileSync)((0, import_path11.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
40592
+ (0, import_fs14.appendFileSync)((0, import_path12.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
39640
40593
  return {
39641
40594
  commandId,
39642
40595
  runDir,
@@ -39697,7 +40650,7 @@ function completeExternalAgentCliInvocation(capture, input) {
39697
40650
  let artifact = null;
39698
40651
  if (output.trim()) {
39699
40652
  artifact = outputArtifactName(capture.commandId);
39700
- (0, import_fs13.writeFileSync)((0, import_path11.join)(capture.runDir, artifact), output, "utf8");
40653
+ (0, import_fs14.writeFileSync)((0, import_path12.join)(capture.runDir, artifact), output, "utf8");
39701
40654
  }
39702
40655
  const record2 = {
39703
40656
  schema_version: "external_agent_cli_command.v2",
@@ -39720,12 +40673,12 @@ function completeExternalAgentCliInvocation(capture, input) {
39720
40673
  output_artifact: artifact,
39721
40674
  output_bytes: Buffer.byteLength(output, "utf8")
39722
40675
  };
39723
- (0, import_fs13.appendFileSync)((0, import_path11.join)(capture.runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
40676
+ (0, import_fs14.appendFileSync)((0, import_path12.join)(capture.runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
39724
40677
  }
39725
40678
  function readCommandRecords(runDir) {
39726
- const commandLogPath = (0, import_path11.join)(runDir, "commands.ndjson");
39727
- if (!(0, import_fs13.existsSync)(commandLogPath)) return [];
39728
- const records = (0, import_fs13.readFileSync)(commandLogPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
40679
+ const commandLogPath = (0, import_path12.join)(runDir, "commands.ndjson");
40680
+ if (!(0, import_fs14.existsSync)(commandLogPath)) return [];
40681
+ const records = (0, import_fs14.readFileSync)(commandLogPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
39729
40682
  const byId = /* @__PURE__ */ new Map();
39730
40683
  const ordered = [];
39731
40684
  for (const record2 of records) {
@@ -39737,13 +40690,13 @@ function readCommandRecords(runDir) {
39737
40690
  }
39738
40691
 
39739
40692
  // src/lib/external-agent-executor.ts
39740
- var import_fs19 = require("fs");
39741
- var import_os2 = require("os");
39742
- var import_path18 = require("path");
39743
- var import_child_process5 = require("child_process");
40693
+ var import_fs20 = require("fs");
40694
+ var import_os3 = require("os");
40695
+ var import_path19 = require("path");
40696
+ var import_child_process6 = require("child_process");
39744
40697
 
39745
40698
  // src/lib/external-agent-executor-env.ts
39746
- var import_path12 = require("path");
40699
+ var import_path13 = require("path");
39747
40700
  var CODEX_EXECUTOR_DENIED_ENV_PREFIXES = [
39748
40701
  "SUPABASE_",
39749
40702
  "DATABASE_",
@@ -39807,7 +40760,7 @@ function buildCodexExecutorEnv(input) {
39807
40760
  env[childKey] = value;
39808
40761
  }
39809
40762
  }
39810
- env.npm_config_cache = (0, import_path12.join)((0, import_path12.dirname)(input.runDir), ".npm-cache");
40763
+ env.npm_config_cache = (0, import_path13.join)((0, import_path13.dirname)(input.runDir), ".npm-cache");
39811
40764
  env.npm_config_prefer_online = "true";
39812
40765
  env.npm_config_update_notifier = "false";
39813
40766
  env.npm_config_yes = "true";
@@ -39819,12 +40772,12 @@ function buildCodexExecutorEnv(input) {
39819
40772
  }
39820
40773
 
39821
40774
  // src/lib/external-agent-executor-artifacts.ts
39822
- var import_fs15 = require("fs");
39823
- var import_path14 = require("path");
40775
+ var import_fs16 = require("fs");
40776
+ var import_path15 = require("path");
39824
40777
 
39825
40778
  // src/lib/external-agent-metadata.ts
39826
- var import_fs14 = require("fs");
39827
- var import_path13 = require("path");
40779
+ var import_fs15 = require("fs");
40780
+ var import_path14 = require("path");
39828
40781
  var EXTERNAL_AGENT_METADATA_FILENAMES = [
39829
40782
  "external-agent-metadata.json",
39830
40783
  "agent-metadata.json"
@@ -39846,10 +40799,10 @@ function collectDocsFrom(value, docs) {
39846
40799
  }
39847
40800
  function readExternalAgentMetadata(runDir) {
39848
40801
  for (const filename of EXTERNAL_AGENT_METADATA_FILENAMES) {
39849
- const path2 = (0, import_path13.join)(runDir, filename);
39850
- if (!(0, import_fs14.existsSync)(path2)) continue;
40802
+ const path2 = (0, import_path14.join)(runDir, filename);
40803
+ if (!(0, import_fs15.existsSync)(path2)) continue;
39851
40804
  try {
39852
- const parsed = JSON.parse((0, import_fs14.readFileSync)(path2, "utf8"));
40805
+ const parsed = JSON.parse((0, import_fs15.readFileSync)(path2, "utf8"));
39853
40806
  const docs = /* @__PURE__ */ new Set();
39854
40807
  collectDocsFrom(parsed.docs_pages_used, docs);
39855
40808
  collectDocsFrom(parsed.docs_pages_observed, docs);
@@ -39878,44 +40831,44 @@ function readExternalAgentMetadata(runDir) {
39878
40831
 
39879
40832
  // src/lib/external-agent-executor-artifacts.ts
39880
40833
  function redactArtifactFile(path2, input = {}) {
39881
- if (!(0, import_fs15.existsSync)(path2)) return;
39882
- const original = (0, import_fs15.readFileSync)(path2, "utf8");
40834
+ if (!(0, import_fs16.existsSync)(path2)) return;
40835
+ const original = (0, import_fs16.readFileSync)(path2, "utf8");
39883
40836
  const redacted = redactExternalAgentArtifactText(original, input);
39884
- if (redacted !== original) (0, import_fs15.writeFileSync)(path2, redacted, "utf8");
40837
+ if (redacted !== original) (0, import_fs16.writeFileSync)(path2, redacted, "utf8");
39885
40838
  }
39886
40839
  function redactExternalAgentOutputArtifacts(run, input = {}) {
39887
40840
  redactArtifactFile(run.outputs.jsonl, input);
39888
40841
  redactArtifactFile(run.outputs.last_message, input);
39889
40842
  redactArtifactFile(run.outputs.stderr, input);
39890
- redactArtifactFile((0, import_path14.join)(run.run_dir, "commands.ndjson"), input);
39891
- if (!(0, import_fs15.existsSync)(run.run_dir)) return;
39892
- for (const name of (0, import_fs15.readdirSync)(run.run_dir)) {
40843
+ redactArtifactFile((0, import_path15.join)(run.run_dir, "commands.ndjson"), input);
40844
+ if (!(0, import_fs16.existsSync)(run.run_dir)) return;
40845
+ for (const name of (0, import_fs16.readdirSync)(run.run_dir)) {
39893
40846
  if (name.startsWith("command-output-cmd_") && !name.endsWith(".redacted")) {
39894
- redactArtifactFile((0, import_path14.join)(run.run_dir, name), input);
40847
+ redactArtifactFile((0, import_path15.join)(run.run_dir, name), input);
39895
40848
  }
39896
40849
  }
39897
40850
  }
39898
40851
  function copyExternalAgentCommandCaptureArtifacts(input) {
39899
- const commandLog = (0, import_path14.join)(input.captureDir, "commands.ndjson");
39900
- if ((0, import_fs15.existsSync)(commandLog)) {
39901
- (0, import_fs15.writeFileSync)((0, import_path14.join)(input.runDir, "commands.ndjson"), (0, import_fs15.readFileSync)(commandLog, "utf8"), "utf8");
40852
+ const commandLog = (0, import_path15.join)(input.captureDir, "commands.ndjson");
40853
+ if ((0, import_fs16.existsSync)(commandLog)) {
40854
+ (0, import_fs16.writeFileSync)((0, import_path15.join)(input.runDir, "commands.ndjson"), (0, import_fs16.readFileSync)(commandLog, "utf8"), "utf8");
39902
40855
  }
39903
- for (const name of (0, import_fs15.readdirSync)(input.captureDir)) {
40856
+ for (const name of (0, import_fs16.readdirSync)(input.captureDir)) {
39904
40857
  if (name.startsWith("command-output-cmd_")) {
39905
- (0, import_fs15.copyFileSync)((0, import_path14.join)(input.captureDir, name), (0, import_path14.join)(input.runDir, name));
40858
+ (0, import_fs16.copyFileSync)((0, import_path15.join)(input.captureDir, name), (0, import_path15.join)(input.runDir, name));
39906
40859
  } else if (EXTERNAL_AGENT_METADATA_FILENAMES.includes(name)) {
39907
- (0, import_fs15.copyFileSync)((0, import_path14.join)(input.captureDir, name), (0, import_path14.join)(input.runDir, name));
40860
+ (0, import_fs16.copyFileSync)((0, import_path15.join)(input.captureDir, name), (0, import_path15.join)(input.runDir, name));
39908
40861
  }
39909
40862
  }
39910
40863
  }
39911
40864
 
39912
40865
  // src/lib/external-agent-executor-classification.ts
39913
- var import_fs17 = require("fs");
39914
- var import_path16 = require("path");
40866
+ var import_fs18 = require("fs");
40867
+ var import_path17 = require("path");
39915
40868
 
39916
40869
  // src/lib/external-agent-run-summary.ts
39917
- var import_fs16 = require("fs");
39918
- var import_path15 = require("path");
40870
+ var import_fs17 = require("fs");
40871
+ var import_path16 = require("path");
39919
40872
  var REQUIRED_RUN_FIELDS = [
39920
40873
  "schema_version",
39921
40874
  "run_id",
@@ -39939,8 +40892,8 @@ function quoteShellArg(value) {
39939
40892
  return `"${text.replace(/(["$`])/g, "\\$1")}"`;
39940
40893
  }
39941
40894
  function externalAgentSummaryCommand(root) {
39942
- const summaryPath = (0, import_path15.join)(root, "latest-summary.json");
39943
- const reportPath = (0, import_path15.join)(root, "summary.report.json");
40895
+ const summaryPath = (0, import_path16.join)(root, "latest-summary.json");
40896
+ const reportPath = (0, import_path16.join)(root, "summary.report.json");
39944
40897
  return [
39945
40898
  "foh",
39946
40899
  "eval",
@@ -39956,11 +40909,11 @@ function externalAgentSummaryCommand(root) {
39956
40909
  ].join(" ");
39957
40910
  }
39958
40911
  function readJson(filePath) {
39959
- return JSON.parse((0, import_fs16.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
40912
+ return JSON.parse((0, import_fs17.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
39960
40913
  }
39961
40914
  function readNdjson(filePath) {
39962
- if (!(0, import_fs16.existsSync)(filePath)) return [];
39963
- return (0, import_fs16.readFileSync)(filePath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
40915
+ if (!(0, import_fs17.existsSync)(filePath)) return [];
40916
+ return (0, import_fs17.readFileSync)(filePath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
39964
40917
  try {
39965
40918
  const parsed = JSON.parse(line);
39966
40919
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -39986,7 +40939,7 @@ function collectDocUrls(text) {
39986
40939
  return Array.from(new Set((String(text || "").match(DOC_URL_RE) || []).map((url2) => url2.replace(/[.?!:]+$/g, "")).filter((url2) => url2.startsWith("https://frontofhouse.okii.uk/")))).sort();
39987
40940
  }
39988
40941
  function findRunCandidates(root) {
39989
- if (!(0, import_fs16.existsSync)(root)) return [];
40942
+ if (!(0, import_fs17.existsSync)(root)) return [];
39990
40943
  const candidates = [];
39991
40944
  const seenRunDirs = /* @__PURE__ */ new Set();
39992
40945
  const captureDirs = [];
@@ -39994,13 +40947,13 @@ function findRunCandidates(root) {
39994
40947
  while (stack.length > 0) {
39995
40948
  const current = stack.pop();
39996
40949
  if (!current) continue;
39997
- for (const entry of (0, import_fs16.readdirSync)(current, { withFileTypes: true })) {
39998
- const absolute = (0, import_path15.join)(current, entry.name);
40950
+ for (const entry of (0, import_fs17.readdirSync)(current, { withFileTypes: true })) {
40951
+ const absolute = (0, import_path16.join)(current, entry.name);
39999
40952
  if (entry.isDirectory()) {
40000
40953
  stack.push(absolute);
40001
40954
  } else if (entry.isFile() && entry.name === "run.json") {
40002
40955
  candidates.push({ path: absolute, synthetic: false });
40003
- seenRunDirs.add((0, import_path15.dirname)(absolute));
40956
+ seenRunDirs.add((0, import_path16.dirname)(absolute));
40004
40957
  } else if (entry.isFile() && entry.name === "commands.ndjson") {
40005
40958
  captureDirs.push(current);
40006
40959
  }
@@ -40008,7 +40961,7 @@ function findRunCandidates(root) {
40008
40961
  }
40009
40962
  for (const captureDir of captureDirs) {
40010
40963
  if (seenRunDirs.has(captureDir)) continue;
40011
- candidates.push({ path: (0, import_path15.join)(captureDir, "run.json"), synthetic: true });
40964
+ candidates.push({ path: (0, import_path16.join)(captureDir, "run.json"), synthetic: true });
40012
40965
  }
40013
40966
  return candidates.sort((a, b) => a.path.localeCompare(b.path));
40014
40967
  }
@@ -40086,9 +41039,9 @@ function syntheticStatusFromCommands(commands) {
40086
41039
  return { status: "pass", reasonCode: null };
40087
41040
  }
40088
41041
  function synthesizeRunFromCapture(runPath) {
40089
- const runDir = (0, import_path15.dirname)(runPath);
40090
- const commands = collapseCommandRecords(readNdjson((0, import_path15.join)(runDir, "commands.ndjson")));
40091
- const metadata = asObject((0, import_fs16.existsSync)((0, import_path15.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path15.join)(runDir, "external-agent-metadata.json")) : {});
41042
+ const runDir = (0, import_path16.dirname)(runPath);
41043
+ const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
41044
+ const metadata = asObject((0, import_fs17.existsSync)((0, import_path16.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path16.join)(runDir, "external-agent-metadata.json")) : {});
40092
41045
  const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
40093
41046
  const commandClassification = syntheticStatusFromCommands(commands);
40094
41047
  const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
@@ -40097,7 +41050,7 @@ function synthesizeRunFromCapture(runPath) {
40097
41050
  const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
40098
41051
  const endedAt = latestCommandTime(commands) || startedAt;
40099
41052
  const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
40100
- const runId = (0, import_path15.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
41053
+ const runId = (0, import_path16.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
40101
41054
  return {
40102
41055
  schema_version: "external_agent_run.v1",
40103
41056
  run_id: runId,
@@ -40121,14 +41074,14 @@ function synthesizeRunFromCapture(runPath) {
40121
41074
  docs_pages_used: docs,
40122
41075
  artifacts: {
40123
41076
  command_log: "commands.ndjson",
40124
- agent_metadata: (0, import_fs16.existsSync)((0, import_path15.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
41077
+ agent_metadata: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
40125
41078
  capture_only: true
40126
41079
  },
40127
41080
  summary: "Synthetic run record created from external-agent capture artifacts because run.json was missing."
40128
41081
  };
40129
41082
  }
40130
41083
  function cohortIdForRunPath(root, runPath) {
40131
- const normalized = (0, import_path15.relative)(root, (0, import_path15.dirname)(runPath)).replaceAll("\\", "/");
41084
+ const normalized = (0, import_path16.relative)(root, (0, import_path16.dirname)(runPath)).replaceAll("\\", "/");
40132
41085
  const parts = normalized.split("/").filter(Boolean);
40133
41086
  if (parts.length === 0) return ".";
40134
41087
  if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) && parts[1]) return `${parts[0]}/${parts[1]}`;
@@ -40143,7 +41096,7 @@ function readRunRecords(root, cwd) {
40143
41096
  const parsed = candidate.synthetic ? synthesizeRunFromCapture(file2) : readJson(file2);
40144
41097
  const findings = validateExternalAgentRun(parsed);
40145
41098
  if (findings.length > 0) {
40146
- invalid_runs.push({ path: (0, import_path15.relative)(cwd, file2).replaceAll("\\", "/"), findings });
41099
+ invalid_runs.push({ path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"), findings });
40147
41100
  continue;
40148
41101
  }
40149
41102
  const run = parsed;
@@ -40155,7 +41108,7 @@ function readRunRecords(root, cwd) {
40155
41108
  });
40156
41109
  } catch (error2) {
40157
41110
  invalid_runs.push({
40158
- path: (0, import_path15.relative)(cwd, file2).replaceAll("\\", "/"),
41111
+ path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"),
40159
41112
  findings: [{ id: "json_parse_failed", detail: error2 instanceof Error ? error2.message : String(error2) }]
40160
41113
  });
40161
41114
  }
@@ -40200,10 +41153,10 @@ function collapseCommandRecords(records) {
40200
41153
  function readCommandOutputJson(runDir, command) {
40201
41154
  const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
40202
41155
  if (!artifact || artifact.includes("..") || artifact.includes("/") || artifact.includes("\\")) return null;
40203
- const artifactPath = (0, import_path15.join)(runDir, artifact);
40204
- if (!(0, import_fs16.existsSync)(artifactPath)) return null;
41156
+ const artifactPath = (0, import_path16.join)(runDir, artifact);
41157
+ if (!(0, import_fs17.existsSync)(artifactPath)) return null;
40205
41158
  try {
40206
- const text = (0, import_fs16.readFileSync)(artifactPath, "utf8");
41159
+ const text = (0, import_fs17.readFileSync)(artifactPath, "utf8");
40207
41160
  const firstObject = text.indexOf("{");
40208
41161
  const lastObject = text.lastIndexOf("}");
40209
41162
  if (firstObject < 0 || lastObject <= firstObject) return null;
@@ -40257,8 +41210,8 @@ function commandTimingBreakdown(command, output) {
40257
41210
  return null;
40258
41211
  }
40259
41212
  function analyzeRunArtifacts(runPath, run, cwd) {
40260
- const runDir = (0, import_path15.dirname)(runPath);
40261
- const commands = collapseCommandRecords(readNdjson((0, import_path15.join)(runDir, "commands.ndjson")));
41213
+ const runDir = (0, import_path16.dirname)(runPath);
41214
+ const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
40262
41215
  const reasonCounts = /* @__PURE__ */ new Map();
40263
41216
  const slowSteps = [];
40264
41217
  const timingBreakdowns = [];
@@ -40270,7 +41223,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40270
41223
  const breakdown = commandTimingBreakdown(command, output);
40271
41224
  if (breakdown) timingBreakdowns.push({
40272
41225
  run_id: run.run_id,
40273
- run_path: (0, import_path15.relative)(cwd, runPath).replaceAll("\\", "/"),
41226
+ run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
40274
41227
  ...breakdown
40275
41228
  });
40276
41229
  if (command.phase === "completed" || command.completed_at) completed += 1;
@@ -40279,7 +41232,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40279
41232
  totalDuration += command.duration_ms;
40280
41233
  slowSteps.push({
40281
41234
  run_id: run.run_id,
40282
- run_path: (0, import_path15.relative)(cwd, runPath).replaceAll("\\", "/"),
41235
+ run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
40283
41236
  command: command.command || "",
40284
41237
  duration_ms: command.duration_ms,
40285
41238
  status: command.status || null,
@@ -40292,7 +41245,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40292
41245
  if (reasonCode) increment(reasonCounts, reasonCode);
40293
41246
  }
40294
41247
  }
40295
- const codexEvents = readNdjson((0, import_path15.join)(runDir, "codex-exec.jsonl"));
41248
+ const codexEvents = readNdjson((0, import_path16.join)(runDir, "codex-exec.jsonl"));
40296
41249
  const codexDocs = /* @__PURE__ */ new Set();
40297
41250
  let codexCommandExecutions = 0;
40298
41251
  let codexFailedExitCodes = 0;
@@ -40309,7 +41262,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40309
41262
  ...Array.from(codexDocs)
40310
41263
  ]);
40311
41264
  return {
40312
- command_log_present: (0, import_fs16.existsSync)((0, import_path15.join)(runDir, "commands.ndjson")),
41265
+ command_log_present: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "commands.ndjson")),
40313
41266
  command_count: commands.length,
40314
41267
  completed_command_count: completed,
40315
41268
  missing_completion_count: Math.max(0, commands.length - completed),
@@ -40324,8 +41277,8 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40324
41277
  };
40325
41278
  }
40326
41279
  function summarizeExternalAgentRuns(options) {
40327
- const cwd = (0, import_path15.resolve)(options.cwd || process.cwd());
40328
- const root = (0, import_path15.resolve)(cwd, options.root);
41280
+ const cwd = (0, import_path16.resolve)(options.cwd || process.cwd());
41281
+ const root = (0, import_path16.resolve)(cwd, options.root);
40329
41282
  const loaded = readRunRecords(root, cwd);
40330
41283
  const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
40331
41284
  const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
@@ -40380,7 +41333,7 @@ function summarizeExternalAgentRuns(options) {
40380
41333
  return {
40381
41334
  schema_version: "external_agent_run_summary.v1",
40382
41335
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
40383
- root: (0, import_path15.relative)(cwd, root).replaceAll("\\", "/") || ".",
41336
+ root: (0, import_path16.relative)(cwd, root).replaceAll("\\", "/") || ".",
40384
41337
  cohort_id: selectedCohortId,
40385
41338
  current_baseline_only: Boolean(selectedCohortId),
40386
41339
  run_count: records.length,
@@ -40415,7 +41368,7 @@ function summarizeExternalAgentRuns(options) {
40415
41368
  },
40416
41369
  next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
40417
41370
  invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
40418
- run_paths: records.map((record2) => (0, import_path15.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
41371
+ run_paths: records.map((record2) => (0, import_path16.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
40419
41372
  };
40420
41373
  }
40421
41374
  function runExternalAgentRunSummary(options) {
@@ -40435,13 +41388,13 @@ function runExternalAgentRunSummary(options) {
40435
41388
  report: summary
40436
41389
  };
40437
41390
  if (options.out) {
40438
- (0, import_fs16.mkdirSync)((0, import_path15.dirname)((0, import_path15.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
40439
- (0, import_fs16.writeFileSync)((0, import_path15.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
41391
+ (0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
41392
+ (0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
40440
41393
  `, "utf8");
40441
41394
  }
40442
41395
  if (options.report) {
40443
- (0, import_fs16.mkdirSync)((0, import_path15.dirname)((0, import_path15.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
40444
- (0, import_fs16.writeFileSync)((0, import_path15.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
41396
+ (0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
41397
+ (0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
40445
41398
  `, "utf8");
40446
41399
  }
40447
41400
  return { summary, report };
@@ -40449,20 +41402,20 @@ function runExternalAgentRunSummary(options) {
40449
41402
 
40450
41403
  // src/lib/external-agent-executor-classification.ts
40451
41404
  function proofArtifactPasses(runDir) {
40452
- const proofPath = (0, import_path16.join)(runDir, "proof.json");
40453
- if (!(0, import_fs17.existsSync)(proofPath)) return false;
41405
+ const proofPath = (0, import_path17.join)(runDir, "proof.json");
41406
+ if (!(0, import_fs18.existsSync)(proofPath)) return false;
40454
41407
  try {
40455
- const parsed = JSON.parse((0, import_fs17.readFileSync)(proofPath, "utf8"));
41408
+ const parsed = JSON.parse((0, import_fs18.readFileSync)(proofPath, "utf8"));
40456
41409
  return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
40457
41410
  } catch {
40458
41411
  return false;
40459
41412
  }
40460
41413
  }
40461
41414
  function readIfExists(path2) {
40462
- return (0, import_fs17.existsSync)(path2) ? (0, import_fs17.readFileSync)(path2, "utf8") : "";
41415
+ return (0, import_fs18.existsSync)(path2) ? (0, import_fs18.readFileSync)(path2, "utf8") : "";
40463
41416
  }
40464
41417
  function relativeArtifactName(path2) {
40465
- return (0, import_path16.basename)(path2);
41418
+ return (0, import_path17.basename)(path2);
40466
41419
  }
40467
41420
  function classifyExternalAgentRun(input) {
40468
41421
  if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
@@ -40617,13 +41570,13 @@ function buildExecutedExternalAgentRunArtifact(input) {
40617
41570
  },
40618
41571
  artifacts: {
40619
41572
  terminal_transcript: relativeArtifactName(input.run.outputs.jsonl),
40620
- command_log: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
40621
- proof_bundle: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
40622
- replay_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
40623
- knowledge_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
41573
+ command_log: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
41574
+ proof_bundle: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
41575
+ replay_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
41576
+ knowledge_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
40624
41577
  improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
40625
41578
  agent_metadata: agentMetadata.path,
40626
- notes: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
41579
+ notes: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
40627
41580
  runner_last_message: relativeArtifactName(input.run.outputs.last_message),
40628
41581
  runner_stderr: relativeArtifactName(input.run.outputs.stderr),
40629
41582
  codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
@@ -40631,25 +41584,25 @@ function buildExecutedExternalAgentRunArtifact(input) {
40631
41584
  artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
40632
41585
  },
40633
41586
  summary: input.status === "pass" ? `Controlled ${input.run.command} external-agent run produced passing proof evidence.` : `Controlled ${input.run.command} external-agent run ended as ${input.status} with reason ${input.reasonCode}.`,
40634
- next_commands: input.status === "pass" ? [externalAgentSummaryCommand((0, import_path16.dirname)(input.run.run_dir))] : [
41587
+ next_commands: input.status === "pass" ? [externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))] : [
40635
41588
  "foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
40636
41589
  "foh bug improve --from external-agent-run --file <run_dir>/run.json --out <run_dir>/improvement-packet.json --json",
40637
- externalAgentSummaryCommand((0, import_path16.dirname)(input.run.run_dir))
41590
+ externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))
40638
41591
  ]
40639
41592
  };
40640
41593
  }
40641
41594
 
40642
41595
  // src/lib/external-agent-runner-execution.ts
40643
- var import_child_process4 = require("child_process");
40644
- var import_fs18 = require("fs");
40645
- var import_path17 = require("path");
41596
+ var import_child_process5 = require("child_process");
41597
+ var import_fs19 = require("fs");
41598
+ var import_path18 = require("path");
40646
41599
  function buildCommandInvocation(command, args) {
40647
41600
  if (process.platform === "win32" && command.toLowerCase().endsWith(".cmd")) {
40648
- const binDir = (0, import_path17.dirname)(command);
40649
- const codexEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
40650
- if ((0, import_fs18.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
40651
- const geminiEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
40652
- if ((0, import_fs18.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
41601
+ const binDir = (0, import_path18.dirname)(command);
41602
+ const codexEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
41603
+ if ((0, import_fs19.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
41604
+ const geminiEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
41605
+ if ((0, import_fs19.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
40653
41606
  }
40654
41607
  return { command, args };
40655
41608
  }
@@ -40657,15 +41610,15 @@ function spawnExternalAgentRunner(input) {
40657
41610
  return new Promise((resolveRun) => {
40658
41611
  const started = Date.now();
40659
41612
  const commandInvocation = buildCommandInvocation(input.command, input.args);
40660
- const child = (0, import_child_process4.spawn)(commandInvocation.command, commandInvocation.args, {
41613
+ const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
40661
41614
  cwd: input.cwd,
40662
41615
  env: input.env,
40663
41616
  shell: false,
40664
41617
  stdio: ["pipe", "pipe", "pipe"],
40665
41618
  windowsHide: true
40666
41619
  });
40667
- const stdout = (0, import_fs18.createWriteStream)(input.stdoutPath, { flags: "w" });
40668
- const stderr = (0, import_fs18.createWriteStream)(input.stderrPath, { flags: "w" });
41620
+ const stdout = (0, import_fs19.createWriteStream)(input.stdoutPath, { flags: "w" });
41621
+ const stderr = (0, import_fs19.createWriteStream)(input.stderrPath, { flags: "w" });
40669
41622
  child.stdout.pipe(stdout);
40670
41623
  child.stderr.pipe(stderr);
40671
41624
  child.stdin.end(input.prompt);
@@ -40673,7 +41626,7 @@ function spawnExternalAgentRunner(input) {
40673
41626
  const timer = setTimeout(() => {
40674
41627
  timedOut = true;
40675
41628
  if (child.pid && process.platform === "win32") {
40676
- (0, import_child_process4.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
41629
+ (0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
40677
41630
  } else {
40678
41631
  child.kill("SIGKILL");
40679
41632
  }
@@ -40777,14 +41730,14 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
40777
41730
  };
40778
41731
  }
40779
41732
  function normalizeForCompare(path2) {
40780
- const resolved = (0, import_path18.resolve)(path2);
41733
+ const resolved = (0, import_path19.resolve)(path2);
40781
41734
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
40782
41735
  }
40783
41736
  function isPathInside(childPath, parentPath) {
40784
41737
  const child = normalizeForCompare(childPath);
40785
41738
  const parent = normalizeForCompare(parentPath);
40786
- const rel = (0, import_path18.relative)(parent, child);
40787
- return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path18.isAbsolute)(rel);
41739
+ const rel = (0, import_path19.relative)(parent, child);
41740
+ return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path19.isAbsolute)(rel);
40788
41741
  }
40789
41742
  function requireString(value, field) {
40790
41743
  if (typeof value !== "string" || value.trim() === "") {
@@ -40793,10 +41746,10 @@ function requireString(value, field) {
40793
41746
  return value;
40794
41747
  }
40795
41748
  function readBatch(batchPath) {
40796
- if (!(0, import_fs19.existsSync)(batchPath)) {
41749
+ if (!(0, import_fs20.existsSync)(batchPath)) {
40797
41750
  throw new ExternalAgentExecutorError("external_agent_batch_not_found", `Batch file not found: ${batchPath}`);
40798
41751
  }
40799
- const parsed = JSON.parse((0, import_fs19.readFileSync)(batchPath, "utf8"));
41752
+ const parsed = JSON.parse((0, import_fs20.readFileSync)(batchPath, "utf8"));
40800
41753
  if (parsed.schema_version !== "external_agent_batch_plan.v1") {
40801
41754
  throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch schema_version must be external_agent_batch_plan.v1.");
40802
41755
  }
@@ -40811,11 +41764,11 @@ function defaultRunnerProbe(command, args) {
40811
41764
  encoding: "utf8",
40812
41765
  timeout: isGeminiHeadlessSmoke ? GEMINI_HEADLESS_PROBE_TIMEOUT_MS : void 0
40813
41766
  };
40814
- const result = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? (0, import_child_process5.spawnSync)(
41767
+ const result = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? (0, import_child_process6.spawnSync)(
40815
41768
  "powershell.exe",
40816
41769
  ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `& ${[command, ...args].map(quotePowerShellArg).join(" ")}`],
40817
41770
  spawnOptions
40818
- ) : (0, import_child_process5.spawnSync)(command, args, spawnOptions);
41771
+ ) : (0, import_child_process6.spawnSync)(command, args, spawnOptions);
40819
41772
  return {
40820
41773
  status: typeof result.status === "number" ? result.status : null,
40821
41774
  stdout: String(result.stdout || ""),
@@ -40833,8 +41786,8 @@ function resolveCodexProbeCommand() {
40833
41786
  if (process.platform !== "win32") return "codex";
40834
41787
  const appData = process.env.APPDATA;
40835
41788
  if (appData) {
40836
- const appDataShim = (0, import_path18.join)(appData, "npm", "codex.cmd");
40837
- if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41789
+ const appDataShim = (0, import_path19.join)(appData, "npm", "codex.cmd");
41790
+ if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
40838
41791
  }
40839
41792
  return "codex.cmd";
40840
41793
  }
@@ -40845,8 +41798,8 @@ function resolveGeminiProbeCommand() {
40845
41798
  if (process.platform !== "win32") return "gemini";
40846
41799
  const appData = process.env.APPDATA;
40847
41800
  if (appData) {
40848
- const appDataShim = (0, import_path18.join)(appData, "npm", "gemini.cmd");
40849
- if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41801
+ const appDataShim = (0, import_path19.join)(appData, "npm", "gemini.cmd");
41802
+ if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
40850
41803
  }
40851
41804
  return "gemini.cmd";
40852
41805
  }
@@ -41117,35 +42070,35 @@ function safeRunId(value) {
41117
42070
  return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
41118
42071
  }
41119
42072
  function resolveWorkspaceRoot(input) {
41120
- if (input.workspaceRoot) return (0, import_path18.resolve)(input.workspaceRoot);
41121
- const batchStem = (0, import_path18.basename)((0, import_path18.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
41122
- const repoStem = (0, import_path18.basename)((0, import_path18.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
41123
- return (0, import_path18.resolve)((0, import_os2.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
42073
+ if (input.workspaceRoot) return (0, import_path19.resolve)(input.workspaceRoot);
42074
+ const batchStem = (0, import_path19.basename)((0, import_path19.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42075
+ const repoStem = (0, import_path19.basename)((0, import_path19.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42076
+ return (0, import_path19.resolve)((0, import_os3.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
41124
42077
  }
41125
42078
  function findNearestGitRoot(startPath) {
41126
- let current = (0, import_path18.resolve)(startPath);
42079
+ let current = (0, import_path19.resolve)(startPath);
41127
42080
  while (true) {
41128
- if ((0, import_fs19.existsSync)((0, import_path18.join)(current, ".git"))) return current;
41129
- const parent = (0, import_path18.dirname)(current);
42081
+ if ((0, import_fs20.existsSync)((0, import_path19.join)(current, ".git"))) return current;
42082
+ const parent = (0, import_path19.dirname)(current);
41130
42083
  if (parent === current) return null;
41131
42084
  current = parent;
41132
42085
  }
41133
42086
  }
41134
42087
  function resolvePrivateRepoRoot(input) {
41135
42088
  if (input.explicitPrivateRepoRoot) {
41136
- return { root: (0, import_path18.resolve)(input.explicitPrivateRepoRoot), explicit: true };
42089
+ return { root: (0, import_path19.resolve)(input.explicitPrivateRepoRoot), explicit: true };
41137
42090
  }
41138
- const cwd = (0, import_path18.resolve)(input.cwd || process.cwd());
42091
+ const cwd = (0, import_path19.resolve)(input.cwd || process.cwd());
41139
42092
  const gitRoot = findNearestGitRoot(cwd);
41140
42093
  if (gitRoot) return { root: gitRoot, explicit: false };
41141
42094
  return {
41142
- root: (0, import_path18.join)(cwd, ".foh-no-private-repo-root-sentinel"),
42095
+ root: (0, import_path19.join)(cwd, ".foh-no-private-repo-root-sentinel"),
41143
42096
  explicit: false
41144
42097
  };
41145
42098
  }
41146
42099
  function promptVersionFromPath(promptPath) {
41147
- const raw = (0, import_fs19.readFileSync)(promptPath, "utf8");
41148
- if (raw.includes("arbitrary-agency setup context") || raw.includes("Mission: prove whether an arbitrary estate agency") || raw.includes("ops reporting agency-setup-preview")) return "arbitrary-agency-setup-release.v1";
42100
+ const raw = (0, import_fs20.readFileSync)(promptPath, "utf8");
42101
+ if (raw.includes("arbitrary-agency setup context") || raw.includes("Mission: prove whether an arbitrary estate agency") || raw.includes("ops reporting agency-setup-preview") || raw.includes("ops reporting agency-setup-workflow")) return "arbitrary-agency-setup-release.v1";
41149
42102
  if (raw.includes("Do not assume access to the private source repository")) return "blank-setup.v1";
41150
42103
  return "unknown";
41151
42104
  }
@@ -41154,7 +42107,7 @@ function createExternalAgentExecutorPlan(options) {
41154
42107
  if (runner !== "codex" && runner !== "gemini") {
41155
42108
  throw new ExternalAgentExecutorError("unsupported_external_agent_runner", `Unsupported runner: ${runner}`);
41156
42109
  }
41157
- const batchPath = (0, import_path18.resolve)(options.batchPath);
42110
+ const batchPath = (0, import_path19.resolve)(options.batchPath);
41158
42111
  const batch = readBatch(batchPath);
41159
42112
  const runnerProbe = validateRunner(options, runner);
41160
42113
  const codexSandboxBackend = normalizeCodexSandboxBackend(options.codexSandboxBackend);
@@ -41173,17 +42126,17 @@ function createExternalAgentExecutorPlan(options) {
41173
42126
  `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
41174
42127
  );
41175
42128
  }
41176
- (0, import_fs19.mkdirSync)(workspaceRoot, { recursive: true });
41177
- const batchDir = (0, import_path18.resolve)(String(batch.batch_dir || (0, import_path18.resolve)(batchPath, "..")));
42129
+ (0, import_fs20.mkdirSync)(workspaceRoot, { recursive: true });
42130
+ const batchDir = (0, import_path19.resolve)(String(batch.batch_dir || (0, import_path19.resolve)(batchPath, "..")));
41178
42131
  const timeoutMinutes = Number.isFinite(options.timeoutMinutes) && Number(options.timeoutMinutes) > 0 ? Number(options.timeoutMinutes) : 30;
41179
42132
  const runs = batch.runs.map((run) => {
41180
42133
  const runId = safeRunId(requireString(run.run_id, "runs[].run_id"));
41181
- const runDir = (0, import_path18.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
41182
- const promptPath = (0, import_path18.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
41183
- const workspaceDir = (0, import_path18.join)(workspaceRoot, runId);
41184
- (0, import_fs19.mkdirSync)(workspaceDir, { recursive: true });
41185
- (0, import_fs19.writeFileSync)(
41186
- (0, import_path18.join)(workspaceDir, "README.md"),
42134
+ const runDir = (0, import_path19.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
42135
+ const promptPath = (0, import_path19.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
42136
+ const workspaceDir = (0, import_path19.join)(workspaceRoot, runId);
42137
+ (0, import_fs20.mkdirSync)(workspaceDir, { recursive: true });
42138
+ (0, import_fs20.writeFileSync)(
42139
+ (0, import_path19.join)(workspaceDir, "README.md"),
41187
42140
  [
41188
42141
  "# FOH External-Agent Workspace",
41189
42142
  "",
@@ -41201,11 +42154,11 @@ function createExternalAgentExecutorPlan(options) {
41201
42154
  });
41202
42155
  const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
41203
42156
  const outputStem = runner === "gemini" ? "gemini" : "codex";
41204
- const jsonlPath = (0, import_path18.join)(runDir, `${outputStem}-exec.jsonl`);
41205
- const lastMessagePath = (0, import_path18.join)(runDir, `${outputStem}-last-message.md`);
41206
- const stderrPath = (0, import_path18.join)(runDir, `${outputStem}-stderr.txt`);
41207
- const runPath = (0, import_path18.join)(runDir, "run.json");
41208
- const artifactSafetyPath = (0, import_path18.join)(runDir, "artifact-safety.json");
42157
+ const jsonlPath = (0, import_path19.join)(runDir, `${outputStem}-exec.jsonl`);
42158
+ const lastMessagePath = (0, import_path19.join)(runDir, `${outputStem}-last-message.md`);
42159
+ const stderrPath = (0, import_path19.join)(runDir, `${outputStem}-stderr.txt`);
42160
+ const runPath = (0, import_path19.join)(runDir, "run.json");
42161
+ const artifactSafetyPath = (0, import_path19.join)(runDir, "artifact-safety.json");
41209
42162
  const args = runner === "gemini" ? [
41210
42163
  ...runnerProbe.globalArgs,
41211
42164
  ...runnerProbe.execArgs
@@ -41296,9 +42249,9 @@ function createExternalAgentExecutorPlan(options) {
41296
42249
  };
41297
42250
  }
41298
42251
  function writeExternalAgentExecutorPlan(plan) {
41299
- const path2 = (0, import_path18.join)(plan.batch_dir, "executor-plan.json");
41300
- (0, import_fs19.mkdirSync)(plan.batch_dir, { recursive: true });
41301
- (0, import_fs19.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
42252
+ const path2 = (0, import_path19.join)(plan.batch_dir, "executor-plan.json");
42253
+ (0, import_fs20.mkdirSync)(plan.batch_dir, { recursive: true });
42254
+ (0, import_fs20.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
41302
42255
  `, "utf8");
41303
42256
  return path2;
41304
42257
  }
@@ -41313,7 +42266,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41313
42266
  if (authPreflight && !authPreflight.ok) {
41314
42267
  const endedAt2 = (/* @__PURE__ */ new Date()).toISOString();
41315
42268
  const blockedResults = plan.runs.map((run) => {
41316
- (0, import_fs19.mkdirSync)(run.run_dir, { recursive: true });
42269
+ (0, import_fs20.mkdirSync)(run.run_dir, { recursive: true });
41317
42270
  const runArtifact = buildExecutedExternalAgentRunArtifact({
41318
42271
  run,
41319
42272
  startedAt,
@@ -41324,7 +42277,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41324
42277
  timedOut: false,
41325
42278
  durationMs: 0
41326
42279
  });
41327
- (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42280
+ (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
41328
42281
  `, "utf8");
41329
42282
  return {
41330
42283
  run_id: run.run_id,
@@ -41351,8 +42304,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41351
42304
  }
41352
42305
  for (const run of plan.runs) {
41353
42306
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
41354
- const commandCaptureDir = (0, import_path18.join)(run.workspace_dir, ".foh-capture");
41355
- (0, import_fs19.mkdirSync)(commandCaptureDir, { recursive: true });
42307
+ const commandCaptureDir = (0, import_path19.join)(run.workspace_dir, ".foh-capture");
42308
+ (0, import_fs20.mkdirSync)(commandCaptureDir, { recursive: true });
41356
42309
  const env = buildCodexExecutorEnv({
41357
42310
  sourceEnv: options.env,
41358
42311
  runDir: commandCaptureDir,
@@ -41363,7 +42316,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41363
42316
  args: run.args,
41364
42317
  cwd: run.workspace_dir,
41365
42318
  env,
41366
- prompt: (0, import_fs19.readFileSync)(run.prompt_path, "utf8"),
42319
+ prompt: (0, import_fs20.readFileSync)(run.prompt_path, "utf8"),
41367
42320
  stdoutPath: run.outputs.jsonl,
41368
42321
  stderrPath: run.outputs.stderr,
41369
42322
  timeoutMs: plan.timeout_minutes * 60 * 1e3
@@ -41376,7 +42329,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41376
42329
  privateRepoRoot,
41377
42330
  writeRedacted: true
41378
42331
  });
41379
- (0, import_fs19.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
42332
+ (0, import_fs20.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
41380
42333
  `, "utf8");
41381
42334
  const runEndedAt = (/* @__PURE__ */ new Date()).toISOString();
41382
42335
  const classification = classifyExternalAgentRun({
@@ -41395,7 +42348,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41395
42348
  timedOut: spawned.timedOut,
41396
42349
  durationMs: spawned.durationMs
41397
42350
  });
41398
- (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42351
+ (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
41399
42352
  `, "utf8");
41400
42353
  results.push({
41401
42354
  run_id: run.run_id,
@@ -41434,7 +42387,7 @@ var PROMPTS = {
41434
42387
  "real-estate-seller-valuation.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a seller-valuation real-estate agent, prove valuation lead capture on widget and voice, and keep strict no-spend behavior. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply seller valuation template; 3) configure widget + voice; 4) run widget smoke and release certification; 5) run voice proof and classify holds. Do not claim precise valuation output without approved tooling; safe fallback or handoff must remain explicit. Do not buy numbers. Final artifact must include: selected template id/slug, lead fields observed, tool/action behavior, reason codes, docs_pages_used, and next fix commands.",
41435
42388
  "real-estate-viewing-and-qa.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a viewing-booking/property-QA real-estate agent and prove booking-safe behavior under no-spend policy. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply viewing template; 3) configure widget + voice; 4) run widget smoke, release certification, and voice proof; 5) explicitly check no-duplicate-side-effect behavior and safe fallback/handoff when booking tooling or contact path is unavailable. Do not buy numbers. Final artifact must include: selected template id/slug, booking/fallback evidence, reason codes, docs_pages_used, and next fix commands.",
41436
42389
  "real-estate-landlord-enquiry.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a landlord-enquiry real-estate agent and prove lead capture + safe escalation under no-spend constraints. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply landlord template; 3) configure widget + voice; 4) run widget smoke, release certification, and voice proof; 5) ensure handoff/escalation remains reason-coded and no-silent-drop. Do not buy numbers. Final artifact must include: selected template id/slug, lead/action/handoff evidence summary, reason codes, docs_pages_used, and next fix commands.",
41437
- "arbitrary-agency-setup-release.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, the deployed production API, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, local source, private repo scripts, or unpublished commands. Do not assume access to the private source repository. Mission: prove whether an arbitrary estate agency can be set up or accurately held using the public launch evidence workflow. Required path: 1) verify CLI version and auth/org scope; 2) run `foh ops reporting agency-setup-preview` with the supplied agency name, official source URL, branch location, requested tool surface, and target mode; 3) run `foh ops reporting launch-packet --json`; 4) run `foh ops reporting launch-packet-skeletons --json`; 5) run `foh ops reporting launch-packet-evidence-check --json` only with redacted synthetic or customer-provided evidence, never raw credentials or secrets; 6) classify the outcome as pass, expected hold, or product failure. No-spend boundary: do not buy phone numbers, provision paid resources, scrape private systems, bypass credential holds, or claim live/customer-ready/production unless the launch packet explicitly allows it. Expected holds such as missing customer credentials, missing behavior approval, missing live voice validation, or missing production signoff are acceptable only when the commands expose clear reason codes and next actions. Final artifact must include commands executed, docs_pages_used, manual_intervention_count, CLI/API version readback, launch mode observed, reason codes, evidence artifacts created, and exact next commands. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing.",
42390
+ "arbitrary-agency-setup-release.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, the deployed production API, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, local source, private repo scripts, or unpublished commands. Do not assume access to the private source repository. Mission: prove whether an arbitrary estate agency can be set up or accurately held using the public launch evidence workflow. Required path: 1) verify CLI version and auth/org scope; 2) run `foh ops reporting agency-setup-workflow` with the supplied agency name, official source URL, branch location, requested tool surface, and target mode; 3) inspect the returned setup dossier, launch packet, evidence packet, operator status, no-overclaim decision, and proof commands; 4) run `foh ops reporting launch-packet-evidence-check --json` only with redacted synthetic or customer-provided evidence, never raw credentials or secrets; 5) run `foh ops reporting customer-live-status --json`; 6) classify the outcome as pass, expected hold, or product failure. No-spend boundary: do not buy phone numbers, provision paid resources, scrape private systems, bypass credential holds, or claim live/customer-ready/production unless the launch packet explicitly allows it. Expected holds such as missing customer credentials, missing behavior approval, missing live voice validation, or missing production signoff are acceptable only when the commands expose clear reason codes and next actions. Final artifact must include commands executed, docs_pages_used, manual_intervention_count, CLI/API version readback, launch mode observed, reason codes, evidence artifacts created, and exact next commands. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing.",
41438
42391
  "debug-proof-failure.v1": "You are given a FOH proof or debug artifact. Use public docs and FOH CLI/API behavior to classify whether the blocker is docs, auth, org setup, agent config, widget, channel, runtime, or product bug. Produce a redacted improvement packet or the exact command needed to produce one. Do not ask the human to interpret logs manually unless no machine-readable artifact exists.",
41439
42392
  "knowledge-miss.v1": "A FOH agent failed to answer a business question. Use CLI/API/docs to determine whether this is a knowledge-ingestion issue, retrieval issue, config issue, prompt/behavior issue, or runtime issue. Prefer foh knowledge query, transcript export, replay, and foh bug improve artifacts over screenshots.",
41440
42393
  "replay-failure.v1": "You are given a FOH transcript or replay artifact. Use CLI/API/docs to replay or inspect the failed interaction, identify expected vs actual behavior, and produce a scenario-test or improvement-packet candidate."
@@ -41449,13 +42402,13 @@ function defaultRunDir(modelName, promptVersion) {
41449
42402
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
41450
42403
  const safeModel = String(modelName || "unknown-model").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
41451
42404
  const safePrompt = String(promptVersion || DEFAULT_PROMPT_VERSION).toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
41452
- return (0, import_path19.resolve)("test-results", "external-agent-runs", date4, `${safeModel}-${safePrompt}-${stamp}`);
42405
+ return (0, import_path20.resolve)("test-results", "external-agent-runs", date4, `${safeModel}-${safePrompt}-${stamp}`);
41453
42406
  }
41454
42407
  function defaultBatchDir(promptVersion) {
41455
42408
  const date4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
41456
42409
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
41457
42410
  const safePrompt = String(promptVersion || DEFAULT_PROMPT_VERSION).toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
41458
- return (0, import_path19.resolve)("test-results", "external-agent-runs", date4, `batch-${safePrompt}-${stamp}`);
42411
+ return (0, import_path20.resolve)("test-results", "external-agent-runs", date4, `batch-${safePrompt}-${stamp}`);
41459
42412
  }
41460
42413
  function safeSlug(value) {
41461
42414
  return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
@@ -41572,7 +42525,7 @@ function agencySetupPromptContext(context = {}) {
41572
42525
  const targetMode = String(context.targetMode || "").trim();
41573
42526
  if (!agencyName && !sourceUrl && !branchLocation && !targetMode) return "";
41574
42527
  const previewArgs = [
41575
- "ops reporting agency-setup-preview",
42528
+ "ops reporting agency-setup-workflow",
41576
42529
  agencyName ? `--agency-name ${quoteArg(agencyName)}` : "--agency-name <agency-name>",
41577
42530
  sourceUrl ? `--source-url ${quoteArg(sourceUrl)}` : "--source-url <official-source-url>",
41578
42531
  branchLocation ? `--branch-location ${quoteArg(branchLocation)}` : "--branch-location <branch-location>",
@@ -41587,7 +42540,7 @@ function agencySetupPromptContext(context = {}) {
41587
42540
  ...branchLocation ? [`- Branch/location: ${branchLocation}`] : [],
41588
42541
  ...targetMode ? [`- Target mode: ${targetMode}`] : [],
41589
42542
  `- Start with: npx --yes @f-o-h/cli@latest ${previewArgs}`,
41590
- "- Then run launch-packet, launch-packet-skeletons, and launch-packet-evidence-check using public CLI commands only.",
42543
+ "- Then inspect the workflow output and run only the returned proof/evidence commands using public CLI commands.",
41591
42544
  "- Treat missing customer credentials or approvals as expected holds only if the CLI gives machine-readable reason codes and next actions."
41592
42545
  ].join("\n");
41593
42546
  }
@@ -41598,14 +42551,14 @@ function writePrompt(runDir, promptVersion, context = {}) {
41598
42551
  knowledgeMissPromptContext(context.knowledgeQuestion, context.expectedAnswer),
41599
42552
  agencySetupPromptContext(context)
41600
42553
  ].join("");
41601
- const path2 = (0, import_path19.join)(runDir, "prompt.txt");
41602
- (0, import_fs20.writeFileSync)(path2, `${prompt}
42554
+ const path2 = (0, import_path20.join)(runDir, "prompt.txt");
42555
+ (0, import_fs21.writeFileSync)(path2, `${prompt}
41603
42556
  `, "utf8");
41604
42557
  return path2;
41605
42558
  }
41606
42559
  function writeSession(runDir, session) {
41607
- const path2 = (0, import_path19.join)(runDir, "session.json");
41608
- (0, import_fs20.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
42560
+ const path2 = (0, import_path20.join)(runDir, "session.json");
42561
+ (0, import_fs21.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
41609
42562
  `, "utf8");
41610
42563
  return path2;
41611
42564
  }
@@ -41685,9 +42638,9 @@ function buildRunArtifact(input) {
41685
42638
  notes: "notes.md"
41686
42639
  },
41687
42640
  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}.`,
41688
- next_commands: status === "pass" ? [externalAgentSummaryCommand((0, import_path19.dirname)(input.runDir))] : [
41689
- `foh bug improve --from external-agent-run --file ${(0, import_path19.join)(input.runDir, "run.json")} --out ${(0, import_path19.join)(input.runDir, "improvement-packet.json")} --json`,
41690
- externalAgentSummaryCommand((0, import_path19.dirname)(input.runDir))
42641
+ next_commands: status === "pass" ? [externalAgentSummaryCommand((0, import_path20.dirname)(input.runDir))] : [
42642
+ `foh bug improve --from external-agent-run --file ${(0, import_path20.join)(input.runDir, "run.json")} --out ${(0, import_path20.join)(input.runDir, "improvement-packet.json")} --json`,
42643
+ externalAgentSummaryCommand((0, import_path20.dirname)(input.runDir))
41691
42644
  ]
41692
42645
  };
41693
42646
  }
@@ -41696,8 +42649,8 @@ function registerEval(program3) {
41696
42649
  const external = evalCommand.command("external-agent").description("Capture clean external coding-agent setup attempts");
41697
42650
  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("--replay-file <path>", "Local transcript/replay artifact to seed replay-failure prompts").option("--knowledge-question <text>", "Question to seed knowledge-miss prompts").option("--expected-answer <text>", "Expected answer or missing fact for planted knowledge-miss prompts").option("--agency-name <name>", "Agency name to seed arbitrary-agency setup prompts").option("--source-url <url>", "Official source URL to seed arbitrary-agency setup prompts").option("--branch-location <value>", "Branch/location to seed arbitrary-agency setup prompts").option("--target-mode <mode>", "Target launch/exposure mode for arbitrary-agency setup prompts").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) => {
41698
42651
  const promptVersion = String(opts.promptVersion || DEFAULT_PROMPT_VERSION);
41699
- const batchDir = (0, import_path19.resolve)(String(opts.outDir || defaultBatchDir(promptVersion)));
41700
- const replayFile = opts.replayFile ? (0, import_path19.resolve)(String(opts.replayFile)) : void 0;
42652
+ const batchDir = (0, import_path20.resolve)(String(opts.outDir || defaultBatchDir(promptVersion)));
42653
+ const replayFile = opts.replayFile ? (0, import_path20.resolve)(String(opts.replayFile)) : void 0;
41701
42654
  const knowledgeQuestion = opts.knowledgeQuestion ? String(opts.knowledgeQuestion) : void 0;
41702
42655
  const expectedAnswer = opts.expectedAnswer ? String(opts.expectedAnswer) : void 0;
41703
42656
  const agencyName = opts.agencyName ? String(opts.agencyName) : void 0;
@@ -41705,11 +42658,11 @@ function registerEval(program3) {
41705
42658
  const branchLocation = opts.branchLocation ? String(opts.branchLocation) : void 0;
41706
42659
  const targetMode = opts.targetMode ? String(opts.targetMode) : void 0;
41707
42660
  const models = parseModelList(String(opts.models || DEFAULT_BATCH_MODELS));
41708
- (0, import_fs20.mkdirSync)(batchDir, { recursive: true });
42661
+ (0, import_fs21.mkdirSync)(batchDir, { recursive: true });
41709
42662
  const runs2 = models.map((model, index) => {
41710
42663
  const runId = `${String(index + 1).padStart(2, "0")}-${safeSlug(model.provider)}-${safeSlug(model.name)}`;
41711
- const runDir = (0, import_path19.join)(batchDir, runId);
41712
- (0, import_fs20.mkdirSync)(runDir, { recursive: true });
42664
+ const runDir = (0, import_path20.join)(batchDir, runId);
42665
+ (0, import_fs21.mkdirSync)(runDir, { recursive: true });
41713
42666
  const promptPath = writePrompt(runDir, promptVersion, {
41714
42667
  replayFile,
41715
42668
  knowledgeQuestion,
@@ -41772,8 +42725,8 @@ function registerEval(program3) {
41772
42725
  runs: runs2,
41773
42726
  summary_command: externalAgentSummaryCommand(batchDir)
41774
42727
  };
41775
- const batchPath = (0, import_path19.join)(batchDir, "batch.json");
41776
- (0, import_fs20.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
42728
+ const batchPath = (0, import_path20.join)(batchDir, "batch.json");
42729
+ (0, import_fs21.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
41777
42730
  `, "utf8");
41778
42731
  format(cliEnvelope({
41779
42732
  schemaVersion: "external_agent_batch_plan_result.v1",
@@ -41793,15 +42746,15 @@ function registerEval(program3) {
41793
42746
  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("--replay-file <path>", "Local transcript/replay artifact to seed replay-failure prompts").option("--knowledge-question <text>", "Question to seed knowledge-miss prompts").option("--expected-answer <text>", "Expected answer or missing fact for planted knowledge-miss prompts").option("--agency-name <name>", "Agency name to seed arbitrary-agency setup prompts").option("--source-url <url>", "Official source URL to seed arbitrary-agency setup prompts").option("--branch-location <value>", "Branch/location to seed arbitrary-agency setup prompts").option("--target-mode <mode>", "Target launch/exposure mode for arbitrary-agency setup prompts").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) => {
41794
42747
  const status = normalizeStatus(opts.status);
41795
42748
  const promptVersion = String(opts.promptVersion || DEFAULT_PROMPT_VERSION);
41796
- const runDir = (0, import_path19.resolve)(String(opts.outDir || defaultRunDir(opts.modelName, promptVersion)));
41797
- const replayFile = opts.replayFile ? (0, import_path19.resolve)(String(opts.replayFile)) : void 0;
42749
+ const runDir = (0, import_path20.resolve)(String(opts.outDir || defaultRunDir(opts.modelName, promptVersion)));
42750
+ const replayFile = opts.replayFile ? (0, import_path20.resolve)(String(opts.replayFile)) : void 0;
41798
42751
  const knowledgeQuestion = opts.knowledgeQuestion ? String(opts.knowledgeQuestion) : void 0;
41799
42752
  const expectedAnswer = opts.expectedAnswer ? String(opts.expectedAnswer) : void 0;
41800
42753
  const agencyName = opts.agencyName ? String(opts.agencyName) : void 0;
41801
42754
  const sourceUrl = opts.sourceUrl ? String(opts.sourceUrl) : void 0;
41802
42755
  const branchLocation = opts.branchLocation ? String(opts.branchLocation) : void 0;
41803
42756
  const targetMode = opts.targetMode ? String(opts.targetMode) : void 0;
41804
- (0, import_fs20.mkdirSync)(runDir, { recursive: true });
42757
+ (0, import_fs21.mkdirSync)(runDir, { recursive: true });
41805
42758
  const runId = runDir.split(/[\\/]/).filter(Boolean).slice(-1)[0];
41806
42759
  const promptPath = writePrompt(runDir, promptVersion, {
41807
42760
  replayFile,
@@ -41839,7 +42792,7 @@ function registerEval(program3) {
41839
42792
  }
41840
42793
  };
41841
42794
  writeSession(runDir, session);
41842
- (0, import_fs20.writeFileSync)((0, import_path19.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
42795
+ (0, import_fs21.writeFileSync)((0, import_path20.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
41843
42796
  let shellExitCode = null;
41844
42797
  if (opts.shell !== false) {
41845
42798
  process.stdout.write(`
@@ -41849,7 +42802,7 @@ Prompt: ${promptPath}
41849
42802
  Exit the shell to finalize run.json.
41850
42803
 
41851
42804
  `);
41852
- const result = (0, import_child_process6.spawnSync)(shell.command, shell.args, {
42805
+ const result = (0, import_child_process7.spawnSync)(shell.command, shell.args, {
41853
42806
  stdio: "inherit",
41854
42807
  env: {
41855
42808
  ...process.env,
@@ -41861,8 +42814,8 @@ Exit the shell to finalize run.json.
41861
42814
  shellExitCode = typeof result.status === "number" ? result.status : null;
41862
42815
  }
41863
42816
  const artifact = buildRunArtifact({ runDir, session, status, reasonCode: opts.reasonCode, shellExitCode });
41864
- const runPath = (0, import_path19.join)(runDir, "run.json");
41865
- (0, import_fs20.writeFileSync)(runPath, `${JSON.stringify(artifact, null, 2)}
42817
+ const runPath = (0, import_path20.join)(runDir, "run.json");
42818
+ (0, import_fs21.writeFileSync)(runPath, `${JSON.stringify(artifact, null, 2)}
41866
42819
  `, "utf8");
41867
42820
  format(cliEnvelope({
41868
42821
  schemaVersion: "external_agent_capture_result.v1",
@@ -41872,7 +42825,7 @@ Exit the shell to finalize run.json.
41872
42825
  artifacts: {
41873
42826
  run: runPath,
41874
42827
  prompt: promptPath,
41875
- commands: (0, import_path19.join)(runDir, "commands.ndjson")
42828
+ commands: (0, import_path20.join)(runDir, "commands.ndjson")
41876
42829
  },
41877
42830
  nextCommands: artifact.next_commands,
41878
42831
  extra: { run: artifact }
@@ -41980,8 +42933,8 @@ Exit the shell to finalize run.json.
41980
42933
  requireExplicitEvalAuth: true,
41981
42934
  minimumEvalAuthTtlMs: (plan.timeout_minutes + 5) * 60 * 1e3
41982
42935
  });
41983
- const resultPath = (0, import_path19.join)(plan.batch_dir, "execution-result.json");
41984
- (0, import_fs20.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
42936
+ const resultPath = (0, import_path20.join)(plan.batch_dir, "execution-result.json");
42937
+ (0, import_fs21.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
41985
42938
  `, "utf8");
41986
42939
  format(cliEnvelope({
41987
42940
  schemaVersion: "external_agent_execution_result.v1",
@@ -42211,11 +43164,13 @@ registerCertify(program2);
42211
43164
  registerDiag(program2);
42212
43165
  registerBug(program2);
42213
43166
  registerProve(program2);
43167
+ registerInteractive(program2);
42214
43168
  registerAgentPublishCommand(program2, { publicAlias: true });
42215
43169
  registerEval(program2);
42216
43170
  registerUpdate(program2);
42217
43171
  registerHome(program2);
42218
43172
  hideInternalApiUrlOptions(program2);
43173
+ initializeCommandGraph(program2);
42219
43174
  maybeDefaultToHome(process.argv);
42220
43175
  program2.parseAsync(process.argv).catch((e) => {
42221
43176
  if (e instanceof CliSoftExit) {