@f-o-h/cli 0.1.86 → 0.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 = resolve15.call(this, root, ref);
6049
+ let _sch = resolve16.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 resolve15(root, ref) {
6076
+ function resolve16(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 resolve15(baseURI, relativeURI, options) {
6651
+ function resolve16(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: resolve15,
6878
+ resolve: resolve16,
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((resolve15) => {
10186
+ return await new Promise((resolve16) => {
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
- resolve15(defaultValue);
10193
+ resolve16(defaultValue);
10194
10194
  return;
10195
10195
  }
10196
10196
  if (!value && !allowEmpty) {
10197
- resolve15("");
10197
+ resolve16("");
10198
10198
  return;
10199
10199
  }
10200
- resolve15(value);
10200
+ resolve16(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((resolve15) => {
10208
+ return await new Promise((resolve16) => {
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
- resolve15(value);
10222
+ resolve16(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 resolve15("");
10231
+ return resolve16("");
10232
10232
  }
10233
10233
  if (char === "\r" || char === "\n") {
10234
10234
  finish();
@@ -10583,7 +10583,7 @@ async function storeAuthenticatedSession(params) {
10583
10583
  return output;
10584
10584
  }
10585
10585
  function sleep(ms) {
10586
- return new Promise((resolve15) => setTimeout(resolve15, ms));
10586
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
10587
10587
  }
10588
10588
  function hasExplicitTimeoutFlag(argv = process.argv) {
10589
10589
  return argv.some((arg) => arg === "--timeout-seconds" || arg.startsWith("--timeout-seconds="));
@@ -11141,7 +11141,7 @@ async function pollUntil(check2, opts) {
11141
11141
  }
11142
11142
  }
11143
11143
  function sleep2(ms) {
11144
- return new Promise((resolve15) => setTimeout(resolve15, ms));
11144
+ return new Promise((resolve16) => setTimeout(resolve16, ms));
11145
11145
  }
11146
11146
 
11147
11147
  // src/commands/compliance.ts
@@ -14265,8 +14265,8 @@ function registerAgentGuardrailCommands(agent) {
14265
14265
  try {
14266
14266
  rule = JSON.parse(opts.rule);
14267
14267
  } catch {
14268
- const { readFileSync: readFileSync19 } = await import("fs");
14269
- rule = JSON.parse(readFileSync19(opts.rule, "utf-8"));
14268
+ const { readFileSync: readFileSync20 } = await import("fs");
14269
+ rule = JSON.parse(readFileSync20(opts.rule, "utf-8"));
14270
14270
  }
14271
14271
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
14272
14272
  method: "POST",
@@ -14835,9 +14835,9 @@ function registerAgent(program3) {
14835
14835
  process.stdout.write(yaml);
14836
14836
  return;
14837
14837
  }
14838
- const { writeFileSync: writeFileSync15 } = await import("fs");
14838
+ const { writeFileSync: writeFileSync16 } = await import("fs");
14839
14839
  const outputPath = opts.output ?? "tenant.yaml";
14840
- writeFileSync15(
14840
+ writeFileSync16(
14841
14841
  outputPath,
14842
14842
  `# tenant.yaml - Front Of House agent manifest
14843
14843
  # Edit this file and run: foh plan tenant.yaml
@@ -15186,6 +15186,10 @@ var SMOKE_TURNS = [
15186
15186
  "I am interested in buying a 3-bedroom house in the area",
15187
15187
  "Can I book a viewing for this week?"
15188
15188
  ];
15189
+ function isGenericTroubleReply(reply) {
15190
+ const normalized = reply.trim().toLowerCase();
15191
+ return /\bi'?m sorry\b/.test(normalized) && /having trouble|try again|something went wrong|unable to help right now/.test(normalized);
15192
+ }
15189
15193
  async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15190
15194
  const data = await apiFetch(
15191
15195
  "/v1/console/channels/widget/ensure",
@@ -15227,12 +15231,17 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15227
15231
  conversationId = data.conversationId;
15228
15232
  if (data.trace_id) traceIds.push(data.trace_id);
15229
15233
  if (data.correlation_id) correlationIds.push(data.correlation_id);
15234
+ const genericTroubleReply = isGenericTroubleReply(data.reply);
15230
15235
  turns.push({
15231
15236
  turn: i + 1,
15232
15237
  message,
15233
- ok: true,
15238
+ ok: !genericTroubleReply,
15234
15239
  latency_ms: latencyMs,
15235
15240
  reply: data.reply,
15241
+ ...genericTroubleReply ? {
15242
+ reason_code: "widget_generic_trouble_reply",
15243
+ error: "Widget returned a generic trouble reply instead of advancing the customer request."
15244
+ } : {},
15236
15245
  conversation_id: data.conversationId,
15237
15246
  trace_id: data.trace_id ?? null,
15238
15247
  correlation_id: data.correlation_id ?? null,
@@ -15368,9 +15377,6 @@ function registerInstagramChannelCommands(instagram, addCommonOptions) {
15368
15377
  }));
15369
15378
  }
15370
15379
 
15371
- // src/commands/channel-whatsapp.ts
15372
- var import_node_fs2 = require("node:fs");
15373
-
15374
15380
  // src/commands/channel-whatsapp-helpers.ts
15375
15381
  function parsePositiveNumber(value, fallback) {
15376
15382
  if (value === void 0 || value === null || String(value).trim() === "") return fallback;
@@ -15403,7 +15409,7 @@ function buildReasonedNextSteps({
15403
15409
  const verifyTokenValue = verifyToken || "<verify_token>";
15404
15410
  if (has("whatsapp_channel_not_ready") || has("whatsapp_access_token_missing") || has("whatsapp_verify_token_missing") || has("whatsapp_app_secret_missing")) {
15405
15411
  steps.push(
15406
- "Connect/update channel credentials: foh channel whatsapp connect --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
15412
+ "Connect/update channel credentials: foh channel whatsapp onboard --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
15407
15413
  );
15408
15414
  }
15409
15415
  if (has("whatsapp_verify_check_failed") || has("whatsapp_webhook_challenge_failed")) {
@@ -15549,6 +15555,9 @@ function resolveLiveProof({
15549
15555
  };
15550
15556
  }
15551
15557
 
15558
+ // src/commands/channel-whatsapp-onboarding.ts
15559
+ var import_node_fs2 = require("node:fs");
15560
+
15552
15561
  // src/commands/channel-whatsapp-setup.ts
15553
15562
  var import_node_crypto = require("node:crypto");
15554
15563
  var WHATSAPP_WEBHOOK_CHALLENGE_TIMEOUT_MS = 1e4;
@@ -15657,7 +15666,7 @@ function assertProofPass(strict, reasons) {
15657
15666
  }
15658
15667
  }
15659
15668
 
15660
- // src/commands/channel-whatsapp.ts
15669
+ // src/commands/channel-whatsapp-onboarding.ts
15661
15670
  async function runWhatsAppOnboardingSession(params) {
15662
15671
  return await apiFetch("/v1/console/channels/whatsapp/onboarding-session", {
15663
15672
  method: "POST",
@@ -15676,24 +15685,6 @@ async function runWhatsAppOnboardingSession(params) {
15676
15685
  apiUrlOverride: params.apiUrl
15677
15686
  });
15678
15687
  }
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
15688
  function parseBatchManifest(manifestPathRaw) {
15698
15689
  const manifestPath = String(manifestPathRaw || "").trim();
15699
15690
  if (!manifestPath) {
@@ -15765,11 +15756,11 @@ async function runWhatsAppOnboardingWizard(opts) {
15765
15756
  }
15766
15757
  ]
15767
15758
  });
15768
- let accessToken = String(wizardState.accessToken || "").trim();
15759
+ const accessToken = String(wizardState.accessToken || "").trim();
15769
15760
  let wabaId = String(opts.wabaId || "").trim();
15770
15761
  let phoneNumberId = String(opts.phoneNumberId || "").trim();
15771
- let verifyToken = String(opts.verifyToken || "").trim();
15772
- let appSecret = String(wizardState.appSecret || "").trim();
15762
+ const verifyToken = String(opts.verifyToken || "").trim();
15763
+ const appSecret = String(wizardState.appSecret || "").trim();
15773
15764
  if (!accessToken) {
15774
15765
  throw new FohError({
15775
15766
  step: "channel.whatsapp.onboard",
@@ -15843,51 +15834,10 @@ async function runWhatsAppOnboardingWizard(opts) {
15843
15834
  throw new FohError({
15844
15835
  step: "channel.whatsapp.onboard",
15845
15836
  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."
15837
+ remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp guide` for deterministic next steps."
15847
15838
  });
15848
15839
  }
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
15880
- });
15881
- format({
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
- ])
15889
- }, { json: opts.json ?? false });
15890
- }));
15840
+ function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
15891
15841
  addCommonOptions(
15892
15842
  whatsapp.command("onboard").description("Run one-session WhatsApp onboarding (discover -> bind -> verify -> prove)")
15893
15843
  ).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 () => {
@@ -15993,75 +15943,11 @@ function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
15993
15943
  results
15994
15944
  }, { json: opts.json ?? false });
15995
15945
  }));
15996
- addCommonOptions(
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"
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({
16043
- orgId: opts.org,
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
16059
- });
16060
- format({
16061
- ...data,
16062
- legacy_wrapper: legacy
16063
- }, { json: opts.json ?? false });
16064
- }));
15946
+ }
15947
+
15948
+ // src/commands/channel-whatsapp.ts
15949
+ function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
15950
+ registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions);
16065
15951
  addCommonOptions(
16066
15952
  whatsapp.command("status").description("Get WhatsApp channel status")
16067
15953
  ).action(async (opts) => withCommandErrorHandling(async () => {
@@ -16438,11 +16324,11 @@ function registerVoice(program3) {
16438
16324
  }
16439
16325
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
16440
16326
  const audio = Buffer.from(await res.arrayBuffer());
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);
16327
+ const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync16 } = await import("fs");
16328
+ const { dirname: dirname13, resolve: resolve16 } = await import("path");
16329
+ const absolutePath = resolve16(outputPath);
16330
+ mkdirSync11(dirname13(absolutePath), { recursive: true });
16331
+ writeFileSync16(absolutePath, audio);
16446
16332
  format({
16447
16333
  status: "ok",
16448
16334
  provider,
@@ -30933,7 +30819,7 @@ var Protocol = class {
30933
30819
  return;
30934
30820
  }
30935
30821
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
30936
- await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
30822
+ await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
30937
30823
  options?.signal?.throwIfAborted();
30938
30824
  }
30939
30825
  } catch (error2) {
@@ -30950,7 +30836,7 @@ var Protocol = class {
30950
30836
  */
30951
30837
  request(request, resultSchema, options) {
30952
30838
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
30953
- return new Promise((resolve15, reject) => {
30839
+ return new Promise((resolve16, reject) => {
30954
30840
  const earlyReject = (error2) => {
30955
30841
  reject(error2);
30956
30842
  };
@@ -31028,7 +30914,7 @@ var Protocol = class {
31028
30914
  if (!parseResult.success) {
31029
30915
  reject(parseResult.error);
31030
30916
  } else {
31031
- resolve15(parseResult.data);
30917
+ resolve16(parseResult.data);
31032
30918
  }
31033
30919
  } catch (error2) {
31034
30920
  reject(error2);
@@ -31289,12 +31175,12 @@ var Protocol = class {
31289
31175
  }
31290
31176
  } catch {
31291
31177
  }
31292
- return new Promise((resolve15, reject) => {
31178
+ return new Promise((resolve16, reject) => {
31293
31179
  if (signal.aborted) {
31294
31180
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
31295
31181
  return;
31296
31182
  }
31297
- const timeoutId = setTimeout(resolve15, interval);
31183
+ const timeoutId = setTimeout(resolve16, interval);
31298
31184
  signal.addEventListener("abort", () => {
31299
31185
  clearTimeout(timeoutId);
31300
31186
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -32394,7 +32280,7 @@ var McpServer = class {
32394
32280
  let task = createTaskResult.task;
32395
32281
  const pollInterval = task.pollInterval ?? 5e3;
32396
32282
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
32397
- await new Promise((resolve15) => setTimeout(resolve15, pollInterval));
32283
+ await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
32398
32284
  const updatedTask = await extra.taskStore.getTask(taskId);
32399
32285
  if (!updatedTask) {
32400
32286
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -33043,19 +32929,19 @@ var StdioServerTransport = class {
33043
32929
  this.onclose?.();
33044
32930
  }
33045
32931
  send(message) {
33046
- return new Promise((resolve15) => {
32932
+ return new Promise((resolve16) => {
33047
32933
  const json3 = serializeMessage(message);
33048
32934
  if (this._stdout.write(json3)) {
33049
- resolve15();
32935
+ resolve16();
33050
32936
  } else {
33051
- this._stdout.once("drain", resolve15);
32937
+ this._stdout.once("drain", resolve16);
33052
32938
  }
33053
32939
  });
33054
32940
  }
33055
32941
  };
33056
32942
 
33057
32943
  // src/lib/cli-version.ts
33058
- var injectedVersion = true ? String("0.1.86").trim() : "";
32944
+ var injectedVersion = true ? String("0.1.88").trim() : "";
33059
32945
  var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
33060
32946
  var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
33061
32947
 
@@ -33242,7 +33128,7 @@ async function runFohCli(params) {
33242
33128
  effectiveArgv.push("--json");
33243
33129
  }
33244
33130
  const command = `foh ${effectiveArgv.join(" ")}`;
33245
- return await new Promise((resolve15) => {
33131
+ return await new Promise((resolve16) => {
33246
33132
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
33247
33133
  stdio: ["ignore", "pipe", "pipe"],
33248
33134
  env: {
@@ -33267,7 +33153,7 @@ async function runFohCli(params) {
33267
33153
  });
33268
33154
  child.once("error", (error2) => {
33269
33155
  clearTimeout(timeoutHandle);
33270
- resolve15({
33156
+ resolve16({
33271
33157
  ok: false,
33272
33158
  command,
33273
33159
  argv: effectiveArgv,
@@ -33283,7 +33169,7 @@ async function runFohCli(params) {
33283
33169
  const stderrText = finalizeBoundedText(stderrBuffer);
33284
33170
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
33285
33171
  const stdoutJson = tryParseJson(stdoutText);
33286
- resolve15({
33172
+ resolve16({
33287
33173
  ok: !timedOut && exitCode === 0,
33288
33174
  command,
33289
33175
  argv: effectiveArgv,
@@ -34877,8 +34763,190 @@ function writeSetupRunReport(reportPayload, reportOut) {
34877
34763
  }
34878
34764
 
34879
34765
  // src/commands/setup.ts
34766
+ var OBJECTIVE_SETUP_INDUSTRIES = ["real_estate", "restaurant", "general"];
34767
+ function normalizeString(value) {
34768
+ return typeof value === "string" ? value.trim() : "";
34769
+ }
34770
+ function csv(value, fallback) {
34771
+ return String(value || fallback).split(",").map((entry) => entry.trim()).filter(Boolean);
34772
+ }
34773
+ function objectiveChannels(tools) {
34774
+ const allowed = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
34775
+ const channels = tools.filter((tool) => allowed.has(tool));
34776
+ return channels.length > 0 ? channels : ["widget"];
34777
+ }
34778
+ function objectiveOptimizationTarget(industry, objective) {
34779
+ const text = objective.toLowerCase();
34780
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(text)) return "booking_rate";
34781
+ if (/support|resolve|resolution/.test(text)) return "support_resolution";
34782
+ if (/speed|lead|callback|call back/.test(text)) return "speed_to_lead";
34783
+ return "lead_quality";
34784
+ }
34785
+ function objectiveSetupCommand(opts) {
34786
+ const parts = [
34787
+ "npx --yes @f-o-h/cli@latest setup",
34788
+ `--objective ${JSON.stringify(normalizeString(opts.objective) || "<front-of-house objective>")}`,
34789
+ `--business-name ${JSON.stringify(normalizeString(opts.businessName) || "<business name>")}`,
34790
+ `--industry ${normalizeString(opts.industry) || "<real_estate|restaurant>"}`,
34791
+ `--source-url ${JSON.stringify(normalizeString(opts.sourceUrl) || "<official url>")}`,
34792
+ "--json"
34793
+ ];
34794
+ return parts.join(" ");
34795
+ }
34796
+ function selectedTemplateSummary(selection) {
34797
+ const candidate = Array.isArray(selection?.candidates) ? selection.candidates[0] : null;
34798
+ const template = candidate?.template;
34799
+ const contract = template?.template_contract ?? {};
34800
+ if (!template) return null;
34801
+ return {
34802
+ template_id: String(template.id || contract.template_id || ""),
34803
+ template_name: String(template.name || contract.name || ""),
34804
+ template_slug: String(contract.slug || ""),
34805
+ industry: String(contract.industry || ""),
34806
+ use_case: String(contract.use_case || ""),
34807
+ match_score: Number.isFinite(Number(candidate.match_score)) ? Number(candidate.match_score) : null,
34808
+ matched_reasons: Array.isArray(candidate.matched_reasons) ? candidate.matched_reasons.map(String) : []
34809
+ };
34810
+ }
34811
+ async function emitObjectiveSetupBootstrap(opts) {
34812
+ const businessName = normalizeString(opts.businessName);
34813
+ const objective = normalizeString(opts.objective);
34814
+ const industry = normalizeString(opts.industry);
34815
+ const sourceUrl = normalizeString(opts.sourceUrl);
34816
+ const tools = csv(opts.tools, "widget,voice,whatsapp");
34817
+ let credentials = null;
34818
+ try {
34819
+ credentials = loadCredentials(opts.apiUrl);
34820
+ if (!opts.org) opts.org = credentials.orgId;
34821
+ } catch {
34822
+ credentials = null;
34823
+ }
34824
+ if (!credentials && !opts.org) {
34825
+ format({
34826
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34827
+ ok: false,
34828
+ status: "blocked",
34829
+ reason_code: "auth_required",
34830
+ summary: "Authenticate before objective setup so template selection and setup planning can use the correct org.",
34831
+ spend_policy: resolveCliSpendPolicy(),
34832
+ next_commands: [
34833
+ "npx --yes @f-o-h/cli@latest auth signup --web --json",
34834
+ "npx --yes @f-o-h/cli@latest auth login --web --json",
34835
+ "npx --yes @f-o-h/cli@latest org list --json",
34836
+ objectiveSetupCommand(opts)
34837
+ ]
34838
+ }, { json: opts.json ?? false });
34839
+ markCommandFailed(1);
34840
+ return;
34841
+ }
34842
+ const missing = [];
34843
+ if (!businessName) missing.push("--business-name");
34844
+ if (!industry) missing.push("--industry");
34845
+ if (!objective) missing.push("--objective");
34846
+ if (!sourceUrl) missing.push("--source-url");
34847
+ if (industry && !OBJECTIVE_SETUP_INDUSTRIES.includes(industry)) missing.push("--industry:supported_value");
34848
+ if (missing.length > 0) {
34849
+ format({
34850
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34851
+ ok: false,
34852
+ status: "blocked",
34853
+ reason_code: "objective_setup_required_options_missing",
34854
+ summary: "Objective setup needs an explicit business name, industry, objective, and official source URL.",
34855
+ missing_options: missing,
34856
+ org_id: opts.org ?? null,
34857
+ next_commands: [
34858
+ objectiveSetupCommand(opts),
34859
+ "npx --yes @f-o-h/cli@latest templates list --category general --json",
34860
+ "npx --yes @f-o-h/cli@latest templates list --category buyer --json"
34861
+ ],
34862
+ claim_boundaries: {
34863
+ customer_live_claim_allowed: false,
34864
+ production_claim_allowed: false
34865
+ }
34866
+ }, { json: opts.json ?? false });
34867
+ markCommandFailed(1);
34868
+ return;
34869
+ }
34870
+ const brief = {
34871
+ schema_version: "business_requirement_brief.v1",
34872
+ business_name: businessName,
34873
+ industry,
34874
+ desired_use_cases: [objective],
34875
+ channels: objectiveChannels(tools),
34876
+ knowledge_sources: [{ type: "website", label: `${businessName} official website`, uri: sourceUrl }],
34877
+ required_outcomes: [objective],
34878
+ handoff_rules: ["handoff when approved facts, credentials, or action availability are missing"],
34879
+ constraints: ["do not invent business facts or claim live readiness without accepted evidence"],
34880
+ optimization_target: objectiveOptimizationTarget(industry, objective)
34881
+ };
34882
+ const templateSelection = await apiFetch("/v1/console/templates/select", {
34883
+ method: "POST",
34884
+ body: JSON.stringify(brief),
34885
+ apiUrlOverride: opts.apiUrl
34886
+ });
34887
+ const selectedTemplate = selectedTemplateSummary(templateSelection);
34888
+ if (!selectedTemplate) {
34889
+ format({
34890
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34891
+ ok: false,
34892
+ status: "blocked",
34893
+ reason_code: "objective_template_selection_empty",
34894
+ summary: "No supported template matched this objective. Do not continue with a guessed template.",
34895
+ requirement_brief: brief,
34896
+ template_selection: templateSelection,
34897
+ next_commands: [objectiveSetupCommand(opts)]
34898
+ }, { json: opts.json ?? false });
34899
+ markCommandFailed(1);
34900
+ return;
34901
+ }
34902
+ const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
34903
+ method: "POST",
34904
+ body: JSON.stringify({
34905
+ agency_name: businessName,
34906
+ business_objective: objective,
34907
+ requested_tool_surface: tools,
34908
+ target_exposure_mode: normalizeString(opts.targetMode) || "customer_owned_voice_trial",
34909
+ source_url: sourceUrl,
34910
+ ...normalizeString(opts.location) ? { branch_location: normalizeString(opts.location) } : {}
34911
+ }),
34912
+ orgId: opts.org,
34913
+ apiUrlOverride: opts.apiUrl
34914
+ });
34915
+ format({
34916
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34917
+ ok: true,
34918
+ status: "hold",
34919
+ reason_code: "objective_setup_plan_ready",
34920
+ summary: "Objective setup plan is ready. Treat this as dry-run planning until evidence is applied and status explicitly allows live claims.",
34921
+ dry_run: opts.apply === true ? false : true,
34922
+ apply_requested: opts.apply === true,
34923
+ org_id: opts.org ?? null,
34924
+ requirement_brief: brief,
34925
+ selected_template: selectedTemplate,
34926
+ setup_workflow: {
34927
+ decision: setupWorkflow?.decision ?? setupWorkflow?.status ?? null,
34928
+ reason_codes: Array.isArray(setupWorkflow?.reason_codes) ? setupWorkflow.reason_codes : [],
34929
+ evidence_packet: setupWorkflow?.evidence_packet ?? null,
34930
+ operator_status: setupWorkflow?.operator_status ?? null
34931
+ },
34932
+ claim_boundaries: {
34933
+ customer_live_claim_allowed: false,
34934
+ production_claim_allowed: false,
34935
+ fully_autonomous_claim_allowed: false
34936
+ },
34937
+ next_commands: [
34938
+ `npx --yes @f-o-h/cli@latest objective status --business-name ${JSON.stringify(businessName)} --industry ${industry} --business-objective ${JSON.stringify(objective)} --source-url ${JSON.stringify(sourceUrl)} --out test-results/objective-status.latest.json --json`,
34939
+ "npx --yes @f-o-h/cli@latest objective debug --from test-results/objective-status.latest.json --json",
34940
+ `npx --yes @f-o-h/cli@latest templates show --template ${selectedTemplate.template_id} --json`
34941
+ ]
34942
+ }, { json: opts.json ?? false });
34943
+ }
34880
34944
  function registerSetup(program3) {
34881
- program3.command("setup").description("Fully provision a new agency customer in one command").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
34945
+ program3.command("setup").description("Fully provision a customer or plan objective-first setup").option("--objective <text>", "Objective-first setup mode: plain-English front-of-house objective").option("--business-name <name>", "Business trading name for objective-first setup").option("--industry <industry>", "Business industry for objective-first setup: real_estate, restaurant, general").option("--source-url <url>", "Official business source URL for objective-first setup").option("--location <value>", "Location or branch represented by this setup").option("--tools <csv>", "Requested surfaces for objective-first setup", "widget,voice,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode for objective-first setup", "customer_owned_voice_trial").option("--apply", "Explicitly request apply mode for objective setup; default is dry-run planning").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
34946
+ if (opts.objective) {
34947
+ await emitObjectiveSetupBootstrap(opts);
34948
+ return;
34949
+ }
34882
34950
  if (!opts.org) {
34883
34951
  try {
34884
34952
  opts.org = loadCredentials(opts.apiUrl).orgId;
@@ -35453,8 +35521,8 @@ function registerSetup(program3) {
35453
35521
  }
35454
35522
  try {
35455
35523
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
35456
- const { writeFileSync: writeFileSync15 } = await import("fs");
35457
- writeFileSync15(
35524
+ const { writeFileSync: writeFileSync16 } = await import("fs");
35525
+ writeFileSync16(
35458
35526
  "tenant.yaml",
35459
35527
  `# tenant.yaml - Front Of House agent manifest
35460
35528
  # Edit this file and run: foh plan tenant.yaml
@@ -35624,8 +35692,8 @@ function registerSim(program3) {
35624
35692
  }
35625
35693
  const cert = response.certificate;
35626
35694
  if (opts.out) {
35627
- const { writeFileSync: writeFileSync15 } = await import("fs");
35628
- writeFileSync15(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35695
+ const { writeFileSync: writeFileSync16 } = await import("fs");
35696
+ writeFileSync16(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35629
35697
  process.stderr.write(` Certificate written to ${opts.out}
35630
35698
  `);
35631
35699
  }
@@ -35675,8 +35743,8 @@ function registerSim(program3) {
35675
35743
  });
35676
35744
  }
35677
35745
  if (opts.out) {
35678
- const { writeFileSync: writeFileSync15 } = await import("fs");
35679
- writeFileSync15(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35746
+ const { writeFileSync: writeFileSync16 } = await import("fs");
35747
+ writeFileSync16(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35680
35748
  process.stderr.write(` Final certificate written to ${opts.out}
35681
35749
  `);
35682
35750
  }
@@ -35733,7 +35801,7 @@ function defaultAdaptiveRuns(profile) {
35733
35801
  if (profile === "stress") return 30;
35734
35802
  return 5;
35735
35803
  }
35736
- function csv(raw) {
35804
+ function csv2(raw) {
35737
35805
  if (!raw) return void 0;
35738
35806
  const values = String(raw).split(",").map((value) => value.trim()).filter(Boolean);
35739
35807
  return values.length > 0 ? values : void 0;
@@ -35759,8 +35827,8 @@ function registerCertify(program3) {
35759
35827
  body: JSON.stringify({
35760
35828
  mode,
35761
35829
  adaptive_runs: adaptiveRuns,
35762
- journeys: csv(opts.journeys),
35763
- scenario_ids: csv(opts.scenarioIds),
35830
+ journeys: csv2(opts.journeys),
35831
+ scenario_ids: csv2(opts.scenarioIds),
35764
35832
  channel: channel(opts.channel)
35765
35833
  }),
35766
35834
  apiUrlOverride: opts.apiUrl
@@ -38109,6 +38177,9 @@ function fail(name, reasonCode, error2, nextCommand) {
38109
38177
  function skipped(name, reasonCode, summary, nextCommand) {
38110
38178
  return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
38111
38179
  }
38180
+ function defaultCertificationProfileForMission(mission) {
38181
+ return mission === "publish" ? "release" : "smoke";
38182
+ }
38112
38183
  function hasBlockingChecks(checks) {
38113
38184
  return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
38114
38185
  }
@@ -38446,11 +38517,12 @@ function registerProve(program3) {
38446
38517
  if (opts.skipCert) {
38447
38518
  checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh certify run --agent ${ctx.agentId} --profile release --json`));
38448
38519
  } else if (!opts.includeCertification) {
38520
+ const certificationProfile = defaultCertificationProfileForMission(mission);
38449
38521
  checks.push(skipped(
38450
38522
  "simulation_certification",
38451
38523
  "certification_explicitly_required",
38452
- "Runtime proof does not run release certification by default.",
38453
- `foh certify run --agent ${ctx.agentId} --profile release --json`
38524
+ certificationProfile === "release" ? "Runtime proof does not run release certification by default." : "Runtime proof does not run certification by default; use smoke certification for bounded external-agent verification.",
38525
+ `foh certify run --agent ${ctx.agentId} --profile ${certificationProfile} --json`
38454
38526
  ));
38455
38527
  } else {
38456
38528
  try {
@@ -38542,6 +38614,808 @@ function registerProve(program3) {
38542
38614
  }));
38543
38615
  }
38544
38616
 
38617
+ // src/commands/objective.ts
38618
+ var import_node_fs5 = require("node:fs");
38619
+ var import_node_path2 = require("node:path");
38620
+ var DEFAULT_OBJECTIVE_REPORT_PATH = "test-results/objective-status.latest.json";
38621
+ var VALID_OBJECTIVE_INDUSTRIES = ["real_estate", "restaurant", "general"];
38622
+ var BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION = "business_requirement_brief.v1";
38623
+ function asRecord3(value) {
38624
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
38625
+ }
38626
+ function normalizeString2(value) {
38627
+ return typeof value === "string" ? value.trim() : "";
38628
+ }
38629
+ function uniqueStrings(values) {
38630
+ return Array.from(new Set(values.map(normalizeString2).filter(Boolean)));
38631
+ }
38632
+ function asArray(value) {
38633
+ return Array.isArray(value) ? value : [];
38634
+ }
38635
+ function collectStringArrays(value, keys, output = []) {
38636
+ if (!value || typeof value !== "object") return output;
38637
+ if (Array.isArray(value)) {
38638
+ for (const item of value) collectStringArrays(item, keys, output);
38639
+ return output;
38640
+ }
38641
+ for (const [key, child] of Object.entries(value)) {
38642
+ if (keys.has(key) && Array.isArray(child)) {
38643
+ for (const item of child) {
38644
+ if (typeof item === "string") output.push(item);
38645
+ }
38646
+ } else if (child && typeof child === "object") {
38647
+ collectStringArrays(child, keys, output);
38648
+ }
38649
+ }
38650
+ return output;
38651
+ }
38652
+ function collectArtifactRefs(value, output = []) {
38653
+ if (!value || typeof value !== "object") return output;
38654
+ if (Array.isArray(value)) {
38655
+ for (const item of value) collectArtifactRefs(item, output);
38656
+ return output;
38657
+ }
38658
+ for (const [key, child] of Object.entries(value)) {
38659
+ if (typeof child === "string") {
38660
+ const normalized = child.trim();
38661
+ if (normalized && (key.toLowerCase().includes("artifact") || key.toLowerCase().includes("report") || normalized.includes("test-results/") || normalized.includes("test-results\\"))) {
38662
+ output.push(normalized);
38663
+ }
38664
+ } else if (child && typeof child === "object") {
38665
+ collectArtifactRefs(child, output);
38666
+ }
38667
+ }
38668
+ return output;
38669
+ }
38670
+ function summarizeSourceReport(value) {
38671
+ const report = asRecord3(value);
38672
+ return {
38673
+ decision: firstString(report, ["decision", "current_decision", "status"]) || null,
38674
+ go_live_allowed: report.go_live_allowed === true || report.customer_live_ready === true || report.production_ready === true,
38675
+ allowed_mode: firstString(report, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || null,
38676
+ blocked_modes: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["blocked_modes"]))),
38677
+ reason_codes: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))),
38678
+ next_commands: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["next_commands"]))),
38679
+ evidence_refs: uniqueStrings(collectArtifactRefs(report))
38680
+ };
38681
+ }
38682
+ function finiteNumber(value) {
38683
+ const number3 = Number(value);
38684
+ return Number.isFinite(number3) ? number3 : null;
38685
+ }
38686
+ function normalizeCustomerEvidenceActions(value) {
38687
+ return asArray(value).map(asRecord3).map((action) => ({
38688
+ id: firstString(action, ["id", "action_id"]),
38689
+ title: firstString(action, ["title"]) || null,
38690
+ owner: firstString(action, ["owner"]) || null,
38691
+ blocker_count: finiteNumber(action.blocker_count) ?? (uniqueStrings(asArray(action.reason_codes)).length || null),
38692
+ target_evidence_paths: uniqueStrings([
38693
+ ...asArray(action.target_evidence_paths),
38694
+ firstString(action, ["target_evidence_path"])
38695
+ ]),
38696
+ validator_commands: uniqueStrings([
38697
+ ...asArray(action.validator_commands),
38698
+ ...asArray(action.next_commands)
38699
+ ]),
38700
+ required_evidence: firstString(action, ["required_evidence"]) || null,
38701
+ unlocks: firstString(action, ["unlocks"]) || null
38702
+ })).filter((action) => normalizeString2(action.id));
38703
+ }
38704
+ function normalizeCustomerEvidenceActionPacket(value) {
38705
+ const packet = asRecord3(value);
38706
+ const actions = normalizeCustomerEvidenceActions(packet.actions);
38707
+ if (actions.length === 0) return void 0;
38708
+ return {
38709
+ packet_role: firstString(packet, ["packet_role"]) || "compressed_customer_operator_action_plan",
38710
+ action_count: finiteNumber(packet.action_count) ?? actions.length,
38711
+ external_hold_count: finiteNumber(packet.external_hold_count),
38712
+ raw_reason_code_count: finiteNumber(packet.raw_reason_code_count),
38713
+ compression_ratio: finiteNumber(packet.compression_ratio),
38714
+ actions,
38715
+ validation_commands: uniqueStrings(asArray(packet.validation_commands)),
38716
+ instructions: uniqueStrings(asArray(packet.instructions))
38717
+ };
38718
+ }
38719
+ function customerEvidenceActionPacketFromSources(...sources) {
38720
+ for (const source of sources) {
38721
+ const report = asRecord3(source);
38722
+ const requestPackets = asRecord3(report.request_packets);
38723
+ const operatorStatus = asRecord3(report.operator_status);
38724
+ const candidates = [
38725
+ requestPackets.customer_operator_action_packet,
38726
+ report.customer_operator_action_packet,
38727
+ report.customer_evidence_action_packet,
38728
+ operatorStatus.customer_operator_action_packet,
38729
+ operatorStatus.customer_evidence_action_packet,
38730
+ operatorStatus.customer_actions ? {
38731
+ packet_role: "compressed_customer_operator_action_plan",
38732
+ action_count: asArray(operatorStatus.customer_actions).length,
38733
+ actions: operatorStatus.customer_actions,
38734
+ validation_commands: collectStringArrays(operatorStatus, /* @__PURE__ */ new Set(["next_commands"]))
38735
+ } : null,
38736
+ report.customer_actions ? { actions: report.customer_actions } : null
38737
+ ];
38738
+ for (const candidate of candidates) {
38739
+ const normalized = normalizeCustomerEvidenceActionPacket(candidate);
38740
+ if (normalized) return normalized;
38741
+ }
38742
+ }
38743
+ return void 0;
38744
+ }
38745
+ function normalizeObjectiveArtifactPath(value) {
38746
+ return value.trim() ? (0, import_node_path2.resolve)(value.trim()) : "";
38747
+ }
38748
+ function resolveObjectiveArtifactPath(value) {
38749
+ return normalizeObjectiveArtifactPath(value);
38750
+ }
38751
+ function firstNonEmptyObject(value) {
38752
+ const record2 = asRecord3(value);
38753
+ return Object.keys(record2).length > 0 ? record2 : void 0;
38754
+ }
38755
+ function pickEvidencePacket(value) {
38756
+ const record2 = firstNonEmptyObject(value);
38757
+ if (!record2) return void 0;
38758
+ const payload = firstNonEmptyObject(record2.payload);
38759
+ return payload ?? record2;
38760
+ }
38761
+ function readEvidencePacketFromPlan(path2) {
38762
+ const plan = asRecord3(readJsonArtifact(path2));
38763
+ const sourceReports = asRecord3(plan.source_reports);
38764
+ return pickEvidencePacket(plan.evidence) ?? pickEvidencePacket(plan.evidence_packet) ?? pickEvidencePacket(sourceReports.customer_live_status && asRecord3(sourceReports.customer_live_status).evidence) ?? pickEvidencePacket(asRecord3(sourceReports.setup_workflow).evidence_packet) ?? {};
38765
+ }
38766
+ function inferEvidenceFromProofPaths(evidence) {
38767
+ const record2 = asRecord3(evidence);
38768
+ if (!record2 || Object.keys(record2).length === 0) return evidence;
38769
+ return record2.evidence ?? evidence;
38770
+ }
38771
+ async function resolveEvidenceInput(opts) {
38772
+ if (opts.evidence) {
38773
+ return inferEvidenceFromProofPaths(await parseJsonOption(opts.evidence, "--evidence"));
38774
+ }
38775
+ if (!opts.plan) {
38776
+ throw new FohError({
38777
+ step: "objective.apply",
38778
+ error: "Missing objective evidence input: pass --evidence or --plan.",
38779
+ remediation: "Provide either --evidence <json|@file> or --plan <path-to-plan-json>.",
38780
+ statusCode: 400
38781
+ });
38782
+ }
38783
+ const planPath = resolveObjectiveArtifactPath(opts.plan);
38784
+ return inferEvidenceFromProofPaths(readEvidencePacketFromPlan(planPath));
38785
+ }
38786
+ function firstString(record2, keys) {
38787
+ for (const key of keys) {
38788
+ const value = normalizeString2(record2[key]);
38789
+ if (value) return value;
38790
+ }
38791
+ return "";
38792
+ }
38793
+ function firstStringFromUnknown(value, keys) {
38794
+ return firstString(asRecord3(value), keys);
38795
+ }
38796
+ function truthy(record2, keys) {
38797
+ return keys.some((key) => record2[key] === true);
38798
+ }
38799
+ function statusFromDecision(value) {
38800
+ const normalized = value.toLowerCase();
38801
+ if (/fail|failed|error/.test(normalized)) return "fail";
38802
+ if (/ready|pass|passed|success|go_live/.test(normalized)) return "pass";
38803
+ return "hold";
38804
+ }
38805
+ function firstCustomerEvidenceAction(packet) {
38806
+ const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString2(action.id));
38807
+ return actions[0] ?? null;
38808
+ }
38809
+ function buildDeveloperReadinessPacket(input) {
38810
+ const businessName = resolveBusinessName(input.opts);
38811
+ const sourceUrl = normalizeString2(input.opts.sourceUrl);
38812
+ const businessObjective = normalizeString2(input.opts.businessObjective);
38813
+ const location = resolveLocation(input.opts);
38814
+ const tools = parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
38815
+ const setupDecision = firstString(input.setupWorkflow, ["decision", "current_decision", "status"]);
38816
+ const liveDecision = firstString(input.customerLiveStatus, ["decision", "current_decision", "status"]);
38817
+ const explicitGoLive = truthy(input.customerLiveStatus, ["go_live_allowed", "customer_live_ready", "production_ready"]) || truthy(input.setupWorkflow, ["go_live_allowed", "customer_live_ready", "production_ready"]);
38818
+ const firstAction = firstCustomerEvidenceAction(input.customerEvidenceActionPacket);
38819
+ const firstActionValidator = firstAction ? uniqueStrings(asArray(firstAction.validator_commands))[0] ?? null : null;
38820
+ const firstNextCommand = input.nextCommands[0] ?? null;
38821
+ return {
38822
+ schema_version: "developer_objective_readiness_packet.v1",
38823
+ packet_role: "coding_agent_objective_readiness",
38824
+ decision: input.classification.status,
38825
+ reason_code: input.classification.reasonCode,
38826
+ business_context: {
38827
+ business_name: businessName || null,
38828
+ business_objective: businessObjective || null,
38829
+ industry: objectiveIndustryOrNull(input.opts),
38830
+ source_url: sourceUrl || null,
38831
+ location: location || null,
38832
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
38833
+ tools
38834
+ },
38835
+ input_completeness: {
38836
+ business_name: Boolean(businessName),
38837
+ source_url: Boolean(sourceUrl),
38838
+ business_objective: Boolean(businessObjective),
38839
+ requested_tools: tools.length > 0,
38840
+ target_mode: Boolean(normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial")
38841
+ },
38842
+ readiness_dimensions: {
38843
+ setup_workflow: {
38844
+ status: statusFromDecision(setupDecision),
38845
+ decision: setupDecision || null
38846
+ },
38847
+ customer_live_status: {
38848
+ status: explicitGoLive ? "pass" : statusFromDecision(liveDecision),
38849
+ decision: liveDecision || null
38850
+ },
38851
+ customer_evidence_actions: {
38852
+ status: firstAction ? "hold" : "pass",
38853
+ action_count: finiteNumber(input.customerEvidenceActionPacket?.action_count) ?? asArray(input.customerEvidenceActionPacket?.actions).length,
38854
+ first_action_id: firstAction ? normalizeString2(firstAction.id) : null,
38855
+ first_validator_command: firstActionValidator
38856
+ },
38857
+ agent_operability: {
38858
+ status: firstNextCommand ? "pass" : "fail",
38859
+ first_next_command: firstNextCommand,
38860
+ normal_path_requires_raw_artifacts: false
38861
+ }
38862
+ },
38863
+ next_command: firstNextCommand,
38864
+ claim_boundaries: {
38865
+ developer_setup_loop_ready: Boolean(firstNextCommand),
38866
+ customer_live_claim_allowed: explicitGoLive && input.classification.status === "pass",
38867
+ production_claim_allowed: explicitGoLive && input.classification.status === "pass",
38868
+ do_not_claim: explicitGoLive && input.classification.status === "pass" ? [] : ["customer-live", "production-ready", "fully autonomous customer setup"]
38869
+ },
38870
+ agent_instructions: [
38871
+ "Use this packet before source_reports or raw artifacts.",
38872
+ "Execute next_command first, then rerun objective status.",
38873
+ "Treat customer evidence actions as request work, not proof."
38874
+ ]
38875
+ };
38876
+ }
38877
+ function resolveObjectiveReportPath(value) {
38878
+ const raw = normalizeString2(value);
38879
+ if (!raw || raw === "latest") return (0, import_node_path2.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
38880
+ return (0, import_node_path2.resolve)(raw);
38881
+ }
38882
+ function writeJsonArtifact2(path2, value) {
38883
+ (0, import_node_fs5.mkdirSync)((0, import_node_path2.dirname)(path2), { recursive: true });
38884
+ (0, import_node_fs5.writeFileSync)(path2, `${JSON.stringify(value, null, 2)}
38885
+ `, "utf8");
38886
+ }
38887
+ function readJsonArtifact(path2) {
38888
+ return JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
38889
+ }
38890
+ function buildSetupBody(opts) {
38891
+ const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
38892
+ const businessName = resolveBusinessName(opts);
38893
+ const location = resolveLocation(opts);
38894
+ const body = {
38895
+ agency_name: businessName,
38896
+ business_objective: normalizeString2(opts.businessObjective) || null,
38897
+ requested_tool_surface: tools,
38898
+ target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
38899
+ };
38900
+ if (opts.sourceUrl) body.source_url = normalizeString2(opts.sourceUrl);
38901
+ if (location) body.branch_location = location;
38902
+ return body;
38903
+ }
38904
+ function buildStatusParams(opts) {
38905
+ const params = new URLSearchParams();
38906
+ const businessName = resolveBusinessName(opts);
38907
+ const location = resolveLocation(opts);
38908
+ if (opts.environment) params.set("environment", normalizeString2(opts.environment));
38909
+ if (businessName) params.set("agency_name", businessName);
38910
+ if (opts.sourceUrl) params.set("source_url", normalizeString2(opts.sourceUrl));
38911
+ if (location) params.set("branch_location", location);
38912
+ if (opts.tools) params.set("tools", normalizeString2(opts.tools));
38913
+ if (opts.targetMode) params.set("target_mode", normalizeString2(opts.targetMode));
38914
+ return params;
38915
+ }
38916
+ function resolveBusinessName(opts) {
38917
+ return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
38918
+ }
38919
+ function resolveLocation(opts) {
38920
+ return normalizeString2(opts.location) || normalizeString2(opts.branchLocation);
38921
+ }
38922
+ function hasAnyToken(value, tokens) {
38923
+ const normalized = ` ${value.toLowerCase().replace(/[^a-z0-9]+/g, " ")} `;
38924
+ return tokens.some((token) => normalized.includes(` ${token} `));
38925
+ }
38926
+ function inferObjectiveIndustry(opts) {
38927
+ const explicit = normalizeString2(opts.industry);
38928
+ if (explicit) {
38929
+ if (VALID_OBJECTIVE_INDUSTRIES.includes(explicit)) return explicit;
38930
+ throw new FohError({
38931
+ step: "objective.brief",
38932
+ error: `Unsupported industry: ${explicit}.`,
38933
+ remediation: `Use one of: ${VALID_OBJECTIVE_INDUSTRIES.join(", ")}.`,
38934
+ statusCode: 400,
38935
+ reasonCode: "objective_industry_unsupported"
38936
+ });
38937
+ }
38938
+ const objectiveText = [
38939
+ opts.businessObjective,
38940
+ opts.businessName,
38941
+ opts.agencyName,
38942
+ opts.sourceUrl
38943
+ ].map(normalizeString2).join(" ");
38944
+ const realEstate = hasAnyToken(objectiveText, [
38945
+ "estate",
38946
+ "property",
38947
+ "properties",
38948
+ "viewing",
38949
+ "valuation",
38950
+ "buyer",
38951
+ "seller",
38952
+ "landlord",
38953
+ "tenant",
38954
+ "lettings"
38955
+ ]);
38956
+ const restaurant = hasAnyToken(objectiveText, [
38957
+ "restaurant",
38958
+ "table",
38959
+ "booking",
38960
+ "reservation",
38961
+ "reservations",
38962
+ "diner",
38963
+ "diners",
38964
+ "menu",
38965
+ "allergy",
38966
+ "hospitality"
38967
+ ]);
38968
+ if (realEstate && !restaurant) return "real_estate";
38969
+ if (restaurant && !realEstate) return "restaurant";
38970
+ throw new FohError({
38971
+ step: "objective.brief",
38972
+ error: realEstate && restaurant ? "Objective matches multiple industries." : "Objective is too ambiguous to select a business template.",
38973
+ remediation: "Pass --industry real_estate or --industry restaurant, and keep --business-objective specific to one front-of-house outcome.",
38974
+ statusCode: 400,
38975
+ reasonCode: realEstate && restaurant ? "objective_industry_ambiguous" : "objective_industry_missing",
38976
+ nextCommands: [
38977
+ 'foh objective plan --business-name <name> --industry restaurant --business-objective "Book tables from website and voice enquiries" --source-url <official_url> --json',
38978
+ 'foh objective plan --business-name <name> --industry real_estate --business-objective "Qualify buyers and book viewings" --source-url <official_url> --json'
38979
+ ]
38980
+ });
38981
+ }
38982
+ function objectiveIndustryOrNull(opts) {
38983
+ try {
38984
+ return inferObjectiveIndustry(opts);
38985
+ } catch {
38986
+ return null;
38987
+ }
38988
+ }
38989
+ function channelsFromTools(tools) {
38990
+ const supported = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
38991
+ const channels = tools.filter((tool) => supported.has(tool));
38992
+ return channels.length > 0 ? channels : ["widget"];
38993
+ }
38994
+ function inferOptimizationTarget(industry, objective) {
38995
+ const normalized = objective.toLowerCase();
38996
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(normalized)) return "booking_rate";
38997
+ if (/speed|fast|callback|call back|lead/.test(normalized)) return "speed_to_lead";
38998
+ if (/support|help|resolve|resolution/.test(normalized)) return "support_resolution";
38999
+ return "lead_quality";
39000
+ }
39001
+ function buildRequirementBrief(opts) {
39002
+ const businessName = resolveBusinessName(opts);
39003
+ const businessObjective = normalizeString2(opts.businessObjective);
39004
+ if (!businessObjective) {
39005
+ throw new FohError({
39006
+ step: "objective.brief",
39007
+ error: "Missing business objective.",
39008
+ remediation: "Pass --business-objective with the concrete front-of-house outcome to configure.",
39009
+ statusCode: 400,
39010
+ reasonCode: "objective_business_objective_missing"
39011
+ });
39012
+ }
39013
+ const industry = inferObjectiveIndustry(opts);
39014
+ const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
39015
+ const knowledgeSources = [];
39016
+ const sourceUrl = normalizeString2(opts.sourceUrl);
39017
+ if (sourceUrl) {
39018
+ knowledgeSources.push({
39019
+ type: "website",
39020
+ label: `${businessName} official website`,
39021
+ uri: sourceUrl
39022
+ });
39023
+ }
39024
+ return {
39025
+ schema_version: BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION,
39026
+ business_name: businessName,
39027
+ industry,
39028
+ desired_use_cases: [businessObjective],
39029
+ channels: channelsFromTools(tools),
39030
+ knowledge_sources: knowledgeSources,
39031
+ required_outcomes: [businessObjective],
39032
+ handoff_rules: ["handoff when customer facts, credentials, or booking/action availability are missing"],
39033
+ constraints: ["do not invent business facts or confirm actions without configured evidence/tool results"],
39034
+ optimization_target: inferOptimizationTarget(industry, businessObjective)
39035
+ };
39036
+ }
39037
+ function selectedTemplateSummary2(templateSelection) {
39038
+ const candidates = asArray(asRecord3(templateSelection).candidates).map(asRecord3);
39039
+ const top = candidates[0];
39040
+ const template = asRecord3(top?.template);
39041
+ if (!template || Object.keys(template).length === 0) return null;
39042
+ const contract = asRecord3(template.template_contract);
39043
+ return {
39044
+ template_id: normalizeString2(template.id) || normalizeString2(contract.template_id) || null,
39045
+ template_name: normalizeString2(template.name) || normalizeString2(contract.name) || null,
39046
+ industry: normalizeString2(contract.industry) || null,
39047
+ use_case: normalizeString2(contract.use_case) || null,
39048
+ match_score: finiteNumber(top.match_score),
39049
+ matched_reasons: uniqueStrings(asArray(top.matched_reasons))
39050
+ };
39051
+ }
39052
+ function assertTemplateSelected(templateSelection) {
39053
+ if (selectedTemplateSummary2(templateSelection)) return;
39054
+ throw new FohError({
39055
+ step: "objective.template_selection",
39056
+ error: "No supported template matched this business objective.",
39057
+ remediation: "Use a more specific --business-objective or choose a supported --industry before setup. Do not continue with a guessed template.",
39058
+ statusCode: 422,
39059
+ reasonCode: "objective_template_selection_empty",
39060
+ nextCommands: [
39061
+ "foh templates select --brief @business-brief.json --json"
39062
+ ]
39063
+ });
39064
+ }
39065
+ async function selectTemplateForObjective(opts) {
39066
+ const brief = buildRequirementBrief(opts);
39067
+ const selection = await apiFetch("/v1/console/templates/select", {
39068
+ method: "POST",
39069
+ body: JSON.stringify(brief),
39070
+ apiUrlOverride: opts.apiUrl
39071
+ });
39072
+ assertTemplateSelected(selection);
39073
+ return {
39074
+ brief,
39075
+ selection,
39076
+ selected: selectedTemplateSummary2(selection)
39077
+ };
39078
+ }
39079
+ function assertBusinessName(opts, step) {
39080
+ if (resolveBusinessName(opts)) return;
39081
+ throw new FohError({
39082
+ step,
39083
+ error: "Missing business name.",
39084
+ remediation: "Pass --business-name <name>. Legacy --agency-name <name> is still accepted.",
39085
+ statusCode: 400
39086
+ });
39087
+ }
39088
+ function classifyStatus(input) {
39089
+ const setup = asRecord3(input.setupWorkflow);
39090
+ const live = asRecord3(input.customerLiveStatus);
39091
+ const allReasonCodes = uniqueStrings([
39092
+ ...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
39093
+ ...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
39094
+ ]);
39095
+ const setupDecision = firstString(setup, ["decision", "current_decision", "status"]);
39096
+ const liveDecision = firstString(live, ["decision", "current_decision", "status"]);
39097
+ const explicitGoLive = truthy(live, ["go_live_allowed", "customer_live_ready", "production_ready"]) || truthy(setup, ["go_live_allowed", "customer_live_ready", "production_ready"]);
39098
+ const hasBlockingReason = allReasonCodes.some((code) => !/_ok$|passed|ready|no_findings/.test(code));
39099
+ const hasHoldDecision = [setupDecision, liveDecision].some(
39100
+ (decision) => /hold|blocked|fail|failed|no_go|developer_supervised|founder_assisted/.test(decision.toLowerCase())
39101
+ );
39102
+ if (explicitGoLive && !hasBlockingReason && !hasHoldDecision) {
39103
+ return {
39104
+ status: "pass",
39105
+ reasonCode: "objective_ready",
39106
+ summary: "Objective evidence is ready for the requested launch mode."
39107
+ };
39108
+ }
39109
+ return {
39110
+ status: "hold",
39111
+ reasonCode: allReasonCodes[0] || "objective_needs_next_action",
39112
+ summary: "Objective is not yet proven for the requested mode; follow next_commands exactly."
39113
+ };
39114
+ }
39115
+ function buildObjectiveReport(input) {
39116
+ const setup = asRecord3(input.setupWorkflow);
39117
+ const live = asRecord3(input.customerLiveStatus);
39118
+ const classification = classifyStatus(input);
39119
+ const reasonCodes = uniqueStrings([
39120
+ classification.reasonCode,
39121
+ ...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
39122
+ ...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
39123
+ ]);
39124
+ const debugSource = normalizeString2(input.opts.out) || "latest";
39125
+ const nextCommands = dedupeCommands([
39126
+ ...collectStringArrays(setup, /* @__PURE__ */ new Set(["next_commands"])),
39127
+ ...collectStringArrays(live, /* @__PURE__ */ new Set(["next_commands"])),
39128
+ `foh objective debug --from ${debugSource} --json`
39129
+ ]);
39130
+ const allowedMode = firstString(live, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || firstString(setup, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || null;
39131
+ const blockedModes = uniqueStrings([
39132
+ ...collectStringArrays(setup, /* @__PURE__ */ new Set(["blocked_modes"])),
39133
+ ...collectStringArrays(live, /* @__PURE__ */ new Set(["blocked_modes"]))
39134
+ ]);
39135
+ const evidenceRefs = uniqueStrings([
39136
+ ...collectArtifactRefs(setup),
39137
+ ...collectArtifactRefs(live)
39138
+ ]);
39139
+ const customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
39140
+ const developerReadinessPacket = buildDeveloperReadinessPacket({
39141
+ opts: input.opts,
39142
+ setupWorkflow: setup,
39143
+ customerLiveStatus: live,
39144
+ classification,
39145
+ nextCommands,
39146
+ customerEvidenceActionPacket
39147
+ });
39148
+ return cliEnvelope({
39149
+ schemaVersion: "agent_workbench_report.v1",
39150
+ status: classification.status,
39151
+ reasonCode: classification.reasonCode,
39152
+ summary: classification.summary,
39153
+ ids: {
39154
+ org_id: input.opts.org ?? null
39155
+ },
39156
+ artifacts: {
39157
+ evidence_refs: evidenceRefs
39158
+ },
39159
+ nextCommands,
39160
+ spendClass: "free",
39161
+ safeToRetry: true,
39162
+ extra: {
39163
+ objective: {
39164
+ business_objective: normalizeString2(input.opts.businessObjective) || null,
39165
+ business_name: resolveBusinessName(input.opts) || null,
39166
+ agency_name: normalizeString2(input.opts.agencyName) || resolveBusinessName(input.opts) || null,
39167
+ industry: objectiveIndustryOrNull(input.opts),
39168
+ source_url: normalizeString2(input.opts.sourceUrl) || null,
39169
+ location: resolveLocation(input.opts) || null,
39170
+ branch_location: normalizeString2(input.opts.branchLocation) || resolveLocation(input.opts) || null,
39171
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
39172
+ tools: parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? []
39173
+ },
39174
+ requirement_brief: firstNonEmptyObject(input.requirementBrief) ?? null,
39175
+ selected_template: firstNonEmptyObject(input.selectedTemplate) ?? null,
39176
+ template_selection: firstNonEmptyObject(input.templateSelection) ?? null,
39177
+ allowed_mode: allowedMode,
39178
+ blocked_modes: blockedModes,
39179
+ reason_codes: reasonCodes,
39180
+ next_action: nextCommands[0] ?? null,
39181
+ developer_readiness_packet: developerReadinessPacket,
39182
+ customer_evidence_action_packet: customerEvidenceActionPacket ?? null,
39183
+ source_report_summaries: {
39184
+ setup_workflow: summarizeSourceReport(setup),
39185
+ customer_live_status: summarizeSourceReport(live)
39186
+ },
39187
+ artifact_policy: {
39188
+ normal_path_fields: ["status", "reason_code", "reason_codes", "next_action", "next_commands", "developer_readiness_packet", "customer_evidence_action_packet", "source_report_summaries", "agent_guidance"],
39189
+ diagnostic_fields: ["source_reports", "artifacts.evidence_refs"],
39190
+ raw_artifact_reads_required: false,
39191
+ mutable_latest_json_policy: "diagnostic_only",
39192
+ immutable_evidence_refs_required: true
39193
+ },
39194
+ source_reports: {
39195
+ setup_workflow: setup,
39196
+ customer_live_status: live
39197
+ },
39198
+ agent_guidance: {
39199
+ normal_user_path: "Use objective commands first; inspect source_reports only when debug asks for it.",
39200
+ do_not_infer_from_raw_artifacts: true
39201
+ }
39202
+ }
39203
+ });
39204
+ }
39205
+ function withObjectiveArtifactPath(report, artifactPath) {
39206
+ return {
39207
+ ...report,
39208
+ artifacts: {
39209
+ ...asRecord3(report.artifacts),
39210
+ objective_report: artifactPath
39211
+ },
39212
+ artifact_path: artifactPath
39213
+ };
39214
+ }
39215
+ function stripDiagnosticField(target, fieldPath) {
39216
+ const parts = fieldPath.split(".").map((part) => part.trim()).filter(Boolean);
39217
+ if (parts.length === 0) return;
39218
+ let current = target;
39219
+ for (let index = 0; index < parts.length - 1; index += 1) {
39220
+ const next = asRecord3(current?.[parts[index]]);
39221
+ if (Object.keys(next).length === 0) return;
39222
+ current = next;
39223
+ }
39224
+ if (!current) return;
39225
+ delete current[parts[parts.length - 1]];
39226
+ }
39227
+ function buildObjectiveNormalPathOutput(report) {
39228
+ const output = JSON.parse(JSON.stringify(report));
39229
+ const artifactPolicy = asRecord3(output.artifact_policy);
39230
+ const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString2(value)));
39231
+ for (const fieldPath of diagnosticFields) stripDiagnosticField(output, fieldPath);
39232
+ return output;
39233
+ }
39234
+ function classifyReasonCode(reasonCode) {
39235
+ const normalized = reasonCode.toLowerCase();
39236
+ if (/credential|token|auth|login|permission|scope|key|secret/.test(normalized)) return "credentials";
39237
+ if (/source|official|fact|approval|conflict|provenance|unknown|intake/.test(normalized)) return "source_facts";
39238
+ if (/customer.*evidence|evidence|signoff|ownership|customer_owned|live.*validation/.test(normalized)) return "customer_evidence";
39239
+ if (/voice|stt|tts|turn|audio|latency|tool|webhook|calendar|crm|email|callback|whatsapp/.test(normalized)) return "voice_or_tool_quality";
39240
+ if (/deploy|production|backup|rollback|runtime|env|release|drift/.test(normalized)) return "deploy_or_runtime";
39241
+ if (/docs|api|route|command|registry|schema|contract/.test(normalized)) return "docs_api_mismatch";
39242
+ if (/proof|simulation|cert|widget|artifact|gate|readiness/.test(normalized)) return "runtime_proof";
39243
+ return "uncategorized";
39244
+ }
39245
+ function buildDebugReport(sourcePath, objectiveReport) {
39246
+ const report = asRecord3(objectiveReport);
39247
+ const reasonCodes = uniqueStrings([
39248
+ firstString(report, ["reason_code"]),
39249
+ ...collectStringArrays(report, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
39250
+ ]);
39251
+ const nextCommands = dedupeCommands([
39252
+ ...collectStringArrays(report, /* @__PURE__ */ new Set(["next_commands"])),
39253
+ "foh objective status --business-name <name> --source-url <official_url> --out test-results/objective-status.latest.json --json"
39254
+ ]);
39255
+ const categories = uniqueStrings(reasonCodes.map(classifyReasonCode));
39256
+ const status = firstString(report, ["status"]);
39257
+ const isReady = report.ok === true || status === "pass" || status === "success";
39258
+ const dominantCategory = categories.find((category) => category !== "uncategorized") ?? categories[0] ?? "uncategorized";
39259
+ const sourceReports = asRecord3(report.source_reports);
39260
+ const customerEvidenceActionPacket = firstNonEmptyObject(report.customer_evidence_action_packet);
39261
+ const developerReadinessPacket = firstNonEmptyObject(report.developer_readiness_packet);
39262
+ if (isReady && reasonCodes.length <= 1 && reasonCodes[0] === "objective_ready") {
39263
+ return cliEnvelope({
39264
+ schemaVersion: "agent_workbench_debug.v1",
39265
+ status: "pass",
39266
+ reasonCode: "objective_debug_no_blockers",
39267
+ summary: "Objective report is ready; no repair packet is required.",
39268
+ artifacts: { objective_report: sourcePath },
39269
+ nextCommands,
39270
+ spendClass: "free",
39271
+ safeToRetry: true,
39272
+ extra: {
39273
+ blocker_categories: [],
39274
+ dominant_category: null,
39275
+ repair_packet: null
39276
+ }
39277
+ });
39278
+ }
39279
+ return cliEnvelope({
39280
+ schemaVersion: "agent_workbench_debug.v1",
39281
+ status: "hold",
39282
+ reasonCode: "objective_debug_repair_packet_ready",
39283
+ summary: `Objective is held by ${dominantCategory}; execute the first repair command before rerunning status.`,
39284
+ artifacts: { objective_report: sourcePath },
39285
+ nextCommands,
39286
+ spendClass: "free",
39287
+ safeToRetry: true,
39288
+ extra: {
39289
+ blocker_categories: categories,
39290
+ dominant_category: dominantCategory,
39291
+ repair_packet: {
39292
+ category: dominantCategory,
39293
+ reason_codes: reasonCodes,
39294
+ first_repair_command: nextCommands[0] ?? null,
39295
+ developer_readiness_packet: developerReadinessPacket ?? null,
39296
+ customer_evidence_action_packet: customerEvidenceActionPacket ?? null,
39297
+ setup_decision: firstStringFromUnknown(sourceReports.setup_workflow, ["decision", "current_decision", "status"]) || null,
39298
+ customer_live_decision: firstStringFromUnknown(sourceReports.customer_live_status, ["decision", "current_decision", "status"]) || null,
39299
+ principle: "Do not infer readiness from raw artifacts; repair the named blocker and rerun objective status."
39300
+ }
39301
+ }
39302
+ });
39303
+ }
39304
+ function registerObjective(program3) {
39305
+ const objective = program3.command("objective").description("Agent-native objective workflow: plan, apply, prove, status, debug");
39306
+ objective.command("plan").description("Generate an objective setup/workflow plan").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/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("--out <path>", "Write objective plan JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39307
+ assertBusinessName(opts, "objective.plan");
39308
+ const templateSelection = await selectTemplateForObjective(opts);
39309
+ const report = await apiFetch("/v1/console/agency-setup/workflow", {
39310
+ method: "POST",
39311
+ body: JSON.stringify(buildSetupBody(opts)),
39312
+ orgId: opts.org,
39313
+ apiUrlOverride: opts.apiUrl
39314
+ });
39315
+ const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
39316
+ const objectiveSetup = {
39317
+ ...asRecord3(report),
39318
+ requirement_brief: templateSelection.brief,
39319
+ template_selection: templateSelection.selection,
39320
+ selected_template: templateSelection.selected
39321
+ };
39322
+ const output = outPath ? { ...objectiveSetup, artifact_path: outPath } : objectiveSetup;
39323
+ if (outPath) writeJsonArtifact2(outPath, output);
39324
+ format(output, { json: opts.json ?? false });
39325
+ }));
39326
+ objective.command("apply").description("Apply objective evidence to release launch packet gating").option("--evidence <json|@file>", "Redacted evidence payload JSON, or @/path/to/file.json").option("--plan <path>", "Optional objective plan file to derive evidence payload from").option("--dry-run", "Validate evidence payload without persisting it").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective apply result JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39327
+ const evidence = await resolveEvidenceInput(opts);
39328
+ const body = {
39329
+ ...evidence && typeof evidence === "object" && !Array.isArray(evidence) ? evidence : { evidence }
39330
+ };
39331
+ if (!opts.dryRun) body.apply = true;
39332
+ const report = await apiFetch("/v1/console/release-launch-packet/evidence-dry-run", {
39333
+ method: "POST",
39334
+ body: JSON.stringify(body),
39335
+ orgId: opts.org,
39336
+ apiUrlOverride: opts.apiUrl
39337
+ });
39338
+ const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
39339
+ const output = outPath ? { ...asRecord3(report), artifact_path: outPath } : report;
39340
+ if (outPath) writeJsonArtifact2(outPath, output);
39341
+ format(output, { json: opts.json ?? false });
39342
+ }));
39343
+ objective.command("prove").description("Run objective proof status against customer-live gate").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective proof JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39344
+ assertBusinessName(opts, "objective.prove");
39345
+ const status = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
39346
+ orgId: opts.org,
39347
+ apiUrlOverride: opts.apiUrl
39348
+ });
39349
+ const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
39350
+ const output = outPath ? { ...asRecord3(status), artifact_path: outPath } : status;
39351
+ if (outPath) writeJsonArtifact2(outPath, output);
39352
+ format(output, { json: opts.json ?? false });
39353
+ }));
39354
+ objective.command("status").description("Compose setup and launch evidence into one agent workbench status envelope").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective report JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39355
+ assertBusinessName(opts, "objective.status");
39356
+ const templateSelection = await selectTemplateForObjective(opts);
39357
+ const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
39358
+ method: "POST",
39359
+ body: JSON.stringify(buildSetupBody(opts)),
39360
+ orgId: opts.org,
39361
+ apiUrlOverride: opts.apiUrl
39362
+ });
39363
+ const customerLiveStatus = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
39364
+ orgId: opts.org,
39365
+ apiUrlOverride: opts.apiUrl
39366
+ });
39367
+ const report = buildObjectiveReport({
39368
+ opts,
39369
+ setupWorkflow,
39370
+ customerLiveStatus,
39371
+ requirementBrief: templateSelection.brief,
39372
+ templateSelection: templateSelection.selection,
39373
+ selectedTemplate: templateSelection.selected
39374
+ });
39375
+ const artifactPath = resolveObjectiveReportPath(opts.out);
39376
+ const fullOutput = withObjectiveArtifactPath(report, artifactPath);
39377
+ writeJsonArtifact2(artifactPath, fullOutput);
39378
+ format(buildObjectiveNormalPathOutput(fullOutput), { json: opts.json ?? false });
39379
+ }));
39380
+ objective.command("debug").description("Explain the next debugging action for an objective report").option("--from <source>", "Report source alias or path", "latest").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39381
+ const sourcePath = resolveObjectiveReportPath(opts.from);
39382
+ if (!(0, import_node_fs5.existsSync)(sourcePath)) {
39383
+ format(cliEnvelope({
39384
+ schemaVersion: "agent_workbench_debug.v1",
39385
+ status: "fail",
39386
+ reasonCode: "objective_report_not_found",
39387
+ summary: `Objective report not found at ${sourcePath}.`,
39388
+ artifacts: { objective_report: sourcePath },
39389
+ nextCommands: [
39390
+ `foh objective status --business-name <name> --source-url <official_url> --out ${DEFAULT_OBJECTIVE_REPORT_PATH} --json`
39391
+ ],
39392
+ spendClass: "free",
39393
+ safeToRetry: true
39394
+ }), { json: opts.json ?? false });
39395
+ return;
39396
+ }
39397
+ let objectiveReport;
39398
+ try {
39399
+ objectiveReport = readJsonArtifact(sourcePath);
39400
+ } catch {
39401
+ format(cliEnvelope({
39402
+ schemaVersion: "agent_workbench_debug.v1",
39403
+ status: "fail",
39404
+ reasonCode: "objective_report_invalid",
39405
+ summary: `Objective report at ${sourcePath} is not valid JSON.`,
39406
+ artifacts: { objective_report: sourcePath },
39407
+ nextCommands: [
39408
+ `foh objective status --business-name <name> --source-url <official_url> --out ${DEFAULT_OBJECTIVE_REPORT_PATH} --json`
39409
+ ],
39410
+ spendClass: "free",
39411
+ safeToRetry: true
39412
+ }), { json: opts.json ?? false });
39413
+ return;
39414
+ }
39415
+ format(buildDebugReport(sourcePath, objectiveReport), { json: opts.json ?? false });
39416
+ }));
39417
+ }
39418
+
38545
39419
  // src/commands/interactive.ts
38546
39420
  var import_readline2 = require("readline");
38547
39421
  var import_child_process2 = require("child_process");
@@ -38651,6 +39525,66 @@ var COMMAND_SURFACE_DEFINITIONS = [
38651
39525
  shellSlash: "/status",
38652
39526
  includeInSuggestions: true
38653
39527
  },
39528
+ {
39529
+ id: "objective_status",
39530
+ commandPath: ["objective", "status"],
39531
+ invokeArgs: ["objective", "status", "--help"],
39532
+ label: "objective status",
39533
+ descriptionFallback: "Compose setup and launch evidence into one agent workbench status envelope.",
39534
+ requiresAuth: true,
39535
+ requiresOrg: true,
39536
+ mutatesState: "read",
39537
+ examples: ['foh objective status --business-name "Acme" --source-url https://example.com --out test-results/objective-status.latest.json --json'],
39538
+ homeContexts: ["authenticated_with_org"],
39539
+ homeAliases: ["objective", "status objective"],
39540
+ shellSlash: "/objective",
39541
+ includeInSuggestions: true
39542
+ },
39543
+ {
39544
+ id: "objective_plan",
39545
+ commandPath: ["objective", "plan"],
39546
+ invokeArgs: ["objective", "plan", "--help"],
39547
+ label: "objective plan",
39548
+ descriptionFallback: "Generate an objective setup/workflow plan.",
39549
+ requiresAuth: true,
39550
+ requiresOrg: true,
39551
+ mutatesState: "read",
39552
+ examples: ['foh objective plan --business-name "Acme" --business-objective "Qualify leads" --source-url https://example.com --out test-results/objective-plan.latest.json --json'],
39553
+ homeContexts: ["authenticated_with_org"],
39554
+ homeAliases: ["objective plan"],
39555
+ shellSlash: "/objective-plan",
39556
+ includeInSuggestions: true
39557
+ },
39558
+ {
39559
+ id: "objective_apply",
39560
+ commandPath: ["objective", "apply"],
39561
+ invokeArgs: ["objective", "apply", "--help"],
39562
+ label: "objective apply",
39563
+ descriptionFallback: "Apply objective evidence packets against customer-live launch packet gates.",
39564
+ requiresAuth: true,
39565
+ requiresOrg: true,
39566
+ mutatesState: "write",
39567
+ examples: ["foh objective apply --evidence @test-results/evidence.json --org <org-id> --out test-results/objective-apply.latest.json --json"],
39568
+ homeContexts: ["authenticated_with_org"],
39569
+ homeAliases: ["objective apply"],
39570
+ shellSlash: "/objective-apply",
39571
+ includeInSuggestions: true
39572
+ },
39573
+ {
39574
+ id: "objective_prove",
39575
+ commandPath: ["objective", "prove"],
39576
+ invokeArgs: ["objective", "prove", "--help"],
39577
+ label: "objective prove",
39578
+ descriptionFallback: "Check one objective against customer-live proof gates.",
39579
+ requiresAuth: true,
39580
+ requiresOrg: true,
39581
+ mutatesState: "read",
39582
+ examples: ['foh objective prove --business-name "Acme" --source-url https://example.com --environment staging --json'],
39583
+ homeContexts: ["authenticated_with_org"],
39584
+ homeAliases: ["objective prove"],
39585
+ shellSlash: "/objective-prove",
39586
+ includeInSuggestions: true
39587
+ },
38654
39588
  {
38655
39589
  id: "templates_list",
38656
39590
  commandPath: ["templates", "list"],
@@ -38735,9 +39669,9 @@ var COMMAND_SURFACE_DEFINITIONS = [
38735
39669
  includeInSuggestions: false
38736
39670
  },
38737
39671
  {
38738
- id: "whatsapp_start",
38739
- commandPath: ["channel", "whatsapp", "start"],
38740
- label: "whatsapp start",
39672
+ id: "whatsapp_onboard",
39673
+ commandPath: ["channel", "whatsapp", "onboard"],
39674
+ label: "whatsapp onboard",
38741
39675
  descriptionFallback: "WhatsApp readiness path",
38742
39676
  mutatesState: "write",
38743
39677
  shellSlash: "/whatsapp",
@@ -38791,6 +39725,7 @@ var HOME_QUICK_ACTION_ORDER = {
38791
39725
  unauthenticated: ["auth_login", "interactive_shell", "full_help", "auth_login_help"],
38792
39726
  authenticated_no_org: ["org_list", "interactive_shell", "org_use_help", "auth_whoami", "full_help"],
38793
39727
  authenticated_with_org: [
39728
+ "objective_status",
38794
39729
  "tenant_status",
38795
39730
  "interactive_shell",
38796
39731
  "templates_list",
@@ -39133,7 +40068,7 @@ async function runSelf(args, apiUrlOverride) {
39133
40068
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
39134
40069
  spawnArgs.push("--api-url", apiUrlOverride);
39135
40070
  }
39136
- return await new Promise((resolve15, reject) => {
40071
+ return await new Promise((resolve16, reject) => {
39137
40072
  const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39138
40073
  stdio: "inherit",
39139
40074
  env: {
@@ -39143,7 +40078,7 @@ async function runSelf(args, apiUrlOverride) {
39143
40078
  }
39144
40079
  });
39145
40080
  child.once("error", reject);
39146
- child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
40081
+ child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
39147
40082
  });
39148
40083
  }
39149
40084
  function resolveInteractiveState(apiUrlOverride) {
@@ -39330,10 +40265,10 @@ async function confirmMutationIfNeeded(rl, args) {
39330
40265
  const commandEntry = getCommandGraphEntryForArgs(args);
39331
40266
  const label = commandEntry ? `foh ${commandEntry.args.join(" ")}` : `foh ${args.join(" ")}`;
39332
40267
  const warning = mutationState === "high_risk" ? "high-risk" : "write";
39333
- const approved = await new Promise((resolve15) => {
40268
+ const approved = await new Promise((resolve16) => {
39334
40269
  rl.question(`Confirm ${warning} command ${label}? [y/N]: `, (answer) => {
39335
40270
  const normalized = answer.trim().toLowerCase();
39336
- resolve15(normalized === "y" || normalized === "yes");
40271
+ resolve16(normalized === "y" || normalized === "yes");
39337
40272
  });
39338
40273
  });
39339
40274
  if (approved) return { approved: true };
@@ -39501,7 +40436,7 @@ function registerInteractive(program3) {
39501
40436
  rl.on("SIGINT", () => {
39502
40437
  rl.close();
39503
40438
  });
39504
- await new Promise((resolve15) => {
40439
+ await new Promise((resolve16) => {
39505
40440
  rl.on("close", () => {
39506
40441
  process.stdout.write("\n");
39507
40442
  memory.history = rl.history.slice(0, 500);
@@ -39509,7 +40444,7 @@ function registerInteractive(program3) {
39509
40444
  flushInteractiveSessionArtifact(sessionArtifact);
39510
40445
  flushInteractiveSessionReport(sessionArtifact);
39511
40446
  saveInteractiveShellMemory(memory);
39512
- resolve15();
40447
+ resolve16();
39513
40448
  });
39514
40449
  });
39515
40450
  });
@@ -39841,7 +40776,7 @@ async function runSelf2(args, apiUrlOverride) {
39841
40776
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
39842
40777
  spawnArgs.push("--api-url", apiUrlOverride);
39843
40778
  }
39844
- return await new Promise((resolve15, reject) => {
40779
+ return await new Promise((resolve16, reject) => {
39845
40780
  const child = (0, import_child_process3.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
39846
40781
  stdio: "inherit",
39847
40782
  env: {
@@ -39851,7 +40786,7 @@ async function runSelf2(args, apiUrlOverride) {
39851
40786
  }
39852
40787
  });
39853
40788
  child.once("error", reject);
39854
- child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
40789
+ child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
39855
40790
  });
39856
40791
  }
39857
40792
  function shouldUseInteractiveHome(argv) {
@@ -40200,17 +41135,17 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
40200
41135
  async function applyRepoUpdate(repoRoot) {
40201
41136
  const scriptPath = (0, import_path10.join)(repoRoot, "scripts", "Install-FohCli.ps1");
40202
41137
  if (process.platform === "win32") {
40203
- return await new Promise((resolve15, reject) => {
41138
+ return await new Promise((resolve16, reject) => {
40204
41139
  const child = (0, import_child_process4.spawn)(
40205
41140
  "powershell",
40206
41141
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
40207
41142
  { stdio: "inherit" }
40208
41143
  );
40209
41144
  child.once("error", reject);
40210
- child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
41145
+ child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
40211
41146
  });
40212
41147
  }
40213
- return await new Promise((resolve15, reject) => {
41148
+ return await new Promise((resolve16, reject) => {
40214
41149
  const child = (0, import_child_process4.spawn)(
40215
41150
  "corepack",
40216
41151
  ["pnpm", "cli:install:global"],
@@ -40220,7 +41155,7 @@ async function applyRepoUpdate(repoRoot) {
40220
41155
  }
40221
41156
  );
40222
41157
  child.once("error", reject);
40223
- child.once("close", (code) => resolve15(typeof code === "number" ? code : 1));
41158
+ child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
40224
41159
  });
40225
41160
  }
40226
41161
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -40692,9 +41627,9 @@ function readCommandRecords(runDir) {
40692
41627
  }
40693
41628
 
40694
41629
  // src/lib/external-agent-executor.ts
40695
- var import_fs20 = require("fs");
41630
+ var import_fs19 = require("fs");
40696
41631
  var import_os3 = require("os");
40697
- var import_path19 = require("path");
41632
+ var import_path18 = require("path");
40698
41633
  var import_child_process6 = require("child_process");
40699
41634
 
40700
41635
  // src/lib/external-agent-executor-env.ts
@@ -40865,824 +41800,303 @@ function copyExternalAgentCommandCaptureArtifacts(input) {
40865
41800
  }
40866
41801
 
40867
41802
  // src/lib/external-agent-executor-classification.ts
40868
- var import_fs18 = require("fs");
40869
- var import_path17 = require("path");
40870
-
40871
- // src/lib/external-agent-run-summary.ts
40872
41803
  var import_fs17 = require("fs");
40873
41804
  var import_path16 = require("path");
40874
- var REQUIRED_RUN_FIELDS = [
40875
- "schema_version",
40876
- "run_id",
40877
- "status",
40878
- "model_provider",
40879
- "model_name",
40880
- "prompt_version",
40881
- "started_at",
40882
- "manual_intervention_count",
40883
- "environment",
40884
- "public_entrypoints",
40885
- "commands_run",
40886
- "docs_pages_used",
40887
- "artifacts"
40888
- ];
40889
- var VALID_STATUSES = /* @__PURE__ */ new Set(["pass", "hold", "fail"]);
40890
- var DOC_URL_RE = /https:\/\/frontofhouse\.okii\.uk\/[^\s"'`)<>,;\\\]}]*/g;
40891
- function quoteShellArg(value) {
40892
- const text = String(value);
40893
- if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
40894
- return `"${text.replace(/(["$`])/g, "\\$1")}"`;
41805
+ function proofArtifactPasses(runDir) {
41806
+ const proofPath = (0, import_path16.join)(runDir, "proof.json");
41807
+ if (!(0, import_fs17.existsSync)(proofPath)) return false;
41808
+ try {
41809
+ const parsed = JSON.parse((0, import_fs17.readFileSync)(proofPath, "utf8"));
41810
+ return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
41811
+ } catch {
41812
+ return false;
41813
+ }
40895
41814
  }
40896
- function externalAgentSummaryCommand(root) {
40897
- const summaryPath = (0, import_path16.join)(root, "latest-summary.json");
40898
- const reportPath = (0, import_path16.join)(root, "summary.report.json");
41815
+ function readIfExists(path2) {
41816
+ return (0, import_fs17.existsSync)(path2) ? (0, import_fs17.readFileSync)(path2, "utf8") : "";
41817
+ }
41818
+ function relativeArtifactName(path2) {
41819
+ return (0, import_path16.basename)(path2);
41820
+ }
41821
+ function externalAgentSummaryTemplateCommand() {
40899
41822
  return [
40900
41823
  "foh",
40901
41824
  "eval",
40902
41825
  "external-agent",
40903
41826
  "summary",
40904
41827
  "--root",
40905
- quoteShellArg(root),
41828
+ "<batch_dir>",
40906
41829
  "--out",
40907
- quoteShellArg(summaryPath),
41830
+ "<batch_dir>/latest-summary.json",
40908
41831
  "--report",
40909
- quoteShellArg(reportPath),
41832
+ "<batch_dir>/summary.report.json",
40910
41833
  "--json"
40911
41834
  ].join(" ");
40912
41835
  }
40913
- function readJson(filePath) {
40914
- return JSON.parse((0, import_fs17.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
40915
- }
40916
- function readNdjson(filePath) {
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) => {
40919
- try {
40920
- const parsed = JSON.parse(line);
40921
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
40922
- } catch {
40923
- return null;
40924
- }
40925
- }).filter((record2) => Boolean(record2));
40926
- }
40927
- function asObject(value) {
40928
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
40929
- }
40930
- function toArray2(value) {
40931
- return Array.isArray(value) ? value : [];
40932
- }
40933
- function increment(map3, key, amount = 1) {
40934
- const normalized = String(key || "unknown");
40935
- map3.set(normalized, (map3.get(normalized) || 0) + amount);
40936
- }
40937
- function ranked(map3) {
40938
- return Array.from(map3.entries()).map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
40939
- }
40940
- function collectDocUrls(text) {
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();
40942
- }
40943
- function findRunCandidates(root) {
40944
- if (!(0, import_fs17.existsSync)(root)) return [];
40945
- const candidates = [];
40946
- const seenRunDirs = /* @__PURE__ */ new Set();
40947
- const captureDirs = [];
40948
- const stack = [root];
40949
- while (stack.length > 0) {
40950
- const current = stack.pop();
40951
- if (!current) continue;
40952
- for (const entry of (0, import_fs17.readdirSync)(current, { withFileTypes: true })) {
40953
- const absolute = (0, import_path16.join)(current, entry.name);
40954
- if (entry.isDirectory()) {
40955
- stack.push(absolute);
40956
- } else if (entry.isFile() && entry.name === "run.json") {
40957
- candidates.push({ path: absolute, synthetic: false });
40958
- seenRunDirs.add((0, import_path16.dirname)(absolute));
40959
- } else if (entry.isFile() && entry.name === "commands.ndjson") {
40960
- captureDirs.push(current);
40961
- }
40962
- }
41836
+ function classifyExternalAgentRun(input) {
41837
+ if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
41838
+ if (!input.artifactSafetyOk) return { status: "fail", reasonCode: "external_agent_artifact_safety_blocked" };
41839
+ const completedCommands = readCommandRecords(input.run.run_dir).filter((record2) => record2.phase === "completed");
41840
+ const observedVersions = completedCommands.map((record2) => String(record2.cli_version || "").trim()).filter((version2) => /^\d+\.\d+\.\d+$/.test(version2));
41841
+ if (observedVersions.some((version2) => version2 !== CLI_VERSION)) {
41842
+ return { status: "hold", reasonCode: "external_agent_cli_version_drift" };
40963
41843
  }
40964
- for (const captureDir of captureDirs) {
40965
- if (seenRunDirs.has(captureDir)) continue;
40966
- candidates.push({ path: (0, import_path16.join)(captureDir, "run.json"), synthetic: true });
41844
+ const commandReasonCodes2 = completedCommands.flatMap((record2) => [
41845
+ String(record2.reason_code || ""),
41846
+ ...Array.isArray(record2.check_reason_codes) ? record2.check_reason_codes.map((code) => String(code || "")) : []
41847
+ ]).filter(Boolean);
41848
+ const hasCommandReason = (pattern) => commandReasonCodes2.some((reason) => pattern.test(reason));
41849
+ if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
41850
+ return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
40967
41851
  }
40968
- return candidates.sort((a, b) => a.path.localeCompare(b.path));
40969
- }
40970
- function validateExternalAgentRun(value) {
40971
- const findings = [];
40972
- const run = asObject(value);
40973
- if (!run) return [{ id: "run_not_object", detail: "run artifact must be an object" }];
40974
- for (const field of REQUIRED_RUN_FIELDS) {
40975
- if (!(field in run)) findings.push({ id: "required_field_missing", field });
41852
+ if (hasCommandReason(/provider_capacity_blocked/i)) {
41853
+ return { status: "hold", reasonCode: "provider_capacity_blocked" };
40976
41854
  }
40977
- if (run.schema_version !== "external_agent_run.v1") {
40978
- findings.push({ id: "schema_version_invalid", expected: "external_agent_run.v1", actual: run.schema_version ?? null });
41855
+ if (hasCommandReason(/byon_voice_number_not_configured/i)) {
41856
+ return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
40979
41857
  }
40980
- if (!VALID_STATUSES.has(String(run.status || ""))) {
40981
- findings.push({ id: "status_invalid", expected: Array.from(VALID_STATUSES), actual: run.status ?? null });
41858
+ if (hasCommandReason(/contact_phone_provisioning_failed/i)) {
41859
+ return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
40982
41860
  }
40983
- if ((run.status === "hold" || run.status === "fail") && !String(run.failure_reason_code || "").trim()) {
40984
- findings.push({ id: "failure_reason_code_missing" });
41861
+ if (hasCommandReason(/voice_contact_expected_no_spend_hold/i)) {
41862
+ return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
40985
41863
  }
40986
- if (!Number.isInteger(run.manual_intervention_count) || Number(run.manual_intervention_count) < 0) {
40987
- findings.push({ id: "manual_intervention_count_invalid" });
41864
+ if (hasCommandReason(/contact_phone_missing/i)) {
41865
+ return { status: "hold", reasonCode: "voice_contact_phone_missing" };
40988
41866
  }
40989
- if (!Array.isArray(run.commands_run)) findings.push({ id: "commands_run_invalid" });
40990
- if (!Array.isArray(run.docs_pages_used)) findings.push({ id: "docs_pages_used_invalid" });
40991
- if (!asObject(run.environment)) findings.push({ id: "environment_invalid" });
40992
- if (!asObject(run.artifacts)) findings.push({ id: "artifacts_invalid" });
40993
- if (toArray2(run.public_entrypoints).length === 0) findings.push({ id: "public_entrypoints_missing" });
40994
- return findings;
40995
- }
40996
- function runSortTime(run) {
40997
- const raw = String(run.ended_at || run.started_at || "");
40998
- const time3 = Date.parse(raw);
40999
- return Number.isFinite(time3) ? time3 : 0;
41000
- }
41001
- function latestCommandTime(commands) {
41002
- const times = commands.map((command) => String(command.completed_at || command.started_at || command.recorded_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => b.time - a.time);
41003
- return times[0]?.raw ?? null;
41004
- }
41005
- function firstCommandTime(commands) {
41006
- const times = commands.map((command) => String(command.started_at || command.recorded_at || command.completed_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => a.time - b.time);
41007
- return times[0]?.raw ?? null;
41008
- }
41009
- function commandReasonCodes(commands) {
41010
- const codes = /* @__PURE__ */ new Set();
41011
- for (const command of commands) {
41012
- if (command.reason_code) codes.add(String(command.reason_code));
41013
- for (const reasonCode of toArray2(command.check_reason_codes)) {
41014
- if (reasonCode) codes.add(String(reasonCode));
41015
- }
41867
+ if (hasCommandReason(/sim(?:ulation)?[_-]?cert(?:ify|ification)?.*failed|simulation_certification_failed/i)) {
41868
+ return { status: "hold", reasonCode: "simulation_certification_failed" };
41016
41869
  }
41017
- return Array.from(codes);
41018
- }
41019
- function syntheticStatusFromCommands(commands) {
41020
- const commandReasons = commandReasonCodes(commands);
41021
- const failed = commands.find((command) => {
41022
- const status = String(command.status || "").toLowerCase();
41023
- return status === "fail" || typeof command.exit_code === "number" && command.exit_code !== 0 && status !== "hold";
41024
- });
41025
- if (failed) {
41026
- return {
41027
- status: "fail",
41028
- reasonCode: String(failed.reason_code || commandReasons[0] || "external_agent_command_failed")
41029
- };
41870
+ if (hasCommandReason(/proof_held/i)) {
41871
+ return { status: "hold", reasonCode: "external_agent_proof_held" };
41030
41872
  }
41031
- const held = commands.find((command) => String(command.status || "").toLowerCase() === "hold");
41032
- if (held) {
41033
- return {
41034
- status: "hold",
41035
- reasonCode: String(held.reason_code || commandReasons[0] || "external_agent_command_held")
41036
- };
41873
+ if (hasCommandReason(/agent_limit_reached/i)) {
41874
+ return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
41037
41875
  }
41038
- if (commands.length === 0) {
41039
- return { status: "hold", reasonCode: "external_agent_capture_empty" };
41876
+ const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
41877
+ const metadataBlockerCodes = agentMetadata.blocker_reason_codes;
41878
+ const hasMetadataBlocker = (pattern) => metadataBlockerCodes.some((reason) => pattern.test(reason));
41879
+ if (hasMetadataBlocker(/^customer_owned_requirement_unverified:/i)) {
41880
+ return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
41040
41881
  }
41041
- return { status: "pass", reasonCode: null };
41882
+ if (hasMetadataBlocker(/^api_health:/i) && metadataBlockerCodes.some((reason) => /^customer_owned_requirement_unverified:/i.test(reason))) {
41883
+ return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
41884
+ }
41885
+ const firstCommandReasonCode = commandReasonCodes2.find((reason) => reason.trim().length > 0) ?? null;
41886
+ if (firstCommandReasonCode) {
41887
+ return { status: "hold", reasonCode: firstCommandReasonCode };
41888
+ }
41889
+ const lastMessage = readIfExists(input.run.outputs.last_message);
41890
+ const stderr = readIfExists(input.run.outputs.stderr);
41891
+ const combined = `${lastMessage}
41892
+ ${stderr}`;
41893
+ if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
41894
+ return { status: "fail", reasonCode: "private_repo_assumption_detected" };
41895
+ }
41896
+ if (/(?:blocked|rejected|declined) by policy|EXEC_POLICY_BLOCKED|command execution was rejected|shell commands were rejected/i.test(combined)) {
41897
+ return { status: "hold", reasonCode: "codex_exec_policy_blocked" };
41898
+ }
41899
+ if (/bwrap:.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|bubblewrap.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|Failed RTM_NEWADDR|ENV_SANDBOX_EXEC_BLOCKED|permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock|legacy[_ -]?landlock.*incompatible/i.test(combined)) {
41900
+ return { status: "hold", reasonCode: "codex_sandbox_exec_blocked" };
41901
+ }
41902
+ if (/ENV_NETWORK_DNS_BLOCK|Could not resolve host|npm ping.*timeout|NO_EXECUTABLE_INSTALL/i.test(combined)) {
41903
+ return { status: "hold", reasonCode: "codex_network_dns_blocked" };
41904
+ }
41905
+ if (new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i").test(combined)) {
41906
+ return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
41907
+ }
41908
+ if (/provider_capacity_blocked/i.test(combined)) {
41909
+ return { status: "hold", reasonCode: "provider_capacity_blocked" };
41910
+ }
41911
+ if (/byon_voice_number_not_configured/i.test(combined)) {
41912
+ return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
41913
+ }
41914
+ if (/contact_phone_provisioning_failed/i.test(combined)) {
41915
+ return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
41916
+ }
41917
+ if (/voice_contact_expected_no_spend_hold/i.test(combined)) {
41918
+ return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
41919
+ }
41920
+ if (/contact_phone_missing/i.test(combined)) {
41921
+ return { status: "hold", reasonCode: "voice_contact_phone_missing" };
41922
+ }
41923
+ if (/simulation_certification_failed/i.test(combined)) {
41924
+ return { status: "hold", reasonCode: "simulation_certification_failed" };
41925
+ }
41926
+ if (/proof_held/i.test(combined)) {
41927
+ return { status: "hold", reasonCode: "external_agent_proof_held" };
41928
+ }
41929
+ if (/agent_limit_reached/i.test(combined)) {
41930
+ return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
41931
+ }
41932
+ if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
41933
+ return { status: "hold", reasonCode: "auth_browser_approval_required" };
41934
+ }
41935
+ if (input.exitCode !== 0) return { status: "hold", reasonCode: `${input.run.command}_runner_nonzero_exit` };
41936
+ if (proofArtifactPasses(input.run.run_dir)) return { status: "pass", reasonCode: null };
41937
+ return { status: "hold", reasonCode: "external_agent_proof_artifact_missing" };
41042
41938
  }
41043
- function synthesizeRunFromCapture(runPath) {
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")) : {});
41047
- const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
41048
- const commandClassification = syntheticStatusFromCommands(commands);
41049
- const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
41050
- const reasonCode = commandClassification.status === "fail" ? commandClassification.reasonCode : blockerCodes[0] || commandClassification.reasonCode;
41051
- const firstCommand = commands[0] || {};
41052
- const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
41053
- const endedAt = latestCommandTime(commands) || startedAt;
41054
- const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
41055
- const runId = (0, import_path16.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
41939
+ function buildExecutedExternalAgentRunArtifact(input) {
41940
+ const commands = readCommandRecords(input.run.run_dir);
41941
+ const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
41056
41942
  return {
41057
41943
  schema_version: "external_agent_run.v1",
41058
- run_id: runId,
41059
- status,
41060
- failure_reason_code: status === "pass" ? null : reasonCode || "external_agent_capture_unfinalized",
41061
- model_provider: "unknown",
41062
- model_name: "unknown",
41063
- prompt_version: String(firstCommand.prompt_version || "unknown"),
41064
- started_at: startedAt,
41065
- ended_at: endedAt,
41944
+ run_id: input.run.run_id,
41945
+ status: input.status,
41946
+ failure_reason_code: input.reasonCode,
41947
+ model_provider: input.run.model_provider,
41948
+ model_name: input.run.model_name,
41949
+ runner_model: input.run.runner_model,
41950
+ agent_shell: `${input.run.command}-exec`,
41951
+ workspace_type: "clean-no-repo-programmatic",
41952
+ prompt_version: input.run.prompt_version,
41953
+ prompt_path: "prompt.txt",
41954
+ started_at: input.startedAt,
41955
+ ended_at: input.endedAt,
41066
41956
  manual_intervention_count: 0,
41957
+ manual_interventions: [],
41067
41958
  environment: {
41068
- foh_cli_version: firstCommand.cli_version || null,
41069
- capture_only: true
41959
+ os: process.platform,
41960
+ node_version: process.version,
41961
+ npm_version: null,
41962
+ foh_cli_version: CLI_VERSION,
41963
+ runner_exit_code: input.exitCode,
41964
+ runner_timed_out: input.timedOut,
41965
+ duration_ms: input.durationMs
41070
41966
  },
41071
41967
  public_entrypoints: [
41072
41968
  "https://frontofhouse.okii.uk",
41969
+ "https://frontofhouse.okii.uk/llms.txt",
41970
+ "https://frontofhouse.okii.uk/openapi.yaml",
41073
41971
  "npx --yes @f-o-h/cli@latest"
41074
41972
  ],
41075
- commands_run: commands.map((command) => String(command.command || "")).filter(Boolean),
41076
- docs_pages_used: docs,
41973
+ commands_run: commands.map((command) => command.command),
41974
+ docs_pages_used: agentMetadata.docs_pages_used,
41975
+ eval_state: {
41976
+ lifecycle_strategy: "reuse_existing_eval_state",
41977
+ org_reuse_expected: true,
41978
+ agent_reuse_expected: true,
41979
+ widget_reuse_expected: true,
41980
+ fresh_org_expected: false,
41981
+ ephemeral_org_expected: false,
41982
+ fresh_agent_expected: false,
41983
+ phone_purchase_expected: false,
41984
+ paid_resource_creation_expected: false,
41985
+ spend_policy_expected: NO_SPEND_POLICY,
41986
+ cleanup_expected: false,
41987
+ cleanup_strategy: "no_cleanup_for_reused_eval_state",
41988
+ paid_resource_strategy: "blocked_unless_explicit_byon_or_operator_approved",
41989
+ rationale: "Mass external-agent evals benchmark public docs/CLI/API clarity; reuse avoids paid phone and Twilio inventory churn."
41990
+ },
41077
41991
  artifacts: {
41078
- command_log: "commands.ndjson",
41079
- agent_metadata: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
41080
- capture_only: true
41992
+ terminal_transcript: relativeArtifactName(input.run.outputs.jsonl),
41993
+ command_log: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
41994
+ proof_bundle: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
41995
+ replay_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
41996
+ knowledge_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
41997
+ improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
41998
+ agent_metadata: agentMetadata.path,
41999
+ notes: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
42000
+ runner_last_message: relativeArtifactName(input.run.outputs.last_message),
42001
+ runner_stderr: relativeArtifactName(input.run.outputs.stderr),
42002
+ codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
42003
+ codex_stderr: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.stderr) : null,
42004
+ artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
41081
42005
  },
41082
- summary: "Synthetic run record created from external-agent capture artifacts because run.json was missing."
42006
+ 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}.`,
42007
+ next_commands: input.status === "pass" ? [externalAgentSummaryTemplateCommand()] : [
42008
+ "foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
42009
+ "foh bug improve --from external-agent-run --file <run_dir>/run.json --out <run_dir>/improvement-packet.json --json",
42010
+ externalAgentSummaryTemplateCommand()
42011
+ ]
41083
42012
  };
41084
42013
  }
41085
- function cohortIdForRunPath(root, runPath) {
41086
- const normalized = (0, import_path16.relative)(root, (0, import_path16.dirname)(runPath)).replaceAll("\\", "/");
41087
- const parts = normalized.split("/").filter(Boolean);
41088
- if (parts.length === 0) return ".";
41089
- if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) && parts[1]) return `${parts[0]}/${parts[1]}`;
41090
- return parts[0];
42014
+
42015
+ // src/lib/external-agent-runner-execution.ts
42016
+ var import_child_process5 = require("child_process");
42017
+ var import_fs18 = require("fs");
42018
+ var import_path17 = require("path");
42019
+ function buildCommandInvocation(command, args) {
42020
+ if (command.toLowerCase().endsWith(".cmd")) {
42021
+ const binDir = (0, import_path17.dirname)(command);
42022
+ const codexEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
42023
+ if ((0, import_fs18.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
42024
+ const geminiEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
42025
+ if ((0, import_fs18.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
42026
+ }
42027
+ return { command, args };
41091
42028
  }
41092
- function readRunRecords(root, cwd) {
41093
- const records = [];
41094
- const invalid_runs = [];
41095
- for (const candidate of findRunCandidates(root)) {
41096
- const file2 = candidate.path;
41097
- try {
41098
- const parsed = candidate.synthetic ? synthesizeRunFromCapture(file2) : readJson(file2);
41099
- const findings = validateExternalAgentRun(parsed);
41100
- if (findings.length > 0) {
41101
- invalid_runs.push({ path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"), findings });
41102
- continue;
42029
+ function spawnExternalAgentRunner(input) {
42030
+ return new Promise((resolveRun) => {
42031
+ const started = Date.now();
42032
+ const commandInvocation = buildCommandInvocation(input.command, input.args);
42033
+ const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
42034
+ cwd: input.cwd,
42035
+ env: input.env,
42036
+ shell: false,
42037
+ stdio: ["pipe", "pipe", "pipe"],
42038
+ windowsHide: true
42039
+ });
42040
+ const stdout = (0, import_fs18.createWriteStream)(input.stdoutPath, { flags: "w" });
42041
+ const stderr = (0, import_fs18.createWriteStream)(input.stderrPath, { flags: "w" });
42042
+ child.stdout.pipe(stdout);
42043
+ child.stderr.pipe(stderr);
42044
+ child.stdin.end(input.prompt);
42045
+ let timedOut = false;
42046
+ const timer = setTimeout(() => {
42047
+ timedOut = true;
42048
+ if (child.pid && process.platform === "win32") {
42049
+ (0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
42050
+ } else {
42051
+ child.kill("SIGKILL");
41103
42052
  }
41104
- const run = parsed;
41105
- records.push({
41106
- path: file2,
41107
- run,
41108
- cohort_id: cohortIdForRunPath(root, file2),
41109
- sort_time: runSortTime(run)
42053
+ }, input.timeoutMs);
42054
+ child.on("close", (exitCode) => {
42055
+ clearTimeout(timer);
42056
+ stdout.end();
42057
+ stderr.end();
42058
+ resolveRun({
42059
+ exitCode,
42060
+ timedOut,
42061
+ durationMs: Date.now() - started
41110
42062
  });
41111
- } catch (error2) {
41112
- invalid_runs.push({
41113
- path: (0, import_path16.relative)(cwd, file2).replaceAll("\\", "/"),
41114
- findings: [{ id: "json_parse_failed", detail: error2 instanceof Error ? error2.message : String(error2) }]
42063
+ });
42064
+ child.on("error", () => {
42065
+ clearTimeout(timer);
42066
+ stdout.end();
42067
+ stderr.end();
42068
+ resolveRun({
42069
+ exitCode: null,
42070
+ timedOut,
42071
+ durationMs: Date.now() - started
41115
42072
  });
41116
- }
41117
- }
41118
- return { records, invalid_runs };
41119
- }
41120
- function latestCohortId(records) {
41121
- return records.slice().sort((a, b) => b.sort_time - a.sort_time || b.path.localeCompare(a.path))[0]?.cohort_id ?? null;
41122
- }
41123
- function ownerSubsystemFor(reasonCode) {
41124
- const reason = String(reasonCode || "").toLowerCase();
41125
- if (reason.includes("simulation") || reason.includes("certification") || reason.includes("scenario")) return "dojo_certification";
41126
- if (reason.includes("contact_phone") || reason.includes("voice_contact") || reason.includes("provider_capacity") || reason.includes("byon")) return "voice_contact";
41127
- if (reason.includes("exec_policy") || reason.includes("policy_blocked") || reason.includes("sandbox") || reason.includes("runner") || reason.includes("codex")) return "infra_runner";
41128
- if (reason.includes("api") || reason.includes("http_4") || reason.includes("http_5") || reason.includes("404") || reason.includes("500") || reason.includes("roundtrip")) return "api_contract";
41129
- if (reason.includes("cli") || reason.includes("command") || reason.includes("flag")) return "cli";
41130
- if (reason.includes("docs") || reason.includes("unclear") || reason.includes("not_found")) return "docs";
41131
- if (reason.includes("auth") || reason.includes("org") || reason.includes("config")) return "infra_runner";
41132
- if (reason.includes("runtime") || reason.includes("widget") || reason.includes("proof")) return "runtime";
41133
- return "product_ux";
41134
- }
41135
- function recommendedFixFor(reasonCode) {
41136
- const owner = ownerSubsystemFor(reasonCode);
41137
- if (owner === "api_contract") return "fix_api";
41138
- if (owner === "cli") return "fix_cli";
41139
- if (owner === "docs") return "fix_docs";
41140
- if (owner === "runtime") return "fix_runtime";
41141
- if (owner === "dojo_certification") return "add_test";
41142
- return "fix_config";
42073
+ });
42074
+ });
41143
42075
  }
41144
- function collapseCommandRecords(records) {
41145
- const order = [];
41146
- const byId = /* @__PURE__ */ new Map();
41147
- for (const record2 of records) {
41148
- const id = String(record2.command_id || `${record2.recorded_at || ""}:${record2.command || ""}`);
41149
- if (!byId.has(id)) order.push(id);
41150
- const previous = byId.get(id);
41151
- byId.set(id, record2.phase === "completed" ? record2 : previous || record2);
42076
+
42077
+ // src/lib/external-agent-executor.ts
42078
+ var GEMINI_HEADLESS_PROBE_TIMEOUT_MS = 15e3;
42079
+ var DEFAULT_FOH_API_URL2 = "https://api.frontofhouse.okii.uk";
42080
+ var ExternalAgentExecutorError = class extends Error {
42081
+ reasonCode;
42082
+ constructor(reasonCode, message) {
42083
+ super(message);
42084
+ this.name = "ExternalAgentExecutorError";
42085
+ this.reasonCode = reasonCode;
41152
42086
  }
41153
- return order.map((id) => byId.get(id)).filter((record2) => Boolean(record2));
42087
+ };
42088
+ function readExternalAgentEvalAuthEnv(env = process.env) {
42089
+ return {
42090
+ token: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || "").trim(),
42091
+ orgId: String(env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || "").trim(),
42092
+ apiUrl: String(env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_API_URL || DEFAULT_FOH_API_URL2).trim() || DEFAULT_FOH_API_URL2,
42093
+ expiresAt: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || "").trim()
42094
+ };
41154
42095
  }
41155
- function readCommandOutputJson(runDir, command) {
41156
- const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
41157
- if (!artifact || artifact.includes("..") || artifact.includes("/") || artifact.includes("\\")) return null;
41158
- const artifactPath = (0, import_path16.join)(runDir, artifact);
41159
- if (!(0, import_fs17.existsSync)(artifactPath)) return null;
41160
- try {
41161
- const text = (0, import_fs17.readFileSync)(artifactPath, "utf8");
41162
- const firstObject = text.indexOf("{");
41163
- const lastObject = text.lastIndexOf("}");
41164
- if (firstObject < 0 || lastObject <= firstObject) return null;
41165
- return JSON.parse(text.slice(firstObject, lastObject + 1));
41166
- } catch {
41167
- return null;
41168
- }
41169
- }
41170
- function commandTimingBreakdown(command, output) {
41171
- const schemaVersion = String(output?.schema_version || "");
41172
- if (schemaVersion === "foh_cli_proof_report.v1") {
41173
- const timing = asObject(output?.timing) || {};
41174
- const cacheSources = /* @__PURE__ */ new Map();
41175
- let cacheWaitMs = 0;
41176
- let cacheHitCount = 0;
41177
- let cacheMissCount = 0;
41178
- for (const check2 of toArray2(output?.checks)) {
41179
- const detail = asObject(check2)?.detail;
41180
- const proofCache = asObject(asObject(detail)?.proof_cache);
41181
- if (!proofCache) continue;
41182
- if (proofCache["hit"] === true) cacheHitCount += 1;
41183
- if (proofCache["miss"] === true) cacheMissCount += 1;
41184
- cacheWaitMs += Number(proofCache["waited_ms"] || 0);
41185
- increment(cacheSources, proofCache["source"] || "unknown");
41186
- }
41187
- return {
41188
- kind: "proof",
41189
- command: command.command || "",
41190
- total_ms: Number(timing.total_ms || command.duration_ms || 0),
41191
- total_check_duration_ms: Number(timing.total_check_duration_ms || 0),
41192
- slow_checks: toArray2(timing.slow_checks).slice(0, 5),
41193
- cache_wait_ms: cacheWaitMs,
41194
- cache_hit_count: cacheHitCount,
41195
- cache_miss_count: cacheMissCount,
41196
- cache_sources: ranked(cacheSources)
41197
- };
41198
- }
41199
- if (schemaVersion === "foh_certification_run.v1") {
41200
- const timing = asObject(output?.timing) || {};
41201
- const certificate = asObject(output?.certificate) || {};
41202
- return {
41203
- kind: "certification",
41204
- command: command.command || "",
41205
- total_ms: Number(timing.total_ms || command.duration_ms || 0),
41206
- api_ms: Number(timing.api_ms || 0),
41207
- status: output?.status || null,
41208
- reason_code: output?.reason_code || null,
41209
- scenario_summary: certificate.scenario_summary || null
41210
- };
41211
- }
41212
- return null;
41213
- }
41214
- function analyzeRunArtifacts(runPath, run, cwd) {
41215
- const runDir = (0, import_path16.dirname)(runPath);
41216
- const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
41217
- const reasonCounts = /* @__PURE__ */ new Map();
41218
- const slowSteps = [];
41219
- const timingBreakdowns = [];
41220
- let completed = 0;
41221
- let withDuration = 0;
41222
- let totalDuration = 0;
41223
- for (const command of commands) {
41224
- const output = readCommandOutputJson(runDir, command);
41225
- const breakdown = commandTimingBreakdown(command, output);
41226
- if (breakdown) timingBreakdowns.push({
41227
- run_id: run.run_id,
41228
- run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
41229
- ...breakdown
41230
- });
41231
- if (command.phase === "completed" || command.completed_at) completed += 1;
41232
- if (typeof command.duration_ms === "number") {
41233
- withDuration += 1;
41234
- totalDuration += command.duration_ms;
41235
- slowSteps.push({
41236
- run_id: run.run_id,
41237
- run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
41238
- command: command.command || "",
41239
- duration_ms: command.duration_ms,
41240
- status: command.status || null,
41241
- reason_code: command.reason_code || null,
41242
- check_reason_codes: Array.isArray(command.check_reason_codes) ? command.check_reason_codes : []
41243
- });
41244
- }
41245
- if (command.reason_code) increment(reasonCounts, command.reason_code);
41246
- for (const reasonCode of toArray2(command.check_reason_codes)) {
41247
- if (reasonCode) increment(reasonCounts, reasonCode);
41248
- }
41249
- }
41250
- const codexEvents = readNdjson((0, import_path16.join)(runDir, "codex-exec.jsonl"));
41251
- const codexDocs = /* @__PURE__ */ new Set();
41252
- let codexCommandExecutions = 0;
41253
- let codexFailedExitCodes = 0;
41254
- for (const event of codexEvents) {
41255
- const item = asObject(event.item) || event;
41256
- if (item.type === "command_execution" && item.status === "completed") {
41257
- codexCommandExecutions += 1;
41258
- if (typeof item.exit_code === "number" && item.exit_code !== 0) codexFailedExitCodes += 1;
41259
- }
41260
- for (const url2 of collectDocUrls(JSON.stringify(event))) codexDocs.add(url2);
41261
- }
41262
- const docs = /* @__PURE__ */ new Set([
41263
- ...toArray2(run.docs_pages_used).map(String),
41264
- ...Array.from(codexDocs)
41265
- ]);
41266
- return {
41267
- command_log_present: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "commands.ndjson")),
41268
- command_count: commands.length,
41269
- completed_command_count: completed,
41270
- missing_completion_count: Math.max(0, commands.length - completed),
41271
- commands_with_duration_count: withDuration,
41272
- total_command_duration_ms: totalDuration,
41273
- command_reason_codes: ranked(reasonCounts),
41274
- slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms) - Number(a.duration_ms)).slice(0, 10),
41275
- timing_breakdowns: timingBreakdowns,
41276
- docs_pages_observed: Array.from(docs).sort(),
41277
- codex_command_execution_completed_count: codexCommandExecutions,
41278
- codex_failed_exit_code_count: codexFailedExitCodes
41279
- };
41280
- }
41281
- function summarizeExternalAgentRuns(options) {
41282
- const cwd = (0, import_path16.resolve)(options.cwd || process.cwd());
41283
- const root = (0, import_path16.resolve)(cwd, options.root);
41284
- const loaded = readRunRecords(root, cwd);
41285
- const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
41286
- const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
41287
- const statusCounts = /* @__PURE__ */ new Map();
41288
- const modelCounts = /* @__PURE__ */ new Map();
41289
- const failureCounts = /* @__PURE__ */ new Map();
41290
- const commandReasonCounts = /* @__PURE__ */ new Map();
41291
- const docsCounts = /* @__PURE__ */ new Map();
41292
- const slowSteps = [];
41293
- const timingBreakdowns = [];
41294
- let manualInterventions = 0;
41295
- let commandCount = 0;
41296
- let completedCommandCount = 0;
41297
- let missingCompletionCount = 0;
41298
- let commandsWithDurationCount = 0;
41299
- let totalCommandDurationMs = 0;
41300
- let commandLogRunCount = 0;
41301
- let codexCommandExecutions = 0;
41302
- let codexFailedExitCodes = 0;
41303
- for (const record2 of records) {
41304
- const run = record2.run;
41305
- increment(statusCounts, run.status);
41306
- increment(modelCounts, `${run.model_provider}/${run.model_name}`);
41307
- manualInterventions += Number(run.manual_intervention_count || 0);
41308
- if (run.status !== "pass") increment(failureCounts, run.failure_reason_code || "unknown");
41309
- const artifactSummary = analyzeRunArtifacts(record2.path, run, cwd);
41310
- if (artifactSummary.command_log_present) commandLogRunCount += 1;
41311
- commandCount += Number(artifactSummary.command_count || 0);
41312
- completedCommandCount += Number(artifactSummary.completed_command_count || 0);
41313
- missingCompletionCount += Number(artifactSummary.missing_completion_count || 0);
41314
- commandsWithDurationCount += Number(artifactSummary.commands_with_duration_count || 0);
41315
- totalCommandDurationMs += Number(artifactSummary.total_command_duration_ms || 0);
41316
- codexCommandExecutions += Number(artifactSummary.codex_command_execution_completed_count || 0);
41317
- codexFailedExitCodes += Number(artifactSummary.codex_failed_exit_code_count || 0);
41318
- for (const row of toArray2(artifactSummary.slow_steps)) slowSteps.push(row);
41319
- for (const row of toArray2(artifactSummary.timing_breakdowns)) timingBreakdowns.push(row);
41320
- for (const row of toArray2(artifactSummary.command_reason_codes)) {
41321
- const entry = asObject(row);
41322
- if (entry) increment(commandReasonCounts, entry.key, Number(entry.count || 1));
41323
- }
41324
- for (const page of toArray2(artifactSummary.docs_pages_observed)) increment(docsCounts, page);
41325
- }
41326
- const topFailures = ranked(failureCounts);
41327
- const commandReasonCodes2 = ranked(commandReasonCounts);
41328
- const recommendedFixes = topFailures.map((failure) => ({
41329
- reason_code: failure.key,
41330
- count: failure.count,
41331
- recommended_fix: recommendedFixFor(failure.key),
41332
- owner_subsystem: ownerSubsystemFor(failure.key)
41333
- }));
41334
- const nextRecommendedFix = recommendedFixes[0] || null;
41335
- return {
41336
- schema_version: "external_agent_run_summary.v1",
41337
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
41338
- root: (0, import_path16.relative)(cwd, root).replaceAll("\\", "/") || ".",
41339
- cohort_id: selectedCohortId,
41340
- current_baseline_only: Boolean(selectedCohortId),
41341
- run_count: records.length,
41342
- invalid_run_count: selectedCohortId ? 0 : loaded.invalid_runs.length,
41343
- status_counts: Object.fromEntries(statusCounts),
41344
- model_counts: ranked(modelCounts),
41345
- manual_intervention_count: manualInterventions,
41346
- top_failure_reason_codes: topFailures,
41347
- docs_pages_observed: ranked(docsCounts),
41348
- command_telemetry: {
41349
- run_count_with_command_log: commandLogRunCount,
41350
- command_count: commandCount,
41351
- completed_command_count: completedCommandCount,
41352
- missing_completion_count: missingCompletionCount,
41353
- commands_with_duration_count: commandsWithDurationCount,
41354
- total_command_duration_ms: totalCommandDurationMs,
41355
- command_reason_codes: commandReasonCodes2,
41356
- slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20),
41357
- timing_breakdowns: timingBreakdowns.sort((a, b) => Number(b.total_ms || 0) - Number(a.total_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
41358
- },
41359
- codex_telemetry: {
41360
- command_execution_completed_count: codexCommandExecutions,
41361
- failed_exit_code_count: codexFailedExitCodes
41362
- },
41363
- recommended_fixes: recommendedFixes,
41364
- next_recommended_fix: nextRecommendedFix,
41365
- fix_selection_policy: {
41366
- mode: "coherent_failure_cluster_first",
41367
- rule: "Fix the highest-impact owner subsystem locally with focused proof, then rerun the same prompt once externally.",
41368
- run_failure_weight: 3,
41369
- command_reason_weight: 1
41370
- },
41371
- next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
41372
- invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
41373
- run_paths: records.map((record2) => (0, import_path16.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
41374
- };
41375
- }
41376
- function runExternalAgentRunSummary(options) {
41377
- const summary = summarizeExternalAgentRuns(options);
41378
- const invalidRuns = toArray2(summary.invalid_runs);
41379
- const status = invalidRuns.length > 0 ? "failed" : "passed";
41380
- const report = {
41381
- report_schema_version: "script_report.v1",
41382
- script: "foh eval external-agent summary",
41383
- checked_at: (/* @__PURE__ */ new Date()).toISOString(),
41384
- status,
41385
- errors: invalidRuns.map((entry) => {
41386
- const object3 = asObject(entry);
41387
- return `${object3?.path || "unknown"}: ${JSON.stringify(object3?.findings || [])}`;
41388
- }),
41389
- warnings: Number(summary.run_count || 0) === 0 ? ["no external-agent run artifacts found"] : [],
41390
- report: summary
41391
- };
41392
- if (options.out) {
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)}
41395
- `, "utf8");
41396
- }
41397
- if (options.report) {
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)}
41400
- `, "utf8");
41401
- }
41402
- return { summary, report };
41403
- }
41404
-
41405
- // src/lib/external-agent-executor-classification.ts
41406
- function proofArtifactPasses(runDir) {
41407
- const proofPath = (0, import_path17.join)(runDir, "proof.json");
41408
- if (!(0, import_fs18.existsSync)(proofPath)) return false;
41409
- try {
41410
- const parsed = JSON.parse((0, import_fs18.readFileSync)(proofPath, "utf8"));
41411
- return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
41412
- } catch {
41413
- return false;
41414
- }
41415
- }
41416
- function readIfExists(path2) {
41417
- return (0, import_fs18.existsSync)(path2) ? (0, import_fs18.readFileSync)(path2, "utf8") : "";
41418
- }
41419
- function relativeArtifactName(path2) {
41420
- return (0, import_path17.basename)(path2);
41421
- }
41422
- function classifyExternalAgentRun(input) {
41423
- if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
41424
- if (!input.artifactSafetyOk) return { status: "fail", reasonCode: "external_agent_artifact_safety_blocked" };
41425
- const completedCommands = readCommandRecords(input.run.run_dir).filter((record2) => record2.phase === "completed");
41426
- const observedVersions = completedCommands.map((record2) => String(record2.cli_version || "").trim()).filter((version2) => /^\d+\.\d+\.\d+$/.test(version2));
41427
- if (observedVersions.some((version2) => version2 !== CLI_VERSION)) {
41428
- return { status: "hold", reasonCode: "external_agent_cli_version_drift" };
41429
- }
41430
- const commandReasonCodes2 = completedCommands.flatMap((record2) => [
41431
- String(record2.reason_code || ""),
41432
- ...Array.isArray(record2.check_reason_codes) ? record2.check_reason_codes.map((code) => String(code || "")) : []
41433
- ]).filter(Boolean);
41434
- const hasCommandReason = (pattern) => commandReasonCodes2.some((reason) => pattern.test(reason));
41435
- if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
41436
- return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
41437
- }
41438
- if (hasCommandReason(/provider_capacity_blocked/i)) {
41439
- return { status: "hold", reasonCode: "provider_capacity_blocked" };
41440
- }
41441
- if (hasCommandReason(/byon_voice_number_not_configured/i)) {
41442
- return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
41443
- }
41444
- if (hasCommandReason(/contact_phone_provisioning_failed/i)) {
41445
- return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
41446
- }
41447
- if (hasCommandReason(/voice_contact_expected_no_spend_hold/i)) {
41448
- return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
41449
- }
41450
- if (hasCommandReason(/contact_phone_missing/i)) {
41451
- return { status: "hold", reasonCode: "voice_contact_phone_missing" };
41452
- }
41453
- if (hasCommandReason(/sim(?:ulation)?[_-]?cert(?:ify|ification)?.*failed|simulation_certification_failed/i)) {
41454
- return { status: "hold", reasonCode: "simulation_certification_failed" };
41455
- }
41456
- if (hasCommandReason(/proof_held/i)) {
41457
- return { status: "hold", reasonCode: "external_agent_proof_held" };
41458
- }
41459
- if (hasCommandReason(/agent_limit_reached/i)) {
41460
- return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
41461
- }
41462
- const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
41463
- const metadataBlockerCodes = agentMetadata.blocker_reason_codes;
41464
- const hasMetadataBlocker = (pattern) => metadataBlockerCodes.some((reason) => pattern.test(reason));
41465
- if (hasMetadataBlocker(/^customer_owned_requirement_unverified:/i)) {
41466
- return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
41467
- }
41468
- if (hasMetadataBlocker(/^api_health:/i) && metadataBlockerCodes.some((reason) => /^customer_owned_requirement_unverified:/i.test(reason))) {
41469
- return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
41470
- }
41471
- const firstCommandReasonCode = commandReasonCodes2.find((reason) => reason.trim().length > 0) ?? null;
41472
- if (firstCommandReasonCode) {
41473
- return { status: "hold", reasonCode: firstCommandReasonCode };
41474
- }
41475
- const lastMessage = readIfExists(input.run.outputs.last_message);
41476
- const stderr = readIfExists(input.run.outputs.stderr);
41477
- const combined = `${lastMessage}
41478
- ${stderr}`;
41479
- if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
41480
- return { status: "fail", reasonCode: "private_repo_assumption_detected" };
41481
- }
41482
- if (/(?:blocked|rejected|declined) by policy|EXEC_POLICY_BLOCKED|command execution was rejected|shell commands were rejected/i.test(combined)) {
41483
- return { status: "hold", reasonCode: "codex_exec_policy_blocked" };
41484
- }
41485
- if (/bwrap:.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|bubblewrap.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|Failed RTM_NEWADDR|ENV_SANDBOX_EXEC_BLOCKED|permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock|legacy[_ -]?landlock.*incompatible/i.test(combined)) {
41486
- return { status: "hold", reasonCode: "codex_sandbox_exec_blocked" };
41487
- }
41488
- if (/ENV_NETWORK_DNS_BLOCK|Could not resolve host|npm ping.*timeout|NO_EXECUTABLE_INSTALL/i.test(combined)) {
41489
- return { status: "hold", reasonCode: "codex_network_dns_blocked" };
41490
- }
41491
- if (new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i").test(combined)) {
41492
- return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
41493
- }
41494
- if (/provider_capacity_blocked/i.test(combined)) {
41495
- return { status: "hold", reasonCode: "provider_capacity_blocked" };
41496
- }
41497
- if (/byon_voice_number_not_configured/i.test(combined)) {
41498
- return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
41499
- }
41500
- if (/contact_phone_provisioning_failed/i.test(combined)) {
41501
- return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
41502
- }
41503
- if (/voice_contact_expected_no_spend_hold/i.test(combined)) {
41504
- return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
41505
- }
41506
- if (/contact_phone_missing/i.test(combined)) {
41507
- return { status: "hold", reasonCode: "voice_contact_phone_missing" };
41508
- }
41509
- if (/simulation_certification_failed/i.test(combined)) {
41510
- return { status: "hold", reasonCode: "simulation_certification_failed" };
41511
- }
41512
- if (/proof_held/i.test(combined)) {
41513
- return { status: "hold", reasonCode: "external_agent_proof_held" };
41514
- }
41515
- if (/agent_limit_reached/i.test(combined)) {
41516
- return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
41517
- }
41518
- if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
41519
- return { status: "hold", reasonCode: "auth_browser_approval_required" };
41520
- }
41521
- if (input.exitCode !== 0) return { status: "hold", reasonCode: `${input.run.command}_runner_nonzero_exit` };
41522
- if (proofArtifactPasses(input.run.run_dir)) return { status: "pass", reasonCode: null };
41523
- return { status: "hold", reasonCode: "external_agent_proof_artifact_missing" };
41524
- }
41525
- function buildExecutedExternalAgentRunArtifact(input) {
41526
- const commands = readCommandRecords(input.run.run_dir);
41527
- const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
41528
- return {
41529
- schema_version: "external_agent_run.v1",
41530
- run_id: input.run.run_id,
41531
- status: input.status,
41532
- failure_reason_code: input.reasonCode,
41533
- model_provider: input.run.model_provider,
41534
- model_name: input.run.model_name,
41535
- runner_model: input.run.runner_model,
41536
- agent_shell: `${input.run.command}-exec`,
41537
- workspace_type: "clean-no-repo-programmatic",
41538
- prompt_version: input.run.prompt_version,
41539
- prompt_path: "prompt.txt",
41540
- started_at: input.startedAt,
41541
- ended_at: input.endedAt,
41542
- manual_intervention_count: 0,
41543
- manual_interventions: [],
41544
- environment: {
41545
- os: process.platform,
41546
- node_version: process.version,
41547
- npm_version: null,
41548
- foh_cli_version: CLI_VERSION,
41549
- runner_exit_code: input.exitCode,
41550
- runner_timed_out: input.timedOut,
41551
- duration_ms: input.durationMs
41552
- },
41553
- public_entrypoints: [
41554
- "https://frontofhouse.okii.uk",
41555
- "https://frontofhouse.okii.uk/llms.txt",
41556
- "https://frontofhouse.okii.uk/openapi.yaml",
41557
- "npx --yes @f-o-h/cli@latest"
41558
- ],
41559
- commands_run: commands.map((command) => command.command),
41560
- docs_pages_used: agentMetadata.docs_pages_used,
41561
- eval_state: {
41562
- lifecycle_strategy: "reuse_existing_eval_state",
41563
- org_reuse_expected: true,
41564
- agent_reuse_expected: true,
41565
- widget_reuse_expected: true,
41566
- fresh_org_expected: false,
41567
- ephemeral_org_expected: false,
41568
- fresh_agent_expected: false,
41569
- phone_purchase_expected: false,
41570
- paid_resource_creation_expected: false,
41571
- spend_policy_expected: NO_SPEND_POLICY,
41572
- cleanup_expected: false,
41573
- cleanup_strategy: "no_cleanup_for_reused_eval_state",
41574
- paid_resource_strategy: "blocked_unless_explicit_byon_or_operator_approved",
41575
- rationale: "Mass external-agent evals benchmark public docs/CLI/API clarity; reuse avoids paid phone and Twilio inventory churn."
41576
- },
41577
- artifacts: {
41578
- terminal_transcript: relativeArtifactName(input.run.outputs.jsonl),
41579
- command_log: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
41580
- proof_bundle: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
41581
- replay_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
41582
- knowledge_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
41583
- improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
41584
- agent_metadata: agentMetadata.path,
41585
- notes: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
41586
- runner_last_message: relativeArtifactName(input.run.outputs.last_message),
41587
- runner_stderr: relativeArtifactName(input.run.outputs.stderr),
41588
- codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
41589
- codex_stderr: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.stderr) : null,
41590
- artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
41591
- },
41592
- 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}.`,
41593
- next_commands: input.status === "pass" ? [externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))] : [
41594
- "foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
41595
- "foh bug improve --from external-agent-run --file <run_dir>/run.json --out <run_dir>/improvement-packet.json --json",
41596
- externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))
41597
- ]
41598
- };
41599
- }
41600
-
41601
- // src/lib/external-agent-runner-execution.ts
41602
- var import_child_process5 = require("child_process");
41603
- var import_fs19 = require("fs");
41604
- var import_path18 = require("path");
41605
- function buildCommandInvocation(command, args) {
41606
- if (process.platform === "win32" && command.toLowerCase().endsWith(".cmd")) {
41607
- const binDir = (0, import_path18.dirname)(command);
41608
- const codexEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
41609
- if ((0, import_fs19.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
41610
- const geminiEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
41611
- if ((0, import_fs19.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
41612
- }
41613
- return { command, args };
41614
- }
41615
- function spawnExternalAgentRunner(input) {
41616
- return new Promise((resolveRun) => {
41617
- const started = Date.now();
41618
- const commandInvocation = buildCommandInvocation(input.command, input.args);
41619
- const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
41620
- cwd: input.cwd,
41621
- env: input.env,
41622
- shell: false,
41623
- stdio: ["pipe", "pipe", "pipe"],
41624
- windowsHide: true
41625
- });
41626
- const stdout = (0, import_fs19.createWriteStream)(input.stdoutPath, { flags: "w" });
41627
- const stderr = (0, import_fs19.createWriteStream)(input.stderrPath, { flags: "w" });
41628
- child.stdout.pipe(stdout);
41629
- child.stderr.pipe(stderr);
41630
- child.stdin.end(input.prompt);
41631
- let timedOut = false;
41632
- const timer = setTimeout(() => {
41633
- timedOut = true;
41634
- if (child.pid && process.platform === "win32") {
41635
- (0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
41636
- } else {
41637
- child.kill("SIGKILL");
41638
- }
41639
- }, input.timeoutMs);
41640
- child.on("close", (exitCode) => {
41641
- clearTimeout(timer);
41642
- stdout.end();
41643
- stderr.end();
41644
- resolveRun({
41645
- exitCode,
41646
- timedOut,
41647
- durationMs: Date.now() - started
41648
- });
41649
- });
41650
- child.on("error", () => {
41651
- clearTimeout(timer);
41652
- stdout.end();
41653
- stderr.end();
41654
- resolveRun({
41655
- exitCode: null,
41656
- timedOut,
41657
- durationMs: Date.now() - started
41658
- });
41659
- });
41660
- });
41661
- }
41662
-
41663
- // src/lib/external-agent-executor.ts
41664
- var GEMINI_HEADLESS_PROBE_TIMEOUT_MS = 15e3;
41665
- var DEFAULT_FOH_API_URL2 = "https://api.frontofhouse.okii.uk";
41666
- var ExternalAgentExecutorError = class extends Error {
41667
- reasonCode;
41668
- constructor(reasonCode, message) {
41669
- super(message);
41670
- this.name = "ExternalAgentExecutorError";
41671
- this.reasonCode = reasonCode;
41672
- }
41673
- };
41674
- function readExternalAgentEvalAuthEnv(env = process.env) {
41675
- return {
41676
- token: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || "").trim(),
41677
- orgId: String(env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || "").trim(),
41678
- apiUrl: String(env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_API_URL || DEFAULT_FOH_API_URL2).trim() || DEFAULT_FOH_API_URL2,
41679
- expiresAt: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || "").trim()
41680
- };
41681
- }
41682
- function shouldRunExternalAgentEvalAuthPreflight(env = process.env) {
41683
- return Boolean(
41684
- env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || env.FOH_SERVICE_TOKEN || env.FOH_ORG_ID || env.FOH_API_URL || env.FOH_TOKEN_EXPIRES_AT
41685
- );
42096
+ function shouldRunExternalAgentEvalAuthPreflight(env = process.env) {
42097
+ return Boolean(
42098
+ env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || env.FOH_SERVICE_TOKEN || env.FOH_ORG_ID || env.FOH_API_URL || env.FOH_TOKEN_EXPIRES_AT
42099
+ );
41686
42100
  }
41687
42101
  async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}) {
41688
42102
  const hasExplicitEvalAuth = Boolean(
@@ -41736,14 +42150,14 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
41736
42150
  };
41737
42151
  }
41738
42152
  function normalizeForCompare(path2) {
41739
- const resolved = (0, import_path19.resolve)(path2);
42153
+ const resolved = (0, import_path18.resolve)(path2);
41740
42154
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
41741
42155
  }
41742
42156
  function isPathInside(childPath, parentPath) {
41743
42157
  const child = normalizeForCompare(childPath);
41744
42158
  const parent = normalizeForCompare(parentPath);
41745
- const rel = (0, import_path19.relative)(parent, child);
41746
- return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path19.isAbsolute)(rel);
42159
+ const rel = (0, import_path18.relative)(parent, child);
42160
+ return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path18.isAbsolute)(rel);
41747
42161
  }
41748
42162
  function requireString(value, field) {
41749
42163
  if (typeof value !== "string" || value.trim() === "") {
@@ -41752,10 +42166,10 @@ function requireString(value, field) {
41752
42166
  return value;
41753
42167
  }
41754
42168
  function readBatch(batchPath) {
41755
- if (!(0, import_fs20.existsSync)(batchPath)) {
42169
+ if (!(0, import_fs19.existsSync)(batchPath)) {
41756
42170
  throw new ExternalAgentExecutorError("external_agent_batch_not_found", `Batch file not found: ${batchPath}`);
41757
42171
  }
41758
- const parsed = JSON.parse((0, import_fs20.readFileSync)(batchPath, "utf8"));
42172
+ const parsed = JSON.parse((0, import_fs19.readFileSync)(batchPath, "utf8"));
41759
42173
  if (parsed.schema_version !== "external_agent_batch_plan.v1") {
41760
42174
  throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch schema_version must be external_agent_batch_plan.v1.");
41761
42175
  }
@@ -41792,8 +42206,8 @@ function resolveCodexProbeCommand() {
41792
42206
  if (process.platform !== "win32") return "codex";
41793
42207
  const appData = process.env.APPDATA;
41794
42208
  if (appData) {
41795
- const appDataShim = (0, import_path19.join)(appData, "npm", "codex.cmd");
41796
- if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
42209
+ const appDataShim = (0, import_path18.join)(appData, "npm", "codex.cmd");
42210
+ if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41797
42211
  }
41798
42212
  return "codex.cmd";
41799
42213
  }
@@ -41804,8 +42218,8 @@ function resolveGeminiProbeCommand() {
41804
42218
  if (process.platform !== "win32") return "gemini";
41805
42219
  const appData = process.env.APPDATA;
41806
42220
  if (appData) {
41807
- const appDataShim = (0, import_path19.join)(appData, "npm", "gemini.cmd");
41808
- if ((0, import_fs20.existsSync)(appDataShim)) return appDataShim;
42221
+ const appDataShim = (0, import_path18.join)(appData, "npm", "gemini.cmd");
42222
+ if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
41809
42223
  }
41810
42224
  return "gemini.cmd";
41811
42225
  }
@@ -42076,34 +42490,34 @@ function safeRunId(value) {
42076
42490
  return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
42077
42491
  }
42078
42492
  function resolveWorkspaceRoot(input) {
42079
- if (input.workspaceRoot) return (0, import_path19.resolve)(input.workspaceRoot);
42080
- const batchStem = (0, import_path19.basename)((0, import_path19.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42081
- const repoStem = (0, import_path19.basename)((0, import_path19.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42082
- return (0, import_path19.resolve)((0, import_os3.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
42493
+ if (input.workspaceRoot) return (0, import_path18.resolve)(input.workspaceRoot);
42494
+ const batchStem = (0, import_path18.basename)((0, import_path18.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42495
+ const repoStem = (0, import_path18.basename)((0, import_path18.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
42496
+ return (0, import_path18.resolve)((0, import_os3.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
42083
42497
  }
42084
42498
  function findNearestGitRoot(startPath) {
42085
- let current = (0, import_path19.resolve)(startPath);
42499
+ let current = (0, import_path18.resolve)(startPath);
42086
42500
  while (true) {
42087
- if ((0, import_fs20.existsSync)((0, import_path19.join)(current, ".git"))) return current;
42088
- const parent = (0, import_path19.dirname)(current);
42501
+ if ((0, import_fs19.existsSync)((0, import_path18.join)(current, ".git"))) return current;
42502
+ const parent = (0, import_path18.dirname)(current);
42089
42503
  if (parent === current) return null;
42090
42504
  current = parent;
42091
42505
  }
42092
42506
  }
42093
42507
  function resolvePrivateRepoRoot(input) {
42094
42508
  if (input.explicitPrivateRepoRoot) {
42095
- return { root: (0, import_path19.resolve)(input.explicitPrivateRepoRoot), explicit: true };
42509
+ return { root: (0, import_path18.resolve)(input.explicitPrivateRepoRoot), explicit: true };
42096
42510
  }
42097
- const cwd = (0, import_path19.resolve)(input.cwd || process.cwd());
42511
+ const cwd = (0, import_path18.resolve)(input.cwd || process.cwd());
42098
42512
  const gitRoot = findNearestGitRoot(cwd);
42099
42513
  if (gitRoot) return { root: gitRoot, explicit: false };
42100
42514
  return {
42101
- root: (0, import_path19.join)(cwd, ".foh-no-private-repo-root-sentinel"),
42515
+ root: (0, import_path18.join)(cwd, ".foh-no-private-repo-root-sentinel"),
42102
42516
  explicit: false
42103
42517
  };
42104
42518
  }
42105
42519
  function promptVersionFromPath(promptPath) {
42106
- const raw = (0, import_fs20.readFileSync)(promptPath, "utf8");
42520
+ const raw = (0, import_fs19.readFileSync)(promptPath, "utf8");
42107
42521
  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";
42108
42522
  if (raw.includes("Do not assume access to the private source repository")) return "blank-setup.v1";
42109
42523
  return "unknown";
@@ -42113,7 +42527,7 @@ function createExternalAgentExecutorPlan(options) {
42113
42527
  if (runner !== "codex" && runner !== "gemini") {
42114
42528
  throw new ExternalAgentExecutorError("unsupported_external_agent_runner", `Unsupported runner: ${runner}`);
42115
42529
  }
42116
- const batchPath = (0, import_path19.resolve)(options.batchPath);
42530
+ const batchPath = (0, import_path18.resolve)(options.batchPath);
42117
42531
  const batch = readBatch(batchPath);
42118
42532
  const runnerProbe = validateRunner(options, runner);
42119
42533
  const codexSandboxBackend = normalizeCodexSandboxBackend(options.codexSandboxBackend);
@@ -42132,17 +42546,17 @@ function createExternalAgentExecutorPlan(options) {
42132
42546
  `Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
42133
42547
  );
42134
42548
  }
42135
- (0, import_fs20.mkdirSync)(workspaceRoot, { recursive: true });
42136
- const batchDir = (0, import_path19.resolve)(String(batch.batch_dir || (0, import_path19.resolve)(batchPath, "..")));
42549
+ (0, import_fs19.mkdirSync)(workspaceRoot, { recursive: true });
42550
+ const batchDir = (0, import_path18.resolve)(String(batch.batch_dir || (0, import_path18.resolve)(batchPath, "..")));
42137
42551
  const timeoutMinutes = Number.isFinite(options.timeoutMinutes) && Number(options.timeoutMinutes) > 0 ? Number(options.timeoutMinutes) : 30;
42138
42552
  const runs = batch.runs.map((run) => {
42139
42553
  const runId = safeRunId(requireString(run.run_id, "runs[].run_id"));
42140
- const runDir = (0, import_path19.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
42141
- const promptPath = (0, import_path19.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
42142
- const workspaceDir = (0, import_path19.join)(workspaceRoot, runId);
42143
- (0, import_fs20.mkdirSync)(workspaceDir, { recursive: true });
42144
- (0, import_fs20.writeFileSync)(
42145
- (0, import_path19.join)(workspaceDir, "README.md"),
42554
+ const runDir = (0, import_path18.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
42555
+ const promptPath = (0, import_path18.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
42556
+ const workspaceDir = (0, import_path18.join)(workspaceRoot, runId);
42557
+ (0, import_fs19.mkdirSync)(workspaceDir, { recursive: true });
42558
+ (0, import_fs19.writeFileSync)(
42559
+ (0, import_path18.join)(workspaceDir, "README.md"),
42146
42560
  [
42147
42561
  "# FOH External-Agent Workspace",
42148
42562
  "",
@@ -42153,18 +42567,19 @@ function createExternalAgentExecutorPlan(options) {
42153
42567
  ].join("\n"),
42154
42568
  "utf8"
42155
42569
  );
42570
+ const explicitPromptVersion = typeof run.prompt_version === "string" && run.prompt_version.trim() ? run.prompt_version.trim() : typeof batch.prompt_version === "string" && batch.prompt_version.trim() ? batch.prompt_version.trim() : promptVersionFromPath(promptPath);
42156
42571
  const env = buildCodexExecutorEnv({
42157
42572
  sourceEnv: options.env,
42158
42573
  runDir,
42159
- promptVersion: promptVersionFromPath(promptPath)
42574
+ promptVersion: explicitPromptVersion
42160
42575
  });
42161
42576
  const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
42162
42577
  const outputStem = runner === "gemini" ? "gemini" : "codex";
42163
- const jsonlPath = (0, import_path19.join)(runDir, `${outputStem}-exec.jsonl`);
42164
- const lastMessagePath = (0, import_path19.join)(runDir, `${outputStem}-last-message.md`);
42165
- const stderrPath = (0, import_path19.join)(runDir, `${outputStem}-stderr.txt`);
42166
- const runPath = (0, import_path19.join)(runDir, "run.json");
42167
- const artifactSafetyPath = (0, import_path19.join)(runDir, "artifact-safety.json");
42578
+ const jsonlPath = (0, import_path18.join)(runDir, `${outputStem}-exec.jsonl`);
42579
+ const lastMessagePath = (0, import_path18.join)(runDir, `${outputStem}-last-message.md`);
42580
+ const stderrPath = (0, import_path18.join)(runDir, `${outputStem}-stderr.txt`);
42581
+ const runPath = (0, import_path18.join)(runDir, "run.json");
42582
+ const artifactSafetyPath = (0, import_path18.join)(runDir, "artifact-safety.json");
42168
42583
  const args = runner === "gemini" ? [
42169
42584
  ...runnerProbe.globalArgs,
42170
42585
  ...runnerProbe.execArgs
@@ -42255,9 +42670,9 @@ function createExternalAgentExecutorPlan(options) {
42255
42670
  };
42256
42671
  }
42257
42672
  function writeExternalAgentExecutorPlan(plan) {
42258
- const path2 = (0, import_path19.join)(plan.batch_dir, "executor-plan.json");
42259
- (0, import_fs20.mkdirSync)(plan.batch_dir, { recursive: true });
42260
- (0, import_fs20.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
42673
+ const path2 = (0, import_path18.join)(plan.batch_dir, "executor-plan.json");
42674
+ (0, import_fs19.mkdirSync)(plan.batch_dir, { recursive: true });
42675
+ (0, import_fs19.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
42261
42676
  `, "utf8");
42262
42677
  return path2;
42263
42678
  }
@@ -42272,7 +42687,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42272
42687
  if (authPreflight && !authPreflight.ok) {
42273
42688
  const endedAt2 = (/* @__PURE__ */ new Date()).toISOString();
42274
42689
  const blockedResults = plan.runs.map((run) => {
42275
- (0, import_fs20.mkdirSync)(run.run_dir, { recursive: true });
42690
+ (0, import_fs19.mkdirSync)(run.run_dir, { recursive: true });
42276
42691
  const runArtifact = buildExecutedExternalAgentRunArtifact({
42277
42692
  run,
42278
42693
  startedAt,
@@ -42283,7 +42698,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42283
42698
  timedOut: false,
42284
42699
  durationMs: 0
42285
42700
  });
42286
- (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42701
+ (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42287
42702
  `, "utf8");
42288
42703
  return {
42289
42704
  run_id: run.run_id,
@@ -42310,8 +42725,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42310
42725
  }
42311
42726
  for (const run of plan.runs) {
42312
42727
  const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
42313
- const commandCaptureDir = (0, import_path19.join)(run.workspace_dir, ".foh-capture");
42314
- (0, import_fs20.mkdirSync)(commandCaptureDir, { recursive: true });
42728
+ const commandCaptureDir = (0, import_path18.join)(run.workspace_dir, ".foh-capture");
42729
+ (0, import_fs19.mkdirSync)(commandCaptureDir, { recursive: true });
42315
42730
  const env = buildCodexExecutorEnv({
42316
42731
  sourceEnv: options.env,
42317
42732
  runDir: commandCaptureDir,
@@ -42322,7 +42737,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42322
42737
  args: run.args,
42323
42738
  cwd: run.workspace_dir,
42324
42739
  env,
42325
- prompt: (0, import_fs20.readFileSync)(run.prompt_path, "utf8"),
42740
+ prompt: (0, import_fs19.readFileSync)(run.prompt_path, "utf8"),
42326
42741
  stdoutPath: run.outputs.jsonl,
42327
42742
  stderrPath: run.outputs.stderr,
42328
42743
  timeoutMs: plan.timeout_minutes * 60 * 1e3
@@ -42335,7 +42750,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42335
42750
  privateRepoRoot,
42336
42751
  writeRedacted: true
42337
42752
  });
42338
- (0, import_fs20.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
42753
+ (0, import_fs19.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
42339
42754
  `, "utf8");
42340
42755
  const runEndedAt = (/* @__PURE__ */ new Date()).toISOString();
42341
42756
  const classification = classifyExternalAgentRun({
@@ -42354,7 +42769,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42354
42769
  timedOut: spawned.timedOut,
42355
42770
  durationMs: spawned.durationMs
42356
42771
  });
42357
- (0, import_fs20.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42772
+ (0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
42358
42773
  `, "utf8");
42359
42774
  results.push({
42360
42775
  run_id: run.run_id,
@@ -42384,11 +42799,546 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
42384
42799
  };
42385
42800
  }
42386
42801
 
42802
+ // src/lib/external-agent-run-summary.ts
42803
+ var import_fs20 = require("fs");
42804
+ var import_path19 = require("path");
42805
+ var REQUIRED_RUN_FIELDS = [
42806
+ "schema_version",
42807
+ "run_id",
42808
+ "status",
42809
+ "model_provider",
42810
+ "model_name",
42811
+ "prompt_version",
42812
+ "started_at",
42813
+ "manual_intervention_count",
42814
+ "environment",
42815
+ "public_entrypoints",
42816
+ "commands_run",
42817
+ "docs_pages_used",
42818
+ "artifacts"
42819
+ ];
42820
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["pass", "hold", "fail"]);
42821
+ var DOC_URL_RE = /https:\/\/frontofhouse\.okii\.uk\/[^\s"'`)<>,;\\\]}]*/g;
42822
+ function quoteShellArg(value) {
42823
+ const text = String(value);
42824
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
42825
+ return `"${text.replace(/(["$`])/g, "\\$1")}"`;
42826
+ }
42827
+ function externalAgentSummaryCommand(root) {
42828
+ const summaryPath = (0, import_path19.join)(root, "latest-summary.json");
42829
+ const reportPath = (0, import_path19.join)(root, "summary.report.json");
42830
+ return [
42831
+ "foh",
42832
+ "eval",
42833
+ "external-agent",
42834
+ "summary",
42835
+ "--root",
42836
+ quoteShellArg(root),
42837
+ "--out",
42838
+ quoteShellArg(summaryPath),
42839
+ "--report",
42840
+ quoteShellArg(reportPath),
42841
+ "--json"
42842
+ ].join(" ");
42843
+ }
42844
+ function readJson(filePath) {
42845
+ return JSON.parse((0, import_fs20.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
42846
+ }
42847
+ function readNdjson(filePath) {
42848
+ if (!(0, import_fs20.existsSync)(filePath)) return [];
42849
+ return (0, import_fs20.readFileSync)(filePath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
42850
+ try {
42851
+ const parsed = JSON.parse(line);
42852
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
42853
+ } catch {
42854
+ return null;
42855
+ }
42856
+ }).filter((record2) => Boolean(record2));
42857
+ }
42858
+ function asObject(value) {
42859
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
42860
+ }
42861
+ function toArray2(value) {
42862
+ return Array.isArray(value) ? value : [];
42863
+ }
42864
+ function increment(map3, key, amount = 1) {
42865
+ const normalized = String(key || "unknown");
42866
+ map3.set(normalized, (map3.get(normalized) || 0) + amount);
42867
+ }
42868
+ function ranked(map3) {
42869
+ return Array.from(map3.entries()).map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
42870
+ }
42871
+ function collectDocUrls(text) {
42872
+ 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();
42873
+ }
42874
+ function findRunCandidates(root) {
42875
+ if (!(0, import_fs20.existsSync)(root)) return [];
42876
+ const candidates = [];
42877
+ const seenRunDirs = /* @__PURE__ */ new Set();
42878
+ const captureDirs = [];
42879
+ const stack = [root];
42880
+ while (stack.length > 0) {
42881
+ const current = stack.pop();
42882
+ if (!current) continue;
42883
+ for (const entry of (0, import_fs20.readdirSync)(current, { withFileTypes: true })) {
42884
+ const absolute = (0, import_path19.join)(current, entry.name);
42885
+ if (entry.isDirectory()) {
42886
+ stack.push(absolute);
42887
+ } else if (entry.isFile() && entry.name === "run.json") {
42888
+ candidates.push({ path: absolute, synthetic: false });
42889
+ seenRunDirs.add((0, import_path19.dirname)(absolute));
42890
+ } else if (entry.isFile() && entry.name === "commands.ndjson") {
42891
+ captureDirs.push(current);
42892
+ }
42893
+ }
42894
+ }
42895
+ for (const captureDir of captureDirs) {
42896
+ if (seenRunDirs.has(captureDir)) continue;
42897
+ candidates.push({ path: (0, import_path19.join)(captureDir, "run.json"), synthetic: true });
42898
+ }
42899
+ return candidates.sort((a, b) => a.path.localeCompare(b.path));
42900
+ }
42901
+ function validateExternalAgentRun(value) {
42902
+ const findings = [];
42903
+ const run = asObject(value);
42904
+ if (!run) return [{ id: "run_not_object", detail: "run artifact must be an object" }];
42905
+ for (const field of REQUIRED_RUN_FIELDS) {
42906
+ if (!(field in run)) findings.push({ id: "required_field_missing", field });
42907
+ }
42908
+ if (run.schema_version !== "external_agent_run.v1") {
42909
+ findings.push({ id: "schema_version_invalid", expected: "external_agent_run.v1", actual: run.schema_version ?? null });
42910
+ }
42911
+ if (!VALID_STATUSES.has(String(run.status || ""))) {
42912
+ findings.push({ id: "status_invalid", expected: Array.from(VALID_STATUSES), actual: run.status ?? null });
42913
+ }
42914
+ if ((run.status === "hold" || run.status === "fail") && !String(run.failure_reason_code || "").trim()) {
42915
+ findings.push({ id: "failure_reason_code_missing" });
42916
+ }
42917
+ if (!Number.isInteger(run.manual_intervention_count) || Number(run.manual_intervention_count) < 0) {
42918
+ findings.push({ id: "manual_intervention_count_invalid" });
42919
+ }
42920
+ if (!Array.isArray(run.commands_run)) findings.push({ id: "commands_run_invalid" });
42921
+ if (!Array.isArray(run.docs_pages_used)) findings.push({ id: "docs_pages_used_invalid" });
42922
+ if (!asObject(run.environment)) findings.push({ id: "environment_invalid" });
42923
+ if (!asObject(run.artifacts)) findings.push({ id: "artifacts_invalid" });
42924
+ if (toArray2(run.public_entrypoints).length === 0) findings.push({ id: "public_entrypoints_missing" });
42925
+ return findings;
42926
+ }
42927
+ function runSortTime(run) {
42928
+ const raw = String(run.ended_at || run.started_at || "");
42929
+ const time3 = Date.parse(raw);
42930
+ return Number.isFinite(time3) ? time3 : 0;
42931
+ }
42932
+ function latestCommandTime(commands) {
42933
+ const times = commands.map((command) => String(command.completed_at || command.started_at || command.recorded_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => b.time - a.time);
42934
+ return times[0]?.raw ?? null;
42935
+ }
42936
+ function firstCommandTime(commands) {
42937
+ const times = commands.map((command) => String(command.started_at || command.recorded_at || command.completed_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => a.time - b.time);
42938
+ return times[0]?.raw ?? null;
42939
+ }
42940
+ function commandReasonCodes(commands) {
42941
+ const codes = /* @__PURE__ */ new Set();
42942
+ for (const command of commands) {
42943
+ if (command.reason_code) codes.add(String(command.reason_code));
42944
+ for (const reasonCode of toArray2(command.check_reason_codes)) {
42945
+ if (reasonCode) codes.add(String(reasonCode));
42946
+ }
42947
+ }
42948
+ return Array.from(codes);
42949
+ }
42950
+ function syntheticStatusFromCommands(commands) {
42951
+ const commandReasons = commandReasonCodes(commands);
42952
+ const failed = commands.find((command) => {
42953
+ const status = String(command.status || "").toLowerCase();
42954
+ return status === "fail" || typeof command.exit_code === "number" && command.exit_code !== 0 && status !== "hold";
42955
+ });
42956
+ if (failed) {
42957
+ return {
42958
+ status: "fail",
42959
+ reasonCode: String(failed.reason_code || commandReasons[0] || "external_agent_command_failed")
42960
+ };
42961
+ }
42962
+ const held = commands.find((command) => String(command.status || "").toLowerCase() === "hold");
42963
+ if (held) {
42964
+ return {
42965
+ status: "hold",
42966
+ reasonCode: String(held.reason_code || commandReasons[0] || "external_agent_command_held")
42967
+ };
42968
+ }
42969
+ if (commands.length === 0) {
42970
+ return { status: "hold", reasonCode: "external_agent_capture_empty" };
42971
+ }
42972
+ return { status: "pass", reasonCode: null };
42973
+ }
42974
+ function synthesizeRunFromCapture(runPath) {
42975
+ const runDir = (0, import_path19.dirname)(runPath);
42976
+ const commands = collapseCommandRecords(readNdjson((0, import_path19.join)(runDir, "commands.ndjson")));
42977
+ const metadata = asObject((0, import_fs20.existsSync)((0, import_path19.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path19.join)(runDir, "external-agent-metadata.json")) : {});
42978
+ const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
42979
+ const commandClassification = syntheticStatusFromCommands(commands);
42980
+ const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
42981
+ const reasonCode = commandClassification.status === "fail" ? commandClassification.reasonCode : blockerCodes[0] || commandClassification.reasonCode;
42982
+ const firstCommand = commands[0] || {};
42983
+ const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
42984
+ const endedAt = latestCommandTime(commands) || startedAt;
42985
+ const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
42986
+ const runId = (0, import_path19.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
42987
+ return {
42988
+ schema_version: "external_agent_run.v1",
42989
+ run_id: runId,
42990
+ status,
42991
+ failure_reason_code: status === "pass" ? null : reasonCode || "external_agent_capture_unfinalized",
42992
+ model_provider: "unknown",
42993
+ model_name: "unknown",
42994
+ prompt_version: String(firstCommand.prompt_version || "unknown"),
42995
+ started_at: startedAt,
42996
+ ended_at: endedAt,
42997
+ manual_intervention_count: 0,
42998
+ environment: {
42999
+ foh_cli_version: firstCommand.cli_version || null,
43000
+ capture_only: true
43001
+ },
43002
+ public_entrypoints: [
43003
+ "https://frontofhouse.okii.uk",
43004
+ "npx --yes @f-o-h/cli@latest"
43005
+ ],
43006
+ commands_run: commands.map((command) => String(command.command || "")).filter(Boolean),
43007
+ docs_pages_used: docs,
43008
+ artifacts: {
43009
+ command_log: "commands.ndjson",
43010
+ agent_metadata: (0, import_fs20.existsSync)((0, import_path19.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
43011
+ capture_only: true
43012
+ },
43013
+ summary: "Synthetic run record created from external-agent capture artifacts because run.json was missing."
43014
+ };
43015
+ }
43016
+ function cohortIdForRunPath(root, runPath) {
43017
+ const normalized = (0, import_path19.relative)(root, (0, import_path19.dirname)(runPath)).replaceAll("\\", "/");
43018
+ const parts = normalized.split("/").filter(Boolean);
43019
+ if (parts.length === 0) return ".";
43020
+ if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) && parts[1]) return `${parts[0]}/${parts[1]}`;
43021
+ return parts[0];
43022
+ }
43023
+ function readRunRecords(root, cwd) {
43024
+ const records = [];
43025
+ const invalid_runs = [];
43026
+ for (const candidate of findRunCandidates(root)) {
43027
+ const file2 = candidate.path;
43028
+ try {
43029
+ const parsed = candidate.synthetic ? synthesizeRunFromCapture(file2) : readJson(file2);
43030
+ const findings = validateExternalAgentRun(parsed);
43031
+ if (findings.length > 0) {
43032
+ invalid_runs.push({ path: (0, import_path19.relative)(cwd, file2).replaceAll("\\", "/"), findings });
43033
+ continue;
43034
+ }
43035
+ const run = parsed;
43036
+ records.push({
43037
+ path: file2,
43038
+ run,
43039
+ cohort_id: cohortIdForRunPath(root, file2),
43040
+ sort_time: runSortTime(run)
43041
+ });
43042
+ } catch (error2) {
43043
+ invalid_runs.push({
43044
+ path: (0, import_path19.relative)(cwd, file2).replaceAll("\\", "/"),
43045
+ findings: [{ id: "json_parse_failed", detail: error2 instanceof Error ? error2.message : String(error2) }]
43046
+ });
43047
+ }
43048
+ }
43049
+ return { records, invalid_runs };
43050
+ }
43051
+ function latestCohortId(records) {
43052
+ return records.slice().sort((a, b) => b.sort_time - a.sort_time || b.path.localeCompare(a.path))[0]?.cohort_id ?? null;
43053
+ }
43054
+ function ownerSubsystemFor(reasonCode) {
43055
+ const reason = String(reasonCode || "").toLowerCase();
43056
+ if (reason.includes("simulation") || reason.includes("certification") || reason.includes("scenario")) return "dojo_certification";
43057
+ if (reason.includes("contact_phone") || reason.includes("voice_contact") || reason.includes("provider_capacity") || reason.includes("byon")) return "voice_contact";
43058
+ if (reason.includes("exec_policy") || reason.includes("policy_blocked") || reason.includes("sandbox") || reason.includes("runner") || reason.includes("codex")) return "infra_runner";
43059
+ if (reason.includes("api") || reason.includes("http_4") || reason.includes("http_5") || reason.includes("404") || reason.includes("500") || reason.includes("roundtrip")) return "api_contract";
43060
+ if (reason.includes("cli") || reason.includes("command") || reason.includes("flag")) return "cli";
43061
+ if (reason.includes("docs") || reason.includes("unclear") || reason.includes("not_found")) return "docs";
43062
+ if (reason.includes("auth") || reason.includes("org") || reason.includes("config")) return "infra_runner";
43063
+ if (reason.includes("runtime") || reason.includes("widget") || reason.includes("proof")) return "runtime";
43064
+ return "product_ux";
43065
+ }
43066
+ function recommendedFixFor(reasonCode) {
43067
+ const owner = ownerSubsystemFor(reasonCode);
43068
+ if (owner === "api_contract") return "fix_api";
43069
+ if (owner === "cli") return "fix_cli";
43070
+ if (owner === "docs") return "fix_docs";
43071
+ if (owner === "runtime") return "fix_runtime";
43072
+ if (owner === "dojo_certification") return "add_test";
43073
+ return "fix_config";
43074
+ }
43075
+ function collapseCommandRecords(records) {
43076
+ const order = [];
43077
+ const byId = /* @__PURE__ */ new Map();
43078
+ for (const record2 of records) {
43079
+ const id = String(record2.command_id || `${record2.recorded_at || ""}:${record2.command || ""}`);
43080
+ if (!byId.has(id)) order.push(id);
43081
+ const previous = byId.get(id);
43082
+ byId.set(id, record2.phase === "completed" ? record2 : previous || record2);
43083
+ }
43084
+ return order.map((id) => byId.get(id)).filter((record2) => Boolean(record2));
43085
+ }
43086
+ function readCommandOutputJson(runDir, command) {
43087
+ const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
43088
+ if (!artifact || artifact.includes("..") || artifact.includes("/") || artifact.includes("\\")) return null;
43089
+ const artifactPath = (0, import_path19.join)(runDir, artifact);
43090
+ if (!(0, import_fs20.existsSync)(artifactPath)) return null;
43091
+ try {
43092
+ const text = (0, import_fs20.readFileSync)(artifactPath, "utf8");
43093
+ const firstObject = text.indexOf("{");
43094
+ const lastObject = text.lastIndexOf("}");
43095
+ if (firstObject < 0 || lastObject <= firstObject) return null;
43096
+ return JSON.parse(text.slice(firstObject, lastObject + 1));
43097
+ } catch {
43098
+ return null;
43099
+ }
43100
+ }
43101
+ function commandTimingBreakdown(command, output) {
43102
+ const schemaVersion = String(output?.schema_version || "");
43103
+ if (schemaVersion === "foh_cli_proof_report.v1") {
43104
+ const timing = asObject(output?.timing) || {};
43105
+ const cacheSources = /* @__PURE__ */ new Map();
43106
+ let cacheWaitMs = 0;
43107
+ let cacheHitCount = 0;
43108
+ let cacheMissCount = 0;
43109
+ for (const check2 of toArray2(output?.checks)) {
43110
+ const detail = asObject(check2)?.detail;
43111
+ const proofCache = asObject(asObject(detail)?.proof_cache);
43112
+ if (!proofCache) continue;
43113
+ if (proofCache["hit"] === true) cacheHitCount += 1;
43114
+ if (proofCache["miss"] === true) cacheMissCount += 1;
43115
+ cacheWaitMs += Number(proofCache["waited_ms"] || 0);
43116
+ increment(cacheSources, proofCache["source"] || "unknown");
43117
+ }
43118
+ return {
43119
+ kind: "proof",
43120
+ command: command.command || "",
43121
+ total_ms: Number(timing.total_ms || command.duration_ms || 0),
43122
+ total_check_duration_ms: Number(timing.total_check_duration_ms || 0),
43123
+ slow_checks: toArray2(timing.slow_checks).slice(0, 5),
43124
+ cache_wait_ms: cacheWaitMs,
43125
+ cache_hit_count: cacheHitCount,
43126
+ cache_miss_count: cacheMissCount,
43127
+ cache_sources: ranked(cacheSources)
43128
+ };
43129
+ }
43130
+ if (schemaVersion === "foh_certification_run.v1") {
43131
+ const timing = asObject(output?.timing) || {};
43132
+ const certificate = asObject(output?.certificate) || {};
43133
+ return {
43134
+ kind: "certification",
43135
+ command: command.command || "",
43136
+ total_ms: Number(timing.total_ms || command.duration_ms || 0),
43137
+ api_ms: Number(timing.api_ms || 0),
43138
+ status: output?.status || null,
43139
+ reason_code: output?.reason_code || null,
43140
+ scenario_summary: certificate.scenario_summary || null
43141
+ };
43142
+ }
43143
+ return null;
43144
+ }
43145
+ function analyzeRunArtifacts(runPath, run, cwd) {
43146
+ const runDir = (0, import_path19.dirname)(runPath);
43147
+ const commands = collapseCommandRecords(readNdjson((0, import_path19.join)(runDir, "commands.ndjson")));
43148
+ const reasonCounts = /* @__PURE__ */ new Map();
43149
+ const slowSteps = [];
43150
+ const timingBreakdowns = [];
43151
+ let completed = 0;
43152
+ let withDuration = 0;
43153
+ let totalDuration = 0;
43154
+ for (const command of commands) {
43155
+ const output = readCommandOutputJson(runDir, command);
43156
+ const breakdown = commandTimingBreakdown(command, output);
43157
+ if (breakdown) timingBreakdowns.push({
43158
+ run_id: run.run_id,
43159
+ run_path: (0, import_path19.relative)(cwd, runPath).replaceAll("\\", "/"),
43160
+ ...breakdown
43161
+ });
43162
+ if (command.phase === "completed" || command.completed_at) completed += 1;
43163
+ if (typeof command.duration_ms === "number") {
43164
+ withDuration += 1;
43165
+ totalDuration += command.duration_ms;
43166
+ slowSteps.push({
43167
+ run_id: run.run_id,
43168
+ run_path: (0, import_path19.relative)(cwd, runPath).replaceAll("\\", "/"),
43169
+ command: command.command || "",
43170
+ duration_ms: command.duration_ms,
43171
+ status: command.status || null,
43172
+ reason_code: command.reason_code || null,
43173
+ check_reason_codes: Array.isArray(command.check_reason_codes) ? command.check_reason_codes : []
43174
+ });
43175
+ }
43176
+ if (command.reason_code) increment(reasonCounts, command.reason_code);
43177
+ for (const reasonCode of toArray2(command.check_reason_codes)) {
43178
+ if (reasonCode) increment(reasonCounts, reasonCode);
43179
+ }
43180
+ }
43181
+ const codexEvents = readNdjson((0, import_path19.join)(runDir, "codex-exec.jsonl"));
43182
+ const codexDocs = /* @__PURE__ */ new Set();
43183
+ let codexCommandExecutions = 0;
43184
+ let codexFailedExitCodes = 0;
43185
+ for (const event of codexEvents) {
43186
+ const item = asObject(event.item) || event;
43187
+ if (item.type === "command_execution" && item.status === "completed") {
43188
+ codexCommandExecutions += 1;
43189
+ if (typeof item.exit_code === "number" && item.exit_code !== 0) codexFailedExitCodes += 1;
43190
+ }
43191
+ for (const url2 of collectDocUrls(JSON.stringify(event))) codexDocs.add(url2);
43192
+ }
43193
+ const docs = /* @__PURE__ */ new Set([
43194
+ ...toArray2(run.docs_pages_used).map(String),
43195
+ ...Array.from(codexDocs)
43196
+ ]);
43197
+ return {
43198
+ command_log_present: (0, import_fs20.existsSync)((0, import_path19.join)(runDir, "commands.ndjson")),
43199
+ command_count: commands.length,
43200
+ completed_command_count: completed,
43201
+ missing_completion_count: Math.max(0, commands.length - completed),
43202
+ commands_with_duration_count: withDuration,
43203
+ total_command_duration_ms: totalDuration,
43204
+ command_reason_codes: ranked(reasonCounts),
43205
+ slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms) - Number(a.duration_ms)).slice(0, 10),
43206
+ timing_breakdowns: timingBreakdowns,
43207
+ docs_pages_observed: Array.from(docs).sort(),
43208
+ codex_command_execution_completed_count: codexCommandExecutions,
43209
+ codex_failed_exit_code_count: codexFailedExitCodes
43210
+ };
43211
+ }
43212
+ function summarizeExternalAgentRuns(options) {
43213
+ const cwd = (0, import_path19.resolve)(options.cwd || process.cwd());
43214
+ const root = (0, import_path19.resolve)(cwd, options.root);
43215
+ const loaded = readRunRecords(root, cwd);
43216
+ const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
43217
+ const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
43218
+ const statusCounts = /* @__PURE__ */ new Map();
43219
+ const modelCounts = /* @__PURE__ */ new Map();
43220
+ const failureCounts = /* @__PURE__ */ new Map();
43221
+ const commandReasonCounts = /* @__PURE__ */ new Map();
43222
+ const docsCounts = /* @__PURE__ */ new Map();
43223
+ const slowSteps = [];
43224
+ const timingBreakdowns = [];
43225
+ let manualInterventions = 0;
43226
+ let commandCount = 0;
43227
+ let completedCommandCount = 0;
43228
+ let missingCompletionCount = 0;
43229
+ let commandsWithDurationCount = 0;
43230
+ let totalCommandDurationMs = 0;
43231
+ let commandLogRunCount = 0;
43232
+ let codexCommandExecutions = 0;
43233
+ let codexFailedExitCodes = 0;
43234
+ for (const record2 of records) {
43235
+ const run = record2.run;
43236
+ increment(statusCounts, run.status);
43237
+ increment(modelCounts, `${run.model_provider}/${run.model_name}`);
43238
+ manualInterventions += Number(run.manual_intervention_count || 0);
43239
+ if (run.status !== "pass") increment(failureCounts, run.failure_reason_code || "unknown");
43240
+ const artifactSummary = analyzeRunArtifacts(record2.path, run, cwd);
43241
+ if (artifactSummary.command_log_present) commandLogRunCount += 1;
43242
+ commandCount += Number(artifactSummary.command_count || 0);
43243
+ completedCommandCount += Number(artifactSummary.completed_command_count || 0);
43244
+ missingCompletionCount += Number(artifactSummary.missing_completion_count || 0);
43245
+ commandsWithDurationCount += Number(artifactSummary.commands_with_duration_count || 0);
43246
+ totalCommandDurationMs += Number(artifactSummary.total_command_duration_ms || 0);
43247
+ codexCommandExecutions += Number(artifactSummary.codex_command_execution_completed_count || 0);
43248
+ codexFailedExitCodes += Number(artifactSummary.codex_failed_exit_code_count || 0);
43249
+ for (const row of toArray2(artifactSummary.slow_steps)) slowSteps.push(row);
43250
+ for (const row of toArray2(artifactSummary.timing_breakdowns)) timingBreakdowns.push(row);
43251
+ for (const row of toArray2(artifactSummary.command_reason_codes)) {
43252
+ const entry = asObject(row);
43253
+ if (entry) increment(commandReasonCounts, entry.key, Number(entry.count || 1));
43254
+ }
43255
+ for (const page of toArray2(artifactSummary.docs_pages_observed)) increment(docsCounts, page);
43256
+ }
43257
+ const topFailures = ranked(failureCounts);
43258
+ const commandReasonCodes2 = ranked(commandReasonCounts);
43259
+ const recommendedFixes = topFailures.map((failure) => ({
43260
+ reason_code: failure.key,
43261
+ count: failure.count,
43262
+ recommended_fix: recommendedFixFor(failure.key),
43263
+ owner_subsystem: ownerSubsystemFor(failure.key)
43264
+ }));
43265
+ const nextRecommendedFix = recommendedFixes[0] || null;
43266
+ return {
43267
+ schema_version: "external_agent_run_summary.v1",
43268
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
43269
+ root: (0, import_path19.relative)(cwd, root).replaceAll("\\", "/") || ".",
43270
+ cohort_id: selectedCohortId,
43271
+ current_baseline_only: Boolean(selectedCohortId),
43272
+ run_count: records.length,
43273
+ invalid_run_count: selectedCohortId ? 0 : loaded.invalid_runs.length,
43274
+ status_counts: Object.fromEntries(statusCounts),
43275
+ model_counts: ranked(modelCounts),
43276
+ manual_intervention_count: manualInterventions,
43277
+ top_failure_reason_codes: topFailures,
43278
+ docs_pages_observed: ranked(docsCounts),
43279
+ command_telemetry: {
43280
+ run_count_with_command_log: commandLogRunCount,
43281
+ command_count: commandCount,
43282
+ completed_command_count: completedCommandCount,
43283
+ missing_completion_count: missingCompletionCount,
43284
+ commands_with_duration_count: commandsWithDurationCount,
43285
+ total_command_duration_ms: totalCommandDurationMs,
43286
+ command_reason_codes: commandReasonCodes2,
43287
+ slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20),
43288
+ timing_breakdowns: timingBreakdowns.sort((a, b) => Number(b.total_ms || 0) - Number(a.total_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
43289
+ },
43290
+ codex_telemetry: {
43291
+ command_execution_completed_count: codexCommandExecutions,
43292
+ failed_exit_code_count: codexFailedExitCodes
43293
+ },
43294
+ recommended_fixes: recommendedFixes,
43295
+ next_recommended_fix: nextRecommendedFix,
43296
+ fix_selection_policy: {
43297
+ mode: "coherent_failure_cluster_first",
43298
+ rule: "Fix the highest-impact owner subsystem locally with focused proof, then rerun the same prompt once externally.",
43299
+ run_failure_weight: 3,
43300
+ command_reason_weight: 1
43301
+ },
43302
+ next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
43303
+ invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
43304
+ run_paths: records.map((record2) => (0, import_path19.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
43305
+ };
43306
+ }
43307
+ function runExternalAgentRunSummary(options) {
43308
+ const summary = summarizeExternalAgentRuns(options);
43309
+ const invalidRuns = toArray2(summary.invalid_runs);
43310
+ const status = invalidRuns.length > 0 ? "failed" : "passed";
43311
+ const report = {
43312
+ report_schema_version: "script_report.v1",
43313
+ script: "foh eval external-agent summary",
43314
+ checked_at: (/* @__PURE__ */ new Date()).toISOString(),
43315
+ status,
43316
+ errors: invalidRuns.map((entry) => {
43317
+ const object3 = asObject(entry);
43318
+ return `${object3?.path || "unknown"}: ${JSON.stringify(object3?.findings || [])}`;
43319
+ }),
43320
+ warnings: Number(summary.run_count || 0) === 0 ? ["no external-agent run artifacts found"] : [],
43321
+ report: summary
43322
+ };
43323
+ if (options.out) {
43324
+ (0, import_fs20.mkdirSync)((0, import_path19.dirname)((0, import_path19.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
43325
+ (0, import_fs20.writeFileSync)((0, import_path19.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
43326
+ `, "utf8");
43327
+ }
43328
+ if (options.report) {
43329
+ (0, import_fs20.mkdirSync)((0, import_path19.dirname)((0, import_path19.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
43330
+ (0, import_fs20.writeFileSync)((0, import_path19.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
43331
+ `, "utf8");
43332
+ }
43333
+ return { summary, report };
43334
+ }
43335
+
42387
43336
  // src/commands/eval.ts
42388
43337
  var DEFAULT_PROMPT_VERSION = "blank-setup.v1";
42389
43338
  var DEFAULT_BATCH_MODELS = "openai/codex,anthropic/claude,cursor/agent";
42390
43339
  var PROMPTS = {
42391
43340
  "blank-setup.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 ...`, because cached older packages can produce invalid evidence. Install or verify the FOH CLI, authenticate or reach a deterministic auth blocker, then create or configure a Front Of House voice agent and website widget. Mass evals reuse existing eval state: run `npx --yes @f-o-h/cli@latest org status --json` and `npx --yes @f-o-h/cli@latest agent list --json` before trying to create a fresh agent; if an existing eval agent is present, configure and prove that agent instead of creating a second bronze-tier agent. Prefer the certification-oriented buyer templates: run `npx --yes @f-o-h/cli@latest templates list --category buyer --json` and use `UK Buyer Qualification` or `Viewing Booking` when available; do not use a greeting-only template for proof/certification. Prefer `npx --yes @f-o-h/cli@latest setup --phone-mode observe` for the free scaffold path: agent, widget, voice config, smoke test, certification, and publish readiness together. Treat phone-number purchasing as an explicit paid/scarce contact-path step, not part of high-volume eval setup. If `FOH_CLI_SPEND_POLICY=no_spend` is active and a command returns `paid_resource_blocked_by_spend_policy`, do not try to bypass it; continue widget/setup proof and report that exact reason code for the phone path. If the customer/operator explicitly owns a number and asks for real PSTN proof, use `npx --yes @f-o-h/cli@latest provision byon attach --phone-number <e164> --confirm-owned --json`; do not invent ownership or buy a FOH-owned number. Run proof/smoke/certification where available, including widget proof, voice proof, and one explicit `foh certify run --agent <id> --profile release --json` before publish. `foh prove` does not run release certification by default; only pass `--include-certification --proof-cache-dir .foh/proof-cache` when an explicit combined proof/certification run is required. If voice proof returns `contact_phone_missing` or `voice_contact_expected_no_spend_hold`, report that exact reason code unless a BYON/customer-approved phone path already exists. 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. Produce a final evidence summary with commands run, docs used, artifacts created, and any blocker reason codes. Do not assume access to the private source repository.",
43341
+ "restaurant-table-booking-blackbox.v1": "Set up a front of house system for a real restaurant that takes table bookings. Find Front Of House online and use only public web pages, public docs, public API docs, and public package installs. Do not use or inspect any private repository, local source checkout, unpublished local commands, or hidden runbooks. Pick a real restaurant name and official website from public information, then try to reach a deterministic setup/proof result for a table-booking front-of-house agent. Use the authenticated environment if the public CLI discovers it, but do not expose credentials or secrets. Do not buy phone numbers or paid resources. If the system blocks because customer-owned credentials, booking-system access, approval, or live validation are missing, treat that as a valid hold only when the command returns machine-readable reason codes 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`, `restaurant_name`, `restaurant_source_url`, `docs_pages_used`, `commands_run`, `artifacts_created`, `final_status`, `blocker_reason_codes`, and `friction_points` before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, final pass/hold/fail status, blocker reason codes, and every friction point encountered.",
42392
43342
  "real-estate-buyer-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 buyer-enquiry real-estate agent (UK Buyer Qualification preferred), prove widget behavior, and prove voice behavior in no-spend mode. Required path: 1) verify auth/org scope and reuse existing eval org/agent where possible; 2) select/apply buyer template; 3) configure widget + voice; 4) run widget smoke and `foh certify run --profile release`; 5) run voice proof and treat no-spend contact holds as expected only when all non-contact gates pass. Do not buy numbers; if spend policy blocks purchase, record `paid_resource_blocked_by_spend_policy` and continue no-spend proof path. Final artifact must include: selected template id/slug, commands executed, pass/hold/fail per gate, reason codes, docs_pages_used, and next fix commands.",
42393
43343
  "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.",
42394
43344
  "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.",
@@ -43066,7 +44016,12 @@ function installSoftExitTrap() {
43066
44016
  // src/lib/mission-help.ts
43067
44017
  var CLI_MISSION_EXAMPLES = [
43068
44018
  { mission: "Start", command: "foh start", description: "guided setup and next action selector" },
43069
- { mission: "Setup", command: "foh setup --phone-mode observe --json", description: "create or update agent, widget, voice config, and proof scaffold" },
44019
+ { mission: "Setup Objective", command: 'foh setup --objective "<goal>" --business-name <name> --industry <industry> --source-url <url> --json', description: "select template and create/update the agent from a business goal" },
44020
+ { mission: "Objective Plan", command: "foh objective plan --business-name <name> --source-url <url> --out test-results/objective-plan.latest.json --json", description: "preview setup and onboarding context without applying" },
44021
+ { mission: "Objective Apply", command: "foh objective apply --evidence <json|@file> --out test-results/objective-apply.latest.json --json", description: "submit verified evidence into customer-live gating path" },
44022
+ { mission: "Objective Prove", command: "foh objective prove --business-name <name> --source-url <url> --out test-results/objective-live.latest.json --json", description: "run customer-live proof check directly" },
44023
+ { mission: "Objective Status", command: "foh objective status --business-name <name> --source-url <url> --out test-results/objective-status.latest.json --json", description: "compose setup and live status into one report envelope" },
44024
+ { mission: "Setup Legacy", command: "foh setup --phone-mode observe --json", description: "create or update agent from an explicit template" },
43070
44025
  { mission: "Prove", command: "foh prove --agent <agent_id> --mission widget --json", description: "produce a machine-readable proof report" },
43071
44026
  { mission: "Publish", command: "foh publish --agent <agent_id> --json", description: "publish when proof and release evidence pass" },
43072
44027
  { mission: "Debug", command: "foh debug --out test-results/foh-cli-diag.latest.json --json", description: "collect auth/org/API diagnostics" }
@@ -43170,6 +44125,7 @@ registerCertify(program2);
43170
44125
  registerDiag(program2);
43171
44126
  registerBug(program2);
43172
44127
  registerProve(program2);
44128
+ registerObjective(program2);
43173
44129
  registerInteractive(program2);
43174
44130
  registerAgentPublishCommand(program2, { publicAlias: true });
43175
44131
  registerEval(program2);