@f-o-h/cli 0.1.83 → 0.1.85

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 -996
  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",
@@ -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,21 @@ 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.83";
33058
+ var injectedVersion = true ? String("0.1.85").trim() : "";
33059
+ var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
33060
+ var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
32996
33061
 
32997
33062
  // src/commands/mcp-serve.ts
32998
33063
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -33177,7 +33242,7 @@ async function runFohCli(params) {
33177
33242
  effectiveArgv.push("--json");
33178
33243
  }
33179
33244
  const command = `foh ${effectiveArgv.join(" ")}`;
33180
- return await new Promise((resolve14) => {
33245
+ return await new Promise((resolve15) => {
33181
33246
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
33182
33247
  stdio: ["ignore", "pipe", "pipe"],
33183
33248
  env: {
@@ -33202,7 +33267,7 @@ async function runFohCli(params) {
33202
33267
  });
33203
33268
  child.once("error", (error2) => {
33204
33269
  clearTimeout(timeoutHandle);
33205
- resolve14({
33270
+ resolve15({
33206
33271
  ok: false,
33207
33272
  command,
33208
33273
  argv: effectiveArgv,
@@ -33218,7 +33283,7 @@ async function runFohCli(params) {
33218
33283
  const stderrText = finalizeBoundedText(stderrBuffer);
33219
33284
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
33220
33285
  const stdoutJson = tryParseJson(stdoutText);
33221
- resolve14({
33286
+ resolve15({
33222
33287
  ok: !timedOut && exitCode === 0,
33223
33288
  command,
33224
33289
  argv: effectiveArgv,
@@ -35388,8 +35453,8 @@ function registerSetup(program3) {
35388
35453
  }
35389
35454
  try {
35390
35455
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
35391
- const { writeFileSync: writeFileSync14 } = await import("fs");
35392
- writeFileSync14(
35456
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35457
+ writeFileSync15(
35393
35458
  "tenant.yaml",
35394
35459
  `# tenant.yaml - Front Of House agent manifest
35395
35460
  # Edit this file and run: foh plan tenant.yaml
@@ -35559,8 +35624,8 @@ function registerSim(program3) {
35559
35624
  }
35560
35625
  const cert = response.certificate;
35561
35626
  if (opts.out) {
35562
- const { writeFileSync: writeFileSync14 } = await import("fs");
35563
- writeFileSync14(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35627
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35628
+ writeFileSync15(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35564
35629
  process.stderr.write(` Certificate written to ${opts.out}
35565
35630
  `);
35566
35631
  }
@@ -35610,8 +35675,8 @@ function registerSim(program3) {
35610
35675
  });
35611
35676
  }
35612
35677
  if (opts.out) {
35613
- const { writeFileSync: writeFileSync14 } = await import("fs");
35614
- writeFileSync14(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35678
+ const { writeFileSync: writeFileSync15 } = await import("fs");
35679
+ writeFileSync15(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35615
35680
  process.stderr.write(` Final certificate written to ${opts.out}
35616
35681
  `);
35617
35682
  }
@@ -35647,7 +35712,7 @@ ${passIcon} Certification loop summary
35647
35712
  }
35648
35713
 
35649
35714
  // src/commands/certify.ts
35650
- var import_node_fs2 = require("node:fs");
35715
+ var import_node_fs3 = require("node:fs");
35651
35716
  function normalizeProfile(raw) {
35652
35717
  const value = String(raw || "release").trim().toLowerCase();
35653
35718
  if (value === "smoke" || value === "release" || value === "stress") return value;
@@ -35725,7 +35790,7 @@ function registerCertify(program3) {
35725
35790
  next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh certify run --agent ${opts.agent} --profile ${profile} --json`]
35726
35791
  };
35727
35792
  if (opts.out) {
35728
- (0, import_node_fs2.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35793
+ (0, import_node_fs3.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35729
35794
  }
35730
35795
  if (opts.json ?? false) {
35731
35796
  format(result, { json: true });
@@ -36675,6 +36740,22 @@ function registerOpsAgencySetupCommands(reporting) {
36675
36740
  });
36676
36741
  format(data, { json: opts.json ?? false });
36677
36742
  }));
36743
+ 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 () => {
36744
+ const body = {
36745
+ agency_name: String(opts.agencyName),
36746
+ requested_tool_surface: parseCsvOption(opts.tools) ?? [],
36747
+ target_exposure_mode: String(opts.targetMode)
36748
+ };
36749
+ if (opts.sourceUrl) body.source_url = String(opts.sourceUrl);
36750
+ if (opts.branchLocation) body.branch_location = String(opts.branchLocation);
36751
+ const data = await apiFetch("/v1/console/agency-setup/workflow", {
36752
+ method: "POST",
36753
+ body: JSON.stringify(body),
36754
+ orgId: opts.org,
36755
+ apiUrlOverride: opts.apiUrl
36756
+ });
36757
+ format(data, { json: opts.json ?? false });
36758
+ }));
36678
36759
  }
36679
36760
 
36680
36761
  // src/commands/ops-diagnostics.ts
@@ -36929,6 +37010,16 @@ function registerOpsReportingCommands(reporting) {
36929
37010
  });
36930
37011
  format(data, { json: opts.json ?? false });
36931
37012
  }));
37013
+ 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 () => {
37014
+ const params = new URLSearchParams();
37015
+ if (opts.environment) params.set("environment", String(opts.environment));
37016
+ appendSetupPreviewContext(params, opts);
37017
+ const data = await apiFetch(withQuery("/v1/console/customer-live-status", params), {
37018
+ orgId: opts.org,
37019
+ apiUrlOverride: opts.apiUrl
37020
+ });
37021
+ format(data, { json: opts.json ?? false });
37022
+ }));
36932
37023
  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
37024
  const limit = Math.max(1, Math.min(100, Number(opts.limit || 10) || 10));
36934
37025
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/release-scorecard?limit=${limit}`, {
@@ -37560,41 +37651,80 @@ function defaultImprovementArtifactPath() {
37560
37651
  }
37561
37652
  async function resolveBugReportWizardInputs(opts) {
37562
37653
  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
- }
37654
+ const resolved = await runWizardSession({
37655
+ step: "bug.report",
37656
+ remediation: "Run with --out and --command in non-interactive mode, or use --wizard in a TTY.",
37657
+ title: "Bug Report Wizard",
37658
+ initialState: { ...opts },
37659
+ enabled: true,
37660
+ prompts: [
37661
+ {
37662
+ key: "out",
37663
+ label: "Output file path",
37664
+ defaultValue: () => defaultArtifactPath(),
37665
+ required: true,
37666
+ shouldPrompt: (state) => !String(state.out ?? "").trim()
37667
+ },
37668
+ {
37669
+ key: "command",
37670
+ label: "Failing command",
37671
+ required: true,
37672
+ shouldPrompt: (state) => !String(state.command ?? "").trim()
37673
+ },
37674
+ {
37675
+ key: "requestMethod",
37676
+ label: "Request method",
37677
+ defaultValue: "GET",
37678
+ required: true,
37679
+ shouldPrompt: (state) => !String(state.requestMethod ?? "").trim()
37680
+ },
37681
+ {
37682
+ key: "requestLocation",
37683
+ label: "Request URL or path (for example /v1/console/agents)",
37684
+ required: true,
37685
+ shouldPrompt: (state) => !String(state.requestUrl ?? "").trim() && !String(state.requestPath ?? "").trim(),
37686
+ onResolved: (value, state) => {
37687
+ const location = String(value ?? "").trim();
37688
+ if (location.startsWith("http://") || location.startsWith("https://")) {
37689
+ state.requestUrl = location;
37690
+ state.requestPath = "";
37691
+ } else {
37692
+ state.requestPath = location;
37693
+ state.requestUrl = "";
37694
+ }
37695
+ }
37696
+ },
37697
+ {
37698
+ key: "responseStatus",
37699
+ label: "Response status",
37700
+ defaultValue: "500",
37701
+ required: true,
37702
+ shouldPrompt: (state) => !String(state.responseStatus ?? "").trim()
37703
+ },
37704
+ {
37705
+ key: "severity",
37706
+ label: "Severity",
37707
+ defaultValue: "medium",
37708
+ required: true,
37709
+ shouldPrompt: (state) => !String(state.severity ?? "").trim()
37710
+ },
37711
+ {
37712
+ key: "source",
37713
+ label: "Source",
37714
+ defaultValue: "cli",
37715
+ required: true,
37716
+ shouldPrompt: (state) => !String(state.source ?? "").trim()
37717
+ },
37718
+ {
37719
+ key: "submit",
37720
+ label: "Submit packet to FOH API now? [y/N]",
37721
+ kind: "confirm",
37722
+ confirmDefault: false,
37723
+ shouldPrompt: (state) => state.submit === void 0
37724
+ }
37725
+ ]
37726
+ });
37727
+ delete resolved.requestLocation;
37598
37728
  return resolved;
37599
37729
  }
37600
37730
  function ensureBugReportRequiredInputs(opts) {
@@ -37790,7 +37920,7 @@ function registerBug(program3) {
37790
37920
 
37791
37921
  // src/lib/proof-cache.ts
37792
37922
  var import_node_crypto2 = require("node:crypto");
37793
- var import_node_fs3 = require("node:fs");
37923
+ var import_node_fs4 = require("node:fs");
37794
37924
  var import_node_path = require("node:path");
37795
37925
  var DEFAULT_MAX_AGE_MS = 15 * 60 * 1e3;
37796
37926
  var DEFAULT_WAIT_MS = 180 * 1e3;
@@ -37814,7 +37944,7 @@ function publicPath(filePath) {
37814
37944
  }
37815
37945
  function readFreshCache(filePath, maxAgeMs) {
37816
37946
  try {
37817
- const payload = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
37947
+ const payload = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
37818
37948
  const createdAt = Date.parse(String(payload.created_at || ""));
37819
37949
  if (!Number.isFinite(createdAt)) return null;
37820
37950
  const ageMs = Date.now() - createdAt;
@@ -37826,8 +37956,8 @@ function readFreshCache(filePath, maxAgeMs) {
37826
37956
  }
37827
37957
  }
37828
37958
  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({
37959
+ (0, import_node_fs4.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37960
+ (0, import_node_fs4.writeFileSync)(filePath, `${JSON.stringify({
37831
37961
  schema_version: "foh_proof_cache_entry.v1",
37832
37962
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
37833
37963
  value
@@ -37862,7 +37992,7 @@ async function withProofCache(options, run) {
37862
37992
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
37863
37993
  const waitMs = options.waitMs ?? Number(process.env.FOH_PROOF_CACHE_WAIT_MS || DEFAULT_WAIT_MS);
37864
37994
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
37865
- (0, import_node_fs3.mkdirSync)(resolvedDir, { recursive: true });
37995
+ (0, import_node_fs4.mkdirSync)(resolvedDir, { recursive: true });
37866
37996
  const existing = readFreshCache(cachePath, maxAgeMs);
37867
37997
  if (existing) {
37868
37998
  return {
@@ -37881,7 +38011,7 @@ async function withProofCache(options, run) {
37881
38011
  }
37882
38012
  let lockOwner = false;
37883
38013
  try {
37884
- (0, import_node_fs3.mkdirSync)(lockPath);
38014
+ (0, import_node_fs4.mkdirSync)(lockPath);
37885
38015
  lockOwner = true;
37886
38016
  } catch {
37887
38017
  const started = Date.now();
@@ -37922,7 +38052,7 @@ async function withProofCache(options, run) {
37922
38052
  }
37923
38053
  };
37924
38054
  } finally {
37925
- if (lockOwner) (0, import_node_fs3.rmSync)(lockPath, { recursive: true, force: true });
38055
+ if (lockOwner) (0, import_node_fs4.rmSync)(lockPath, { recursive: true, force: true });
37926
38056
  }
37927
38057
  }
37928
38058
 
@@ -38412,7 +38542,582 @@ function registerProve(program3) {
38412
38542
  }));
38413
38543
  }
38414
38544
 
38415
- // src/tui/command-palette.ts
38545
+ // src/commands/interactive.ts
38546
+ var import_readline2 = require("readline");
38547
+ var import_child_process2 = require("child_process");
38548
+ var import_fs11 = require("fs");
38549
+ var import_os2 = require("os");
38550
+ var import_path9 = require("path");
38551
+
38552
+ // src/commands/command-graph.ts
38553
+ var COMMAND_SURFACE_DEFINITIONS = [
38554
+ {
38555
+ id: "home",
38556
+ commandPath: ["home"],
38557
+ label: "home",
38558
+ descriptionFallback: "Open onboarding home",
38559
+ mutatesState: "read",
38560
+ shellSlash: "/home",
38561
+ includeInSuggestions: false,
38562
+ homeContexts: []
38563
+ },
38564
+ {
38565
+ id: "start",
38566
+ commandPath: ["home"],
38567
+ invokeArgs: ["start"],
38568
+ label: "start",
38569
+ descriptionFallback: "Run guided onboarding start",
38570
+ mutatesState: "read",
38571
+ shellSlash: "/start",
38572
+ includeInSuggestions: false,
38573
+ homeContexts: []
38574
+ },
38575
+ {
38576
+ id: "auth_login",
38577
+ commandPath: ["auth", "login"],
38578
+ label: "login now (recommended)",
38579
+ descriptionFallback: "Run interactive login flow.",
38580
+ mutatesState: "write",
38581
+ examples: ["foh auth login --wizard"],
38582
+ homeContexts: ["unauthenticated"],
38583
+ homeAliases: ["start", "login", "auth", "auth login"],
38584
+ includeInSuggestions: true
38585
+ },
38586
+ {
38587
+ id: "auth_login_help",
38588
+ commandPath: ["auth", "login"],
38589
+ invokeArgs: ["auth", "login", "--help"],
38590
+ label: "auth login help",
38591
+ descriptionFallback: "Show auth login examples.",
38592
+ mutatesState: "read",
38593
+ homeContexts: ["unauthenticated"],
38594
+ includeInSuggestions: true
38595
+ },
38596
+ {
38597
+ id: "auth_login_wizard",
38598
+ commandPath: ["auth", "login"],
38599
+ invokeArgs: ["auth", "login", "--wizard"],
38600
+ label: "auth login wizard",
38601
+ descriptionFallback: "Run login wizard",
38602
+ mutatesState: "write",
38603
+ shellSlash: "/login",
38604
+ includeInSuggestions: true
38605
+ },
38606
+ {
38607
+ id: "auth_whoami",
38608
+ commandPath: ["auth", "whoami"],
38609
+ label: "auth whoami",
38610
+ descriptionFallback: "Show current auth context",
38611
+ requiresAuth: true,
38612
+ mutatesState: "read",
38613
+ homeContexts: ["authenticated_no_org"],
38614
+ homeAliases: ["whoami"],
38615
+ shellSlash: "/auth",
38616
+ includeInSuggestions: true
38617
+ },
38618
+ {
38619
+ id: "org_list",
38620
+ commandPath: ["org", "list"],
38621
+ label: "list my orgs",
38622
+ descriptionFallback: "View org memberships.",
38623
+ requiresAuth: true,
38624
+ mutatesState: "read",
38625
+ homeContexts: ["authenticated_no_org"],
38626
+ homeAliases: ["org"],
38627
+ shellSlash: "/orgs",
38628
+ includeInSuggestions: true
38629
+ },
38630
+ {
38631
+ id: "org_use_help",
38632
+ commandPath: ["org", "use"],
38633
+ invokeArgs: ["org", "use", "--help"],
38634
+ label: "org use help",
38635
+ descriptionFallback: "Set default org guidance.",
38636
+ requiresAuth: true,
38637
+ mutatesState: "read",
38638
+ homeContexts: ["authenticated_no_org"],
38639
+ includeInSuggestions: true
38640
+ },
38641
+ {
38642
+ id: "tenant_status",
38643
+ commandPath: ["tenant", "status"],
38644
+ label: "tenant status",
38645
+ descriptionFallback: "Check tenant status and readiness.",
38646
+ requiresAuth: true,
38647
+ requiresOrg: true,
38648
+ mutatesState: "read",
38649
+ homeContexts: ["authenticated_with_org"],
38650
+ homeAliases: ["tenant"],
38651
+ shellSlash: "/status",
38652
+ includeInSuggestions: true
38653
+ },
38654
+ {
38655
+ id: "templates_list",
38656
+ commandPath: ["templates", "list"],
38657
+ label: "template list",
38658
+ descriptionFallback: "List templates.",
38659
+ requiresAuth: true,
38660
+ requiresOrg: true,
38661
+ mutatesState: "read",
38662
+ homeContexts: ["authenticated_with_org"],
38663
+ homeAliases: ["templates", "template list"],
38664
+ shellSlash: "/templates",
38665
+ includeInSuggestions: true
38666
+ },
38667
+ {
38668
+ id: "agent_list_all",
38669
+ commandPath: ["agent", "list"],
38670
+ invokeArgs: ["agent", "list", "--segment", "all"],
38671
+ label: "agent list",
38672
+ descriptionFallback: "List all agents.",
38673
+ requiresAuth: true,
38674
+ requiresOrg: true,
38675
+ mutatesState: "read",
38676
+ homeContexts: ["authenticated_with_org"],
38677
+ homeAliases: ["agent"],
38678
+ shellSlash: "/agent",
38679
+ includeInSuggestions: true
38680
+ },
38681
+ {
38682
+ id: "setup_help",
38683
+ commandPath: ["setup"],
38684
+ invokeArgs: ["setup", "--help"],
38685
+ label: "setup help",
38686
+ descriptionFallback: "Show setup flow options.",
38687
+ mutatesState: "read",
38688
+ homeContexts: ["authenticated_with_org"],
38689
+ homeAliases: ["setup"],
38690
+ shellSlash: "/setup",
38691
+ includeInSuggestions: true
38692
+ },
38693
+ {
38694
+ id: "bug_report_help",
38695
+ commandPath: ["bug", "report"],
38696
+ invokeArgs: ["bug", "report", "--help"],
38697
+ label: "bug report help",
38698
+ descriptionFallback: "Show bug-report guidance.",
38699
+ mutatesState: "read",
38700
+ homeContexts: ["authenticated_with_org"],
38701
+ homeAliases: ["bug report", "bug report help"],
38702
+ includeInSuggestions: false
38703
+ },
38704
+ {
38705
+ id: "bug_list",
38706
+ commandPath: ["bug", "list"],
38707
+ label: "bug list",
38708
+ descriptionFallback: "List centralized bug reports.",
38709
+ requiresAuth: true,
38710
+ requiresOrg: true,
38711
+ mutatesState: "read",
38712
+ homeContexts: ["authenticated_with_org"],
38713
+ homeAliases: ["bug list"],
38714
+ includeInSuggestions: false
38715
+ },
38716
+ {
38717
+ id: "interactive_shell",
38718
+ commandPath: ["interactive"],
38719
+ label: "interactive shell",
38720
+ descriptionFallback: "Open slash-command shell.",
38721
+ mutatesState: "read",
38722
+ homeContexts: ["unauthenticated", "authenticated_no_org", "authenticated_with_org"],
38723
+ homeAliases: ["interactive", "interactive shell", "shell"],
38724
+ includeInSuggestions: false
38725
+ },
38726
+ {
38727
+ id: "full_help",
38728
+ commandPath: ["home"],
38729
+ invokeArgs: ["--help"],
38730
+ label: "full help",
38731
+ descriptionFallback: "Show all CLI commands.",
38732
+ mutatesState: "read",
38733
+ homeContexts: ["unauthenticated", "authenticated_no_org"],
38734
+ homeAliases: ["full help"],
38735
+ includeInSuggestions: false
38736
+ },
38737
+ {
38738
+ id: "whatsapp_start",
38739
+ commandPath: ["channel", "whatsapp", "start"],
38740
+ label: "whatsapp start",
38741
+ descriptionFallback: "WhatsApp readiness path",
38742
+ mutatesState: "write",
38743
+ shellSlash: "/whatsapp",
38744
+ includeInSuggestions: true
38745
+ },
38746
+ {
38747
+ id: "voice_help",
38748
+ commandPath: ["voice"],
38749
+ invokeArgs: ["voice", "--help"],
38750
+ label: "voice help",
38751
+ descriptionFallback: "Voice command help",
38752
+ mutatesState: "read",
38753
+ shellSlash: "/voice",
38754
+ includeInSuggestions: true
38755
+ },
38756
+ {
38757
+ id: "widget_help",
38758
+ commandPath: ["widget"],
38759
+ invokeArgs: ["widget", "--help"],
38760
+ label: "widget help",
38761
+ descriptionFallback: "Widget command help",
38762
+ mutatesState: "read",
38763
+ shellSlash: "/widget",
38764
+ includeInSuggestions: true
38765
+ },
38766
+ {
38767
+ id: "diag_debug",
38768
+ commandPath: ["diag"],
38769
+ invokeArgs: ["debug"],
38770
+ label: "debug",
38771
+ descriptionFallback: "Run diagnostics",
38772
+ mutatesState: "read",
38773
+ shellSlash: "/debug",
38774
+ includeInSuggestions: true
38775
+ }
38776
+ ];
38777
+ var SHELL_BUILTIN_COMMANDS = [
38778
+ { slash: "/help", description: "Show shell help" },
38779
+ { slash: "/?", description: "Show keymap help" },
38780
+ { slash: "/keys", description: "Show keymap help" },
38781
+ { slash: "/commands", description: "List available slash commands" },
38782
+ { slash: "/again", description: "Replay last successful command" },
38783
+ { slash: "/retry", description: "Replay last failed command" },
38784
+ { slash: "/editlast", description: "Prefill last command for editing" },
38785
+ { slash: "/snippet", description: "Manage named command snippets" },
38786
+ { slash: "/clear", description: "Clear the terminal output" },
38787
+ { slash: "/exit", description: "Exit shell" },
38788
+ { slash: "/quit", description: "Exit shell" }
38789
+ ];
38790
+ var HOME_QUICK_ACTION_ORDER = {
38791
+ unauthenticated: ["auth_login", "interactive_shell", "full_help", "auth_login_help"],
38792
+ authenticated_no_org: ["org_list", "interactive_shell", "org_use_help", "auth_whoami", "full_help"],
38793
+ authenticated_with_org: [
38794
+ "tenant_status",
38795
+ "interactive_shell",
38796
+ "templates_list",
38797
+ "agent_list_all",
38798
+ "setup_help",
38799
+ "bug_report_help",
38800
+ "bug_list"
38801
+ ]
38802
+ };
38803
+ var commandPathIndex = /* @__PURE__ */ new Map();
38804
+ function contextFromState(state) {
38805
+ if (!state.authenticated) return "unauthenticated";
38806
+ if (!state.orgId) return "authenticated_no_org";
38807
+ return "authenticated_with_org";
38808
+ }
38809
+ function commandKey(parts) {
38810
+ return parts.join(" ");
38811
+ }
38812
+ function normalizeCommandToken(value) {
38813
+ return String(value ?? "").trim().toLowerCase();
38814
+ }
38815
+ function readCommandDescription(command) {
38816
+ try {
38817
+ return (command.description() ?? "").trim();
38818
+ } catch {
38819
+ return "";
38820
+ }
38821
+ }
38822
+ function collectCommandPaths(program3) {
38823
+ const paths = /* @__PURE__ */ new Map();
38824
+ const walk = (parentPath, scope) => {
38825
+ for (const child of scope.commands) {
38826
+ const childName = normalizeCommandToken(child.name());
38827
+ if (!childName) continue;
38828
+ const nextPath = [...parentPath, childName];
38829
+ const canonicalPathKey = commandKey(nextPath);
38830
+ const description = readCommandDescription(child);
38831
+ paths.set(canonicalPathKey, { pathKey: canonicalPathKey, description });
38832
+ const aliases = child.aliases().map((alias) => normalizeCommandToken(alias)).filter(Boolean);
38833
+ for (const alias of aliases) {
38834
+ const aliasPath = [...parentPath, alias];
38835
+ paths.set(commandKey(aliasPath), { pathKey: canonicalPathKey, description });
38836
+ }
38837
+ walk(nextPath, child);
38838
+ }
38839
+ };
38840
+ walk([], program3);
38841
+ return paths;
38842
+ }
38843
+ function resolveDescription(definition) {
38844
+ const record2 = commandPathIndex.get(commandKey(definition.commandPath));
38845
+ if (record2 && record2.description) return record2.description;
38846
+ return definition.descriptionFallback;
38847
+ }
38848
+ function buildEntry(definition) {
38849
+ return {
38850
+ id: definition.id,
38851
+ label: definition.label,
38852
+ description: resolveDescription(definition),
38853
+ args: [...definition.invokeArgs ?? definition.commandPath],
38854
+ commandPath: [...definition.commandPath],
38855
+ requiresAuth: definition.requiresAuth === true,
38856
+ requiresOrg: definition.requiresOrg === true,
38857
+ mutatesState: definition.mutatesState,
38858
+ examples: [...definition.examples ?? []],
38859
+ homeContexts: [...definition.homeContexts ?? []],
38860
+ homeAliases: [...definition.homeAliases ?? []],
38861
+ shellSlash: definition.shellSlash,
38862
+ includeInSuggestions: definition.includeInSuggestions !== false
38863
+ };
38864
+ }
38865
+ function initializeCommandGraph(program3) {
38866
+ commandPathIndex = collectCommandPaths(program3);
38867
+ const missing = COMMAND_SURFACE_DEFINITIONS.filter((definition) => !commandPathIndex.has(commandKey(definition.commandPath))).map((definition) => definition.id);
38868
+ if (missing.length > 0) {
38869
+ throw new Error(`command_graph_definition_missing_paths:${missing.join(",")}`);
38870
+ }
38871
+ }
38872
+ function getCommandGraphEntries() {
38873
+ return COMMAND_SURFACE_DEFINITIONS.map(buildEntry);
38874
+ }
38875
+ function getHomeQuickActions(state) {
38876
+ const context = contextFromState(state);
38877
+ const rank = new Map(HOME_QUICK_ACTION_ORDER[context].map((id, index) => [id, index]));
38878
+ return getCommandGraphEntries().filter((entry) => entry.homeContexts.includes(context)).sort((left, right) => {
38879
+ const leftRank = rank.get(left.id) ?? Number.MAX_SAFE_INTEGER;
38880
+ const rightRank = rank.get(right.id) ?? Number.MAX_SAFE_INTEGER;
38881
+ if (leftRank !== rightRank) return leftRank - rightRank;
38882
+ return left.label.localeCompare(right.label);
38883
+ }).map((entry) => ({
38884
+ label: entry.label,
38885
+ description: entry.description,
38886
+ args: [...entry.args],
38887
+ aliases: [...entry.homeAliases]
38888
+ }));
38889
+ }
38890
+ function getHomePaletteCommands(state) {
38891
+ return getCommandGraphEntries().filter((entry) => {
38892
+ if (entry.requiresAuth && !state.authenticated) return false;
38893
+ if (entry.requiresOrg && (!state.authenticated || !state.orgId)) return false;
38894
+ if (entry.homeContexts.length > 0) return true;
38895
+ return Boolean(entry.shellSlash || entry.includeInSuggestions);
38896
+ }).map((entry) => ({
38897
+ label: entry.args.join(" "),
38898
+ description: entry.description,
38899
+ args: [...entry.args],
38900
+ requiresAuth: entry.requiresAuth || void 0,
38901
+ requiresOrg: entry.requiresOrg || void 0,
38902
+ mutatesState: entry.mutatesState,
38903
+ examples: [...entry.examples]
38904
+ }));
38905
+ }
38906
+ function getInteractiveSlashCommands() {
38907
+ const generated = getCommandGraphEntries().filter((entry) => typeof entry.shellSlash === "string").map((entry) => ({
38908
+ slash: entry.shellSlash,
38909
+ description: entry.description,
38910
+ args: [...entry.args]
38911
+ })).sort((left, right) => left.slash.localeCompare(right.slash));
38912
+ return [...SHELL_BUILTIN_COMMANDS, ...generated];
38913
+ }
38914
+ function getInteractiveCommandSuggestions() {
38915
+ return getCommandGraphEntries().filter((entry) => entry.includeInSuggestions).map((entry) => entry.args.join(" ")).filter((value, index, values) => values.indexOf(value) === index);
38916
+ }
38917
+ function normalizeArgs(args) {
38918
+ return args.map((token) => normalizeCommandToken(token)).filter(Boolean);
38919
+ }
38920
+ function startsWithTokens(input, expected) {
38921
+ if (expected.length === 0 || input.length < expected.length) return false;
38922
+ for (let index = 0; index < expected.length; index += 1) {
38923
+ if (input[index] !== expected[index]) return false;
38924
+ }
38925
+ return true;
38926
+ }
38927
+ function getCommandGraphEntryForArgs(args) {
38928
+ const normalizedArgs = normalizeArgs(args);
38929
+ if (normalizedArgs.length === 0) return void 0;
38930
+ const entries = getCommandGraphEntries().map((entry) => ({
38931
+ entry,
38932
+ normalizedEntryArgs: normalizeArgs(entry.args),
38933
+ normalizedCommandPath: normalizeArgs(entry.commandPath),
38934
+ entryArgsMatch: 0,
38935
+ commandPathMatch: 0
38936
+ })).map((candidate) => ({
38937
+ ...candidate,
38938
+ entryArgsMatch: startsWithTokens(normalizedArgs, candidate.normalizedEntryArgs) ? candidate.normalizedEntryArgs.length : 0,
38939
+ commandPathMatch: startsWithTokens(normalizedArgs, candidate.normalizedCommandPath) ? candidate.normalizedCommandPath.length : 0
38940
+ })).filter(({ entryArgsMatch, commandPathMatch }) => entryArgsMatch > 0 || commandPathMatch > 0).sort((left, right) => {
38941
+ if (left.entryArgsMatch !== right.entryArgsMatch) return right.entryArgsMatch - left.entryArgsMatch;
38942
+ if (left.commandPathMatch !== right.commandPathMatch) return right.commandPathMatch - left.commandPathMatch;
38943
+ return left.entry.id.localeCompare(right.entry.id);
38944
+ });
38945
+ return entries[0]?.entry;
38946
+ }
38947
+ function classifyCommandMutation(args) {
38948
+ return getCommandGraphEntryForArgs(args)?.mutatesState ?? "read";
38949
+ }
38950
+
38951
+ // src/commands/interactive.ts
38952
+ var SLASH_COMMANDS = getInteractiveSlashCommands();
38953
+ var COMMAND_SUGGESTIONS = getInteractiveCommandSuggestions();
38954
+ function nowIso2() {
38955
+ return (/* @__PURE__ */ new Date()).toISOString();
38956
+ }
38957
+ function interactiveSessionArtifactPath() {
38958
+ const override = String(process.env.FOH_CLI_INTERACTIVE_SESSION_OUT ?? "").trim();
38959
+ if (override.length > 0) return (0, import_path9.resolve)(override);
38960
+ return (0, import_path9.resolve)("test-results/interactive-shell-session.latest.json");
38961
+ }
38962
+ function createInteractiveSessionArtifact() {
38963
+ const seed = `${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`;
38964
+ return {
38965
+ schema_version: "foh_cli_interactive_session.v1",
38966
+ session_id: `shell_${seed}`,
38967
+ started_at: nowIso2(),
38968
+ status: "idle",
38969
+ command_count: 0,
38970
+ failure_count: 0,
38971
+ events: []
38972
+ };
38973
+ }
38974
+ function detectTerminalCapabilities() {
38975
+ const stdinTty = Boolean(process.stdin.isTTY);
38976
+ const stdoutTty = Boolean(process.stdout.isTTY);
38977
+ const supportsRawMode = stdinTty && typeof process.stdin.setRawMode === "function";
38978
+ const term = String(process.env.TERM ?? "").trim().toLowerCase();
38979
+ const supportsAnsiClear = stdoutTty && term !== "dumb";
38980
+ return {
38981
+ stdinTty,
38982
+ stdoutTty,
38983
+ supportsRawMode,
38984
+ supportsLinePrefill: supportsRawMode,
38985
+ supportsAnsiClear
38986
+ };
38987
+ }
38988
+ function recordInteractiveShellEvent(artifact, event) {
38989
+ artifact.events.push({
38990
+ at: nowIso2(),
38991
+ ...event
38992
+ });
38993
+ if (event.type === "command_started") {
38994
+ artifact.status = "running";
38995
+ artifact.command_count += 1;
38996
+ artifact.last_command = event.command;
38997
+ } else if (event.type === "command_succeeded") {
38998
+ artifact.status = "idle";
38999
+ artifact.last_command = event.command;
39000
+ } else if (event.type === "command_failed" || event.type === "shell_error") {
39001
+ artifact.status = "failed";
39002
+ artifact.failure_count += 1;
39003
+ artifact.last_error = event.reason;
39004
+ artifact.last_command = event.command;
39005
+ } else if (event.type === "command_blocked") {
39006
+ artifact.status = "idle";
39007
+ artifact.last_error = event.reason;
39008
+ artifact.last_command = event.command;
39009
+ } else if (event.type === "shell_closed") {
39010
+ artifact.status = artifact.status === "failed" ? "failed" : "closed";
39011
+ artifact.completed_at = nowIso2();
39012
+ }
39013
+ }
39014
+ function flushInteractiveSessionArtifact(artifact) {
39015
+ const outputPath = interactiveSessionArtifactPath();
39016
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39017
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify(artifact, null, 2), "utf8");
39018
+ }
39019
+ function interactiveMemoryPath() {
39020
+ const override = String(process.env.FOH_CLI_INTERACTIVE_MEMORY_OUT ?? "").trim();
39021
+ if (override.length > 0) return (0, import_path9.resolve)(override);
39022
+ return (0, import_path9.resolve)((0, import_os2.homedir)(), ".foh", "interactive-shell-memory.json");
39023
+ }
39024
+ function loadInteractiveShellMemory() {
39025
+ const outputPath = interactiveMemoryPath();
39026
+ if (!(0, import_fs11.existsSync)(outputPath)) {
39027
+ return {
39028
+ schema_version: "foh_cli_interactive_memory.v1",
39029
+ updated_at: nowIso2(),
39030
+ history: [],
39031
+ snippets: {}
39032
+ };
39033
+ }
39034
+ try {
39035
+ const raw = JSON.parse((0, import_fs11.readFileSync)(outputPath, "utf8"));
39036
+ return {
39037
+ schema_version: "foh_cli_interactive_memory.v1",
39038
+ updated_at: String(raw.updated_at ?? nowIso2()),
39039
+ history: Array.isArray(raw.history) ? raw.history.map((entry) => String(entry)).filter(Boolean) : [],
39040
+ snippets: raw && typeof raw.snippets === "object" && raw.snippets !== null ? Object.fromEntries(
39041
+ Object.entries(raw.snippets).map(([key, value]) => [String(key), String(value)])
39042
+ ) : {}
39043
+ };
39044
+ } catch {
39045
+ return {
39046
+ schema_version: "foh_cli_interactive_memory.v1",
39047
+ updated_at: nowIso2(),
39048
+ history: [],
39049
+ snippets: {}
39050
+ };
39051
+ }
39052
+ }
39053
+ function saveInteractiveShellMemory(memory) {
39054
+ const outputPath = interactiveMemoryPath();
39055
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39056
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify({ ...memory, updated_at: nowIso2() }, null, 2), "utf8");
39057
+ }
39058
+ function interactiveSessionReportPath() {
39059
+ const override = String(process.env.FOH_CLI_INTERACTIVE_SESSION_REPORT_OUT ?? "").trim();
39060
+ if (override.length > 0) return (0, import_path9.resolve)(override);
39061
+ return (0, import_path9.resolve)("test-results/interactive-cli-session-report.latest.json");
39062
+ }
39063
+ function median(values) {
39064
+ if (values.length === 0) return null;
39065
+ const sorted = [...values].sort((a, b) => a - b);
39066
+ const middle = Math.floor(sorted.length / 2);
39067
+ if (sorted.length % 2 === 1) return sorted[middle];
39068
+ return Math.round((sorted[middle - 1] + sorted[middle]) / 2);
39069
+ }
39070
+ function buildInteractiveSessionReport(artifact) {
39071
+ const commandStarts = [];
39072
+ const successDurations = [];
39073
+ const errorFamilies = {};
39074
+ let completedCommands = 0;
39075
+ let deadEndCount = 0;
39076
+ for (const event of artifact.events) {
39077
+ if (event.type === "command_started") {
39078
+ commandStarts.push(Date.parse(event.at));
39079
+ continue;
39080
+ }
39081
+ if (event.type === "command_succeeded") {
39082
+ completedCommands += 1;
39083
+ const startedAt = commandStarts.shift();
39084
+ if (startedAt !== void 0 && Number.isFinite(startedAt)) {
39085
+ const duration3 = Math.max(0, Date.parse(event.at) - startedAt);
39086
+ successDurations.push(duration3);
39087
+ }
39088
+ continue;
39089
+ }
39090
+ if (event.type === "command_failed" || event.type === "command_blocked" || event.type === "shell_error") {
39091
+ deadEndCount += 1;
39092
+ const key = String(event.reason ?? event.type).trim() || event.type;
39093
+ errorFamilies[key] = (errorFamilies[key] ?? 0) + 1;
39094
+ if (event.type === "command_failed") {
39095
+ commandStarts.shift();
39096
+ }
39097
+ }
39098
+ }
39099
+ const totalCommands = Math.max(artifact.command_count, 0);
39100
+ const completionRate = totalCommands > 0 ? Number((completedCommands / totalCommands).toFixed(4)) : 0;
39101
+ const topRemediationLoops = Object.entries(errorFamilies).sort((left, right) => right[1] - left[1]).slice(0, 5).map(([reason, count]) => ({ reason, count }));
39102
+ return {
39103
+ schema_version: "interactive_cli_session_report.v1",
39104
+ generated_at: nowIso2(),
39105
+ session_id: artifact.session_id,
39106
+ command_count: totalCommands,
39107
+ completion_rate: completionRate,
39108
+ dead_end_count: deadEndCount,
39109
+ command_error_families: errorFamilies,
39110
+ median_time_to_success_ms: median(successDurations),
39111
+ top_remediation_loops: topRemediationLoops,
39112
+ terminal_capabilities: artifact.terminal_capabilities
39113
+ };
39114
+ }
39115
+ function flushInteractiveSessionReport(artifact) {
39116
+ const outputPath = interactiveSessionReportPath();
39117
+ const report = buildInteractiveSessionReport(artifact);
39118
+ (0, import_fs11.mkdirSync)((0, import_path9.dirname)(outputPath), { recursive: true });
39119
+ (0, import_fs11.writeFileSync)(outputPath, JSON.stringify(report, null, 2), "utf8");
39120
+ }
38416
39121
  function tokenizeInput(value) {
38417
39122
  const tokens = [];
38418
39123
  const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
@@ -38423,193 +39128,391 @@ function tokenizeInput(value) {
38423
39128
  }
38424
39129
  return tokens;
38425
39130
  }
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();
39131
+ async function runSelf(args, apiUrlOverride) {
39132
+ const spawnArgs = [...args];
39133
+ if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
39134
+ spawnArgs.push("--api-url", apiUrlOverride);
38433
39135
  }
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);
39136
+ return await new Promise((resolve15, reject) => {
39137
+ const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39138
+ stdio: "inherit",
39139
+ env: {
39140
+ ...process.env,
39141
+ FOH_CLI_NO_HOME: "1",
39142
+ FOH_CLI_SUPPRESS_BANNER: "1"
39143
+ }
39144
+ });
39145
+ child.once("error", reject);
39146
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
38442
39147
  });
38443
39148
  }
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
- ];
39149
+ function resolveInteractiveState(apiUrlOverride) {
39150
+ try {
39151
+ const creds = loadCredentials(apiUrlOverride);
39152
+ return {
39153
+ authenticated: true,
39154
+ orgId: creds.orgId
39155
+ };
39156
+ } catch {
39157
+ return { authenticated: false };
38482
39158
  }
39159
+ }
39160
+ function nextRecommendedCommand(state) {
39161
+ const next = getHomeQuickActions(state)[0];
39162
+ if (!next) return "foh --help";
39163
+ return `foh ${next.args.join(" ")}`;
39164
+ }
39165
+ function keymapHelpText() {
38483
39166
  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
- ];
39167
+ "",
39168
+ "Shell Keymap",
39169
+ "-----------",
39170
+ " Up / Down Recall command history",
39171
+ " Tab Complete ranked slash/command suggestions",
39172
+ " Enter Run current input",
39173
+ " Esc Cancel palette-style slash entry",
39174
+ " Ctrl+C Exit shell safely",
39175
+ "",
39176
+ "Fallback: if your terminal does not forward these keys, run equivalent slash commands (/help, /commands, /exit).",
39177
+ ""
39178
+ ].join("\n");
38491
39179
  }
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
- });
39180
+ function shellHelpText(state) {
39181
+ const slashLines = SLASH_COMMANDS.map((entry) => ` ${entry.slash.padEnd(12)} ${entry.description}`);
39182
+ return [
39183
+ "",
39184
+ "FOH Interactive Shell",
39185
+ "---------------------",
39186
+ "Use slash commands or type any normal `foh` command without the `foh` prefix.",
39187
+ "Keys: Up/Down history, Tab completion, Ctrl+C or /exit to leave. Run /keys for full mapping.",
39188
+ "",
39189
+ "Slash commands:",
39190
+ ...slashLines,
39191
+ "",
39192
+ `Recommended next command: ${nextRecommendedCommand(state)}`,
39193
+ ""
39194
+ ].join("\n");
38498
39195
  }
38499
- function filterHomePaletteCommands(state, query) {
38500
- return filterPaletteCommands(getPaletteCommands(state), query);
39196
+ function printPrompt(rl) {
39197
+ rl.setPrompt("foh> ");
39198
+ rl.prompt();
38501
39199
  }
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
- `);
39200
+ function safePrintPrompt(rl) {
39201
+ if (rl.closed) return;
39202
+ printPrompt(rl);
39203
+ }
39204
+ function prefillCommand(rl, command) {
39205
+ rl.write(null, { ctrl: true, name: "u" });
39206
+ rl.write(command);
39207
+ }
39208
+ function resolveSlashCommand(input, state, terminal, context) {
39209
+ const normalized = input.trim().toLowerCase();
39210
+ if (normalized === "/exit" || normalized === "/quit") return { quit: true, args: null };
39211
+ if (normalized === "/?" || normalized === "/keys") return { quit: false, args: [], message: keymapHelpText() };
39212
+ if (normalized === "/help") return { quit: false, args: [], message: shellHelpText(state) };
39213
+ if (normalized === "/commands") {
39214
+ const rows = SLASH_COMMANDS.map((entry) => ` ${entry.slash.padEnd(12)} ${entry.description}`);
39215
+ return {
39216
+ quit: false,
39217
+ args: [],
39218
+ message: `
39219
+ Slash commands:
39220
+ ${rows.join("\n")}
39221
+ `
39222
+ };
38524
39223
  }
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
- });
39224
+ if (normalized === "/clear") {
39225
+ if (terminal.supportsAnsiClear) return { quit: false, args: [], message: "\x1B[2J\x1B[H" };
39226
+ return {
39227
+ quit: false,
39228
+ args: [],
39229
+ message: "\n\n\n\n\nTerminal does not support ANSI clear. Screen reset skipped.\n"
39230
+ };
39231
+ }
39232
+ if (normalized === "/again") {
39233
+ if (context.lastSuccessfulArgs && context.lastSuccessfulArgs.length > 0) {
39234
+ return { quit: false, args: [...context.lastSuccessfulArgs] };
38552
39235
  }
38553
- } else {
38554
- process.stdout.write('\nPress "/" to open command palette.\n');
39236
+ return { quit: false, args: [], message: "No successful command in current session yet.\n" };
38555
39237
  }
38556
- if (isBusy) {
38557
- process.stdout.write("\nRunning command...\n");
38558
- } else if (notice) {
38559
- process.stdout.write(`
38560
- ${notice}
38561
- `);
39238
+ if (normalized === "/retry") {
39239
+ if (context.lastFailedArgs && context.lastFailedArgs.length > 0) {
39240
+ return { quit: false, args: [...context.lastFailedArgs] };
39241
+ }
39242
+ return { quit: false, args: [], message: "No failed command in current session yet.\n" };
38562
39243
  }
39244
+ if (normalized === "/editlast") {
39245
+ const candidate = context.lastFailedArgs ?? context.lastSuccessfulArgs;
39246
+ if (!candidate || candidate.length === 0) {
39247
+ return { quit: false, args: [], message: "No previous command to prefill.\n" };
39248
+ }
39249
+ return {
39250
+ quit: false,
39251
+ args: [],
39252
+ prefill: candidate.join(" "),
39253
+ message: terminal.supportsLinePrefill ? "Prefilled last command. Edit and press Enter to run.\n" : `Terminal cannot prefill input. Replay manually: foh ${candidate.join(" ")}
39254
+ `
39255
+ };
39256
+ }
39257
+ if (normalized.startsWith("/run ")) {
39258
+ const raw = input.trim().slice("/run ".length).trim();
39259
+ const args = tokenizeInput(raw);
39260
+ return { quit: false, args: args.length > 0 ? args : null };
39261
+ }
39262
+ if (normalized === "/snippet list") {
39263
+ const rows = Object.entries(context.snippets).sort((left, right) => left[0].localeCompare(right[0])).map(([name, command]) => ` ${name.padEnd(16)} ${command}`);
39264
+ if (rows.length === 0) return { quit: false, args: [], message: "No snippets saved.\n" };
39265
+ return { quit: false, args: [], message: `
39266
+ Saved snippets:
39267
+ ${rows.join("\n")}
39268
+ ` };
39269
+ }
39270
+ if (normalized.startsWith("/snippet save ")) {
39271
+ const parts = tokenizeInput(input.trim());
39272
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39273
+ const command = parts.slice(3).join(" ").trim();
39274
+ if (!name || !command) {
39275
+ return { quit: false, args: [], message: "Usage: /snippet save <name> <command>\n" };
39276
+ }
39277
+ const normalizedCommand = command.toLowerCase().startsWith("foh ") ? command.slice(4).trim() : command;
39278
+ context.snippets[name] = normalizedCommand;
39279
+ return { quit: false, args: [], message: `Saved snippet "${name}": ${normalizedCommand}
39280
+ ` };
39281
+ }
39282
+ if (normalized.startsWith("/snippet run ")) {
39283
+ const parts = tokenizeInput(input.trim());
39284
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39285
+ const command = context.snippets[name];
39286
+ if (!command) return { quit: false, args: [], message: `Snippet "${name}" not found.
39287
+ ` };
39288
+ return { quit: false, args: tokenizeInput(command) };
39289
+ }
39290
+ if (normalized.startsWith("/snippet edit ")) {
39291
+ const parts = tokenizeInput(input.trim());
39292
+ const name = String(parts[2] ?? "").trim().toLowerCase();
39293
+ const command = context.snippets[name];
39294
+ if (!command) return { quit: false, args: [], message: `Snippet "${name}" not found.
39295
+ ` };
39296
+ return {
39297
+ quit: false,
39298
+ args: [],
39299
+ prefill: command,
39300
+ message: terminal.supportsLinePrefill ? `Prefilled snippet "${name}".
39301
+ ` : `Terminal cannot prefill input. Snippet "${name}": ${command}
39302
+ `
39303
+ };
39304
+ }
39305
+ const matched = SLASH_COMMANDS.find((entry) => entry.slash.toLowerCase() === normalized);
39306
+ if (matched?.args && matched.args.length > 0) {
39307
+ return { quit: false, args: [...matched.args] };
39308
+ }
39309
+ if (normalized.startsWith("/")) {
39310
+ return { quit: false, args: [], message: `Unknown slash command: ${input.trim()}
39311
+ Use /help.
39312
+ ` };
39313
+ }
39314
+ return { quit: false, args: null };
38563
39315
  }
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;
39316
+ function normalizeCommandInput(input) {
39317
+ const trimmed = input.trim();
39318
+ if (!trimmed) return [];
39319
+ const withoutPrefix = trimmed.toLowerCase().startsWith("foh ") ? trimmed.slice(4).trim() : trimmed;
39320
+ return tokenizeInput(withoutPrefix);
39321
+ }
39322
+ function hasYesFlag(args) {
39323
+ return args.some((token) => token === "--yes" || token === "-y");
39324
+ }
39325
+ async function confirmMutationIfNeeded(rl, args) {
39326
+ const mutationState = classifyCommandMutation(args);
39327
+ if (mutationState === "read" || hasYesFlag(args)) {
39328
+ return { approved: true };
38581
39329
  }
38582
- if (!state.orgId) {
38583
- process.stdout.write(`Status: Authenticated
38584
- API: ${state.apiUrl}
39330
+ const commandEntry = getCommandGraphEntryForArgs(args);
39331
+ const label = commandEntry ? `foh ${commandEntry.args.join(" ")}` : `foh ${args.join(" ")}`;
39332
+ const warning = mutationState === "high_risk" ? "high-risk" : "write";
39333
+ const approved = await new Promise((resolve15) => {
39334
+ rl.question(`Confirm ${warning} command ${label}? [y/N]: `, (answer) => {
39335
+ const normalized = answer.trim().toLowerCase();
39336
+ resolve15(normalized === "y" || normalized === "yes");
39337
+ });
39338
+ });
39339
+ if (approved) return { approved: true };
39340
+ return {
39341
+ approved: false,
39342
+ message: `Command blocked (interactive_confirmation_required): foh ${args.join(" ")}`
39343
+ };
39344
+ }
39345
+ function commandSuggestionsForState(state) {
39346
+ const contextual = getHomePaletteCommands(state).map((entry) => entry.args.join(" "));
39347
+ const merged = [...contextual, ...COMMAND_SUGGESTIONS];
39348
+ return merged.filter((entry, index, values) => values.indexOf(entry) === index);
39349
+ }
39350
+ function fuzzyScore(query, candidate) {
39351
+ const normalizedQuery = query.trim().toLowerCase();
39352
+ const normalizedCandidate = candidate.toLowerCase();
39353
+ if (!normalizedQuery) return 0;
39354
+ if (normalizedCandidate.startsWith(normalizedQuery)) return 0;
39355
+ const includesAt = normalizedCandidate.indexOf(normalizedQuery);
39356
+ if (includesAt >= 0) return 100 + includesAt;
39357
+ let queryIndex = 0;
39358
+ let gaps = 0;
39359
+ for (let i = 0; i < normalizedCandidate.length && queryIndex < normalizedQuery.length; i += 1) {
39360
+ if (normalizedCandidate[i] === normalizedQuery[queryIndex]) {
39361
+ queryIndex += 1;
39362
+ } else if (queryIndex > 0) {
39363
+ gaps += 1;
39364
+ }
39365
+ }
39366
+ if (queryIndex !== normalizedQuery.length) return Number.POSITIVE_INFINITY;
39367
+ return 200 + gaps;
39368
+ }
39369
+ function rankMatches(query, pool) {
39370
+ const scored = pool.map((candidate) => ({ candidate, score: fuzzyScore(query, candidate) })).filter((entry) => Number.isFinite(entry.score)).sort((left, right) => {
39371
+ if (left.score !== right.score) return left.score - right.score;
39372
+ return left.candidate.localeCompare(right.candidate);
39373
+ });
39374
+ return scored.map((entry) => entry.candidate);
39375
+ }
39376
+ function buildCompleter(apiUrlOverride) {
39377
+ const slashLabels = SLASH_COMMANDS.map((entry) => entry.slash);
39378
+ return (line) => {
39379
+ const raw = String(line ?? "");
39380
+ const lower = raw.toLowerCase();
39381
+ const state = resolveInteractiveState(apiUrlOverride);
39382
+ const commandLabels = commandSuggestionsForState(state);
39383
+ const pool = lower.trim().startsWith("/") ? slashLabels : commandLabels;
39384
+ const hits = rankMatches(lower, pool);
39385
+ return [hits.length > 0 ? hits : pool, raw];
39386
+ };
39387
+ }
39388
+ function registerInteractive(program3) {
39389
+ 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) => {
39390
+ const sessionArtifact = createInteractiveSessionArtifact();
39391
+ const terminalCapabilities = detectTerminalCapabilities();
39392
+ sessionArtifact.terminal_capabilities = {
39393
+ stdin_tty: terminalCapabilities.stdinTty,
39394
+ stdout_tty: terminalCapabilities.stdoutTty,
39395
+ supports_raw_mode: terminalCapabilities.supportsRawMode,
39396
+ supports_line_prefill: terminalCapabilities.supportsLinePrefill,
39397
+ supports_ansi_clear: terminalCapabilities.supportsAnsiClear
39398
+ };
39399
+ const memory = loadInteractiveShellMemory();
39400
+ recordInteractiveShellEvent(sessionArtifact, { type: "shell_started" });
39401
+ ensureInteractive(
39402
+ "interactive.shell",
39403
+ "Run this in a TTY terminal. For automation use normal non-interactive commands with --json."
39404
+ );
39405
+ const rl = (0, import_readline2.createInterface)({
39406
+ input: process.stdin,
39407
+ output: process.stdout,
39408
+ terminal: true,
39409
+ historySize: 500,
39410
+ removeHistoryDuplicates: true,
39411
+ history: memory.history.slice(0, 500),
39412
+ completer: buildCompleter(opts.apiUrl)
39413
+ });
39414
+ process.stdout.write(shellHelpText(resolveInteractiveState(opts.apiUrl)));
39415
+ printPrompt(rl);
39416
+ let busy = false;
39417
+ let lastSuccessfulArgs = null;
39418
+ let lastFailedArgs = null;
39419
+ rl.on("line", (line) => {
39420
+ void (async () => {
39421
+ if (busy) return;
39422
+ const raw = String(line ?? "");
39423
+ const slash = resolveSlashCommand(raw, resolveInteractiveState(opts.apiUrl), terminalCapabilities, {
39424
+ lastSuccessfulArgs,
39425
+ lastFailedArgs,
39426
+ snippets: memory.snippets
39427
+ });
39428
+ if (slash.quit) {
39429
+ rl.close();
39430
+ return;
39431
+ }
39432
+ if (slash.message) {
39433
+ process.stdout.write(`${slash.message}${slash.message.endsWith("\n") ? "" : "\n"}`);
39434
+ }
39435
+ if (slash.prefill) {
39436
+ if (terminalCapabilities.supportsLinePrefill) {
39437
+ prefillCommand(rl, slash.prefill);
39438
+ }
39439
+ return;
39440
+ }
39441
+ const commandArgs = slash.args ?? normalizeCommandInput(raw);
39442
+ if (commandArgs.length === 0) {
39443
+ safePrintPrompt(rl);
39444
+ return;
39445
+ }
39446
+ const confirmation = await confirmMutationIfNeeded(rl, commandArgs);
39447
+ if (!confirmation.approved) {
39448
+ if (confirmation.message) {
39449
+ process.stdout.write(`${confirmation.message}
38585
39450
  `);
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}
39451
+ recordInteractiveShellEvent(sessionArtifact, {
39452
+ type: "command_blocked",
39453
+ command: `foh ${commandArgs.join(" ")}`,
39454
+ reason: "interactive_confirmation_required"
39455
+ });
39456
+ }
39457
+ printPrompt(rl);
39458
+ return;
39459
+ }
39460
+ recordInteractiveShellEvent(sessionArtifact, {
39461
+ type: "command_started",
39462
+ command: `foh ${commandArgs.join(" ")}`
39463
+ });
39464
+ busy = true;
39465
+ try {
39466
+ const code = await runSelf(commandArgs, opts.apiUrl);
39467
+ if (code !== 0) {
39468
+ lastFailedArgs = [...commandArgs];
39469
+ process.stdout.write(`Command exited with code ${code}: foh ${commandArgs.join(" ")}
38599
39470
  `);
38600
- process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
38601
-
39471
+ recordInteractiveShellEvent(sessionArtifact, {
39472
+ type: "command_failed",
39473
+ command: `foh ${commandArgs.join(" ")}`,
39474
+ exit_code: code,
39475
+ reason: "non_zero_exit"
39476
+ });
39477
+ } else {
39478
+ lastSuccessfulArgs = [...commandArgs];
39479
+ recordInteractiveShellEvent(sessionArtifact, {
39480
+ type: "command_succeeded",
39481
+ command: `foh ${commandArgs.join(" ")}`,
39482
+ exit_code: code
39483
+ });
39484
+ }
39485
+ } finally {
39486
+ busy = false;
39487
+ safePrintPrompt(rl);
39488
+ }
39489
+ })().catch((error2) => {
39490
+ const message = error2 instanceof Error ? error2.message : String(error2);
39491
+ process.stdout.write(`Shell error: ${message}
38602
39492
  `);
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");
39493
+ recordInteractiveShellEvent(sessionArtifact, {
39494
+ type: "shell_error",
39495
+ reason: message
39496
+ });
39497
+ busy = false;
39498
+ safePrintPrompt(rl);
39499
+ });
39500
+ });
39501
+ rl.on("SIGINT", () => {
39502
+ rl.close();
39503
+ });
39504
+ await new Promise((resolve15) => {
39505
+ rl.on("close", () => {
39506
+ process.stdout.write("\n");
39507
+ memory.history = rl.history.slice(0, 500);
39508
+ recordInteractiveShellEvent(sessionArtifact, { type: "shell_closed" });
39509
+ flushInteractiveSessionArtifact(sessionArtifact);
39510
+ flushInteractiveSessionReport(sessionArtifact);
39511
+ saveInteractiveShellMemory(memory);
39512
+ resolve15();
39513
+ });
39514
+ });
39515
+ });
38613
39516
  }
38614
39517
 
38615
39518
  // src/commands/home-actions.ts
@@ -38625,65 +39528,6 @@ async function promptChoice(prompt) {
38625
39528
  return await promptLine(`
38626
39529
  ${prompt}`, { allowEmpty: true });
38627
39530
  }
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
39531
  async function runGuidedStart(apiUrlOverride, executeCommand) {
38688
39532
  process.stdout.write("\nGuided Start\n");
38689
39533
  process.stdout.write("------------\n");
@@ -38798,13 +39642,153 @@ async function chooseDefaultOrg(orgs) {
38798
39642
  `);
38799
39643
  }
38800
39644
  }
38801
- function getHomeQuickActions(state) {
38802
- return getQuickActions(state);
39645
+ function getHomeQuickActions2(state) {
39646
+ return getHomeQuickActions(state);
39647
+ }
39648
+
39649
+ // src/tui/command-palette.ts
39650
+ function tokenizeInput2(value) {
39651
+ const tokens = [];
39652
+ const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
39653
+ let match = matcher.exec(value);
39654
+ while (match) {
39655
+ tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
39656
+ match = matcher.exec(value);
39657
+ }
39658
+ return tokens;
39659
+ }
39660
+ function normalizeTypedCommandInput(raw) {
39661
+ const normalized = String(raw ?? "").trim();
39662
+ if (!normalized) return [];
39663
+ const withoutLeadingSlash = normalized.startsWith("/") ? normalized.slice(1) : normalized;
39664
+ const tokens = tokenizeInput2(withoutLeadingSlash);
39665
+ if (tokens.length > 0 && tokens[0].toLowerCase() === "foh") {
39666
+ tokens.shift();
39667
+ }
39668
+ return tokens;
39669
+ }
39670
+ function filterPaletteCommands(commands, query) {
39671
+ const normalized = String(query ?? "").trim().toLowerCase();
39672
+ if (!normalized) return commands;
39673
+ return commands.filter((entry) => {
39674
+ const commandText = `foh ${entry.args.join(" ")}`.toLowerCase();
39675
+ return entry.label.toLowerCase().includes(normalized) || commandText.includes(normalized);
39676
+ });
39677
+ }
39678
+ function shouldStartPaletteFromHome(char) {
39679
+ if (typeof char !== "string" || char.length !== 1) return false;
39680
+ if (/\s/.test(char)) return false;
39681
+ if (char === "/") return false;
39682
+ return /^[0-9A-Za-z._:-]$/.test(char);
39683
+ }
39684
+
39685
+ // src/commands/home-render.ts
39686
+ function getQuickActions(state) {
39687
+ return getHomeQuickActions(state);
39688
+ }
39689
+ function getPaletteCommands(state) {
39690
+ return getHomePaletteCommands(state);
39691
+ }
39692
+ function filterHomePaletteCommands(state, query) {
39693
+ const commands = getPaletteCommands(state);
39694
+ const commandIndex = new Map(commands.map((entry) => [entry.args.join(" "), entry]));
39695
+ const filtered = filterPaletteCommands(commands, query);
39696
+ return filtered.map((entry) => commandIndex.get(entry.args.join(" "))).filter((entry) => entry !== void 0);
39697
+ }
39698
+ function renderInteractiveHomeScreen({
39699
+ state,
39700
+ actions,
39701
+ selectedActionIndex,
39702
+ paletteOpen,
39703
+ paletteQuery,
39704
+ paletteSuggestions,
39705
+ selectedPaletteIndex,
39706
+ notice,
39707
+ isBusy
39708
+ }) {
39709
+ process.stdout.write("\x1B[2J\x1B[H");
39710
+ process.stdout.write("Front Of House CLI Home\n");
39711
+ process.stdout.write("=======================\n\n");
39712
+ if (!state.authenticated) {
39713
+ process.stdout.write("Status: Not authenticated\n");
39714
+ } else {
39715
+ process.stdout.write("Status: Authenticated\n");
39716
+ process.stdout.write(`API: ${state.apiUrl}
39717
+ `);
39718
+ process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
39719
+ `);
39720
+ }
39721
+ process.stdout.write("\nQuick actions (Up/Down, j/k, Enter, 1-9):\n");
39722
+ actions.forEach((action, index) => {
39723
+ const isSelected = !paletteOpen && index === selectedActionIndex;
39724
+ const cursor = isSelected ? ">" : " ";
39725
+ process.stdout.write(` ${cursor} [${index + 1}] ${action.label}
39726
+ `);
39727
+ process.stdout.write(` ${action.description}
39728
+ `);
39729
+ });
39730
+ process.stdout.write("\nShortcuts: [/] palette [h] redraw [q] exit [Ctrl+C] exit\n");
39731
+ process.stdout.write("Tip: start typing any command (for example auth, help, org list) to search instantly.\n");
39732
+ if (paletteOpen) {
39733
+ process.stdout.write("\nCommand palette\n");
39734
+ process.stdout.write("---------------\n");
39735
+ process.stdout.write(`/${paletteQuery}
39736
+ `);
39737
+ if (paletteSuggestions.length === 0) {
39738
+ process.stdout.write(" No matches. Press Enter to run typed command, or Esc to close.\n");
39739
+ } else {
39740
+ paletteSuggestions.slice(0, 8).forEach((entry, index) => {
39741
+ const isSelected = index === selectedPaletteIndex;
39742
+ const cursor = isSelected ? ">" : " ";
39743
+ process.stdout.write(` ${cursor} ${entry.label}
39744
+ `);
39745
+ process.stdout.write(` foh ${entry.args.join(" ")}
39746
+ `);
39747
+ });
39748
+ }
39749
+ } else {
39750
+ process.stdout.write('\nPress "/" to open command palette.\n');
39751
+ }
39752
+ if (isBusy) {
39753
+ process.stdout.write("\nRunning command...\n");
39754
+ } else if (notice) {
39755
+ process.stdout.write(`
39756
+ ${notice}
39757
+ `);
39758
+ }
39759
+ }
39760
+ function printHome(state) {
39761
+ process.stdout.write("\nFront Of House CLI Home\n");
39762
+ process.stdout.write("-----------------------\n");
39763
+ if (!state.authenticated) {
39764
+ process.stdout.write("Status: Not authenticated\n\n");
39765
+ } else {
39766
+ process.stdout.write(`Status: Authenticated
39767
+ API: ${state.apiUrl}
39768
+ `);
39769
+ process.stdout.write(`Default org: ${state.orgId ?? "(not set)"}
39770
+
39771
+ `);
39772
+ }
39773
+ const actions = getQuickActions(state);
39774
+ if (actions.length > 0) {
39775
+ process.stdout.write("Quick options:\n");
39776
+ actions.forEach((action, index) => {
39777
+ process.stdout.write(` [${index + 1}] ${action.label}
39778
+ `);
39779
+ });
39780
+ process.stdout.write(" [h] redraw home\n");
39781
+ process.stdout.write(" [q] exit\n");
39782
+ }
39783
+ const exampleAction = actions.find((entry) => entry.args.length > 0);
39784
+ const example = exampleAction ? `Type any foh command (example: ${exampleAction.args.join(" ")})` : "Type any foh command (example: --help)";
39785
+ process.stdout.write(` ${example}
39786
+ `);
38803
39787
  }
38804
39788
 
38805
39789
  // src/commands/home-interaction.ts
38806
- var import_child_process2 = require("child_process");
38807
- var import_readline2 = require("readline");
39790
+ var import_child_process3 = require("child_process");
39791
+ var import_readline3 = require("readline");
38808
39792
 
38809
39793
  // src/tui/keypress.ts
38810
39794
  function normalizeInputChar(inputChar) {
@@ -38852,13 +39836,13 @@ function resolveNumericSelection(inputChar, length) {
38852
39836
  }
38853
39837
 
38854
39838
  // src/commands/home-interaction.ts
38855
- async function runSelf(args, apiUrlOverride) {
39839
+ async function runSelf2(args, apiUrlOverride) {
38856
39840
  const spawnArgs = [...args];
38857
39841
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
38858
39842
  spawnArgs.push("--api-url", apiUrlOverride);
38859
39843
  }
38860
- return await new Promise((resolve14, reject) => {
38861
- const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39844
+ return await new Promise((resolve15, reject) => {
39845
+ const child = (0, import_child_process3.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
38862
39846
  stdio: "inherit",
38863
39847
  env: {
38864
39848
  ...process.env,
@@ -38867,7 +39851,7 @@ async function runSelf(args, apiUrlOverride) {
38867
39851
  }
38868
39852
  });
38869
39853
  child.once("error", reject);
38870
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
39854
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
38871
39855
  });
38872
39856
  }
38873
39857
  function shouldUseInteractiveHome(argv) {
@@ -38880,7 +39864,7 @@ function numericSelection(inputChar, actionCount) {
38880
39864
  return resolveNumericSelection(inputChar, actionCount);
38881
39865
  }
38882
39866
  function renderHome(state, selectedActionIndex, paletteOpen, paletteQuery, paletteSuggestions, selectedPaletteIndex, notice, isBusy) {
38883
- const actions = getHomeQuickActions(state);
39867
+ const actions = getHomeQuickActions2(state);
38884
39868
  renderInteractiveHomeScreen({
38885
39869
  state,
38886
39870
  actions,
@@ -38893,31 +39877,6 @@ function renderHome(state, selectedActionIndex, paletteOpen, paletteQuery, palet
38893
39877
  isBusy
38894
39878
  });
38895
39879
  }
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
39880
  async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38922
39881
  const stateAccessor = () => resolveHomeState(apiUrlOverride);
38923
39882
  let state = stateAccessor();
@@ -38929,7 +39888,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38929
39888
  let notice = null;
38930
39889
  let isBusy = false;
38931
39890
  let finished = false;
38932
- let actions = getHomeQuickActions(state);
39891
+ let actions = getHomeQuickActions2(state);
38933
39892
  const stdin = process.stdin;
38934
39893
  const clampPaletteIndex = () => {
38935
39894
  if (paletteSuggestions.length === 0) {
@@ -38942,7 +39901,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
38942
39901
  }
38943
39902
  };
38944
39903
  const refreshDerivedState = () => {
38945
- actions = getHomeQuickActions(state);
39904
+ actions = getHomeQuickActions2(state);
38946
39905
  if (actions.length === 0) {
38947
39906
  selectedActionIndex = 0;
38948
39907
  } else if (selectedActionIndex >= actions.length) {
@@ -39106,7 +40065,7 @@ async function runKeyDrivenHomeLoop(apiUrlOverride, runCommand) {
39106
40065
  if (stdin.isTTY) stdin.setRawMode(false);
39107
40066
  };
39108
40067
  const attachInput = () => {
39109
- (0, import_readline2.emitKeypressEvents)(stdin);
40068
+ (0, import_readline3.emitKeypressEvents)(stdin);
39110
40069
  stdin.on("keypress", onKeypress);
39111
40070
  if (stdin.isTTY) stdin.setRawMode(true);
39112
40071
  stdin.resume();
@@ -39129,18 +40088,14 @@ function registerHome(program3) {
39129
40088
  try {
39130
40089
  const explicitStart = process.argv.slice(2).some((token) => token.toLowerCase() === "start");
39131
40090
  if (explicitStart) {
39132
- await runGuidedStart(opts.apiUrl, runSelf);
40091
+ await runGuidedStart(opts.apiUrl, runSelf2);
39133
40092
  }
39134
40093
  const state = resolveHomeState(opts.apiUrl);
39135
40094
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
39136
40095
  printHome(state);
39137
40096
  return;
39138
40097
  }
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);
40098
+ await runKeyDrivenHomeLoop(opts.apiUrl, runSelf2);
39144
40099
  } catch (e) {
39145
40100
  if (e instanceof FohError) {
39146
40101
  formatError(e);
@@ -39157,9 +40112,9 @@ function maybeDefaultToHome(argv = process.argv) {
39157
40112
  }
39158
40113
 
39159
40114
  // src/lib/update.ts
39160
- var import_fs11 = require("fs");
39161
- var import_path9 = require("path");
39162
- var import_child_process3 = require("child_process");
40115
+ var import_fs12 = require("fs");
40116
+ var import_path10 = require("path");
40117
+ var import_child_process4 = require("child_process");
39163
40118
  var import_crypto5 = require("crypto");
39164
40119
  function parseSemver(version2) {
39165
40120
  const trimmed = String(version2 ?? "").trim();
@@ -39179,7 +40134,7 @@ function compareSemver(a, b) {
39179
40134
  }
39180
40135
  function readPackageJsonVersion(path2) {
39181
40136
  try {
39182
- const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
40137
+ const raw = (0, import_fs12.readFileSync)(path2, "utf-8");
39183
40138
  const parsed = JSON.parse(raw);
39184
40139
  const version2 = String(parsed.version ?? "").trim();
39185
40140
  return version2 || void 0;
@@ -39188,13 +40143,13 @@ function readPackageJsonVersion(path2) {
39188
40143
  }
39189
40144
  }
39190
40145
  function findRepoRoot(startCwd = process.cwd()) {
39191
- let current = (0, import_path9.resolve)(startCwd);
40146
+ let current = (0, import_path10.resolve)(startCwd);
39192
40147
  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)) {
40148
+ const rootPackageJsonPath = (0, import_path10.join)(current, "package.json");
40149
+ const cliPackageJsonPath = (0, import_path10.join)(current, "packages", "cli", "package.json");
40150
+ if ((0, import_fs12.existsSync)(rootPackageJsonPath) && (0, import_fs12.existsSync)(cliPackageJsonPath)) {
39196
40151
  try {
39197
- const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
40152
+ const raw = (0, import_fs12.readFileSync)(rootPackageJsonPath, "utf-8");
39198
40153
  const parsed = JSON.parse(raw);
39199
40154
  if (String(parsed.name ?? "").trim() === "front-of-house") {
39200
40155
  return current;
@@ -39202,7 +40157,7 @@ function findRepoRoot(startCwd = process.cwd()) {
39202
40157
  } catch {
39203
40158
  }
39204
40159
  }
39205
- const parent = (0, import_path9.dirname)(current);
40160
+ const parent = (0, import_path10.dirname)(current);
39206
40161
  if (parent === current) return void 0;
39207
40162
  current = parent;
39208
40163
  }
@@ -39216,7 +40171,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
39216
40171
  remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
39217
40172
  };
39218
40173
  }
39219
- const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
40174
+ const cliPackageJsonPath = (0, import_path10.join)(repoRoot, "packages", "cli", "package.json");
39220
40175
  const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
39221
40176
  if (!latestVersion) {
39222
40177
  return {
@@ -39243,20 +40198,20 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
39243
40198
  };
39244
40199
  }
39245
40200
  async function applyRepoUpdate(repoRoot) {
39246
- const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
40201
+ const scriptPath = (0, import_path10.join)(repoRoot, "scripts", "Install-FohCli.ps1");
39247
40202
  if (process.platform === "win32") {
39248
- return await new Promise((resolve14, reject) => {
39249
- const child = (0, import_child_process3.spawn)(
40203
+ return await new Promise((resolve15, reject) => {
40204
+ const child = (0, import_child_process4.spawn)(
39250
40205
  "powershell",
39251
40206
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
39252
40207
  { stdio: "inherit" }
39253
40208
  );
39254
40209
  child.once("error", reject);
39255
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
40210
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
39256
40211
  });
39257
40212
  }
39258
- return await new Promise((resolve14, reject) => {
39259
- const child = (0, import_child_process3.spawn)(
40213
+ return await new Promise((resolve15, reject) => {
40214
+ const child = (0, import_child_process4.spawn)(
39260
40215
  "corepack",
39261
40216
  ["pnpm", "cli:install:global"],
39262
40217
  {
@@ -39265,7 +40220,7 @@ async function applyRepoUpdate(repoRoot) {
39265
40220
  }
39266
40221
  );
39267
40222
  child.once("error", reject);
39268
- child.once("close", (code) => resolve14(typeof code === "number" ? code : 1));
40223
+ child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
39269
40224
  });
39270
40225
  }
39271
40226
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -39279,7 +40234,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
39279
40234
  }
39280
40235
  function hashFileSha256(filePath) {
39281
40236
  try {
39282
- const bytes = (0, import_fs11.readFileSync)(filePath);
40237
+ const bytes = (0, import_fs12.readFileSync)(filePath);
39283
40238
  return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
39284
40239
  } catch {
39285
40240
  return void 0;
@@ -39289,10 +40244,10 @@ function verifyCliArtifactIntegrity(params = {}) {
39289
40244
  const cwd = params.cwd ?? process.cwd();
39290
40245
  const argv = params.argv ?? process.argv;
39291
40246
  const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
39292
- const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
40247
+ const runtimePath = (0, import_path10.resolve)(String(argv[1] || ""));
39293
40248
  const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
39294
40249
  const warnings = [];
39295
- if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
40250
+ if (!runtimePath || !(0, import_fs12.existsSync)(runtimePath)) {
39296
40251
  warnings.push("runtime_path_unreadable");
39297
40252
  }
39298
40253
  if (!runtimeHash) {
@@ -39310,8 +40265,8 @@ function verifyCliArtifactIntegrity(params = {}) {
39310
40265
  let repoDistHash;
39311
40266
  let runtimeMatchesRepoDist;
39312
40267
  if (repoRoot) {
39313
- repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
39314
- if ((0, import_fs11.existsSync)(repoDistPath)) {
40268
+ repoDistPath = (0, import_path10.join)(repoRoot, "packages", "cli", "dist", "foh.js");
40269
+ if ((0, import_fs12.existsSync)(repoDistPath)) {
39315
40270
  repoDistHash = hashFileSha256(repoDistPath);
39316
40271
  if (runtimeHash && repoDistHash) {
39317
40272
  runtimeMatchesRepoDist = runtimeHash === repoDistHash;
@@ -39401,13 +40356,13 @@ function registerUpdate(program3) {
39401
40356
  }
39402
40357
 
39403
40358
  // src/commands/eval.ts
39404
- var import_fs20 = require("fs");
39405
- var import_path19 = require("path");
39406
- var import_child_process6 = require("child_process");
40359
+ var import_fs21 = require("fs");
40360
+ var import_path20 = require("path");
40361
+ var import_child_process7 = require("child_process");
39407
40362
 
39408
40363
  // src/lib/external-agent-artifact-safety.ts
39409
- var import_fs12 = require("fs");
39410
- var import_path10 = require("path");
40364
+ var import_fs13 = require("fs");
40365
+ var import_path11 = require("path");
39411
40366
  var DEFAULT_MAX_BYTES_PER_FILE = 5 * 1024 * 1024;
39412
40367
  var TEXT_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
39413
40368
  "codex-events.jsonl",
@@ -39431,7 +40386,7 @@ function escapeRegExp(value) {
39431
40386
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39432
40387
  }
39433
40388
  function pathVariants(path2) {
39434
- const resolved = (0, import_path10.resolve)(path2);
40389
+ const resolved = (0, import_path11.resolve)(path2);
39435
40390
  const slash = resolved.replace(/\\/g, "/");
39436
40391
  const backslash = resolved.replace(/\//g, "\\");
39437
40392
  return Array.from(new Set([
@@ -39474,9 +40429,9 @@ function redactExternalAgentSecretText(text) {
39474
40429
  return text.replace(SECRET_QUERY_PARAM_RE, "$1[redacted_secret]").replace(SECRET_RE2, "[redacted_secret]");
39475
40430
  }
39476
40431
  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);
40432
+ if (!(0, import_fs13.existsSync)(runDir)) return [];
40433
+ return (0, import_fs13.readdirSync)(runDir).map((name) => (0, import_path11.join)(runDir, name)).filter((path2) => {
40434
+ const stat = (0, import_fs13.statSync)(path2);
39480
40435
  const name = path2.split(/[\\/]/).pop() || "";
39481
40436
  if (name.endsWith(".redacted")) return false;
39482
40437
  return stat.isFile() && (TEXT_ARTIFACT_NAMES.has(name) || name.startsWith("command-output-cmd_"));
@@ -39502,15 +40457,15 @@ function scanText(input) {
39502
40457
  ].filter(Boolean);
39503
40458
  }
39504
40459
  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;
40460
+ const runDir = (0, import_path11.resolve)(options.runDir);
40461
+ const privateRepoRoot = options.privateRepoRoot ? (0, import_path11.resolve)(options.privateRepoRoot) : void 0;
39507
40462
  const homeDir = options.homeDir || process.env.USERPROFILE || process.env.HOME;
39508
40463
  const maxBytes = options.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
39509
40464
  const files = artifactFiles(runDir);
39510
40465
  const findings = [];
39511
40466
  const redactedFiles = [];
39512
40467
  for (const file2 of files) {
39513
- const stat = (0, import_fs12.statSync)(file2);
40468
+ const stat = (0, import_fs13.statSync)(file2);
39514
40469
  if (stat.size > maxBytes) {
39515
40470
  findings.push({
39516
40471
  kind: "artifact_too_large",
@@ -39520,12 +40475,12 @@ function scanExternalAgentArtifacts(options) {
39520
40475
  });
39521
40476
  continue;
39522
40477
  }
39523
- const text = (0, import_fs12.readFileSync)(file2, "utf8");
40478
+ const text = (0, import_fs13.readFileSync)(file2, "utf8");
39524
40479
  findings.push(...scanText({ text, file: file2, privateRepoRoot, homeDir }));
39525
40480
  if (options.writeRedacted) {
39526
40481
  const redacted = redactExternalAgentArtifactText(text, { privateRepoRoot, homeDir });
39527
40482
  const out = `${file2}.redacted`;
39528
- (0, import_fs12.writeFileSync)(out, redacted, "utf8");
40483
+ (0, import_fs13.writeFileSync)(out, redacted, "utf8");
39529
40484
  redactedFiles.push(out);
39530
40485
  }
39531
40486
  }
@@ -39546,8 +40501,8 @@ function scanExternalAgentArtifacts(options) {
39546
40501
  }
39547
40502
 
39548
40503
  // src/lib/external-agent-capture.ts
39549
- var import_fs13 = require("fs");
39550
- var import_path11 = require("path");
40504
+ var import_fs14 = require("fs");
40505
+ var import_path12 = require("path");
39551
40506
  var EXTERNAL_AGENT_RUN_DIR_ENV = "FOH_EXTERNAL_AGENT_RUN_DIR";
39552
40507
  var EXTERNAL_AGENT_PROMPT_VERSION_ENV = "FOH_EXTERNAL_AGENT_PROMPT_VERSION";
39553
40508
  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 +40521,7 @@ function safeJsonLine(value) {
39566
40521
  function getExternalAgentRunDir() {
39567
40522
  const raw = process.env[EXTERNAL_AGENT_RUN_DIR_ENV];
39568
40523
  if (!raw || !raw.trim()) return null;
39569
- return (0, import_path11.resolve)(raw);
40524
+ return (0, import_path12.resolve)(raw);
39570
40525
  }
39571
40526
  function commandIdFor(input) {
39572
40527
  const seed = `${input.startedAt}:${input.argv.join("\0")}:${process.pid}`;
@@ -39617,7 +40572,7 @@ function recordExternalAgentCliInvocation(input) {
39617
40572
  const runDir = getExternalAgentRunDir();
39618
40573
  if (!runDir) return null;
39619
40574
  try {
39620
- (0, import_fs13.mkdirSync)(runDir, { recursive: true });
40575
+ (0, import_fs14.mkdirSync)(runDir, { recursive: true });
39621
40576
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
39622
40577
  const args = input.argv.slice(2).map((arg) => redactText(String(arg)));
39623
40578
  const commandId = commandIdFor({ startedAt, argv: args });
@@ -39636,7 +40591,7 @@ function recordExternalAgentCliInvocation(input) {
39636
40591
  prompt_version: promptVersion,
39637
40592
  started_at: startedAt
39638
40593
  };
39639
- (0, import_fs13.appendFileSync)((0, import_path11.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
40594
+ (0, import_fs14.appendFileSync)((0, import_path12.join)(runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
39640
40595
  return {
39641
40596
  commandId,
39642
40597
  runDir,
@@ -39697,7 +40652,7 @@ function completeExternalAgentCliInvocation(capture, input) {
39697
40652
  let artifact = null;
39698
40653
  if (output.trim()) {
39699
40654
  artifact = outputArtifactName(capture.commandId);
39700
- (0, import_fs13.writeFileSync)((0, import_path11.join)(capture.runDir, artifact), output, "utf8");
40655
+ (0, import_fs14.writeFileSync)((0, import_path12.join)(capture.runDir, artifact), output, "utf8");
39701
40656
  }
39702
40657
  const record2 = {
39703
40658
  schema_version: "external_agent_cli_command.v2",
@@ -39720,12 +40675,12 @@ function completeExternalAgentCliInvocation(capture, input) {
39720
40675
  output_artifact: artifact,
39721
40676
  output_bytes: Buffer.byteLength(output, "utf8")
39722
40677
  };
39723
- (0, import_fs13.appendFileSync)((0, import_path11.join)(capture.runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
40678
+ (0, import_fs14.appendFileSync)((0, import_path12.join)(capture.runDir, "commands.ndjson"), safeJsonLine(record2), "utf8");
39724
40679
  }
39725
40680
  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));
40681
+ const commandLogPath = (0, import_path12.join)(runDir, "commands.ndjson");
40682
+ if (!(0, import_fs14.existsSync)(commandLogPath)) return [];
40683
+ const records = (0, import_fs14.readFileSync)(commandLogPath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
39729
40684
  const byId = /* @__PURE__ */ new Map();
39730
40685
  const ordered = [];
39731
40686
  for (const record2 of records) {
@@ -39737,13 +40692,13 @@ function readCommandRecords(runDir) {
39737
40692
  }
39738
40693
 
39739
40694
  // 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");
40695
+ var import_fs20 = require("fs");
40696
+ var import_os3 = require("os");
40697
+ var import_path19 = require("path");
40698
+ var import_child_process6 = require("child_process");
39744
40699
 
39745
40700
  // src/lib/external-agent-executor-env.ts
39746
- var import_path12 = require("path");
40701
+ var import_path13 = require("path");
39747
40702
  var CODEX_EXECUTOR_DENIED_ENV_PREFIXES = [
39748
40703
  "SUPABASE_",
39749
40704
  "DATABASE_",
@@ -39807,7 +40762,7 @@ function buildCodexExecutorEnv(input) {
39807
40762
  env[childKey] = value;
39808
40763
  }
39809
40764
  }
39810
- env.npm_config_cache = (0, import_path12.join)((0, import_path12.dirname)(input.runDir), ".npm-cache");
40765
+ env.npm_config_cache = (0, import_path13.join)((0, import_path13.dirname)(input.runDir), ".npm-cache");
39811
40766
  env.npm_config_prefer_online = "true";
39812
40767
  env.npm_config_update_notifier = "false";
39813
40768
  env.npm_config_yes = "true";
@@ -39819,12 +40774,12 @@ function buildCodexExecutorEnv(input) {
39819
40774
  }
39820
40775
 
39821
40776
  // src/lib/external-agent-executor-artifacts.ts
39822
- var import_fs15 = require("fs");
39823
- var import_path14 = require("path");
40777
+ var import_fs16 = require("fs");
40778
+ var import_path15 = require("path");
39824
40779
 
39825
40780
  // src/lib/external-agent-metadata.ts
39826
- var import_fs14 = require("fs");
39827
- var import_path13 = require("path");
40781
+ var import_fs15 = require("fs");
40782
+ var import_path14 = require("path");
39828
40783
  var EXTERNAL_AGENT_METADATA_FILENAMES = [
39829
40784
  "external-agent-metadata.json",
39830
40785
  "agent-metadata.json"
@@ -39846,10 +40801,10 @@ function collectDocsFrom(value, docs) {
39846
40801
  }
39847
40802
  function readExternalAgentMetadata(runDir) {
39848
40803
  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;
40804
+ const path2 = (0, import_path14.join)(runDir, filename);
40805
+ if (!(0, import_fs15.existsSync)(path2)) continue;
39851
40806
  try {
39852
- const parsed = JSON.parse((0, import_fs14.readFileSync)(path2, "utf8"));
40807
+ const parsed = JSON.parse((0, import_fs15.readFileSync)(path2, "utf8"));
39853
40808
  const docs = /* @__PURE__ */ new Set();
39854
40809
  collectDocsFrom(parsed.docs_pages_used, docs);
39855
40810
  collectDocsFrom(parsed.docs_pages_observed, docs);
@@ -39878,44 +40833,44 @@ function readExternalAgentMetadata(runDir) {
39878
40833
 
39879
40834
  // src/lib/external-agent-executor-artifacts.ts
39880
40835
  function redactArtifactFile(path2, input = {}) {
39881
- if (!(0, import_fs15.existsSync)(path2)) return;
39882
- const original = (0, import_fs15.readFileSync)(path2, "utf8");
40836
+ if (!(0, import_fs16.existsSync)(path2)) return;
40837
+ const original = (0, import_fs16.readFileSync)(path2, "utf8");
39883
40838
  const redacted = redactExternalAgentArtifactText(original, input);
39884
- if (redacted !== original) (0, import_fs15.writeFileSync)(path2, redacted, "utf8");
40839
+ if (redacted !== original) (0, import_fs16.writeFileSync)(path2, redacted, "utf8");
39885
40840
  }
39886
40841
  function redactExternalAgentOutputArtifacts(run, input = {}) {
39887
40842
  redactArtifactFile(run.outputs.jsonl, input);
39888
40843
  redactArtifactFile(run.outputs.last_message, input);
39889
40844
  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)) {
40845
+ redactArtifactFile((0, import_path15.join)(run.run_dir, "commands.ndjson"), input);
40846
+ if (!(0, import_fs16.existsSync)(run.run_dir)) return;
40847
+ for (const name of (0, import_fs16.readdirSync)(run.run_dir)) {
39893
40848
  if (name.startsWith("command-output-cmd_") && !name.endsWith(".redacted")) {
39894
- redactArtifactFile((0, import_path14.join)(run.run_dir, name), input);
40849
+ redactArtifactFile((0, import_path15.join)(run.run_dir, name), input);
39895
40850
  }
39896
40851
  }
39897
40852
  }
39898
40853
  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");
40854
+ const commandLog = (0, import_path15.join)(input.captureDir, "commands.ndjson");
40855
+ if ((0, import_fs16.existsSync)(commandLog)) {
40856
+ (0, import_fs16.writeFileSync)((0, import_path15.join)(input.runDir, "commands.ndjson"), (0, import_fs16.readFileSync)(commandLog, "utf8"), "utf8");
39902
40857
  }
39903
- for (const name of (0, import_fs15.readdirSync)(input.captureDir)) {
40858
+ for (const name of (0, import_fs16.readdirSync)(input.captureDir)) {
39904
40859
  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));
40860
+ (0, import_fs16.copyFileSync)((0, import_path15.join)(input.captureDir, name), (0, import_path15.join)(input.runDir, name));
39906
40861
  } 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));
40862
+ (0, import_fs16.copyFileSync)((0, import_path15.join)(input.captureDir, name), (0, import_path15.join)(input.runDir, name));
39908
40863
  }
39909
40864
  }
39910
40865
  }
39911
40866
 
39912
40867
  // src/lib/external-agent-executor-classification.ts
39913
- var import_fs17 = require("fs");
39914
- var import_path16 = require("path");
40868
+ var import_fs18 = require("fs");
40869
+ var import_path17 = require("path");
39915
40870
 
39916
40871
  // src/lib/external-agent-run-summary.ts
39917
- var import_fs16 = require("fs");
39918
- var import_path15 = require("path");
40872
+ var import_fs17 = require("fs");
40873
+ var import_path16 = require("path");
39919
40874
  var REQUIRED_RUN_FIELDS = [
39920
40875
  "schema_version",
39921
40876
  "run_id",
@@ -39939,8 +40894,8 @@ function quoteShellArg(value) {
39939
40894
  return `"${text.replace(/(["$`])/g, "\\$1")}"`;
39940
40895
  }
39941
40896
  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");
40897
+ const summaryPath = (0, import_path16.join)(root, "latest-summary.json");
40898
+ const reportPath = (0, import_path16.join)(root, "summary.report.json");
39944
40899
  return [
39945
40900
  "foh",
39946
40901
  "eval",
@@ -39956,11 +40911,11 @@ function externalAgentSummaryCommand(root) {
39956
40911
  ].join(" ");
39957
40912
  }
39958
40913
  function readJson(filePath) {
39959
- return JSON.parse((0, import_fs16.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
40914
+ return JSON.parse((0, import_fs17.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
39960
40915
  }
39961
40916
  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) => {
40917
+ if (!(0, import_fs17.existsSync)(filePath)) return [];
40918
+ return (0, import_fs17.readFileSync)(filePath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
39964
40919
  try {
39965
40920
  const parsed = JSON.parse(line);
39966
40921
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
@@ -39986,7 +40941,7 @@ function collectDocUrls(text) {
39986
40941
  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
40942
  }
39988
40943
  function findRunCandidates(root) {
39989
- if (!(0, import_fs16.existsSync)(root)) return [];
40944
+ if (!(0, import_fs17.existsSync)(root)) return [];
39990
40945
  const candidates = [];
39991
40946
  const seenRunDirs = /* @__PURE__ */ new Set();
39992
40947
  const captureDirs = [];
@@ -39994,13 +40949,13 @@ function findRunCandidates(root) {
39994
40949
  while (stack.length > 0) {
39995
40950
  const current = stack.pop();
39996
40951
  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);
40952
+ for (const entry of (0, import_fs17.readdirSync)(current, { withFileTypes: true })) {
40953
+ const absolute = (0, import_path16.join)(current, entry.name);
39999
40954
  if (entry.isDirectory()) {
40000
40955
  stack.push(absolute);
40001
40956
  } else if (entry.isFile() && entry.name === "run.json") {
40002
40957
  candidates.push({ path: absolute, synthetic: false });
40003
- seenRunDirs.add((0, import_path15.dirname)(absolute));
40958
+ seenRunDirs.add((0, import_path16.dirname)(absolute));
40004
40959
  } else if (entry.isFile() && entry.name === "commands.ndjson") {
40005
40960
  captureDirs.push(current);
40006
40961
  }
@@ -40008,7 +40963,7 @@ function findRunCandidates(root) {
40008
40963
  }
40009
40964
  for (const captureDir of captureDirs) {
40010
40965
  if (seenRunDirs.has(captureDir)) continue;
40011
- candidates.push({ path: (0, import_path15.join)(captureDir, "run.json"), synthetic: true });
40966
+ candidates.push({ path: (0, import_path16.join)(captureDir, "run.json"), synthetic: true });
40012
40967
  }
40013
40968
  return candidates.sort((a, b) => a.path.localeCompare(b.path));
40014
40969
  }
@@ -40086,9 +41041,9 @@ function syntheticStatusFromCommands(commands) {
40086
41041
  return { status: "pass", reasonCode: null };
40087
41042
  }
40088
41043
  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")) : {});
41044
+ const runDir = (0, import_path16.dirname)(runPath);
41045
+ const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
41046
+ 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
41047
  const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
40093
41048
  const commandClassification = syntheticStatusFromCommands(commands);
40094
41049
  const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
@@ -40097,7 +41052,7 @@ function synthesizeRunFromCapture(runPath) {
40097
41052
  const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
40098
41053
  const endedAt = latestCommandTime(commands) || startedAt;
40099
41054
  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";
41055
+ const runId = (0, import_path16.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
40101
41056
  return {
40102
41057
  schema_version: "external_agent_run.v1",
40103
41058
  run_id: runId,
@@ -40121,14 +41076,14 @@ function synthesizeRunFromCapture(runPath) {
40121
41076
  docs_pages_used: docs,
40122
41077
  artifacts: {
40123
41078
  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,
41079
+ agent_metadata: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
40125
41080
  capture_only: true
40126
41081
  },
40127
41082
  summary: "Synthetic run record created from external-agent capture artifacts because run.json was missing."
40128
41083
  };
40129
41084
  }
40130
41085
  function cohortIdForRunPath(root, runPath) {
40131
- const normalized = (0, import_path15.relative)(root, (0, import_path15.dirname)(runPath)).replaceAll("\\", "/");
41086
+ const normalized = (0, import_path16.relative)(root, (0, import_path16.dirname)(runPath)).replaceAll("\\", "/");
40132
41087
  const parts = normalized.split("/").filter(Boolean);
40133
41088
  if (parts.length === 0) return ".";
40134
41089
  if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) && parts[1]) return `${parts[0]}/${parts[1]}`;
@@ -40143,7 +41098,7 @@ function readRunRecords(root, cwd) {
40143
41098
  const parsed = candidate.synthetic ? synthesizeRunFromCapture(file2) : readJson(file2);
40144
41099
  const findings = validateExternalAgentRun(parsed);
40145
41100
  if (findings.length > 0) {
40146
- invalid_runs.push({ path: (0, import_path15.relative)(cwd, file2).replaceAll("\\", "/"), findings });
41101
+ invalid_runs.push({ path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"), findings });
40147
41102
  continue;
40148
41103
  }
40149
41104
  const run = parsed;
@@ -40155,7 +41110,7 @@ function readRunRecords(root, cwd) {
40155
41110
  });
40156
41111
  } catch (error2) {
40157
41112
  invalid_runs.push({
40158
- path: (0, import_path15.relative)(cwd, file2).replaceAll("\\", "/"),
41113
+ path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"),
40159
41114
  findings: [{ id: "json_parse_failed", detail: error2 instanceof Error ? error2.message : String(error2) }]
40160
41115
  });
40161
41116
  }
@@ -40200,10 +41155,10 @@ function collapseCommandRecords(records) {
40200
41155
  function readCommandOutputJson(runDir, command) {
40201
41156
  const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
40202
41157
  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;
41158
+ const artifactPath = (0, import_path16.join)(runDir, artifact);
41159
+ if (!(0, import_fs17.existsSync)(artifactPath)) return null;
40205
41160
  try {
40206
- const text = (0, import_fs16.readFileSync)(artifactPath, "utf8");
41161
+ const text = (0, import_fs17.readFileSync)(artifactPath, "utf8");
40207
41162
  const firstObject = text.indexOf("{");
40208
41163
  const lastObject = text.lastIndexOf("}");
40209
41164
  if (firstObject < 0 || lastObject <= firstObject) return null;
@@ -40257,8 +41212,8 @@ function commandTimingBreakdown(command, output) {
40257
41212
  return null;
40258
41213
  }
40259
41214
  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")));
41215
+ const runDir = (0, import_path16.dirname)(runPath);
41216
+ const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
40262
41217
  const reasonCounts = /* @__PURE__ */ new Map();
40263
41218
  const slowSteps = [];
40264
41219
  const timingBreakdowns = [];
@@ -40270,7 +41225,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40270
41225
  const breakdown = commandTimingBreakdown(command, output);
40271
41226
  if (breakdown) timingBreakdowns.push({
40272
41227
  run_id: run.run_id,
40273
- run_path: (0, import_path15.relative)(cwd, runPath).replaceAll("\\", "/"),
41228
+ run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
40274
41229
  ...breakdown
40275
41230
  });
40276
41231
  if (command.phase === "completed" || command.completed_at) completed += 1;
@@ -40279,7 +41234,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40279
41234
  totalDuration += command.duration_ms;
40280
41235
  slowSteps.push({
40281
41236
  run_id: run.run_id,
40282
- run_path: (0, import_path15.relative)(cwd, runPath).replaceAll("\\", "/"),
41237
+ run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
40283
41238
  command: command.command || "",
40284
41239
  duration_ms: command.duration_ms,
40285
41240
  status: command.status || null,
@@ -40292,7 +41247,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40292
41247
  if (reasonCode) increment(reasonCounts, reasonCode);
40293
41248
  }
40294
41249
  }
40295
- const codexEvents = readNdjson((0, import_path15.join)(runDir, "codex-exec.jsonl"));
41250
+ const codexEvents = readNdjson((0, import_path16.join)(runDir, "codex-exec.jsonl"));
40296
41251
  const codexDocs = /* @__PURE__ */ new Set();
40297
41252
  let codexCommandExecutions = 0;
40298
41253
  let codexFailedExitCodes = 0;
@@ -40309,7 +41264,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40309
41264
  ...Array.from(codexDocs)
40310
41265
  ]);
40311
41266
  return {
40312
- command_log_present: (0, import_fs16.existsSync)((0, import_path15.join)(runDir, "commands.ndjson")),
41267
+ command_log_present: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "commands.ndjson")),
40313
41268
  command_count: commands.length,
40314
41269
  completed_command_count: completed,
40315
41270
  missing_completion_count: Math.max(0, commands.length - completed),
@@ -40324,8 +41279,8 @@ function analyzeRunArtifacts(runPath, run, cwd) {
40324
41279
  };
40325
41280
  }
40326
41281
  function summarizeExternalAgentRuns(options) {
40327
- const cwd = (0, import_path15.resolve)(options.cwd || process.cwd());
40328
- const root = (0, import_path15.resolve)(cwd, options.root);
41282
+ const cwd = (0, import_path16.resolve)(options.cwd || process.cwd());
41283
+ const root = (0, import_path16.resolve)(cwd, options.root);
40329
41284
  const loaded = readRunRecords(root, cwd);
40330
41285
  const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
40331
41286
  const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
@@ -40380,7 +41335,7 @@ function summarizeExternalAgentRuns(options) {
40380
41335
  return {
40381
41336
  schema_version: "external_agent_run_summary.v1",
40382
41337
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
40383
- root: (0, import_path15.relative)(cwd, root).replaceAll("\\", "/") || ".",
41338
+ root: (0, import_path16.relative)(cwd, root).replaceAll("\\", "/") || ".",
40384
41339
  cohort_id: selectedCohortId,
40385
41340
  current_baseline_only: Boolean(selectedCohortId),
40386
41341
  run_count: records.length,
@@ -40415,7 +41370,7 @@ function summarizeExternalAgentRuns(options) {
40415
41370
  },
40416
41371
  next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
40417
41372
  invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
40418
- run_paths: records.map((record2) => (0, import_path15.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
41373
+ run_paths: records.map((record2) => (0, import_path16.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
40419
41374
  };
40420
41375
  }
40421
41376
  function runExternalAgentRunSummary(options) {
@@ -40435,13 +41390,13 @@ function runExternalAgentRunSummary(options) {
40435
41390
  report: summary
40436
41391
  };
40437
41392
  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)}
41393
+ (0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
41394
+ (0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
40440
41395
  `, "utf8");
40441
41396
  }
40442
41397
  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)}
41398
+ (0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
41399
+ (0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
40445
41400
  `, "utf8");
40446
41401
  }
40447
41402
  return { summary, report };
@@ -40449,20 +41404,20 @@ function runExternalAgentRunSummary(options) {
40449
41404
 
40450
41405
  // src/lib/external-agent-executor-classification.ts
40451
41406
  function proofArtifactPasses(runDir) {
40452
- const proofPath = (0, import_path16.join)(runDir, "proof.json");
40453
- if (!(0, import_fs17.existsSync)(proofPath)) return false;
41407
+ const proofPath = (0, import_path17.join)(runDir, "proof.json");
41408
+ if (!(0, import_fs18.existsSync)(proofPath)) return false;
40454
41409
  try {
40455
- const parsed = JSON.parse((0, import_fs17.readFileSync)(proofPath, "utf8"));
41410
+ const parsed = JSON.parse((0, import_fs18.readFileSync)(proofPath, "utf8"));
40456
41411
  return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
40457
41412
  } catch {
40458
41413
  return false;
40459
41414
  }
40460
41415
  }
40461
41416
  function readIfExists(path2) {
40462
- return (0, import_fs17.existsSync)(path2) ? (0, import_fs17.readFileSync)(path2, "utf8") : "";
41417
+ return (0, import_fs18.existsSync)(path2) ? (0, import_fs18.readFileSync)(path2, "utf8") : "";
40463
41418
  }
40464
41419
  function relativeArtifactName(path2) {
40465
- return (0, import_path16.basename)(path2);
41420
+ return (0, import_path17.basename)(path2);
40466
41421
  }
40467
41422
  function classifyExternalAgentRun(input) {
40468
41423
  if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
@@ -40617,13 +41572,13 @@ function buildExecutedExternalAgentRunArtifact(input) {
40617
41572
  },
40618
41573
  artifacts: {
40619
41574
  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,
41575
+ command_log: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
41576
+ proof_bundle: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
41577
+ replay_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
41578
+ knowledge_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
40624
41579
  improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
40625
41580
  agent_metadata: agentMetadata.path,
40626
- notes: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
41581
+ notes: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
40627
41582
  runner_last_message: relativeArtifactName(input.run.outputs.last_message),
40628
41583
  runner_stderr: relativeArtifactName(input.run.outputs.stderr),
40629
41584
  codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
@@ -40631,25 +41586,25 @@ function buildExecutedExternalAgentRunArtifact(input) {
40631
41586
  artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
40632
41587
  },
40633
41588
  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))] : [
41589
+ next_commands: input.status === "pass" ? [externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))] : [
40635
41590
  "foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
40636
41591
  "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))
41592
+ externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))
40638
41593
  ]
40639
41594
  };
40640
41595
  }
40641
41596
 
40642
41597
  // 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");
41598
+ var import_child_process5 = require("child_process");
41599
+ var import_fs19 = require("fs");
41600
+ var import_path18 = require("path");
40646
41601
  function buildCommandInvocation(command, args) {
40647
41602
  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] };
41603
+ const binDir = (0, import_path18.dirname)(command);
41604
+ const codexEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
41605
+ if ((0, import_fs19.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
41606
+ const geminiEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
41607
+ if ((0, import_fs19.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
40653
41608
  }
40654
41609
  return { command, args };
40655
41610
  }
@@ -40657,15 +41612,15 @@ function spawnExternalAgentRunner(input) {
40657
41612
  return new Promise((resolveRun) => {
40658
41613
  const started = Date.now();
40659
41614
  const commandInvocation = buildCommandInvocation(input.command, input.args);
40660
- const child = (0, import_child_process4.spawn)(commandInvocation.command, commandInvocation.args, {
41615
+ const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
40661
41616
  cwd: input.cwd,
40662
41617
  env: input.env,
40663
41618
  shell: false,
40664
41619
  stdio: ["pipe", "pipe", "pipe"],
40665
41620
  windowsHide: true
40666
41621
  });
40667
- const stdout = (0, import_fs18.createWriteStream)(input.stdoutPath, { flags: "w" });
40668
- const stderr = (0, import_fs18.createWriteStream)(input.stderrPath, { flags: "w" });
41622
+ const stdout = (0, import_fs19.createWriteStream)(input.stdoutPath, { flags: "w" });
41623
+ const stderr = (0, import_fs19.createWriteStream)(input.stderrPath, { flags: "w" });
40669
41624
  child.stdout.pipe(stdout);
40670
41625
  child.stderr.pipe(stderr);
40671
41626
  child.stdin.end(input.prompt);
@@ -40673,7 +41628,7 @@ function spawnExternalAgentRunner(input) {
40673
41628
  const timer = setTimeout(() => {
40674
41629
  timedOut = true;
40675
41630
  if (child.pid && process.platform === "win32") {
40676
- (0, import_child_process4.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
41631
+ (0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
40677
41632
  } else {
40678
41633
  child.kill("SIGKILL");
40679
41634
  }
@@ -40777,14 +41732,14 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
40777
41732
  };
40778
41733
  }
40779
41734
  function normalizeForCompare(path2) {
40780
- const resolved = (0, import_path18.resolve)(path2);
41735
+ const resolved = (0, import_path19.resolve)(path2);
40781
41736
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
40782
41737
  }
40783
41738
  function isPathInside(childPath, parentPath) {
40784
41739
  const child = normalizeForCompare(childPath);
40785
41740
  const parent = normalizeForCompare(parentPath);
40786
- const rel = (0, import_path18.relative)(parent, child);
40787
- return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path18.isAbsolute)(rel);
41741
+ const rel = (0, import_path19.relative)(parent, child);
41742
+ return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path19.isAbsolute)(rel);
40788
41743
  }
40789
41744
  function requireString(value, field) {
40790
41745
  if (typeof value !== "string" || value.trim() === "") {
@@ -40793,10 +41748,10 @@ function requireString(value, field) {
40793
41748
  return value;
40794
41749
  }
40795
41750
  function readBatch(batchPath) {
40796
- if (!(0, import_fs19.existsSync)(batchPath)) {
41751
+ if (!(0, import_fs20.existsSync)(batchPath)) {
40797
41752
  throw new ExternalAgentExecutorError("external_agent_batch_not_found", `Batch file not found: ${batchPath}`);
40798
41753
  }
40799
- const parsed = JSON.parse((0, import_fs19.readFileSync)(batchPath, "utf8"));
41754
+ const parsed = JSON.parse((0, import_fs20.readFileSync)(batchPath, "utf8"));
40800
41755
  if (parsed.schema_version !== "external_agent_batch_plan.v1") {
40801
41756
  throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch schema_version must be external_agent_batch_plan.v1.");
40802
41757
  }
@@ -40811,11 +41766,11 @@ function defaultRunnerProbe(command, args) {
40811
41766
  encoding: "utf8",
40812
41767
  timeout: isGeminiHeadlessSmoke ? GEMINI_HEADLESS_PROBE_TIMEOUT_MS : void 0
40813
41768
  };
40814
- const result = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? (0, import_child_process5.spawnSync)(
41769
+ const result = process.platform === "win32" && command.toLowerCase().endsWith(".cmd") ? (0, import_child_process6.spawnSync)(
40815
41770
  "powershell.exe",
40816
41771
  ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `& ${[command, ...args].map(quotePowerShellArg).join(" ")}`],
40817
41772
  spawnOptions
40818
- ) : (0, import_child_process5.spawnSync)(command, args, spawnOptions);
41773
+ ) : (0, import_child_process6.spawnSync)(command, args, spawnOptions);
40819
41774
  return {
40820
41775
  status: typeof result.status === "number" ? result.status : null,
40821
41776
  stdout: String(result.stdout || ""),
@@ -40833,8 +41788,8 @@ function resolveCodexProbeCommand() {
40833
41788
  if (process.platform !== "win32") return "codex";
40834
41789
  const appData = process.env.APPDATA;
40835
41790
  if (appData) {
40836
- const appDataShim = (0, import_path18.join)(appData, "npm", "codex.cmd");
40837
- if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41791
+ const appDataShim = (0, import_path19.join)(appData, "npm", "codex.cmd");
41792
+ if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
40838
41793
  }
40839
41794
  return "codex.cmd";
40840
41795
  }
@@ -40845,8 +41800,8 @@ function resolveGeminiProbeCommand() {
40845
41800
  if (process.platform !== "win32") return "gemini";
40846
41801
  const appData = process.env.APPDATA;
40847
41802
  if (appData) {
40848
- const appDataShim = (0, import_path18.join)(appData, "npm", "gemini.cmd");
40849
- if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41803
+ const appDataShim = (0, import_path19.join)(appData, "npm", "gemini.cmd");
41804
+ if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
40850
41805
  }
40851
41806
  return "gemini.cmd";
40852
41807
  }
@@ -41117,35 +42072,35 @@ function safeRunId(value) {
41117
42072
  return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
41118
42073
  }
41119
42074
  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);
42075
+ if (input.workspaceRoot) return (0, import_path19.resolve)(input.workspaceRoot);
42076
+ const batchStem = (0, import_path19.basename)((0, import_path19.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42077
+ const repoStem = (0, import_path19.basename)((0, import_path19.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42078
+ return (0, import_path19.resolve)((0, import_os3.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
41124
42079
  }
41125
42080
  function findNearestGitRoot(startPath) {
41126
- let current = (0, import_path18.resolve)(startPath);
42081
+ let current = (0, import_path19.resolve)(startPath);
41127
42082
  while (true) {
41128
- if ((0, import_fs19.existsSync)((0, import_path18.join)(current, ".git"))) return current;
41129
- const parent = (0, import_path18.dirname)(current);
42083
+ if ((0, import_fs20.existsSync)((0, import_path19.join)(current, ".git"))) return current;
42084
+ const parent = (0, import_path19.dirname)(current);
41130
42085
  if (parent === current) return null;
41131
42086
  current = parent;
41132
42087
  }
41133
42088
  }
41134
42089
  function resolvePrivateRepoRoot(input) {
41135
42090
  if (input.explicitPrivateRepoRoot) {
41136
- return { root: (0, import_path18.resolve)(input.explicitPrivateRepoRoot), explicit: true };
42091
+ return { root: (0, import_path19.resolve)(input.explicitPrivateRepoRoot), explicit: true };
41137
42092
  }
41138
- const cwd = (0, import_path18.resolve)(input.cwd || process.cwd());
42093
+ const cwd = (0, import_path19.resolve)(input.cwd || process.cwd());
41139
42094
  const gitRoot = findNearestGitRoot(cwd);
41140
42095
  if (gitRoot) return { root: gitRoot, explicit: false };
41141
42096
  return {
41142
- root: (0, import_path18.join)(cwd, ".foh-no-private-repo-root-sentinel"),
42097
+ root: (0, import_path19.join)(cwd, ".foh-no-private-repo-root-sentinel"),
41143
42098
  explicit: false
41144
42099
  };
41145
42100
  }
41146
42101
  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";
42102
+ const raw = (0, import_fs20.readFileSync)(promptPath, "utf8");
42103
+ 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
42104
  if (raw.includes("Do not assume access to the private source repository")) return "blank-setup.v1";
41150
42105
  return "unknown";
41151
42106
  }
@@ -41154,7 +42109,7 @@ function createExternalAgentExecutorPlan(options) {
41154
42109
  if (runner !== "codex" && runner !== "gemini") {
41155
42110
  throw new ExternalAgentExecutorError("unsupported_external_agent_runner", `Unsupported runner: ${runner}`);
41156
42111
  }
41157
- const batchPath = (0, import_path18.resolve)(options.batchPath);
42112
+ const batchPath = (0, import_path19.resolve)(options.batchPath);
41158
42113
  const batch = readBatch(batchPath);
41159
42114
  const runnerProbe = validateRunner(options, runner);
41160
42115
  const codexSandboxBackend = normalizeCodexSandboxBackend(options.codexSandboxBackend);
@@ -41173,17 +42128,17 @@ function createExternalAgentExecutorPlan(options) {
41173
42128
  `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
41174
42129
  );
41175
42130
  }
41176
- (0, import_fs19.mkdirSync)(workspaceRoot, { recursive: true });
41177
- const batchDir = (0, import_path18.resolve)(String(batch.batch_dir || (0, import_path18.resolve)(batchPath, "..")));
42131
+ (0, import_fs20.mkdirSync)(workspaceRoot, { recursive: true });
42132
+ const batchDir = (0, import_path19.resolve)(String(batch.batch_dir || (0, import_path19.resolve)(batchPath, "..")));
41178
42133
  const timeoutMinutes = Number.isFinite(options.timeoutMinutes) && Number(options.timeoutMinutes) > 0 ? Number(options.timeoutMinutes) : 30;
41179
42134
  const runs = batch.runs.map((run) => {
41180
42135
  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"),
42136
+ const runDir = (0, import_path19.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
42137
+ const promptPath = (0, import_path19.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
42138
+ const workspaceDir = (0, import_path19.join)(workspaceRoot, runId);
42139
+ (0, import_fs20.mkdirSync)(workspaceDir, { recursive: true });
42140
+ (0, import_fs20.writeFileSync)(
42141
+ (0, import_path19.join)(workspaceDir, "README.md"),
41187
42142
  [
41188
42143
  "# FOH External-Agent Workspace",
41189
42144
  "",
@@ -41201,11 +42156,11 @@ function createExternalAgentExecutorPlan(options) {
41201
42156
  });
41202
42157
  const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
41203
42158
  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");
42159
+ const jsonlPath = (0, import_path19.join)(runDir, `${outputStem}-exec.jsonl`);
42160
+ const lastMessagePath = (0, import_path19.join)(runDir, `${outputStem}-last-message.md`);
42161
+ const stderrPath = (0, import_path19.join)(runDir, `${outputStem}-stderr.txt`);
42162
+ const runPath = (0, import_path19.join)(runDir, "run.json");
42163
+ const artifactSafetyPath = (0, import_path19.join)(runDir, "artifact-safety.json");
41209
42164
  const args = runner === "gemini" ? [
41210
42165
  ...runnerProbe.globalArgs,
41211
42166
  ...runnerProbe.execArgs
@@ -41296,9 +42251,9 @@ function createExternalAgentExecutorPlan(options) {
41296
42251
  };
41297
42252
  }
41298
42253
  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)}
42254
+ const path2 = (0, import_path19.join)(plan.batch_dir, "executor-plan.json");
42255
+ (0, import_fs20.mkdirSync)(plan.batch_dir, { recursive: true });
42256
+ (0, import_fs20.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
41302
42257
  `, "utf8");
41303
42258
  return path2;
41304
42259
  }
@@ -41313,7 +42268,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41313
42268
  if (authPreflight && !authPreflight.ok) {
41314
42269
  const endedAt2 = (/* @__PURE__ */ new Date()).toISOString();
41315
42270
  const blockedResults = plan.runs.map((run) => {
41316
- (0, import_fs19.mkdirSync)(run.run_dir, { recursive: true });
42271
+ (0, import_fs20.mkdirSync)(run.run_dir, { recursive: true });
41317
42272
  const runArtifact = buildExecutedExternalAgentRunArtifact({
41318
42273
  run,
41319
42274
  startedAt,
@@ -41324,7 +42279,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41324
42279
  timedOut: false,
41325
42280
  durationMs: 0
41326
42281
  });
41327
- (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42282
+ (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
41328
42283
  `, "utf8");
41329
42284
  return {
41330
42285
  run_id: run.run_id,
@@ -41351,8 +42306,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41351
42306
  }
41352
42307
  for (const run of plan.runs) {
41353
42308
  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 });
42309
+ const commandCaptureDir = (0, import_path19.join)(run.workspace_dir, ".foh-capture");
42310
+ (0, import_fs20.mkdirSync)(commandCaptureDir, { recursive: true });
41356
42311
  const env = buildCodexExecutorEnv({
41357
42312
  sourceEnv: options.env,
41358
42313
  runDir: commandCaptureDir,
@@ -41363,7 +42318,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41363
42318
  args: run.args,
41364
42319
  cwd: run.workspace_dir,
41365
42320
  env,
41366
- prompt: (0, import_fs19.readFileSync)(run.prompt_path, "utf8"),
42321
+ prompt: (0, import_fs20.readFileSync)(run.prompt_path, "utf8"),
41367
42322
  stdoutPath: run.outputs.jsonl,
41368
42323
  stderrPath: run.outputs.stderr,
41369
42324
  timeoutMs: plan.timeout_minutes * 60 * 1e3
@@ -41376,7 +42331,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41376
42331
  privateRepoRoot,
41377
42332
  writeRedacted: true
41378
42333
  });
41379
- (0, import_fs19.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
42334
+ (0, import_fs20.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
41380
42335
  `, "utf8");
41381
42336
  const runEndedAt = (/* @__PURE__ */ new Date()).toISOString();
41382
42337
  const classification = classifyExternalAgentRun({
@@ -41395,7 +42350,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
41395
42350
  timedOut: spawned.timedOut,
41396
42351
  durationMs: spawned.durationMs
41397
42352
  });
41398
- (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42353
+ (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
41399
42354
  `, "utf8");
41400
42355
  results.push({
41401
42356
  run_id: run.run_id,
@@ -41434,7 +42389,7 @@ var PROMPTS = {
41434
42389
  "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
42390
  "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
42391
  "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.",
42392
+ "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
42393
  "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
42394
  "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
42395
  "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 +42404,13 @@ function defaultRunDir(modelName, promptVersion) {
41449
42404
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
41450
42405
  const safeModel = String(modelName || "unknown-model").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
41451
42406
  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}`);
42407
+ return (0, import_path20.resolve)("test-results", "external-agent-runs", date4, `${safeModel}-${safePrompt}-${stamp}`);
41453
42408
  }
41454
42409
  function defaultBatchDir(promptVersion) {
41455
42410
  const date4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
41456
42411
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 23);
41457
42412
  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}`);
42413
+ return (0, import_path20.resolve)("test-results", "external-agent-runs", date4, `batch-${safePrompt}-${stamp}`);
41459
42414
  }
41460
42415
  function safeSlug(value) {
41461
42416
  return String(value || "unknown").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
@@ -41572,7 +42527,7 @@ function agencySetupPromptContext(context = {}) {
41572
42527
  const targetMode = String(context.targetMode || "").trim();
41573
42528
  if (!agencyName && !sourceUrl && !branchLocation && !targetMode) return "";
41574
42529
  const previewArgs = [
41575
- "ops reporting agency-setup-preview",
42530
+ "ops reporting agency-setup-workflow",
41576
42531
  agencyName ? `--agency-name ${quoteArg(agencyName)}` : "--agency-name <agency-name>",
41577
42532
  sourceUrl ? `--source-url ${quoteArg(sourceUrl)}` : "--source-url <official-source-url>",
41578
42533
  branchLocation ? `--branch-location ${quoteArg(branchLocation)}` : "--branch-location <branch-location>",
@@ -41587,7 +42542,7 @@ function agencySetupPromptContext(context = {}) {
41587
42542
  ...branchLocation ? [`- Branch/location: ${branchLocation}`] : [],
41588
42543
  ...targetMode ? [`- Target mode: ${targetMode}`] : [],
41589
42544
  `- 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.",
42545
+ "- Then inspect the workflow output and run only the returned proof/evidence commands using public CLI commands.",
41591
42546
  "- Treat missing customer credentials or approvals as expected holds only if the CLI gives machine-readable reason codes and next actions."
41592
42547
  ].join("\n");
41593
42548
  }
@@ -41598,14 +42553,14 @@ function writePrompt(runDir, promptVersion, context = {}) {
41598
42553
  knowledgeMissPromptContext(context.knowledgeQuestion, context.expectedAnswer),
41599
42554
  agencySetupPromptContext(context)
41600
42555
  ].join("");
41601
- const path2 = (0, import_path19.join)(runDir, "prompt.txt");
41602
- (0, import_fs20.writeFileSync)(path2, `${prompt}
42556
+ const path2 = (0, import_path20.join)(runDir, "prompt.txt");
42557
+ (0, import_fs21.writeFileSync)(path2, `${prompt}
41603
42558
  `, "utf8");
41604
42559
  return path2;
41605
42560
  }
41606
42561
  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)}
42562
+ const path2 = (0, import_path20.join)(runDir, "session.json");
42563
+ (0, import_fs21.writeFileSync)(path2, `${JSON.stringify(session, null, 2)}
41609
42564
  `, "utf8");
41610
42565
  return path2;
41611
42566
  }
@@ -41685,9 +42640,9 @@ function buildRunArtifact(input) {
41685
42640
  notes: "notes.md"
41686
42641
  },
41687
42642
  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))
42643
+ next_commands: status === "pass" ? [externalAgentSummaryCommand((0, import_path20.dirname)(input.runDir))] : [
42644
+ `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`,
42645
+ externalAgentSummaryCommand((0, import_path20.dirname)(input.runDir))
41691
42646
  ]
41692
42647
  };
41693
42648
  }
@@ -41696,8 +42651,8 @@ function registerEval(program3) {
41696
42651
  const external = evalCommand.command("external-agent").description("Capture clean external coding-agent setup attempts");
41697
42652
  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
42653
  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;
42654
+ const batchDir = (0, import_path20.resolve)(String(opts.outDir || defaultBatchDir(promptVersion)));
42655
+ const replayFile = opts.replayFile ? (0, import_path20.resolve)(String(opts.replayFile)) : void 0;
41701
42656
  const knowledgeQuestion = opts.knowledgeQuestion ? String(opts.knowledgeQuestion) : void 0;
41702
42657
  const expectedAnswer = opts.expectedAnswer ? String(opts.expectedAnswer) : void 0;
41703
42658
  const agencyName = opts.agencyName ? String(opts.agencyName) : void 0;
@@ -41705,11 +42660,11 @@ function registerEval(program3) {
41705
42660
  const branchLocation = opts.branchLocation ? String(opts.branchLocation) : void 0;
41706
42661
  const targetMode = opts.targetMode ? String(opts.targetMode) : void 0;
41707
42662
  const models = parseModelList(String(opts.models || DEFAULT_BATCH_MODELS));
41708
- (0, import_fs20.mkdirSync)(batchDir, { recursive: true });
42663
+ (0, import_fs21.mkdirSync)(batchDir, { recursive: true });
41709
42664
  const runs2 = models.map((model, index) => {
41710
42665
  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 });
42666
+ const runDir = (0, import_path20.join)(batchDir, runId);
42667
+ (0, import_fs21.mkdirSync)(runDir, { recursive: true });
41713
42668
  const promptPath = writePrompt(runDir, promptVersion, {
41714
42669
  replayFile,
41715
42670
  knowledgeQuestion,
@@ -41772,8 +42727,8 @@ function registerEval(program3) {
41772
42727
  runs: runs2,
41773
42728
  summary_command: externalAgentSummaryCommand(batchDir)
41774
42729
  };
41775
- const batchPath = (0, import_path19.join)(batchDir, "batch.json");
41776
- (0, import_fs20.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
42730
+ const batchPath = (0, import_path20.join)(batchDir, "batch.json");
42731
+ (0, import_fs21.writeFileSync)(batchPath, `${JSON.stringify(batch, null, 2)}
41777
42732
  `, "utf8");
41778
42733
  format(cliEnvelope({
41779
42734
  schemaVersion: "external_agent_batch_plan_result.v1",
@@ -41793,15 +42748,15 @@ function registerEval(program3) {
41793
42748
  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
42749
  const status = normalizeStatus(opts.status);
41795
42750
  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;
42751
+ const runDir = (0, import_path20.resolve)(String(opts.outDir || defaultRunDir(opts.modelName, promptVersion)));
42752
+ const replayFile = opts.replayFile ? (0, import_path20.resolve)(String(opts.replayFile)) : void 0;
41798
42753
  const knowledgeQuestion = opts.knowledgeQuestion ? String(opts.knowledgeQuestion) : void 0;
41799
42754
  const expectedAnswer = opts.expectedAnswer ? String(opts.expectedAnswer) : void 0;
41800
42755
  const agencyName = opts.agencyName ? String(opts.agencyName) : void 0;
41801
42756
  const sourceUrl = opts.sourceUrl ? String(opts.sourceUrl) : void 0;
41802
42757
  const branchLocation = opts.branchLocation ? String(opts.branchLocation) : void 0;
41803
42758
  const targetMode = opts.targetMode ? String(opts.targetMode) : void 0;
41804
- (0, import_fs20.mkdirSync)(runDir, { recursive: true });
42759
+ (0, import_fs21.mkdirSync)(runDir, { recursive: true });
41805
42760
  const runId = runDir.split(/[\\/]/).filter(Boolean).slice(-1)[0];
41806
42761
  const promptPath = writePrompt(runDir, promptVersion, {
41807
42762
  replayFile,
@@ -41839,7 +42794,7 @@ function registerEval(program3) {
41839
42794
  }
41840
42795
  };
41841
42796
  writeSession(runDir, session);
41842
- (0, import_fs20.writeFileSync)((0, import_path19.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
42797
+ (0, import_fs21.writeFileSync)((0, import_path20.join)(runDir, "notes.md"), "# External Agent Run Notes\n\n", "utf8");
41843
42798
  let shellExitCode = null;
41844
42799
  if (opts.shell !== false) {
41845
42800
  process.stdout.write(`
@@ -41849,7 +42804,7 @@ Prompt: ${promptPath}
41849
42804
  Exit the shell to finalize run.json.
41850
42805
 
41851
42806
  `);
41852
- const result = (0, import_child_process6.spawnSync)(shell.command, shell.args, {
42807
+ const result = (0, import_child_process7.spawnSync)(shell.command, shell.args, {
41853
42808
  stdio: "inherit",
41854
42809
  env: {
41855
42810
  ...process.env,
@@ -41861,8 +42816,8 @@ Exit the shell to finalize run.json.
41861
42816
  shellExitCode = typeof result.status === "number" ? result.status : null;
41862
42817
  }
41863
42818
  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)}
42819
+ const runPath = (0, import_path20.join)(runDir, "run.json");
42820
+ (0, import_fs21.writeFileSync)(runPath, `${JSON.stringify(artifact, null, 2)}
41866
42821
  `, "utf8");
41867
42822
  format(cliEnvelope({
41868
42823
  schemaVersion: "external_agent_capture_result.v1",
@@ -41872,7 +42827,7 @@ Exit the shell to finalize run.json.
41872
42827
  artifacts: {
41873
42828
  run: runPath,
41874
42829
  prompt: promptPath,
41875
- commands: (0, import_path19.join)(runDir, "commands.ndjson")
42830
+ commands: (0, import_path20.join)(runDir, "commands.ndjson")
41876
42831
  },
41877
42832
  nextCommands: artifact.next_commands,
41878
42833
  extra: { run: artifact }
@@ -41980,8 +42935,8 @@ Exit the shell to finalize run.json.
41980
42935
  requireExplicitEvalAuth: true,
41981
42936
  minimumEvalAuthTtlMs: (plan.timeout_minutes + 5) * 60 * 1e3
41982
42937
  });
41983
- const resultPath = (0, import_path19.join)(plan.batch_dir, "execution-result.json");
41984
- (0, import_fs20.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
42938
+ const resultPath = (0, import_path20.join)(plan.batch_dir, "execution-result.json");
42939
+ (0, import_fs21.writeFileSync)(resultPath, `${JSON.stringify(result, null, 2)}
41985
42940
  `, "utf8");
41986
42941
  format(cliEnvelope({
41987
42942
  schemaVersion: "external_agent_execution_result.v1",
@@ -42211,11 +43166,13 @@ registerCertify(program2);
42211
43166
  registerDiag(program2);
42212
43167
  registerBug(program2);
42213
43168
  registerProve(program2);
43169
+ registerInteractive(program2);
42214
43170
  registerAgentPublishCommand(program2, { publicAlias: true });
42215
43171
  registerEval(program2);
42216
43172
  registerUpdate(program2);
42217
43173
  registerHome(program2);
42218
43174
  hideInternalApiUrlOptions(program2);
43175
+ initializeCommandGraph(program2);
42219
43176
  maybeDefaultToHome(process.argv);
42220
43177
  program2.parseAsync(process.argv).catch((e) => {
42221
43178
  if (e instanceof CliSoftExit) {