@f-o-h/cli 0.1.4 → 0.1.6

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 = resolve5.call(this, root, ref);
6049
+ let _sch = resolve8.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 resolve5(root, ref) {
6076
+ function resolve8(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 resolve5(baseURI, relativeURI, options) {
6651
+ function resolve8(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: resolve5,
6878
+ resolve: resolve8,
6879
6879
  resolveComponent,
6880
6880
  equal,
6881
6881
  serialize,
@@ -9889,6 +9889,8 @@ var FohError = class extends Error {
9889
9889
  remediation;
9890
9890
  statusCode;
9891
9891
  detail;
9892
+ reasonCode;
9893
+ nextCommands;
9892
9894
  constructor(opts) {
9893
9895
  super(opts.error);
9894
9896
  this.name = "FohError";
@@ -9898,6 +9900,8 @@ var FohError = class extends Error {
9898
9900
  this.remediation = opts.remediation;
9899
9901
  this.statusCode = opts.statusCode;
9900
9902
  this.detail = opts.detail;
9903
+ this.reasonCode = opts.reasonCode;
9904
+ this.nextCommands = opts.nextCommands;
9901
9905
  }
9902
9906
  toJSON() {
9903
9907
  return {
@@ -9905,6 +9909,8 @@ var FohError = class extends Error {
9905
9909
  status: this.status,
9906
9910
  error: this.error,
9907
9911
  remediation: this.remediation,
9912
+ reason_code: this.reasonCode,
9913
+ next_commands: this.nextCommands,
9908
9914
  status_code: this.statusCode,
9909
9915
  detail: this.detail
9910
9916
  };
@@ -9913,6 +9919,34 @@ var FohError = class extends Error {
9913
9919
 
9914
9920
  // src/lib/output.ts
9915
9921
  var import_picocolors = __toESM(require_picocolors());
9922
+
9923
+ // src/lib/cli-envelope.ts
9924
+ function envelopeOk(status) {
9925
+ return status === "pass" || status === "success" || status === "exported";
9926
+ }
9927
+ function dedupeCommands(commands = []) {
9928
+ return Array.from(new Set(commands.map((command) => String(command || "").trim()).filter(Boolean)));
9929
+ }
9930
+ function cliEnvelope(input) {
9931
+ return {
9932
+ schema_version: input.schemaVersion ?? "foh_cli_envelope.v1",
9933
+ ok: envelopeOk(input.status),
9934
+ status: input.status,
9935
+ reason_code: input.reasonCode,
9936
+ summary: input.summary,
9937
+ ids: input.ids ?? {},
9938
+ checks: input.checks ?? [],
9939
+ artifacts: input.artifacts ?? {},
9940
+ next_commands: dedupeCommands(input.nextCommands),
9941
+ ...input.extra ?? {}
9942
+ };
9943
+ }
9944
+ function reasonCodeFromStep(step, fallback = "cli_command_failed") {
9945
+ const normalized = step.trim().toLowerCase().replace(/^\/+/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9946
+ return normalized ? `${normalized}_failed` : fallback;
9947
+ }
9948
+
9949
+ // src/lib/output.ts
9916
9950
  function resolveJsonMode(opts = {}) {
9917
9951
  return Boolean(opts.json || process.argv.includes("--json"));
9918
9952
  }
@@ -9927,13 +9961,21 @@ function formatError(e, opts = {}) {
9927
9961
  if (resolveJsonMode(opts)) {
9928
9962
  process.stderr.write(
9929
9963
  JSON.stringify(
9930
- {
9931
- error: {
9932
- step: e.step,
9933
- message: e.error,
9934
- remediation: e.remediation
9964
+ cliEnvelope({
9965
+ status: "fail",
9966
+ reasonCode: e.reasonCode ?? reasonCodeFromStep(e.step),
9967
+ summary: e.error,
9968
+ nextCommands: e.nextCommands,
9969
+ extra: {
9970
+ error: {
9971
+ step: e.step,
9972
+ message: e.error,
9973
+ remediation: e.remediation,
9974
+ status_code: e.statusCode,
9975
+ detail: e.detail
9976
+ }
9935
9977
  }
9936
- },
9978
+ }),
9937
9979
  null,
9938
9980
  2
9939
9981
  ) + "\n"
@@ -10063,21 +10105,21 @@ async function promptLine(label, {
10063
10105
  allowEmpty = false,
10064
10106
  defaultValue
10065
10107
  } = {}) {
10066
- return await new Promise((resolve5) => {
10108
+ return await new Promise((resolve8) => {
10067
10109
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
10068
10110
  const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
10069
10111
  rl.question(`${label}${suffix}: `, (answer) => {
10070
10112
  rl.close();
10071
10113
  const value = String(answer ?? "").trim();
10072
10114
  if (!value && typeof defaultValue === "string") {
10073
- resolve5(defaultValue);
10115
+ resolve8(defaultValue);
10074
10116
  return;
10075
10117
  }
10076
10118
  if (!value && !allowEmpty) {
10077
- resolve5("");
10119
+ resolve8("");
10078
10120
  return;
10079
10121
  }
10080
- resolve5(value);
10122
+ resolve8(value);
10081
10123
  });
10082
10124
  });
10083
10125
  }
@@ -10085,7 +10127,7 @@ async function promptSecret(label) {
10085
10127
  if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
10086
10128
  return await promptLine(label);
10087
10129
  }
10088
- return await new Promise((resolve5) => {
10130
+ return await new Promise((resolve8) => {
10089
10131
  const stdin = process.stdin;
10090
10132
  const stdout = process.stdout;
10091
10133
  const wasRaw = Boolean(stdin.isRaw);
@@ -10099,7 +10141,7 @@ async function promptSecret(label) {
10099
10141
  const finish = () => {
10100
10142
  cleanup();
10101
10143
  stdout.write("\n");
10102
- resolve5(value);
10144
+ resolve8(value);
10103
10145
  };
10104
10146
  const onData = (chunk) => {
10105
10147
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
@@ -10108,7 +10150,7 @@ async function promptSecret(label) {
10108
10150
  cleanup();
10109
10151
  process.exitCode = 130;
10110
10152
  stdout.write("\n");
10111
- return resolve5("");
10153
+ return resolve8("");
10112
10154
  }
10113
10155
  if (char === "\r" || char === "\n") {
10114
10156
  finish();
@@ -10377,7 +10419,7 @@ async function storeAuthenticatedSession(params) {
10377
10419
  return output;
10378
10420
  }
10379
10421
  function sleep(ms) {
10380
- return new Promise((resolve5) => setTimeout(resolve5, ms));
10422
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
10381
10423
  }
10382
10424
  async function runDeviceLogin(opts) {
10383
10425
  const jsonMode = Boolean(opts.json);
@@ -10915,7 +10957,7 @@ async function pollUntil(check2, opts) {
10915
10957
  }
10916
10958
  }
10917
10959
  function sleep2(ms) {
10918
- return new Promise((resolve5) => setTimeout(resolve5, ms));
10960
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
10919
10961
  }
10920
10962
 
10921
10963
  // src/commands/compliance.ts
@@ -13718,6 +13760,176 @@ function agentDraftFromManifest(manifest) {
13718
13760
  return patch;
13719
13761
  }
13720
13762
 
13763
+ // src/lib/local-replay-packet.ts
13764
+ var import_fs2 = require("fs");
13765
+
13766
+ // src/lib/transcript-export.ts
13767
+ var EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
13768
+ var PHONE_RE = /(?<!\w)(?:\+?\d[\d\s().-]{7,}\d)(?!\w)/g;
13769
+ var SECRET_RE = /\b(?:sk|pk|xai|whsec|EAAN)[A-Za-z0-9_\-]{16,}\b/g;
13770
+ function redactString(value) {
13771
+ return value.replace(EMAIL_RE, "[redacted_email]").replace(SECRET_RE, "[redacted_secret]").replace(PHONE_RE, "[redacted_phone]");
13772
+ }
13773
+ function redactObject(value) {
13774
+ if (typeof value === "string") return redactString(value);
13775
+ if (Array.isArray(value)) return value.map((entry) => redactObject(entry));
13776
+ if (!value || typeof value !== "object") return value;
13777
+ const result = {};
13778
+ for (const [key, entry] of Object.entries(value)) {
13779
+ result[key] = redactObject(entry);
13780
+ }
13781
+ return result;
13782
+ }
13783
+ function extractUserTurns(transcriptText) {
13784
+ const turns = [];
13785
+ for (const line of transcriptText.split(/\r?\n/)) {
13786
+ const match = line.match(/^\s*(?:user|customer|lead)\s*:\s*(.+)\s*$/i);
13787
+ if (match?.[1]?.trim()) turns.push({ user: match[1].trim() });
13788
+ }
13789
+ if (turns.length === 0 && transcriptText.trim()) {
13790
+ turns.push({ user: transcriptText.trim().slice(0, 500) });
13791
+ }
13792
+ return turns;
13793
+ }
13794
+ function buildReplayReadyConversation(input) {
13795
+ const conversationId = String(input.conversation.id || "");
13796
+ const transcriptText = String(input.conversation.transcript_text || "");
13797
+ return {
13798
+ ...input.conversation,
13799
+ replay_command: conversationId ? `foh agent replay --agent ${input.agentId} --conversation ${conversationId} --json` : null,
13800
+ test_fixture: {
13801
+ schema_version: "foh_scenario_fixture.v1",
13802
+ conversation_id: conversationId || null,
13803
+ turns: extractUserTurns(transcriptText)
13804
+ }
13805
+ };
13806
+ }
13807
+
13808
+ // src/lib/local-replay-packet.ts
13809
+ function asRecord(value) {
13810
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
13811
+ }
13812
+ function nonEmpty(value) {
13813
+ const text = String(value ?? "").trim();
13814
+ return text.length > 0 ? text : void 0;
13815
+ }
13816
+ function arrayOfRecords(value) {
13817
+ return Array.isArray(value) ? value.map(asRecord).filter(Boolean) : [];
13818
+ }
13819
+ function collectStrings(value, keys) {
13820
+ const found = /* @__PURE__ */ new Set();
13821
+ const visit = (entry) => {
13822
+ if (Array.isArray(entry)) {
13823
+ for (const item of entry) visit(item);
13824
+ return;
13825
+ }
13826
+ const record2 = asRecord(entry);
13827
+ if (!record2) return;
13828
+ for (const key of keys) {
13829
+ const text = nonEmpty(record2[key]);
13830
+ if (text) found.add(text);
13831
+ }
13832
+ for (const nested of Object.values(record2)) visit(nested);
13833
+ };
13834
+ visit(value);
13835
+ return Array.from(found);
13836
+ }
13837
+ function selectConversation(artifact, agentId) {
13838
+ const conversations = arrayOfRecords(artifact.conversations);
13839
+ if (conversations.length > 0) {
13840
+ if (agentId) {
13841
+ const byAgent = conversations.find((conversation) => nonEmpty(conversation.agent_id) === agentId);
13842
+ if (byAgent) return byAgent;
13843
+ }
13844
+ return conversations[0] ?? null;
13845
+ }
13846
+ if (artifact.transcript_text || artifact.test_fixture || artifact.traces) return artifact;
13847
+ return null;
13848
+ }
13849
+ function buildDiff(conversation) {
13850
+ const expectedTurns = arrayOfRecords(conversation.expected_turns);
13851
+ const fixtureTurns = arrayOfRecords((asRecord(conversation.test_fixture) ?? {}).turns);
13852
+ const actualTurns = fixtureTurns.length > 0 ? fixtureTurns : extractUserTurns(String(conversation.transcript_text ?? ""));
13853
+ if (expectedTurns.length === 0) {
13854
+ return {
13855
+ status: "not_evaluated",
13856
+ reason_code: "expected_turns_missing",
13857
+ expected_turn_count: 0,
13858
+ actual_turn_count: actualTurns.length,
13859
+ differences: []
13860
+ };
13861
+ }
13862
+ const differences = [];
13863
+ const max = Math.max(expectedTurns.length, actualTurns.length);
13864
+ for (let i = 0; i < max; i += 1) {
13865
+ const expected = nonEmpty(expectedTurns[i]?.user);
13866
+ const actual = nonEmpty(actualTurns[i]?.user);
13867
+ if (expected !== actual) differences.push(`turn_${i + 1}: expected "${expected ?? ""}" got "${actual ?? ""}"`);
13868
+ }
13869
+ return {
13870
+ status: differences.length === 0 ? "match" : "diff",
13871
+ reason_code: differences.length === 0 ? "replay_diff_match" : "replay_diff_mismatch",
13872
+ expected_turn_count: expectedTurns.length,
13873
+ actual_turn_count: actualTurns.length,
13874
+ differences
13875
+ };
13876
+ }
13877
+ function buildLocalReplayPacket(input) {
13878
+ const artifact = JSON.parse((0, import_fs2.readFileSync)(input.filePath, "utf-8"));
13879
+ const conversation = selectConversation(artifact, input.agentId);
13880
+ if (!conversation) {
13881
+ return {
13882
+ schema_version: "foh_agent_replay_packet.v1",
13883
+ status: "local_artifact_not_replayable",
13884
+ source: { type: "file", file: input.filePath, agent_id: input.agentId ?? null },
13885
+ not_replayable_reason: "file_has_no_conversation_payload",
13886
+ next_commands: ["foh transcripts export --agent <agent-id> --hydrate --include-traces --format json --out transcript-export.json --json"]
13887
+ };
13888
+ }
13889
+ const traces = arrayOfRecords(conversation.traces ?? artifact.traces);
13890
+ const traceIds = traces.map((trace) => nonEmpty(trace.id)).filter(Boolean);
13891
+ const agentId = input.agentId ?? nonEmpty(conversation.agent_id) ?? nonEmpty(artifact.agent_id);
13892
+ const conversationId = nonEmpty(conversation.id) ?? nonEmpty((asRecord(conversation.test_fixture) ?? {}).conversation_id);
13893
+ const firstTraceId = traceIds[0];
13894
+ return {
13895
+ schema_version: "foh_agent_replay_packet.v1",
13896
+ status: "local_replay_packet_created",
13897
+ source: {
13898
+ type: "file",
13899
+ file: input.filePath,
13900
+ agent_id: agentId ?? null,
13901
+ conversation_id: conversationId ?? null
13902
+ },
13903
+ replay_runtime: {
13904
+ mode: "local_artifact",
13905
+ version: "foh_agent_replay_local_v1"
13906
+ },
13907
+ agent_reference: {
13908
+ agent_id: agentId ?? null,
13909
+ agent_version: conversation.agent_version ?? artifact.agent_version ?? null,
13910
+ config_ref: conversation.config_ref ?? artifact.config_ref ?? null
13911
+ },
13912
+ trace_ids: traceIds,
13913
+ correlation_ids: collectStrings([conversation, artifact], ["correlation_id", "correlationId"]),
13914
+ tool_references: collectStrings(traces, ["tool_name", "toolName", "name"]),
13915
+ knowledge_snapshot_refs: collectStrings([conversation, artifact], ["knowledge_snapshot_id", "knowledgeSnapshotId", "knowledge_ref"]),
13916
+ conversation,
13917
+ trace_count: traces.length,
13918
+ traces,
13919
+ diff: buildDiff(conversation),
13920
+ not_replayable_reason: null,
13921
+ next_commands: [
13922
+ ...agentId && firstTraceId ? [
13923
+ `foh agent replay --agent ${agentId} --trace ${firstTraceId} --json`,
13924
+ `foh tests from-trace --agent ${agentId} --trace ${firstTraceId} --json`
13925
+ ] : [],
13926
+ ...agentId && conversationId ? [
13927
+ `foh test run --suite <suite.yml> --agent ${agentId} --json`
13928
+ ] : []
13929
+ ]
13930
+ };
13931
+ }
13932
+
13721
13933
  // src/commands/agent-candidate-decision.ts
13722
13934
  function registerAgentCandidateDecisionCommands(agent) {
13723
13935
  const candidateDecision = agent.command("candidate-decision").description("Read/write candidate promotion decisions");
@@ -13783,8 +13995,8 @@ function registerAgentGuardrailCommands(agent) {
13783
13995
  try {
13784
13996
  rule = JSON.parse(opts.rule);
13785
13997
  } catch {
13786
- const { readFileSync: readFileSync6 } = await import("fs");
13787
- rule = JSON.parse(readFileSync6(opts.rule, "utf-8"));
13998
+ const { readFileSync: readFileSync9 } = await import("fs");
13999
+ rule = JSON.parse(readFileSync9(opts.rule, "utf-8"));
13788
14000
  }
13789
14001
  const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
13790
14002
  method: "POST",
@@ -14190,6 +14402,74 @@ function registerAgent(program3) {
14190
14402
  const data = await apiFetch(`/v1/console/agents/${opts.agent}`, { apiUrlOverride: opts.apiUrl });
14191
14403
  format(data, { json: opts.json ?? false });
14192
14404
  }));
14405
+ agent.command("replay").description("Create a replay/debug packet from a trace or conversation").option("--trace <id>", "Trace event ID to replay through the server trace replay endpoint").option("--conversation <id>", "Conversation ID to package with transcript and traces").option("--file <path>", "Local transcript/replay artifact JSON to package without API access").option("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14406
+ const sourceCount = [opts.trace, opts.conversation, opts.file].filter(Boolean).length;
14407
+ if (sourceCount === 0) {
14408
+ throw new FohError({
14409
+ step: "agent.replay",
14410
+ error: "Missing replay source",
14411
+ remediation: "Pass --trace <id>, --conversation <id>, or --file <transcript.json>.",
14412
+ statusCode: 400
14413
+ });
14414
+ }
14415
+ if (sourceCount > 1) {
14416
+ throw new FohError({
14417
+ step: "agent.replay",
14418
+ error: "Ambiguous replay source",
14419
+ remediation: "Pass only one of --trace, --conversation, or --file.",
14420
+ statusCode: 400
14421
+ });
14422
+ }
14423
+ if ((opts.trace || opts.conversation) && !opts.agent) {
14424
+ throw new FohError({
14425
+ step: "agent.replay",
14426
+ error: "Missing agent id for server replay source",
14427
+ remediation: "Pass --agent <id>, or use --file with a local transcript artifact that includes agent_id.",
14428
+ statusCode: 400
14429
+ });
14430
+ }
14431
+ if (opts.file) {
14432
+ format(buildLocalReplayPacket({
14433
+ filePath: String(opts.file),
14434
+ agentId: opts.agent ? String(opts.agent) : void 0
14435
+ }), { json: opts.json ?? false });
14436
+ return;
14437
+ }
14438
+ if (opts.trace) {
14439
+ const data2 = await apiFetch(`/v1/console/traces/${opts.trace}/replay`, {
14440
+ method: "POST",
14441
+ orgId: opts.org,
14442
+ apiUrlOverride: opts.apiUrl
14443
+ });
14444
+ format({
14445
+ schema_version: "foh_agent_replay_packet.v1",
14446
+ status: "server_replay_completed",
14447
+ source: { type: "trace", trace_id: opts.trace, agent_id: opts.agent },
14448
+ replay: data2,
14449
+ next_commands: [`foh tests from-trace --agent ${opts.agent} --trace ${opts.trace} --json`]
14450
+ }, { json: opts.json ?? false });
14451
+ return;
14452
+ }
14453
+ const data = await apiFetch(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}?include_traces=true`, {
14454
+ orgId: opts.org,
14455
+ apiUrlOverride: opts.apiUrl
14456
+ });
14457
+ const traces = Array.isArray(data.traces) ? data.traces : [];
14458
+ const firstTraceId = traces.map((trace) => String(trace.id || "").trim()).find(Boolean);
14459
+ format({
14460
+ schema_version: "foh_agent_replay_packet.v1",
14461
+ status: firstTraceId ? "conversation_replay_packet_created" : "conversation_not_replayable",
14462
+ source: { type: "conversation", conversation_id: opts.conversation, agent_id: opts.agent },
14463
+ conversation: data.conversation ?? null,
14464
+ trace_count: traces.length,
14465
+ traces,
14466
+ not_replayable_reason: firstTraceId ? null : "conversation_has_no_trace_events",
14467
+ next_commands: firstTraceId ? [
14468
+ `foh agent replay --agent ${opts.agent} --trace ${firstTraceId} --json`,
14469
+ `foh tests from-trace --agent ${opts.agent} --trace ${firstTraceId} --json`
14470
+ ] : [`foh transcripts get --agent ${opts.agent} --conversation ${opts.conversation} --include-traces --json`]
14471
+ }, { json: opts.json ?? false });
14472
+ }));
14193
14473
  const blueprint = agent.command("blueprint").description("Compile or apply Conversation Blueprint v1");
14194
14474
  blueprint.command("compile").description("Compile a Conversation Blueprint v1 file to the current policy graph draft without saving it").requiredOption("--agent <id>", "Agent ID").requiredOption("--blueprint <json|@file>", "Conversation Blueprint v1 JSON or @path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14195
14475
  const parsedBlueprint = await parseJsonOption(opts.blueprint, "--blueprint");
@@ -14316,9 +14596,9 @@ function registerAgent(program3) {
14316
14596
  process.stdout.write(yaml);
14317
14597
  return;
14318
14598
  }
14319
- const { writeFileSync: writeFileSync4 } = await import("fs");
14599
+ const { writeFileSync: writeFileSync6 } = await import("fs");
14320
14600
  const outputPath = opts.output ?? "tenant.yaml";
14321
- writeFileSync4(
14601
+ writeFileSync6(
14322
14602
  outputPath,
14323
14603
  `# tenant.yaml - Front Of House agent manifest
14324
14604
  # Edit this file and run: foh plan tenant.yaml
@@ -15753,11 +16033,11 @@ function registerVoice(program3) {
15753
16033
  }
15754
16034
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
15755
16035
  const audio = Buffer.from(await res.arrayBuffer());
15756
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4 } = await import("fs");
15757
- const { dirname: dirname5, resolve: resolve5 } = await import("path");
15758
- const absolutePath = resolve5(outputPath);
16036
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync6 } = await import("fs");
16037
+ const { dirname: dirname5, resolve: resolve8 } = await import("path");
16038
+ const absolutePath = resolve8(outputPath);
15759
16039
  mkdirSync4(dirname5(absolutePath), { recursive: true });
15760
- writeFileSync4(absolutePath, audio);
16040
+ writeFileSync6(absolutePath, audio);
15761
16041
  format({
15762
16042
  status: "ok",
15763
16043
  provider,
@@ -30238,7 +30518,7 @@ var Protocol = class {
30238
30518
  return;
30239
30519
  }
30240
30520
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
30241
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
30521
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
30242
30522
  options?.signal?.throwIfAborted();
30243
30523
  }
30244
30524
  } catch (error2) {
@@ -30255,7 +30535,7 @@ var Protocol = class {
30255
30535
  */
30256
30536
  request(request, resultSchema, options) {
30257
30537
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
30258
- return new Promise((resolve5, reject) => {
30538
+ return new Promise((resolve8, reject) => {
30259
30539
  const earlyReject = (error2) => {
30260
30540
  reject(error2);
30261
30541
  };
@@ -30333,7 +30613,7 @@ var Protocol = class {
30333
30613
  if (!parseResult.success) {
30334
30614
  reject(parseResult.error);
30335
30615
  } else {
30336
- resolve5(parseResult.data);
30616
+ resolve8(parseResult.data);
30337
30617
  }
30338
30618
  } catch (error2) {
30339
30619
  reject(error2);
@@ -30594,12 +30874,12 @@ var Protocol = class {
30594
30874
  }
30595
30875
  } catch {
30596
30876
  }
30597
- return new Promise((resolve5, reject) => {
30877
+ return new Promise((resolve8, reject) => {
30598
30878
  if (signal.aborted) {
30599
30879
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
30600
30880
  return;
30601
30881
  }
30602
- const timeoutId = setTimeout(resolve5, interval);
30882
+ const timeoutId = setTimeout(resolve8, interval);
30603
30883
  signal.addEventListener("abort", () => {
30604
30884
  clearTimeout(timeoutId);
30605
30885
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -31699,7 +31979,7 @@ var McpServer = class {
31699
31979
  let task = createTaskResult.task;
31700
31980
  const pollInterval = task.pollInterval ?? 5e3;
31701
31981
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
31702
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
31982
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
31703
31983
  const updatedTask = await extra.taskStore.getTask(taskId);
31704
31984
  if (!updatedTask) {
31705
31985
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -32348,19 +32628,19 @@ var StdioServerTransport = class {
32348
32628
  this.onclose?.();
32349
32629
  }
32350
32630
  send(message) {
32351
- return new Promise((resolve5) => {
32631
+ return new Promise((resolve8) => {
32352
32632
  const json3 = serializeMessage(message);
32353
32633
  if (this._stdout.write(json3)) {
32354
- resolve5();
32634
+ resolve8();
32355
32635
  } else {
32356
- this._stdout.once("drain", resolve5);
32636
+ this._stdout.once("drain", resolve8);
32357
32637
  }
32358
32638
  });
32359
32639
  }
32360
32640
  };
32361
32641
 
32362
32642
  // src/lib/cli-version.ts
32363
- var CLI_VERSION = "0.1.4";
32643
+ var CLI_VERSION = "0.1.6";
32364
32644
 
32365
32645
  // src/commands/mcp-serve.ts
32366
32646
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -32545,7 +32825,7 @@ async function runFohCli(params) {
32545
32825
  effectiveArgv.push("--json");
32546
32826
  }
32547
32827
  const command = `foh ${effectiveArgv.join(" ")}`;
32548
- return await new Promise((resolve5) => {
32828
+ return await new Promise((resolve8) => {
32549
32829
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
32550
32830
  stdio: ["ignore", "pipe", "pipe"],
32551
32831
  env: {
@@ -32570,7 +32850,7 @@ async function runFohCli(params) {
32570
32850
  });
32571
32851
  child.once("error", (error2) => {
32572
32852
  clearTimeout(timeoutHandle);
32573
- resolve5({
32853
+ resolve8({
32574
32854
  ok: false,
32575
32855
  command,
32576
32856
  argv: effectiveArgv,
@@ -32586,7 +32866,7 @@ async function runFohCli(params) {
32586
32866
  const stderrText = finalizeBoundedText(stderrBuffer);
32587
32867
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
32588
32868
  const stdoutJson = tryParseJson(stdoutText);
32589
- resolve5({
32869
+ resolve8({
32590
32870
  ok: !timedOut && exitCode === 0,
32591
32871
  command,
32592
32872
  argv: effectiveArgv,
@@ -33359,18 +33639,143 @@ function registerMcp(program3) {
33359
33639
  }
33360
33640
 
33361
33641
  // src/commands/knowledge.ts
33362
- var import_fs2 = require("fs");
33642
+ var import_fs3 = require("fs");
33363
33643
  var import_path2 = require("path");
33644
+
33645
+ // src/lib/query-options.ts
33646
+ function parsePositiveInt(value, fallback, min, max) {
33647
+ const parsed = Number(value ?? fallback);
33648
+ if (!Number.isFinite(parsed)) return fallback;
33649
+ return Math.max(min, Math.min(max, Math.trunc(parsed)));
33650
+ }
33651
+ function withQuery(path2, params) {
33652
+ const query = params.toString();
33653
+ return query ? `${path2}?${query}` : path2;
33654
+ }
33655
+
33656
+ // src/commands/knowledge.ts
33364
33657
  function readDraftKnowledgeText(draft) {
33365
33658
  const fromRaw = typeof draft.knowledge_base_raw === "string" ? draft.knowledge_base_raw : "";
33366
33659
  if (fromRaw.trim().length > 0) return fromRaw;
33367
33660
  const fromLegacy = typeof draft.knowledge_base === "string" ? draft.knowledge_base : "";
33368
33661
  return fromLegacy;
33369
33662
  }
33663
+ function tokenize(value) {
33664
+ return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
33665
+ }
33666
+ function chunkKnowledgeText(text) {
33667
+ return text.split(/\n\s*\n|---+/g).map((chunk) => chunk.trim()).filter(Boolean).map((chunk, index) => ({ index, text: chunk }));
33668
+ }
33669
+ function scoreChunk(queryTokens, chunkText) {
33670
+ if (queryTokens.size === 0) return 0;
33671
+ const chunkTokens = new Set(tokenize(chunkText));
33672
+ let overlap = 0;
33673
+ for (const token of queryTokens) {
33674
+ if (chunkTokens.has(token)) overlap += 1;
33675
+ }
33676
+ return Number((overlap / queryTokens.size).toFixed(6));
33677
+ }
33370
33678
  function registerKnowledge(program3) {
33371
33679
  const knowledge = program3.command("knowledge").description("Manage agent knowledge base");
33680
+ knowledge.command("query").description("Debug agent knowledge retrieval for a question").requiredOption("--agent <id>", "Agent ID").requiredOption("--text <query>", "Question/query text").option("--limit <n>", "Max chunks to return (1-20)", "5").option("--min-score <n>", "Minimum overlap score for pass threshold", "0.2").option("--explain", "Include token/scoring metadata").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
33681
+ const query = String(opts.text || "").trim();
33682
+ if (query.length < 3) {
33683
+ throw new FohError({
33684
+ step: "knowledge.query",
33685
+ error: "Query text must be at least 3 characters",
33686
+ remediation: 'Pass --text "<question>" with 3+ characters.',
33687
+ statusCode: 400
33688
+ });
33689
+ }
33690
+ const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
33691
+ orgId: opts.org,
33692
+ apiUrlOverride: opts.apiUrl
33693
+ });
33694
+ const knowledgeText = readDraftKnowledgeText(draft);
33695
+ const chunks = chunkKnowledgeText(knowledgeText);
33696
+ const queryTokens = new Set(tokenize(query));
33697
+ const minScore = Math.max(0, Math.min(1, Number(opts.minScore ?? 0.2) || 0.2));
33698
+ const limit = parsePositiveInt(opts.limit, 5, 1, 20);
33699
+ const matches = chunks.map((chunk) => ({
33700
+ chunk_id: `agent-draft-${opts.agent}#${chunk.index + 1}`,
33701
+ source: "agent_draft_knowledge",
33702
+ citation: `agent:${opts.agent}:chunk:${chunk.index + 1}`,
33703
+ score: scoreChunk(queryTokens, chunk.text),
33704
+ lineage: {
33705
+ source: "agent_draft_direct",
33706
+ agent_id: opts.agent,
33707
+ chunk_index: chunk.index + 1
33708
+ },
33709
+ text: chunk.text.slice(0, 1200)
33710
+ })).filter((chunk) => chunk.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
33711
+ const topScore = matches[0]?.score ?? 0;
33712
+ const status = matches.length === 0 ? "no_match" : topScore >= minScore ? "pass" : "low_confidence";
33713
+ const reasonCode = status === "pass" ? "knowledge_query_match" : status === "low_confidence" ? "knowledge_query_low_confidence" : "knowledge_query_no_match";
33714
+ const packet = status === "pass" ? null : {
33715
+ schema_version: "foh_knowledge_query_failure_packet.v1",
33716
+ agent_id: opts.agent,
33717
+ query,
33718
+ reason_code: reasonCode,
33719
+ top_score: topScore,
33720
+ chunk_count: chunks.length,
33721
+ lineage: {
33722
+ source: "agent_draft_direct",
33723
+ agent_id: opts.agent,
33724
+ candidate_chunk_count: chunks.length
33725
+ },
33726
+ next_commands: [
33727
+ `foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
33728
+ `foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
33729
+ ]
33730
+ };
33731
+ format({
33732
+ schema_version: "foh_knowledge_query_debug.v1",
33733
+ ok: status === "pass",
33734
+ status,
33735
+ reason_code: reasonCode,
33736
+ summary: status === "pass" ? `Knowledge query matched ${matches.length} chunk(s).` : status === "low_confidence" ? `Knowledge query matched weakly; top score ${topScore}.` : "Knowledge query returned no usable matches.",
33737
+ ids: {
33738
+ agent_id: opts.agent
33739
+ },
33740
+ checks: [{
33741
+ name: "knowledge_retrieval",
33742
+ status,
33743
+ reason_code: reasonCode,
33744
+ top_score: topScore,
33745
+ min_score: minScore,
33746
+ match_count: matches.length
33747
+ }],
33748
+ artifacts: {},
33749
+ next_commands: status === "pass" ? [] : [
33750
+ `foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
33751
+ `foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
33752
+ ],
33753
+ agent_id: opts.agent,
33754
+ query,
33755
+ retrieval: {
33756
+ source: "agent_draft_direct",
33757
+ chunk_count: chunks.length,
33758
+ match_count: matches.length,
33759
+ top_score: topScore,
33760
+ min_score: minScore,
33761
+ lineage: {
33762
+ agent_id: opts.agent,
33763
+ source: "agent_draft_direct",
33764
+ candidate_chunk_count: chunks.length
33765
+ }
33766
+ },
33767
+ matches,
33768
+ failure_packet: packet,
33769
+ ...opts.explain ? {
33770
+ explain: {
33771
+ query_tokens: Array.from(queryTokens),
33772
+ scoring: "token_overlap_ratio_v1"
33773
+ }
33774
+ } : {}
33775
+ }, { json: opts.json ?? false });
33776
+ }));
33372
33777
  knowledge.command("ingest-file").description("Ingest a local file into the knowledge base").requiredOption("--file <path>", "Path to file to ingest").option("--agent <id>", "Scope to agent").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
33373
- const content = (0, import_fs2.readFileSync)(opts.file, "utf-8");
33778
+ const content = (0, import_fs3.readFileSync)(opts.file, "utf-8");
33374
33779
  let data;
33375
33780
  if (opts.agent) {
33376
33781
  const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
@@ -33529,7 +33934,7 @@ var import_crypto3 = require("crypto");
33529
33934
 
33530
33935
  // src/lib/signed-report.ts
33531
33936
  var import_crypto2 = require("crypto");
33532
- var import_fs3 = require("fs");
33937
+ var import_fs4 = require("fs");
33533
33938
  var import_path3 = require("path");
33534
33939
  function canonicalize(value) {
33535
33940
  if (value === null || value === void 0) return null;
@@ -33561,13 +33966,13 @@ function signReport(reportPayload) {
33561
33966
  }
33562
33967
  function writeSignedJsonArtifact(path2, value) {
33563
33968
  const absolutePath = (0, import_path3.resolve)(path2);
33564
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
33565
- (0, import_fs3.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
33969
+ (0, import_fs4.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
33970
+ (0, import_fs4.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
33566
33971
  return absolutePath;
33567
33972
  }
33568
33973
 
33569
33974
  // src/commands/manifest.ts
33570
- var import_fs4 = require("fs");
33975
+ var import_fs5 = require("fs");
33571
33976
  var import_picocolors3 = __toESM(require_picocolors());
33572
33977
  function formatDiff(diffs) {
33573
33978
  if (diffs.length === 0) return "No changes";
@@ -33603,7 +34008,7 @@ function formatDiff(diffs) {
33603
34008
  function loadManifestFile(filePath) {
33604
34009
  let raw;
33605
34010
  try {
33606
- raw = (0, import_fs4.readFileSync)(filePath, "utf-8");
34011
+ raw = (0, import_fs5.readFileSync)(filePath, "utf-8");
33607
34012
  } catch {
33608
34013
  throw new FohError({
33609
34014
  step: "manifest.load",
@@ -33871,27 +34276,32 @@ function optionNameToFlag(key) {
33871
34276
  function buildMissingOptionsPlan(missing, opts) {
33872
34277
  const missingFlags = missing.map(optionNameToFlag);
33873
34278
  const signInUrl = buildConsoleSignInUrl(resolveConsoleBaseUrl(opts.consoleUrl));
33874
- return {
34279
+ return cliEnvelope({
33875
34280
  status: "blocked",
33876
- code: "setup_required_options_missing",
33877
- missing_options: missingFlags,
33878
- reason: "setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources",
33879
- sign_in_url: signInUrl,
33880
- next_commands: [
34281
+ reasonCode: "setup_required_options_missing",
34282
+ summary: "Setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources.",
34283
+ ids: {},
34284
+ nextCommands: [
33881
34285
  "foh auth signup --web --json",
33882
34286
  "foh auth login --web --json",
33883
34287
  ...buildCliAuthFallbackCommands(),
33884
34288
  "foh templates list --json",
33885
34289
  'foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --widget-domains <domain> --report-out setup-report.json --json'
33886
34290
  ],
33887
- text_fallback: buildCliAuthFallbackInstructions(signInUrl),
33888
- ai_agent_instruction: [
33889
- "Do not guess org IDs, template IDs, or customer domains.",
33890
- "If no browser is available, print sign_in_url and ask the user to sign in.",
33891
- "After auth, discover orgs and templates with the listed commands.",
33892
- "Rerun setup only after all missing_options are resolved."
33893
- ]
33894
- };
34291
+ extra: {
34292
+ code: "setup_required_options_missing",
34293
+ missing_options: missingFlags,
34294
+ reason: "setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources",
34295
+ sign_in_url: signInUrl,
34296
+ text_fallback: buildCliAuthFallbackInstructions(signInUrl),
34297
+ ai_agent_instruction: [
34298
+ "Do not guess org IDs, template IDs, or customer domains.",
34299
+ "If no browser is available, print sign_in_url and ask the user to sign in.",
34300
+ "After auth, discover orgs and templates with the listed commands.",
34301
+ "Rerun setup only after all missing_options are resolved."
34302
+ ]
34303
+ }
34304
+ });
33895
34305
  }
33896
34306
  function emitMissingOptionsPlan(missing, opts) {
33897
34307
  const plan = buildMissingOptionsPlan(missing, { consoleUrl: opts.consoleUrl });
@@ -33997,7 +34407,7 @@ function registerSetup(program3) {
33997
34407
  const startedAtIso = nowIso();
33998
34408
  const startedAtMs = Date.now();
33999
34409
  if (shouldResumeSkip(name)) {
34000
- const skipped = timedStepResult(
34410
+ const skipped2 = timedStepResult(
34001
34411
  {
34002
34412
  step: name,
34003
34413
  status: "skipped",
@@ -34006,10 +34416,10 @@ function registerSetup(program3) {
34006
34416
  startedAtIso,
34007
34417
  startedAtMs
34008
34418
  );
34009
- completed.push(skipped);
34419
+ completed.push(skipped2);
34010
34420
  process.stderr.write(import_picocolors4.default.dim(` [RESUME] ${name}: skipped (resume-from ${resumeState.resumeFrom})
34011
34421
  `));
34012
- return skipped;
34422
+ return skipped2;
34013
34423
  }
34014
34424
  if (opts.dryRun) {
34015
34425
  const dryRunResult = timedStepResult(
@@ -34365,8 +34775,8 @@ function registerSetup(program3) {
34365
34775
  }
34366
34776
  try {
34367
34777
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
34368
- const { writeFileSync: writeFileSync4 } = await import("fs");
34369
- writeFileSync4(
34778
+ const { writeFileSync: writeFileSync6 } = await import("fs");
34779
+ writeFileSync6(
34370
34780
  "tenant.yaml",
34371
34781
  `# tenant.yaml - Front Of House agent manifest
34372
34782
  # Edit this file and run: foh plan tenant.yaml
@@ -34382,7 +34792,15 @@ ${serialiseManifest(manifest)}`,
34382
34792
  });
34383
34793
  const reportMeta = emitSetupReport("success");
34384
34794
  const summary = {
34795
+ schema_version: "foh_cli_setup_summary.v1",
34796
+ ok: true,
34385
34797
  status: "success",
34798
+ reason_code: "setup_completed",
34799
+ summary: "Setup completed and produced a signed setup report.",
34800
+ ids: {
34801
+ org_id: opts.org,
34802
+ agent_id: agentId ?? null
34803
+ },
34386
34804
  org_id: opts.org,
34387
34805
  agent_id: agentId,
34388
34806
  phone_number: phoneNumber,
@@ -34394,7 +34812,15 @@ ${serialiseManifest(manifest)}`,
34394
34812
  manifest_written: manifestWritten ? "tenant.yaml" : null,
34395
34813
  resume_from: resumeState.resumeFrom,
34396
34814
  setup_report_hash: reportMeta.reportHash,
34397
- setup_report_path: reportMeta.reportPath
34815
+ setup_report_path: reportMeta.reportPath,
34816
+ artifacts: {
34817
+ setup_report_path: reportMeta.reportPath,
34818
+ tenant_manifest_path: manifestWritten ? "tenant.yaml" : null
34819
+ },
34820
+ next_commands: agentId ? [
34821
+ `foh prove --agent ${agentId} --json --out test-results/foh-proof.latest.json`,
34822
+ `foh agent publish --agent ${agentId} --json`
34823
+ ] : []
34398
34824
  };
34399
34825
  format(summary, { json: opts.json ?? false });
34400
34826
  } catch (error2) {
@@ -34407,13 +34833,25 @@ ${serialiseManifest(manifest)}`,
34407
34833
  });
34408
34834
  format(
34409
34835
  {
34836
+ schema_version: "foh_cli_setup_failure.v1",
34837
+ ok: false,
34410
34838
  status: "failed",
34839
+ reason_code: error2.reasonCode ?? "setup_failed",
34840
+ summary: error2.error,
34841
+ ids: {
34842
+ org_id: opts.org,
34843
+ agent_id: agentId ?? null
34844
+ },
34411
34845
  step: error2.step,
34412
34846
  completed_steps: completed.map((stepResult) => stepResult.step),
34413
34847
  error: error2.error,
34414
34848
  remediation: error2.remediation,
34415
34849
  setup_report_hash: reportMeta.reportHash,
34416
- setup_report_path: reportMeta.reportPath
34850
+ setup_report_path: reportMeta.reportPath,
34851
+ artifacts: {
34852
+ setup_report_path: reportMeta.reportPath
34853
+ },
34854
+ next_commands: error2.nextCommands ?? [error2.remediation.replace(/^Run:\s*/, "")].filter(Boolean)
34417
34855
  },
34418
34856
  { json: opts.json ?? false }
34419
34857
  );
@@ -34506,8 +34944,8 @@ function registerSim(program3) {
34506
34944
  }
34507
34945
  const cert = response.certificate;
34508
34946
  if (opts.out) {
34509
- const { writeFileSync: writeFileSync4 } = await import("fs");
34510
- writeFileSync4(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
34947
+ const { writeFileSync: writeFileSync6 } = await import("fs");
34948
+ writeFileSync6(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
34511
34949
  process.stderr.write(` Certificate written to ${opts.out}
34512
34950
  `);
34513
34951
  }
@@ -34557,8 +34995,8 @@ function registerSim(program3) {
34557
34995
  });
34558
34996
  }
34559
34997
  if (opts.out) {
34560
- const { writeFileSync: writeFileSync4 } = await import("fs");
34561
- writeFileSync4(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
34998
+ const { writeFileSync: writeFileSync6 } = await import("fs");
34999
+ writeFileSync6(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
34562
35000
  process.stderr.write(` Final certificate written to ${opts.out}
34563
35001
  `);
34564
35002
  }
@@ -34595,19 +35033,6 @@ ${passIcon} Certification loop summary
34595
35033
 
34596
35034
  // src/commands/conversations.ts
34597
35035
  var import_crypto4 = require("crypto");
34598
-
34599
- // src/lib/query-options.ts
34600
- function parsePositiveInt(value, fallback, min, max) {
34601
- const parsed = Number(value ?? fallback);
34602
- if (!Number.isFinite(parsed)) return fallback;
34603
- return Math.max(min, Math.min(max, Math.trunc(parsed)));
34604
- }
34605
- function withQuery(path2, params) {
34606
- const query = params.toString();
34607
- return query ? `${path2}?${query}` : path2;
34608
- }
34609
-
34610
- // src/commands/conversations.ts
34611
35036
  function registerConversations(program3) {
34612
35037
  const conversations = program3.command("conversations").description("Search and operate on conversation traces and lead data");
34613
35038
  conversations.command("list").description("List/search conversations for an agent").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--terminal-state <value>", "Terminal state filter").option("--journey <value>", "Journey filter").option("--has-tool-call <value>", "Tool outcome/tool id filter").option("--scope <value>", "Scope: agent or org").option("--scope-agent-id <id>", "Optional scoped agent id when --scope org").option("--guardrail-triggered", "Only conversations with guardrail_triggered trace events").option("--provider <value>", "Provider filter from trace payload").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (1-100)", "20").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
@@ -34769,6 +35194,227 @@ function registerConversations(program3) {
34769
35194
  }));
34770
35195
  }
34771
35196
 
35197
+ // src/commands/transcripts.ts
35198
+ var import_fs6 = require("fs");
35199
+ var import_path4 = require("path");
35200
+ function listPath(agentId, opts) {
35201
+ const params = new URLSearchParams();
35202
+ if (opts.q) params.set("q", String(opts.q));
35203
+ if (opts.from) params.set("from", String(opts.from));
35204
+ if (opts.to) params.set("to", String(opts.to));
35205
+ params.set("page", String(parsePositiveInt(opts.page, 1, 1, 1e4)));
35206
+ params.set("limit", String(parsePositiveInt(opts.limit, 20, 1, 100)));
35207
+ return withQuery(`/v1/console/agents/${agentId}/conversations`, params);
35208
+ }
35209
+ function jsonl(rows) {
35210
+ return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
35211
+ }
35212
+ async function hydrateRows(input) {
35213
+ const hydrated = [];
35214
+ for (const row of input.rows) {
35215
+ const conversationId = String(row.id || "").trim();
35216
+ if (!conversationId) {
35217
+ hydrated.push(row);
35218
+ continue;
35219
+ }
35220
+ const params = new URLSearchParams();
35221
+ if (input.includeTraces) params.set("include_traces", "true");
35222
+ const detail = await apiFetch(
35223
+ withQuery(`/v1/console/agents/${input.agentId}/conversations/${conversationId}`, params),
35224
+ { orgId: input.orgId, apiUrlOverride: input.apiUrl }
35225
+ );
35226
+ hydrated.push({
35227
+ ...detail.conversation || row,
35228
+ ...input.includeTraces ? { traces: Array.isArray(detail.traces) ? detail.traces : [] } : {}
35229
+ });
35230
+ }
35231
+ return hydrated;
35232
+ }
35233
+ function registerTranscripts(program3) {
35234
+ const transcripts = program3.command("transcripts").description("List, fetch, and export conversation transcripts");
35235
+ transcripts.command("list").description("List transcript-bearing conversations for an agent").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--page <n>", "Page number", "1").option("--limit <n>", "Page size (1-100)", "20").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35236
+ const data = await apiFetch(listPath(opts.agent, opts), { orgId: opts.org, apiUrlOverride: opts.apiUrl });
35237
+ format(data, { json: opts.json ?? false });
35238
+ }));
35239
+ transcripts.command("get").description("Fetch one conversation transcript and optional trace events").requiredOption("--agent <id>", "Agent ID").requiredOption("--conversation <id>", "Conversation ID").option("--include-traces", "Include ordered trace events").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35240
+ const params = new URLSearchParams();
35241
+ if (opts.includeTraces) params.set("include_traces", "true");
35242
+ const path2 = withQuery(`/v1/console/agents/${opts.agent}/conversations/${opts.conversation}`, params);
35243
+ const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
35244
+ format(data, { json: opts.json ?? false });
35245
+ }));
35246
+ transcripts.command("export").description("Export recent transcripts as JSON or JSONL").requiredOption("--agent <id>", "Agent ID").option("--q <query>", "Full-text transcript query").option("--from <iso-date>", "Start datetime (ISO8601)").option("--to <iso-date>", "End datetime (ISO8601)").option("--limit <n>", "Rows to export (1-100)", "100").option("--format <value>", "Export format: jsonl or json", "jsonl").option("--hydrate", "Fetch full conversation detail for every exported row").option("--include-traces", "Hydrate each conversation with ordered trace events").option("--no-redact", "Disable default redaction of emails, phone numbers, and obvious secrets").option("--out <path>", "Output file path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35247
+ const exportFormat = String(opts.format || "jsonl").trim().toLowerCase();
35248
+ if (!["jsonl", "json"].includes(exportFormat)) {
35249
+ throw new FohError({
35250
+ step: "transcripts.export",
35251
+ error: `Invalid format: ${opts.format}`,
35252
+ remediation: "Use --format jsonl or --format json.",
35253
+ statusCode: 400
35254
+ });
35255
+ }
35256
+ const data = await apiFetch(listPath(opts.agent, { ...opts, page: "1" }), {
35257
+ orgId: opts.org,
35258
+ apiUrlOverride: opts.apiUrl
35259
+ });
35260
+ const listRows = Array.isArray(data.conversations) ? data.conversations : [];
35261
+ const shouldHydrate = Boolean(opts.hydrate || opts.includeTraces);
35262
+ const includeTraces = Boolean(opts.includeTraces);
35263
+ const rawRows = shouldHydrate ? await hydrateRows({
35264
+ agentId: opts.agent,
35265
+ rows: listRows,
35266
+ includeTraces,
35267
+ orgId: opts.org,
35268
+ apiUrl: opts.apiUrl
35269
+ }) : listRows;
35270
+ const replayReadyRows = rawRows.map((conversation) => buildReplayReadyConversation({
35271
+ agentId: opts.agent,
35272
+ conversation
35273
+ }));
35274
+ const rows = opts.redact === false ? replayReadyRows : redactObject(replayReadyRows);
35275
+ const exportPacket = {
35276
+ schema_version: "foh_transcript_export.v2",
35277
+ redaction: {
35278
+ enabled: opts.redact !== false,
35279
+ fields: opts.redact === false ? [] : ["email", "phone", "secret-like-token"]
35280
+ },
35281
+ hydration: {
35282
+ enabled: shouldHydrate,
35283
+ include_traces: includeTraces
35284
+ },
35285
+ conversations: rows
35286
+ };
35287
+ const content = exportFormat === "json" ? stableStringify(exportPacket) : jsonl(rows);
35288
+ if (opts.out) {
35289
+ const outputPath = (0, import_path4.resolve)(String(opts.out));
35290
+ (0, import_fs6.writeFileSync)(outputPath, content, "utf-8");
35291
+ format({
35292
+ schema_version: "foh_transcript_export_result.v1",
35293
+ ok: true,
35294
+ status: "exported",
35295
+ reason_code: "transcripts_exported",
35296
+ summary: `Exported ${rows.length} transcript row(s).`,
35297
+ ids: { agent_id: opts.agent },
35298
+ artifacts: { output_path: outputPath },
35299
+ next_commands: [
35300
+ `foh agent replay --agent ${opts.agent} --conversation <conversation-id> --json`,
35301
+ `foh test run --suite <suite.yml> --agent ${opts.agent} --json`
35302
+ ],
35303
+ format: exportFormat,
35304
+ count: rows.length,
35305
+ redaction: exportPacket.redaction,
35306
+ hydration: exportPacket.hydration,
35307
+ output_path: outputPath
35308
+ }, { json: opts.json ?? false });
35309
+ return;
35310
+ }
35311
+ if (opts.json || exportFormat === "json") {
35312
+ format({
35313
+ schema_version: "foh_transcript_export.v1",
35314
+ ok: true,
35315
+ status: "pass",
35316
+ reason_code: "transcripts_exported",
35317
+ summary: `Prepared ${rows.length} transcript row(s).`,
35318
+ ids: { agent_id: opts.agent },
35319
+ artifacts: {},
35320
+ next_commands: [
35321
+ `foh agent replay --agent ${opts.agent} --conversation <conversation-id> --json`,
35322
+ `foh test run --suite <suite.yml> --agent ${opts.agent} --json`
35323
+ ],
35324
+ redaction: exportPacket.redaction,
35325
+ hydration: exportPacket.hydration,
35326
+ conversations: exportPacket.conversations
35327
+ }, { json: opts.json ?? false });
35328
+ return;
35329
+ }
35330
+ process.stdout.write(content);
35331
+ }));
35332
+ }
35333
+
35334
+ // src/commands/analytics.ts
35335
+ function parsePreset(raw) {
35336
+ const value = String(raw || "7d").trim().toLowerCase();
35337
+ if (value === "today" || value === "7d" || value === "failed" || value === "lead-capture") return value;
35338
+ throw new FohError({
35339
+ step: "analytics.fetch",
35340
+ error: `Invalid preset: ${raw}`,
35341
+ remediation: "Use --preset today, 7d, failed, or lead-capture.",
35342
+ statusCode: 400
35343
+ });
35344
+ }
35345
+ function presetWindowDays(preset, rawWindowDays) {
35346
+ if (rawWindowDays !== void 0) return parsePositiveInt(String(rawWindowDays), preset === "today" ? 1 : 7, 1, 90);
35347
+ if (preset === "today") return 1;
35348
+ return 7;
35349
+ }
35350
+ function registerAnalytics(program3) {
35351
+ const analytics = program3.command("analytics").description("Fetch runtime analytics and inspection summaries");
35352
+ analytics.command("fetch").description("Fetch an agent analytics summary from existing reporting lanes").requiredOption("--agent <id>", "Agent ID").option("--preset <value>", "Preset: today, 7d, failed, lead-capture", "7d").option("--window-days <n>", "Lookback window override (1-90)").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35353
+ const preset = parsePreset(opts.preset);
35354
+ const windowDays = presetWindowDays(preset, opts.windowDays);
35355
+ const qualityParams = new URLSearchParams({ windowDays: String(windowDays), environment: "production" });
35356
+ const conversationParams = new URLSearchParams({
35357
+ page: "1",
35358
+ limit: "10"
35359
+ });
35360
+ if (preset === "failed") conversationParams.set("terminal_state", "failed_visible");
35361
+ if (preset === "lead-capture") conversationParams.set("journey", "lead_capture");
35362
+ const [qualityScorecard, leadDataTrends, loopKpis, conversations, voiceSlo] = await Promise.all([
35363
+ apiFetch(`/v1/console/agents/${opts.agent}/quality-scorecard?${qualityParams.toString()}`, {
35364
+ orgId: opts.org,
35365
+ apiUrlOverride: opts.apiUrl
35366
+ }),
35367
+ apiFetch(`/v1/console/agents/${opts.agent}/lead-data-trends?windowDays=${windowDays}`, {
35368
+ orgId: opts.org,
35369
+ apiUrlOverride: opts.apiUrl
35370
+ }),
35371
+ apiFetch(`/v1/console/agents/${opts.agent}/loop-kpis?windowDays=${windowDays}`, {
35372
+ orgId: opts.org,
35373
+ apiUrlOverride: opts.apiUrl
35374
+ }),
35375
+ apiFetch(withQuery(`/v1/console/agents/${opts.agent}/conversations`, conversationParams), {
35376
+ orgId: opts.org,
35377
+ apiUrlOverride: opts.apiUrl
35378
+ }),
35379
+ apiFetch(`/v1/console/voice-slo?agentId=${opts.agent}&days=${windowDays}`, {
35380
+ orgId: opts.org,
35381
+ apiUrlOverride: opts.apiUrl
35382
+ }).catch((error2) => ({
35383
+ ok: false,
35384
+ skipped: true,
35385
+ reason_code: "voice_slo_unavailable",
35386
+ message: error2 instanceof Error ? error2.message : String(error2)
35387
+ }))
35388
+ ]);
35389
+ format({
35390
+ schema_version: "foh_analytics_fetch.v1",
35391
+ ok: true,
35392
+ status: "pass",
35393
+ reason_code: "analytics_fetch_completed",
35394
+ summary: `Fetched analytics summary for preset ${preset}.`,
35395
+ ids: {
35396
+ agent_id: opts.agent
35397
+ },
35398
+ checks: [],
35399
+ artifacts: {},
35400
+ agent_id: opts.agent,
35401
+ preset,
35402
+ window_days: windowDays,
35403
+ summaries: {
35404
+ quality_scorecard: qualityScorecard,
35405
+ lead_data_trends: leadDataTrends,
35406
+ loop_kpis: loopKpis,
35407
+ conversations,
35408
+ voice_slo: voiceSlo
35409
+ },
35410
+ next_commands: [
35411
+ `foh transcripts list --agent ${opts.agent} --limit 10 --json`,
35412
+ `foh ops reporting weekly-report --agent ${opts.agent} --window-days ${Math.min(30, windowDays)} --json`
35413
+ ]
35414
+ }, { json: opts.json ?? false });
35415
+ }));
35416
+ }
35417
+
34772
35418
  // src/commands/tests.ts
34773
35419
  function registerTests(program3) {
34774
35420
  const tests = program3.command("tests").description("Manage agent test catalog and test-run lifecycle");
@@ -34953,19 +35599,361 @@ function registerTests(program3) {
34953
35599
  }));
34954
35600
  }
34955
35601
 
34956
- // src/commands/ops.ts
34957
- function parseClientErrorSource(raw) {
34958
- if (!raw) return void 0;
34959
- const source = String(raw).trim().toLowerCase();
34960
- if (source === "window" || source === "react" || source === "unhandledrejection") {
34961
- return source;
34962
- }
34963
- throw new FohError({
34964
- step: "ops.client-errors.report",
34965
- error: `Invalid --source "${raw}"`,
34966
- remediation: "Use one of: window, react, unhandledrejection.",
34967
- statusCode: 400
34968
- });
35602
+ // src/commands/test.ts
35603
+ var import_fs8 = require("fs");
35604
+ var import_path6 = require("path");
35605
+
35606
+ // src/lib/scenario-suite.ts
35607
+ var import_fs7 = require("fs");
35608
+ var import_path5 = require("path");
35609
+ function asStringList(value) {
35610
+ if (typeof value === "string" && value.trim()) return [value.trim()];
35611
+ if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
35612
+ return [];
35613
+ }
35614
+ function getPath(source, path2) {
35615
+ if (!source || typeof source !== "object") return void 0;
35616
+ return path2.split(".").reduce((current, part) => {
35617
+ if (!current || typeof current !== "object") return void 0;
35618
+ return current[part];
35619
+ }, source);
35620
+ }
35621
+ function valuesEqual(actual, expected) {
35622
+ if (Array.isArray(expected)) {
35623
+ return expected.some((entry) => valuesEqual(actual, entry));
35624
+ }
35625
+ if (expected && typeof expected === "object") {
35626
+ return JSON.stringify(actual) === JSON.stringify(expected);
35627
+ }
35628
+ return String(actual ?? "") === String(expected ?? "");
35629
+ }
35630
+ function parseStructuredFile(path2) {
35631
+ const raw = (0, import_fs7.readFileSync)(path2, "utf-8");
35632
+ return path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
35633
+ }
35634
+ function parseSuiteFile(path2) {
35635
+ const parsed = parseStructuredFile(path2);
35636
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
35637
+ throw new FohError({
35638
+ step: "test.run",
35639
+ error: "Suite file must contain an object",
35640
+ remediation: "Use a JSON/YAML object with scenarios[].turns[].",
35641
+ statusCode: 400
35642
+ });
35643
+ }
35644
+ return parsed;
35645
+ }
35646
+ function turnFromFixtureEntry(entry) {
35647
+ if (typeof entry === "string" && entry.trim()) return { user: entry.trim() };
35648
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
35649
+ const row = entry;
35650
+ const role = typeof row.role === "string" ? row.role.toLowerCase() : "";
35651
+ if (role && role !== "user") return null;
35652
+ const content = row.user ?? row.message ?? row.content ?? row.text;
35653
+ if (typeof content !== "string" || !content.trim()) return null;
35654
+ return {
35655
+ user: content.trim(),
35656
+ expect: row.expect && typeof row.expect === "object" && !Array.isArray(row.expect) ? row.expect : void 0
35657
+ };
35658
+ }
35659
+ function loadFixtureTurns(path2) {
35660
+ const parsed = parseStructuredFile(path2);
35661
+ const entries = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.turns) ? parsed.turns : parsed && typeof parsed === "object" && Array.isArray(parsed.messages) ? parsed.messages : [];
35662
+ return entries.map(turnFromFixtureEntry).filter((entry) => !!entry);
35663
+ }
35664
+ function resolveScenarioTurns(scenario, suitePath) {
35665
+ const inlineTurns = Array.isArray(scenario.turns) ? scenario.turns : [];
35666
+ const fixture = scenario.transcript_fixture || scenario.fixture_transcript || scenario.fixture;
35667
+ if (!fixture) return inlineTurns;
35668
+ const fixturePath = (0, import_path5.resolve)(suitePath, "..", fixture);
35669
+ const fixtureTurns = loadFixtureTurns(fixturePath);
35670
+ return [...fixtureTurns, ...inlineTurns];
35671
+ }
35672
+ function validateSuite(suite, suitePath) {
35673
+ const scenarios = Array.isArray(suite.scenarios) ? suite.scenarios : [];
35674
+ if (scenarios.length === 0) {
35675
+ throw new FohError({
35676
+ step: "test.run",
35677
+ error: "Suite contains no scenarios",
35678
+ remediation: "Add at least one scenarios[] entry with turns[].",
35679
+ statusCode: 400
35680
+ });
35681
+ }
35682
+ return scenarios.map((scenario) => {
35683
+ const turns = resolveScenarioTurns(scenario, suitePath);
35684
+ if (turns.length === 0) {
35685
+ throw new FohError({
35686
+ step: "test.run",
35687
+ error: `Scenario "${scenario.id || scenario.name || "(unnamed)"}" has no turns`,
35688
+ remediation: "Add inline turns or a fixture_transcript file with user messages.",
35689
+ statusCode: 400
35690
+ });
35691
+ }
35692
+ return { ...scenario, turns };
35693
+ });
35694
+ }
35695
+ function evaluateReply(reply, expect) {
35696
+ const lowerReply = reply.toLowerCase();
35697
+ const contains = asStringList(expect?.contains);
35698
+ const notContains = asStringList(expect?.not_contains);
35699
+ const failures = [];
35700
+ for (const expected of contains) {
35701
+ if (!lowerReply.includes(expected.toLowerCase())) {
35702
+ failures.push(`missing expected text: ${expected}`);
35703
+ }
35704
+ }
35705
+ for (const forbidden of notContains) {
35706
+ if (lowerReply.includes(forbidden.toLowerCase())) {
35707
+ failures.push(`contained forbidden text: ${forbidden}`);
35708
+ }
35709
+ }
35710
+ return failures;
35711
+ }
35712
+ function pickVariables(response) {
35713
+ for (const key of ["variables", "context_variables", "lead_data", "extracted_variables"]) {
35714
+ const value = response[key];
35715
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
35716
+ }
35717
+ return null;
35718
+ }
35719
+ function pickToolCalls(response) {
35720
+ for (const path2 of ["tool_calls", "toolCalls", "telemetry.tool_calls", "trace.tool_calls"]) {
35721
+ const value = getPath(response, path2);
35722
+ if (Array.isArray(value)) return value;
35723
+ }
35724
+ return [];
35725
+ }
35726
+ function toolCallName(call) {
35727
+ if (typeof call === "string") return call;
35728
+ if (!call || typeof call !== "object") return "";
35729
+ const row = call;
35730
+ return String(row.name ?? row.tool ?? row.tool_id ?? row.id ?? getPath(row, "function.name") ?? "");
35731
+ }
35732
+ function pickLeadCapture(response) {
35733
+ for (const key of ["lead_capture", "lead", "lead_data"]) {
35734
+ const value = response[key];
35735
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
35736
+ }
35737
+ if (response.lead_id || response.leadId) {
35738
+ return { id: response.lead_id ?? response.leadId };
35739
+ }
35740
+ return null;
35741
+ }
35742
+ function evaluateStructuredExpectations(response, expect, latencyMs) {
35743
+ if (!expect) return [];
35744
+ const failures = [];
35745
+ if (typeof expect.trace_present === "boolean") {
35746
+ const present = Boolean(response.trace_id);
35747
+ if (present !== expect.trace_present) failures.push(`trace presence expected ${expect.trace_present} but got ${present}`);
35748
+ }
35749
+ if (typeof expect.correlation_present === "boolean") {
35750
+ const present = Boolean(response.correlation_id);
35751
+ if (present !== expect.correlation_present) failures.push(`correlation presence expected ${expect.correlation_present} but got ${present}`);
35752
+ }
35753
+ const expectedActions = asStringList(expect.action);
35754
+ if (expectedActions.length > 0) {
35755
+ const actual = String(response.action ?? "");
35756
+ if (!expectedActions.includes(actual)) failures.push(`action expected ${expectedActions.join("|")} but got ${actual || "(empty)"}`);
35757
+ }
35758
+ const expectedTerminalStates = asStringList(expect.terminal_state);
35759
+ if (expectedTerminalStates.length > 0) {
35760
+ const actual = String(response.terminal_state ?? response.terminalState ?? response.action ?? "");
35761
+ if (!expectedTerminalStates.includes(actual)) {
35762
+ failures.push(`terminal_state expected ${expectedTerminalStates.join("|")} but got ${actual || "(empty)"}`);
35763
+ }
35764
+ }
35765
+ const minLatency = expect.latency_ms?.min;
35766
+ const maxLatency = expect.latency_ms?.max ?? expect.max_latency_ms;
35767
+ if (typeof minLatency === "number" && latencyMs < minLatency) {
35768
+ failures.push(`latency_ms expected >=${minLatency} but got ${latencyMs}`);
35769
+ }
35770
+ if (typeof maxLatency === "number" && latencyMs > maxLatency) {
35771
+ failures.push(`latency_ms expected <=${maxLatency} but got ${latencyMs}`);
35772
+ }
35773
+ if (expect.variables && typeof expect.variables === "object") {
35774
+ const variables = pickVariables(response);
35775
+ if (!variables) failures.push("variables expected but response had none");
35776
+ else {
35777
+ for (const [path2, expected] of Object.entries(expect.variables)) {
35778
+ const actual = getPath(variables, path2);
35779
+ if (!valuesEqual(actual, expected)) failures.push(`variables.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
35780
+ }
35781
+ }
35782
+ }
35783
+ if (expect.tool_calls) {
35784
+ const calls = pickToolCalls(response);
35785
+ const names = calls.map(toolCallName).filter(Boolean);
35786
+ const includes = asStringList(expect.tool_calls.includes);
35787
+ const excludes = asStringList(expect.tool_calls.excludes);
35788
+ for (const name of includes) {
35789
+ if (!names.includes(name)) failures.push(`tool_calls missing expected tool: ${name}`);
35790
+ }
35791
+ for (const name of excludes) {
35792
+ if (names.includes(name)) failures.push(`tool_calls contained forbidden tool: ${name}`);
35793
+ }
35794
+ if (typeof expect.tool_calls.min_count === "number" && calls.length < expect.tool_calls.min_count) {
35795
+ failures.push(`tool_calls expected >=${expect.tool_calls.min_count} but got ${calls.length}`);
35796
+ }
35797
+ if (typeof expect.tool_calls.max_count === "number" && calls.length > expect.tool_calls.max_count) {
35798
+ failures.push(`tool_calls expected <=${expect.tool_calls.max_count} but got ${calls.length}`);
35799
+ }
35800
+ }
35801
+ if (expect.escalation) {
35802
+ const requested = Boolean(getPath(response, "escalation.requested") ?? response.handoff?.requested);
35803
+ if (typeof expect.escalation.requested === "boolean" && requested !== expect.escalation.requested) {
35804
+ failures.push(`escalation.requested expected ${expect.escalation.requested} but got ${requested}`);
35805
+ }
35806
+ const expectedReasons = asStringList(expect.escalation.reason);
35807
+ if (expectedReasons.length > 0) {
35808
+ const actual = String(getPath(response, "escalation.reason") ?? response.handoff?.reason ?? "");
35809
+ if (!expectedReasons.includes(actual)) failures.push(`escalation.reason expected ${expectedReasons.join("|")} but got ${actual || "(empty)"}`);
35810
+ }
35811
+ }
35812
+ if (expect.lead_capture) {
35813
+ const lead = pickLeadCapture(response);
35814
+ if (expect.lead_capture.required === true && !lead) failures.push("lead_capture expected but response had none");
35815
+ for (const field of expect.lead_capture.fields || []) {
35816
+ const actual = lead ? getPath(lead, field) : void 0;
35817
+ if (actual == null || actual === "") failures.push(`lead_capture.${field} missing`);
35818
+ }
35819
+ }
35820
+ if (expect.fields && typeof expect.fields === "object") {
35821
+ for (const [path2, expected] of Object.entries(expect.fields)) {
35822
+ const actual = getPath(response, path2);
35823
+ if (!valuesEqual(actual, expected)) failures.push(`fields.${path2} expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`);
35824
+ }
35825
+ }
35826
+ return failures;
35827
+ }
35828
+
35829
+ // src/commands/test.ts
35830
+ function registerTest(program3) {
35831
+ const test = program3.command("test").description("Run local scenario suites against runtime channels");
35832
+ test.command("run").description("Run a local YAML/JSON scenario suite").requiredOption("--suite <path>", "Suite YAML/JSON path").option("--agent <id>", "Agent ID (defaults to suite.agent)").option("--out <path>", "Write report JSON to path").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
35833
+ const suitePath = (0, import_path6.resolve)(String(opts.suite));
35834
+ const suite = parseSuiteFile(suitePath);
35835
+ const agentId = String(opts.agent || suite.agent || "").trim();
35836
+ if (!agentId) {
35837
+ throw new FohError({
35838
+ step: "test.run",
35839
+ error: "Missing agent ID",
35840
+ remediation: "Pass --agent <id> or include agent in the suite file.",
35841
+ statusCode: 400
35842
+ });
35843
+ }
35844
+ const scenarios = validateSuite(suite, suitePath);
35845
+ const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
35846
+ method: "POST",
35847
+ body: JSON.stringify({ agentId }),
35848
+ orgId: opts.org,
35849
+ apiUrlOverride: opts.apiUrl
35850
+ });
35851
+ const publicKey = ensure.channel?.public_key;
35852
+ if (!publicKey) {
35853
+ throw new FohError({
35854
+ step: "test.run",
35855
+ error: "Widget channel public key missing",
35856
+ remediation: `Run: foh widget ensure --agent ${agentId} --json`,
35857
+ statusCode: 409
35858
+ });
35859
+ }
35860
+ let passed = 0;
35861
+ let failed = 0;
35862
+ const scenarioResults = [];
35863
+ for (let scenarioIndex = 0; scenarioIndex < scenarios.length; scenarioIndex += 1) {
35864
+ const scenario = scenarios[scenarioIndex];
35865
+ let conversationId;
35866
+ const turnResults = [];
35867
+ for (let turnIndex = 0; turnIndex < scenario.turns.length; turnIndex += 1) {
35868
+ const turn = scenario.turns[turnIndex];
35869
+ const message = String(turn.user || turn.message || "").trim();
35870
+ if (!message) {
35871
+ failed += 1;
35872
+ turnResults.push({ turn: turnIndex + 1, ok: false, failures: ["missing user/message"] });
35873
+ continue;
35874
+ }
35875
+ const start = Date.now();
35876
+ const response = await apiFetch("/v1/widget/inbound", {
35877
+ method: "POST",
35878
+ body: JSON.stringify({
35879
+ channel_public_key: publicKey,
35880
+ message_body: message,
35881
+ preview: true,
35882
+ ...conversationId ? { conversation_id: conversationId } : {}
35883
+ }),
35884
+ apiUrlOverride: opts.apiUrl
35885
+ });
35886
+ const latencyMs = Date.now() - start;
35887
+ conversationId = response.conversationId || conversationId;
35888
+ const reply = String(response.reply || "");
35889
+ const failures = reply ? [
35890
+ ...evaluateReply(reply, turn.expect),
35891
+ ...evaluateStructuredExpectations(response, turn.expect, latencyMs)
35892
+ ] : ["empty reply"];
35893
+ if (failures.length === 0) passed += 1;
35894
+ else failed += 1;
35895
+ turnResults.push({
35896
+ turn: turnIndex + 1,
35897
+ ok: failures.length === 0,
35898
+ message,
35899
+ reply,
35900
+ failures,
35901
+ latency_ms: latencyMs,
35902
+ conversation_id: response.conversationId ?? null,
35903
+ trace_id: response.trace_id ?? null,
35904
+ correlation_id: response.correlation_id ?? null,
35905
+ action: response.action ?? null,
35906
+ handoff: response.handoff ?? null
35907
+ });
35908
+ }
35909
+ scenarioResults.push({
35910
+ id: scenario.id || `scenario-${scenarioIndex + 1}`,
35911
+ name: scenario.name ?? null,
35912
+ ok: turnResults.every((turn) => turn.ok),
35913
+ turns: turnResults
35914
+ });
35915
+ }
35916
+ const report = {
35917
+ schema_version: "foh_local_scenario_suite_report.v1",
35918
+ ok: failed === 0,
35919
+ status: failed === 0 ? "pass" : "fail",
35920
+ reason_code: failed === 0 ? "scenario_suite_passed" : "scenario_suite_failed",
35921
+ summary: failed === 0 ? "All scenario-suite assertions passed." : `${failed} scenario-suite assertion(s) failed.`,
35922
+ ids: {
35923
+ agent_id: agentId
35924
+ },
35925
+ artifacts: {},
35926
+ next_commands: failed === 0 ? [`foh prove --agent ${agentId} --json`] : [`foh transcripts list --agent ${agentId} --limit 10 --json`],
35927
+ suite_path: suitePath,
35928
+ agent_id: agentId,
35929
+ passed,
35930
+ failed,
35931
+ scenarios: scenarioResults
35932
+ };
35933
+ if (opts.out) {
35934
+ const out = (0, import_path6.resolve)(String(opts.out));
35935
+ (0, import_fs8.writeFileSync)(out, stableStringify(report), "utf-8");
35936
+ format({ ...report, output_path: out }, { json: opts.json ?? false });
35937
+ } else {
35938
+ format(report, { json: opts.json ?? false });
35939
+ }
35940
+ if (failed > 0) markCommandFailed(1);
35941
+ }));
35942
+ }
35943
+
35944
+ // src/commands/ops.ts
35945
+ function parseClientErrorSource(raw) {
35946
+ if (!raw) return void 0;
35947
+ const source = String(raw).trim().toLowerCase();
35948
+ if (source === "window" || source === "react" || source === "unhandledrejection") {
35949
+ return source;
35950
+ }
35951
+ throw new FohError({
35952
+ step: "ops.client-errors.report",
35953
+ error: `Invalid --source "${raw}"`,
35954
+ remediation: "Use one of: window, react, unhandledrejection.",
35955
+ statusCode: 400
35956
+ });
34969
35957
  }
34970
35958
  function registerOps(program3) {
34971
35959
  const ops = program3.command("ops").description("Operational/reporting workflows (incidents, recommendations, traces, evidence)");
@@ -35367,8 +36355,210 @@ function registerDiag(program3) {
35367
36355
  }
35368
36356
 
35369
36357
  // src/commands/bug.ts
35370
- var import_fs5 = require("fs");
35371
- var import_path4 = require("path");
36358
+ var import_fs10 = require("fs");
36359
+ var import_path8 = require("path");
36360
+
36361
+ // src/lib/improvement-packet.ts
36362
+ var import_fs9 = require("fs");
36363
+ var import_path7 = require("path");
36364
+ var IMPROVEMENT_SOURCE_TYPES = [
36365
+ "setup_failure",
36366
+ "proof_failure",
36367
+ "replay_failure",
36368
+ "knowledge_miss",
36369
+ "runtime_miss",
36370
+ "live_proof_failure"
36371
+ ];
36372
+ var IMPROVEMENT_DECISIONS = [
36373
+ "ignore",
36374
+ "fix_docs",
36375
+ "fix_config",
36376
+ "fix_runtime",
36377
+ "add_test"
36378
+ ];
36379
+ var SECRET_LEAK_RE = /\b(?:sk|pk|xai|whsec|EAAN)[A-Za-z0-9_\-]{16,}\b/i;
36380
+ var MAX_REDACTED_SOURCE_BYTES = 4e3;
36381
+ function asRecord2(value) {
36382
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
36383
+ }
36384
+ function nonEmpty2(value) {
36385
+ const text = String(value ?? "").trim();
36386
+ return text.length > 0 ? text : void 0;
36387
+ }
36388
+ function getPath2(value, path2) {
36389
+ let current = value;
36390
+ for (const segment of path2.split(".")) {
36391
+ const record2 = asRecord2(current);
36392
+ if (!record2) return void 0;
36393
+ current = record2[segment];
36394
+ }
36395
+ return current;
36396
+ }
36397
+ function parseEnum(raw, allowed, label) {
36398
+ const value = nonEmpty2(raw);
36399
+ if (!value) return void 0;
36400
+ if (allowed.includes(value)) return value;
36401
+ throw new FohError({
36402
+ step: "bug.improve",
36403
+ error: `Invalid ${label} "${value}"`,
36404
+ remediation: `Use one of: ${allowed.join(", ")}.`,
36405
+ statusCode: 400
36406
+ });
36407
+ }
36408
+ function inferSourceType(artifact) {
36409
+ const schema2 = nonEmpty2(getPath2(artifact, "schema_version")) || "";
36410
+ const status = nonEmpty2(getPath2(artifact, "status")) || "";
36411
+ if (schema2.includes("knowledge_query") || nonEmpty2(getPath2(artifact, "failure_packet.schema_version"))?.includes("knowledge_query")) {
36412
+ return "knowledge_miss";
36413
+ }
36414
+ if (schema2.includes("agent_replay") || status.includes("replay")) return "replay_failure";
36415
+ if (schema2.includes("proof") || schema2.includes("live_proof")) return schema2.includes("live") ? "live_proof_failure" : "proof_failure";
36416
+ if (schema2.includes("setup")) return "setup_failure";
36417
+ return "runtime_miss";
36418
+ }
36419
+ function inferReasonCode(artifact) {
36420
+ const direct = nonEmpty2(getPath2(artifact, "reason_code"));
36421
+ if (direct) return direct;
36422
+ const nested = nonEmpty2(getPath2(artifact, "failure_packet.reason_code"));
36423
+ if (nested) return nested;
36424
+ const checks = getPath2(artifact, "checks");
36425
+ if (Array.isArray(checks)) {
36426
+ for (const check2 of checks) {
36427
+ const reason = nonEmpty2(getPath2(check2, "reason_code"));
36428
+ const status = nonEmpty2(getPath2(check2, "status"));
36429
+ if (reason && status !== "pass" && status !== "success") return reason;
36430
+ }
36431
+ }
36432
+ return nonEmpty2(getPath2(artifact, "status"));
36433
+ }
36434
+ function inferPromotionDecision(sourceType) {
36435
+ if (sourceType === "knowledge_miss") return "fix_docs";
36436
+ if (sourceType === "setup_failure" || sourceType === "proof_failure" || sourceType === "live_proof_failure") return "fix_config";
36437
+ if (sourceType === "replay_failure" || sourceType === "runtime_miss") return "add_test";
36438
+ return "fix_runtime";
36439
+ }
36440
+ function collectIds(artifact, explicit = {}) {
36441
+ const source = getPath2(artifact, "source");
36442
+ const ids = {
36443
+ org_id: explicit.org_id ?? nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id")),
36444
+ agent_id: explicit.agent_id ?? nonEmpty2(getPath2(source, "agent_id")) ?? nonEmpty2(getPath2(artifact, "ids.agent_id")) ?? nonEmpty2(getPath2(artifact, "agent_id")),
36445
+ conversation_id: explicit.conversation_id ?? nonEmpty2(getPath2(source, "conversation_id")) ?? nonEmpty2(getPath2(artifact, "ids.conversation_id")) ?? nonEmpty2(getPath2(artifact, "conversation_id")),
36446
+ trace_id: explicit.trace_id ?? nonEmpty2(getPath2(source, "trace_id")) ?? nonEmpty2(getPath2(artifact, "ids.trace_id")) ?? nonEmpty2(getPath2(artifact, "trace_id")),
36447
+ correlation_id: explicit.correlation_id ?? nonEmpty2(getPath2(artifact, "ids.correlation_id")) ?? nonEmpty2(getPath2(artifact, "correlation_id")),
36448
+ proof_artifact: explicit.proof_artifact
36449
+ };
36450
+ return Object.fromEntries(Object.entries(ids).filter(([, value]) => value !== void 0));
36451
+ }
36452
+ function compactSourceArtifact(artifact) {
36453
+ const redacted = redactObject(artifact);
36454
+ const text = JSON.stringify(redacted);
36455
+ if (text.length <= MAX_REDACTED_SOURCE_BYTES) {
36456
+ return { truncated: false, value: redacted };
36457
+ }
36458
+ return {
36459
+ truncated: true,
36460
+ bytes_before_truncate: text.length,
36461
+ value_preview: text.slice(0, MAX_REDACTED_SOURCE_BYTES)
36462
+ };
36463
+ }
36464
+ function defaultNextCommands(input) {
36465
+ const commands = [];
36466
+ if (input.ids.trace_id && input.ids.agent_id) {
36467
+ commands.push(`foh tests from-trace --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
36468
+ commands.push(`foh agent replay --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
36469
+ }
36470
+ if (input.ids.conversation_id && input.ids.agent_id) {
36471
+ commands.push(`foh agent replay --agent ${input.ids.agent_id} --conversation ${input.ids.conversation_id} --json`);
36472
+ }
36473
+ if (input.sourceType === "knowledge_miss" && input.ids.agent_id) {
36474
+ commands.push(`foh knowledge query --agent ${input.ids.agent_id} --text "<question>" --explain --json`);
36475
+ }
36476
+ if (input.sourceArtifactPath) {
36477
+ commands.push(`foh bug report --out test-results/bug-report.from-improvement.json --command "investigate ${input.reasonCode}" --request-url https://front-of-house-api.stldocs.app/api --response-status 500 --next-check "Review ${(0, import_path7.basename)(input.sourceArtifactPath)}" --json`);
36478
+ }
36479
+ return commands;
36480
+ }
36481
+ function assertOrgBoundary(artifact, explicitOrgId) {
36482
+ const artifactOrgId = nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id"));
36483
+ if (explicitOrgId && artifactOrgId && explicitOrgId !== artifactOrgId) {
36484
+ throw new FohError({
36485
+ step: "bug.improve",
36486
+ error: "Org boundary check failed for improvement packet.",
36487
+ remediation: "Use the org id from the source artifact, or omit --org when building a local redacted packet.",
36488
+ statusCode: 403
36489
+ });
36490
+ }
36491
+ }
36492
+ function assertRedacted(value) {
36493
+ const text = JSON.stringify(value);
36494
+ if (SECRET_LEAK_RE.test(text)) {
36495
+ throw new FohError({
36496
+ step: "bug.improve",
36497
+ error: "Improvement packet still contains a secret-like value after redaction.",
36498
+ remediation: "Remove raw credentials from the source artifact and rebuild the packet.",
36499
+ statusCode: 400
36500
+ });
36501
+ }
36502
+ }
36503
+ function readSourceArtifact(path2) {
36504
+ if (!path2) return null;
36505
+ try {
36506
+ return JSON.parse((0, import_fs9.readFileSync)(path2, "utf-8"));
36507
+ } catch (error2) {
36508
+ throw new FohError({
36509
+ step: "bug.improve",
36510
+ error: `Failed to read source artifact: ${error2 instanceof Error ? error2.message : String(error2)}`,
36511
+ remediation: "Pass --from-file with a readable JSON artifact.",
36512
+ statusCode: 400
36513
+ });
36514
+ }
36515
+ }
36516
+ function buildImprovementPacket(input) {
36517
+ const artifact = input.sourceArtifact ?? null;
36518
+ const sourceType = parseEnum(input.sourceType, IMPROVEMENT_SOURCE_TYPES, "--source-type") ?? inferSourceType(artifact);
36519
+ const promotionDecision = parseEnum(input.promotionDecision, IMPROVEMENT_DECISIONS, "--recommendation") ?? inferPromotionDecision(sourceType);
36520
+ const ids = collectIds(artifact, input.ids);
36521
+ assertOrgBoundary(artifact, input.ids?.org_id);
36522
+ const reasonCode = nonEmpty2(input.reasonCode) ?? inferReasonCode(artifact);
36523
+ if (!reasonCode) {
36524
+ throw new FohError({
36525
+ step: "bug.improve",
36526
+ error: "Missing improvement reason code.",
36527
+ remediation: "Pass --reason-code <code> or build from a source artifact that includes reason_code/status.",
36528
+ statusCode: 400
36529
+ });
36530
+ }
36531
+ const evidenceSummary = redactString(
36532
+ nonEmpty2(input.evidenceSummary) ?? nonEmpty2(getPath2(artifact, "summary")) ?? `Improvement candidate generated from ${sourceType} with reason ${reasonCode}.`
36533
+ );
36534
+ const nextCommands = Array.from(new Set([
36535
+ ...input.nextCommands ?? [],
36536
+ ...defaultNextCommands({ sourceType, ids, sourceArtifactPath: input.sourceArtifactPath, reasonCode })
36537
+ ].map((command) => command.trim()).filter(Boolean)));
36538
+ const packet = {
36539
+ schema_version: "foh_improvement_packet.v1",
36540
+ created_at: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
36541
+ source_type: sourceType,
36542
+ reason_code: reasonCode,
36543
+ promotion_decision: promotionDecision,
36544
+ ids,
36545
+ evidence: {
36546
+ summary: evidenceSummary,
36547
+ source_artifact_path: input.sourceArtifactPath ?? null,
36548
+ source_command: input.sourceCommand ?? null,
36549
+ redaction: {
36550
+ enabled: true,
36551
+ fields: ["email", "phone", "secret-like-token"]
36552
+ },
36553
+ redacted_source: artifact ? compactSourceArtifact(artifact) : null
36554
+ },
36555
+ next_commands: nextCommands
36556
+ };
36557
+ assertRedacted(packet);
36558
+ return packet;
36559
+ }
36560
+
36561
+ // src/commands/bug.ts
35372
36562
  var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
35373
36563
  var MAX_BODY_PREVIEW_LENGTH = 200;
35374
36564
  function parseMethod(raw) {
@@ -35484,14 +36674,18 @@ function parseRequestBody(raw) {
35484
36674
  }
35485
36675
  }
35486
36676
  function writeJsonArtifact(path2, value) {
35487
- const absolutePath = (0, import_path4.resolve)(path2);
35488
- (0, import_fs5.mkdirSync)((0, import_path4.dirname)(absolutePath), { recursive: true });
35489
- (0, import_fs5.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
36677
+ const absolutePath = (0, import_path8.resolve)(path2);
36678
+ (0, import_fs10.mkdirSync)((0, import_path8.dirname)(absolutePath), { recursive: true });
36679
+ (0, import_fs10.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
35490
36680
  return absolutePath;
35491
36681
  }
35492
36682
  function defaultArtifactPath() {
35493
36683
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
35494
- return (0, import_path4.resolve)(`test-results/bug-report.${timestamp2}.json`);
36684
+ return (0, import_path8.resolve)(`test-results/bug-report.${timestamp2}.json`);
36685
+ }
36686
+ function defaultImprovementArtifactPath() {
36687
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
36688
+ return (0, import_path8.resolve)(`test-results/improvement-packet.${timestamp2}.json`);
35495
36689
  }
35496
36690
  async function resolveBugReportWizardInputs(opts) {
35497
36691
  if (!opts.wizard) return opts;
@@ -35641,6 +36835,44 @@ function registerBug(program3) {
35641
36835
  throw e;
35642
36836
  }
35643
36837
  });
36838
+ bug.command("improve").description("Write a redacted failure-to-improvement packet from a setup/proof/replay/knowledge/runtime artifact").option("--from-file <path>", "Source JSON artifact to convert into an improvement packet").option("--out <path>", "Output JSON file path for improvement packet").option("--source-type <type>", "setup_failure|proof_failure|replay_failure|knowledge_miss|runtime_miss|live_proof_failure").option("--reason-code <code>", "Deterministic failure reason code").option("--recommendation <decision>", "ignore|fix_docs|fix_config|fix_runtime|add_test").option("--evidence-summary <text>", "Compact redacted evidence summary").option("--source-command <text>", "Command or operation that produced the source artifact").option("--org <id>", "Org ID for boundary check and packet IDs").option("--agent <id>", "Agent ID to attach").option("--conversation <id>", "Conversation ID to attach").option("--trace <id>", "Trace ID to attach").option("--correlation <id>", "Correlation ID to attach").option("--proof-artifact <path>", "Proof artifact path to attach").option(
36839
+ "--next-command <text>",
36840
+ "Deterministic next command (repeat this flag for multiple commands)",
36841
+ (value, previous = []) => [...previous, value],
36842
+ []
36843
+ ).option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36844
+ const sourceArtifact = readSourceArtifact(opts.fromFile);
36845
+ const outPath = String(opts.out || defaultImprovementArtifactPath());
36846
+ const packet = buildImprovementPacket({
36847
+ sourceArtifact,
36848
+ sourceArtifactPath: opts.fromFile,
36849
+ sourceType: opts.sourceType,
36850
+ reasonCode: opts.reasonCode,
36851
+ evidenceSummary: opts.evidenceSummary,
36852
+ promotionDecision: opts.recommendation,
36853
+ sourceCommand: opts.sourceCommand,
36854
+ ids: {
36855
+ org_id: opts.org,
36856
+ agent_id: opts.agent,
36857
+ conversation_id: opts.conversation,
36858
+ trace_id: opts.trace,
36859
+ correlation_id: opts.correlation,
36860
+ proof_artifact: opts.proofArtifact
36861
+ },
36862
+ nextCommands: Array.isArray(opts.nextCommand) ? opts.nextCommand : []
36863
+ });
36864
+ const artifactPath = writeJsonArtifact(outPath, packet);
36865
+ format(cliEnvelope({
36866
+ schemaVersion: "foh_improvement_packet_result.v1",
36867
+ status: "exported",
36868
+ reasonCode: "improvement_packet_created",
36869
+ summary: "Improvement packet created.",
36870
+ ids: packet.ids,
36871
+ artifacts: { improvement_packet: artifactPath },
36872
+ nextCommands: packet.next_commands,
36873
+ extra: { packet }
36874
+ }), { json: opts.json ?? false });
36875
+ }));
35644
36876
  bug.command("list").description("List bug reports from the FOH bug inbox").option("--state <state>", "Filter by state: open, triaged, closed").option("--severity <level>", "Filter by severity: low, medium, high, critical").option("--source <source>", "Filter by source: cli, eval, api").option("--request-id <id>", "Filter by request id").option("--search <text>", "Case-insensitive command search").option("--limit <n>", "Result limit (1-200)", "50").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "Internal API base URL override (operators only)").option("--json", "Output as JSON").action(async (opts) => {
35645
36877
  try {
35646
36878
  const params = new URLSearchParams();
@@ -35683,6 +36915,344 @@ function registerBug(program3) {
35683
36915
  });
35684
36916
  }
35685
36917
 
36918
+ // src/commands/prove.ts
36919
+ function categoryForCheck(name) {
36920
+ if (name === "auth") return "auth";
36921
+ if (name === "contact_channel" || name === "voice_realtime_health") return "voice";
36922
+ if (name.startsWith("widget_")) return "widget";
36923
+ if (name === "simulation_certification") return "certification";
36924
+ if (name === "agent_validation") return "publish_readiness";
36925
+ return "setup";
36926
+ }
36927
+ function pass(name, summary, detail) {
36928
+ return { name, category: categoryForCheck(name), status: "pass", reason_code: `${name}_ok`, summary, detail };
36929
+ }
36930
+ function hold(name, reasonCode, summary, nextCommand, detail) {
36931
+ return { name, category: categoryForCheck(name), status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
36932
+ }
36933
+ function fail(name, reasonCode, error2, nextCommand) {
36934
+ const message = error2 instanceof Error ? error2.message : String(error2);
36935
+ return { name, category: categoryForCheck(name), status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
36936
+ }
36937
+ function skipped(name, reasonCode, summary, nextCommand) {
36938
+ return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
36939
+ }
36940
+ function hasBlockingChecks(checks) {
36941
+ return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
36942
+ }
36943
+ function publicKeyFromEnsureResponse(response) {
36944
+ const record2 = response && typeof response === "object" ? response : {};
36945
+ const channel = record2.channel && typeof record2.channel === "object" ? record2.channel : {};
36946
+ const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
36947
+ return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
36948
+ }
36949
+ function publicKeyFromEmbedResponse(response) {
36950
+ const record2 = response && typeof response === "object" ? response : {};
36951
+ const publicKey = record2.widget_public_key ?? record2.public_key;
36952
+ return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
36953
+ }
36954
+ function normalizeMission(raw) {
36955
+ const value = String(raw || "setup").trim().toLowerCase();
36956
+ if (value === "setup" || value === "widget" || value === "voice" || value === "publish") return value;
36957
+ return "setup";
36958
+ }
36959
+ function normalizeMutationMode(raw, repair) {
36960
+ if (repair) return "ensure";
36961
+ const value = String(raw || "read-only").trim().toLowerCase();
36962
+ return value === "ensure" ? "ensure" : "read-only";
36963
+ }
36964
+ function agentIdFromList(response) {
36965
+ const agents = Array.isArray(response.agents) ? response.agents : [];
36966
+ const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
36967
+ if (usable.length === 1) return { agentId: usable[0].id, count: usable.length };
36968
+ if (usable.length === 0) return { count: 0, reason: "no_agents" };
36969
+ return { count: usable.length, reason: "multiple_agents" };
36970
+ }
36971
+ function firstUsableOrgId(response) {
36972
+ const record2 = response && typeof response === "object" ? response : {};
36973
+ const orgs = Array.isArray(record2.orgs) ? record2.orgs : [];
36974
+ const usable = orgs.map((org) => org && typeof org === "object" ? org : {}).map((org) => String(org.org_id ?? org.id ?? "").trim()).filter(Boolean);
36975
+ return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
36976
+ }
36977
+ function registerProve(program3) {
36978
+ program3.command("prove").description("Produce one setup/runtime proof bundle for an agent").option("--agent <id>", "Agent ID to prove").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--mutation-mode <mode>", "Proof mutation mode: read-only or ensure", "read-only").option("--repair", "Alias for --mutation-mode ensure").option("--require-phone", "Hold proof if no phone/contact number is provisioned").option("--skip-cert", "Skip simulation certification check").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--out <path>", "Write signed proof report JSON to this path").option("--strict", "Exit non-zero unless all non-skipped checks pass").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36979
+ const checks = [];
36980
+ const mission = normalizeMission(opts.mission);
36981
+ const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
36982
+ const ctx = {
36983
+ tokenPresent: false,
36984
+ traceIds: [],
36985
+ correlationIds: []
36986
+ };
36987
+ try {
36988
+ const creds = loadCredentials(opts.apiUrl);
36989
+ ctx.apiUrl = creds.apiUrl;
36990
+ ctx.tokenPresent = Boolean(creds.token);
36991
+ ctx.orgId = opts.org || creds.orgId;
36992
+ checks.push(pass("auth", "CLI credentials are present and not expired.", {
36993
+ api_url: creds.apiUrl,
36994
+ org_id_from_credentials: creds.orgId ?? null
36995
+ }));
36996
+ } catch (error2) {
36997
+ checks.push(hold("auth", "auth_missing_or_expired", "CLI is not authenticated.", "foh auth login --web", {
36998
+ message: error2 instanceof Error ? error2.message : String(error2)
36999
+ }));
37000
+ }
37001
+ if (ctx.tokenPresent && !ctx.orgId) {
37002
+ try {
37003
+ const orgs = await apiFetch("/v1/console/auth/my-orgs", { apiUrlOverride: opts.apiUrl });
37004
+ const resolved = firstUsableOrgId(orgs);
37005
+ if (resolved.orgId) {
37006
+ ctx.orgId = resolved.orgId;
37007
+ checks.push(pass("org", "Resolved the only available org.", { org_id: resolved.orgId }));
37008
+ } else {
37009
+ checks.push(hold(
37010
+ "org",
37011
+ resolved.count === 0 ? "org_missing" : "org_ambiguous",
37012
+ resolved.count === 0 ? "No usable org was found for this account." : `Found ${resolved.count} orgs; choose one explicitly.`,
37013
+ "foh org list --json && foh org use --org <org-id>",
37014
+ { org_count: resolved.count }
37015
+ ));
37016
+ }
37017
+ } catch (error2) {
37018
+ checks.push(fail("org", "org_resolution_failed", error2, "foh org list --json"));
37019
+ }
37020
+ } else if (ctx.orgId) {
37021
+ checks.push(pass("org", "Org context is selected.", { org_id: ctx.orgId }));
37022
+ } else {
37023
+ checks.push(skipped("org", "auth_required", "Skipped until authentication is fixed.", "foh auth login --web"));
37024
+ }
37025
+ if (ctx.orgId) {
37026
+ if (opts.agent) {
37027
+ ctx.agentId = String(opts.agent);
37028
+ checks.push(pass("agent_selection", "Using explicitly supplied agent.", { agent_id: ctx.agentId }));
37029
+ } else {
37030
+ try {
37031
+ const list = await apiFetch("/v1/console/agents", {
37032
+ orgId: ctx.orgId,
37033
+ apiUrlOverride: opts.apiUrl
37034
+ });
37035
+ const resolved = agentIdFromList(list);
37036
+ if (resolved.agentId) {
37037
+ ctx.agentId = resolved.agentId;
37038
+ checks.push(pass("agent_selection", "Resolved the only available agent.", { agent_id: resolved.agentId }));
37039
+ } else {
37040
+ checks.push(hold(
37041
+ "agent_selection",
37042
+ resolved.reason === "no_agents" ? "agent_missing" : "agent_ambiguous",
37043
+ resolved.reason === "no_agents" ? "No agent exists in this org." : `Found ${resolved.count} agents; choose one explicitly.`,
37044
+ resolved.reason === "no_agents" ? "foh setup --json" : "foh agent list --json && foh prove --agent <agent-id> --json",
37045
+ { agent_count: resolved.count }
37046
+ ));
37047
+ }
37048
+ } catch (error2) {
37049
+ checks.push(fail("agent_selection", "agent_selection_failed", error2, "foh agent list --json"));
37050
+ }
37051
+ }
37052
+ } else {
37053
+ checks.push(skipped("agent_selection", "org_required", "Skipped until org context is fixed.", "foh org use --org <org-id>"));
37054
+ }
37055
+ if (ctx.agentId) {
37056
+ try {
37057
+ const validation = await apiFetch(`/v1/console/agents/${ctx.agentId}/validate`, {
37058
+ method: "POST",
37059
+ orgId: ctx.orgId,
37060
+ apiUrlOverride: opts.apiUrl
37061
+ });
37062
+ const issues = Array.isArray(validation.issues) ? validation.issues : [];
37063
+ if (validation.ok === false || issues.length > 0) {
37064
+ checks.push(hold("agent_validation", "agent_validation_issues", `Agent validation returned ${issues.length} issue(s).`, `foh agent validate --agent ${ctx.agentId} --json`, validation));
37065
+ } else {
37066
+ checks.push(pass("agent_validation", "Agent validation passed.", validation));
37067
+ }
37068
+ } catch (error2) {
37069
+ checks.push(fail("agent_validation", "agent_validation_failed", error2, `foh agent validate --agent ${ctx.agentId} --json`));
37070
+ }
37071
+ if (ctx.orgId) {
37072
+ try {
37073
+ const onboarding = await apiFetch(`/v1/console/org/${ctx.orgId}/onboarding`, {
37074
+ orgId: ctx.orgId,
37075
+ apiUrlOverride: opts.apiUrl
37076
+ });
37077
+ const phoneNumber = typeof onboarding.phone_number === "string" && onboarding.phone_number.trim() ? onboarding.phone_number.trim() : null;
37078
+ if (phoneNumber) {
37079
+ checks.push(pass("contact_channel", "Contact phone number is provisioned.", {
37080
+ phone_number_present: true,
37081
+ provisioning_status: onboarding.provisioning_status ?? null
37082
+ }));
37083
+ } else if (opts.requirePhone || mission === "voice") {
37084
+ checks.push(hold("contact_channel", "contact_phone_missing", "No phone/contact number is provisioned for this org.", `foh provision buy --org ${ctx.orgId} --json`, {
37085
+ provisioning_status: onboarding.provisioning_status ?? null,
37086
+ mission
37087
+ }));
37088
+ } else {
37089
+ checks.push(skipped("contact_channel", "contact_phone_not_required", "No phone/contact number is provisioned; pass --require-phone to make this a blocker.", `foh provision buy --org ${ctx.orgId} --json`));
37090
+ }
37091
+ } catch (error2) {
37092
+ checks.push(fail("contact_channel", "contact_channel_check_failed", error2, `foh provision status --org ${ctx.orgId} --json`));
37093
+ }
37094
+ }
37095
+ if (opts.skipVoiceHealth) {
37096
+ checks.push(skipped("voice_realtime_health", "operator_skipped", "Skipped by --skip-voice-health.", "foh voice realtime-health --json"));
37097
+ } else {
37098
+ try {
37099
+ const health = await apiFetch(
37100
+ "/v1/console/realtime/health",
37101
+ { apiUrlOverride: opts.apiUrl }
37102
+ );
37103
+ const providers = Array.isArray(health.providers) ? health.providers : [];
37104
+ if (providers.length === 0) {
37105
+ checks.push(skipped("voice_realtime_health", "voice_health_no_providers", "Realtime voice health returned no providers.", "foh voice realtime-health --json"));
37106
+ } else if (providers.every((provider) => provider?.ready === true)) {
37107
+ checks.push(pass("voice_realtime_health", "Realtime voice providers are ready.", {
37108
+ provider_count: providers.length
37109
+ }));
37110
+ } else {
37111
+ checks.push(hold("voice_realtime_health", "voice_realtime_provider_not_ready", "One or more realtime voice providers are not ready.", "foh voice realtime-health --json", {
37112
+ providers
37113
+ }));
37114
+ }
37115
+ } catch (error2) {
37116
+ checks.push(fail("voice_realtime_health", "voice_realtime_health_failed", error2, "foh voice realtime-health --json"));
37117
+ }
37118
+ }
37119
+ try {
37120
+ const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
37121
+ orgId: ctx.orgId,
37122
+ apiUrlOverride: opts.apiUrl,
37123
+ headers: { "x-agent-id": ctx.agentId }
37124
+ });
37125
+ const publicKey = publicKeyFromEmbedResponse(embed);
37126
+ if (publicKey) {
37127
+ ctx.widgetPublicKey = publicKey;
37128
+ checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
37129
+ public_key_present: true,
37130
+ mutation_mode: mutationMode
37131
+ }));
37132
+ }
37133
+ if (typeof embed.snippet === "string" && embed.snippet.trim()) {
37134
+ checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
37135
+ } else {
37136
+ checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
37137
+ }
37138
+ } catch (error2) {
37139
+ if (mutationMode === "ensure") {
37140
+ try {
37141
+ const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
37142
+ method: "POST",
37143
+ body: JSON.stringify({ agentId: ctx.agentId }),
37144
+ orgId: ctx.orgId,
37145
+ apiUrlOverride: opts.apiUrl
37146
+ });
37147
+ const publicKey = publicKeyFromEnsureResponse(ensure);
37148
+ if (!publicKey) {
37149
+ checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel ensure returned no public key.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
37150
+ } else {
37151
+ ctx.widgetPublicKey = publicKey;
37152
+ checks.push(pass("widget_channel", "Widget channel was ensured explicitly.", {
37153
+ public_key_present: true,
37154
+ mutation_mode: mutationMode
37155
+ }));
37156
+ }
37157
+ checks.push(skipped("widget_embed", "widget_embed_recheck_required", "Widget channel was ensured; rerun read-only proof to verify embed snippet.", `foh prove --agent ${ctx.agentId} --mutation-mode read-only --json`));
37158
+ } catch (ensureError) {
37159
+ checks.push(fail("widget_channel", "widget_channel_ensure_failed", ensureError, `foh widget ensure --agent ${ctx.agentId} --json`));
37160
+ checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel could not be ensured.", `foh widget ensure --agent ${ctx.agentId} --json`));
37161
+ }
37162
+ } else {
37163
+ checks.push(hold("widget_channel", "widget_channel_missing_read_only", "Widget channel/embed was not observable in read-only proof mode.", `foh prove --agent ${ctx.agentId} --mutation-mode ensure --json`, {
37164
+ message: error2 instanceof Error ? error2.message : String(error2),
37165
+ mutation_mode: mutationMode
37166
+ }));
37167
+ checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel was not observable.", `foh widget ensure --agent ${ctx.agentId} --json`));
37168
+ }
37169
+ }
37170
+ if (opts.skipSmoke) {
37171
+ checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
37172
+ } else if (!ctx.widgetPublicKey) {
37173
+ checks.push(skipped("widget_smoke", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
37174
+ } else {
37175
+ try {
37176
+ const smoke = await runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl);
37177
+ ctx.conversationId = smoke.conversation_id;
37178
+ ctx.traceIds = smoke.trace_ids;
37179
+ ctx.correlationIds = smoke.correlation_ids;
37180
+ if (smoke.failed > 0) {
37181
+ checks.push(hold("widget_smoke", "widget_smoke_failed", `${smoke.failed} widget smoke turn(s) failed.`, `foh widget smoke --agent ${ctx.agentId} --json`, smoke));
37182
+ } else {
37183
+ checks.push(pass("widget_smoke", "Widget runtime smoke passed.", smoke));
37184
+ }
37185
+ } catch (error2) {
37186
+ checks.push(fail("widget_smoke", "widget_smoke_failed", error2, `foh widget smoke --agent ${ctx.agentId} --json`));
37187
+ }
37188
+ }
37189
+ if (opts.skipCert) {
37190
+ checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh sim certify-loop --agent ${ctx.agentId} --json`));
37191
+ } else {
37192
+ try {
37193
+ const certMode = normalizeAgentCertMode(opts.certMode);
37194
+ const loop = await runSetupCertifyLoop(ctx.agentId, {
37195
+ mode: certMode,
37196
+ adaptiveRuns: Math.max(1, Number(opts.certAdaptiveRuns ?? 30) || 30),
37197
+ maxImprovementRounds: Math.max(0, Math.min(5, Number(opts.certMaxImprovementRounds ?? 1) || 1)),
37198
+ orgId: ctx.orgId,
37199
+ apiUrlOverride: opts.apiUrl
37200
+ });
37201
+ if (!loop.overall_pass) {
37202
+ checks.push(hold("simulation_certification", "simulation_certification_failed", "Simulation certification did not pass.", `foh sim certify-loop --agent ${ctx.agentId} --${certMode === "quick" ? "full" : certMode} --json`, loop));
37203
+ } else {
37204
+ checks.push(pass("simulation_certification", "Simulation certification passed.", {
37205
+ mode: loop.mode,
37206
+ attempts: loop.attempts?.length ?? 0,
37207
+ improvement_runs: loop.improvement_runs,
37208
+ scenario_summary: loop.certificate?.scenario_summary
37209
+ }));
37210
+ }
37211
+ } catch (error2) {
37212
+ checks.push(fail("simulation_certification", "simulation_certification_failed", error2, `foh sim certify-loop --agent ${ctx.agentId} --json`));
37213
+ }
37214
+ }
37215
+ } else {
37216
+ checks.push(skipped("agent_validation", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37217
+ checks.push(skipped("contact_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37218
+ checks.push(skipped("voice_realtime_health", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37219
+ checks.push(skipped("widget_channel", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37220
+ checks.push(skipped("widget_embed", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37221
+ checks.push(skipped("widget_smoke", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37222
+ checks.push(skipped("simulation_certification", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37223
+ }
37224
+ const status = hasBlockingChecks(checks) ? "hold" : "pass";
37225
+ const nextCommands = Array.from(new Set(checks.map((check2) => check2.next_command).filter((command) => Boolean(command))));
37226
+ if (status === "pass" && ctx.agentId) {
37227
+ nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
37228
+ }
37229
+ const report = signReport(cliEnvelope({
37230
+ schemaVersion: "foh_cli_proof_report.v1",
37231
+ status,
37232
+ reasonCode: status === "pass" ? "proof_passed" : "proof_held",
37233
+ summary: status === "pass" ? "All non-skipped proof checks passed." : "One or more proof checks require operator action.",
37234
+ ids: {
37235
+ org_id: ctx.orgId ?? null,
37236
+ agent_id: ctx.agentId ?? null,
37237
+ mission,
37238
+ mutation_mode: mutationMode,
37239
+ widget_public_key_present: Boolean(ctx.widgetPublicKey),
37240
+ conversation_id: ctx.conversationId ?? null,
37241
+ trace_ids: ctx.traceIds,
37242
+ correlation_ids: ctx.correlationIds
37243
+ },
37244
+ checks,
37245
+ nextCommands,
37246
+ extra: {
37247
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
37248
+ }
37249
+ }));
37250
+ const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
37251
+ format(artifactPath ? { ...report, artifact_path: artifactPath } : report, { json: opts.json ?? false });
37252
+ if (opts.strict && status !== "pass") markCommandFailed(1);
37253
+ }));
37254
+ }
37255
+
35686
37256
  // src/tui/command-palette.ts
35687
37257
  function tokenizeInput(value) {
35688
37258
  const tokens = [];
@@ -36128,7 +37698,7 @@ async function runSelf(args, apiUrlOverride) {
36128
37698
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
36129
37699
  spawnArgs.push("--api-url", apiUrlOverride);
36130
37700
  }
36131
- return await new Promise((resolve5, reject) => {
37701
+ return await new Promise((resolve8, reject) => {
36132
37702
  const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
36133
37703
  stdio: "inherit",
36134
37704
  env: {
@@ -36138,7 +37708,7 @@ async function runSelf(args, apiUrlOverride) {
36138
37708
  }
36139
37709
  });
36140
37710
  child.once("error", reject);
36141
- child.once("close", (code) => resolve5(typeof code === "number" ? code : 1));
37711
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
36142
37712
  });
36143
37713
  }
36144
37714
  function shouldUseInteractiveHome(argv) {
@@ -36428,8 +37998,8 @@ function maybeDefaultToHome(argv = process.argv) {
36428
37998
  }
36429
37999
 
36430
38000
  // src/lib/update.ts
36431
- var import_fs6 = require("fs");
36432
- var import_path5 = require("path");
38001
+ var import_fs11 = require("fs");
38002
+ var import_path9 = require("path");
36433
38003
  var import_child_process3 = require("child_process");
36434
38004
  var import_crypto5 = require("crypto");
36435
38005
  function parseSemver(version2) {
@@ -36450,7 +38020,7 @@ function compareSemver(a, b) {
36450
38020
  }
36451
38021
  function readPackageJsonVersion(path2) {
36452
38022
  try {
36453
- const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
38023
+ const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
36454
38024
  const parsed = JSON.parse(raw);
36455
38025
  const version2 = String(parsed.version ?? "").trim();
36456
38026
  return version2 || void 0;
@@ -36459,13 +38029,13 @@ function readPackageJsonVersion(path2) {
36459
38029
  }
36460
38030
  }
36461
38031
  function findRepoRoot(startCwd = process.cwd()) {
36462
- let current = (0, import_path5.resolve)(startCwd);
38032
+ let current = (0, import_path9.resolve)(startCwd);
36463
38033
  while (true) {
36464
- const rootPackageJsonPath = (0, import_path5.join)(current, "package.json");
36465
- const cliPackageJsonPath = (0, import_path5.join)(current, "packages", "cli", "package.json");
36466
- if ((0, import_fs6.existsSync)(rootPackageJsonPath) && (0, import_fs6.existsSync)(cliPackageJsonPath)) {
38034
+ const rootPackageJsonPath = (0, import_path9.join)(current, "package.json");
38035
+ const cliPackageJsonPath = (0, import_path9.join)(current, "packages", "cli", "package.json");
38036
+ if ((0, import_fs11.existsSync)(rootPackageJsonPath) && (0, import_fs11.existsSync)(cliPackageJsonPath)) {
36467
38037
  try {
36468
- const raw = (0, import_fs6.readFileSync)(rootPackageJsonPath, "utf-8");
38038
+ const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
36469
38039
  const parsed = JSON.parse(raw);
36470
38040
  if (String(parsed.name ?? "").trim() === "front-of-house") {
36471
38041
  return current;
@@ -36473,7 +38043,7 @@ function findRepoRoot(startCwd = process.cwd()) {
36473
38043
  } catch {
36474
38044
  }
36475
38045
  }
36476
- const parent = (0, import_path5.dirname)(current);
38046
+ const parent = (0, import_path9.dirname)(current);
36477
38047
  if (parent === current) return void 0;
36478
38048
  current = parent;
36479
38049
  }
@@ -36487,7 +38057,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
36487
38057
  remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
36488
38058
  };
36489
38059
  }
36490
- const cliPackageJsonPath = (0, import_path5.join)(repoRoot, "packages", "cli", "package.json");
38060
+ const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
36491
38061
  const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
36492
38062
  if (!latestVersion) {
36493
38063
  return {
@@ -36514,19 +38084,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
36514
38084
  };
36515
38085
  }
36516
38086
  async function applyRepoUpdate(repoRoot) {
36517
- const scriptPath = (0, import_path5.join)(repoRoot, "scripts", "Install-FohCli.ps1");
38087
+ const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
36518
38088
  if (process.platform === "win32") {
36519
- return await new Promise((resolve5, reject) => {
38089
+ return await new Promise((resolve8, reject) => {
36520
38090
  const child = (0, import_child_process3.spawn)(
36521
38091
  "powershell",
36522
38092
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
36523
38093
  { stdio: "inherit" }
36524
38094
  );
36525
38095
  child.once("error", reject);
36526
- child.once("close", (code) => resolve5(typeof code === "number" ? code : 1));
38096
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
36527
38097
  });
36528
38098
  }
36529
- return await new Promise((resolve5, reject) => {
38099
+ return await new Promise((resolve8, reject) => {
36530
38100
  const child = (0, import_child_process3.spawn)(
36531
38101
  "corepack",
36532
38102
  ["pnpm", "cli:install:global"],
@@ -36536,7 +38106,7 @@ async function applyRepoUpdate(repoRoot) {
36536
38106
  }
36537
38107
  );
36538
38108
  child.once("error", reject);
36539
- child.once("close", (code) => resolve5(typeof code === "number" ? code : 1));
38109
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
36540
38110
  });
36541
38111
  }
36542
38112
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -36550,7 +38120,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
36550
38120
  }
36551
38121
  function hashFileSha256(filePath) {
36552
38122
  try {
36553
- const bytes = (0, import_fs6.readFileSync)(filePath);
38123
+ const bytes = (0, import_fs11.readFileSync)(filePath);
36554
38124
  return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
36555
38125
  } catch {
36556
38126
  return void 0;
@@ -36560,10 +38130,10 @@ function verifyCliArtifactIntegrity(params = {}) {
36560
38130
  const cwd = params.cwd ?? process.cwd();
36561
38131
  const argv = params.argv ?? process.argv;
36562
38132
  const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
36563
- const runtimePath = (0, import_path5.resolve)(String(argv[1] || ""));
38133
+ const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
36564
38134
  const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
36565
38135
  const warnings = [];
36566
- if (!runtimePath || !(0, import_fs6.existsSync)(runtimePath)) {
38136
+ if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
36567
38137
  warnings.push("runtime_path_unreadable");
36568
38138
  }
36569
38139
  if (!runtimeHash) {
@@ -36581,8 +38151,8 @@ function verifyCliArtifactIntegrity(params = {}) {
36581
38151
  let repoDistHash;
36582
38152
  let runtimeMatchesRepoDist;
36583
38153
  if (repoRoot) {
36584
- repoDistPath = (0, import_path5.join)(repoRoot, "packages", "cli", "dist", "foh.js");
36585
- if ((0, import_fs6.existsSync)(repoDistPath)) {
38154
+ repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
38155
+ if ((0, import_fs11.existsSync)(repoDistPath)) {
36586
38156
  repoDistHash = hashFileSha256(repoDistPath);
36587
38157
  if (runtimeHash && repoDistHash) {
36588
38158
  runtimeMatchesRepoDist = runtimeHash === repoDistHash;
@@ -36770,13 +38340,19 @@ function writeKnownError(message, remediation) {
36770
38340
  if (jsonRequested()) {
36771
38341
  process.stderr.write(
36772
38342
  JSON.stringify(
36773
- {
36774
- error: {
36775
- step: "cli.run",
36776
- message,
36777
- remediation
38343
+ cliEnvelope({
38344
+ status: "fail",
38345
+ reasonCode: reasonCodeFromStep(message, "cli_run_failed"),
38346
+ summary: message,
38347
+ nextCommands: remediation.startsWith("Run: ") ? [remediation.slice("Run: ".length)] : [],
38348
+ extra: {
38349
+ error: {
38350
+ step: "cli.run",
38351
+ message,
38352
+ remediation
38353
+ }
36778
38354
  }
36779
- },
38355
+ }),
36780
38356
  null,
36781
38357
  2
36782
38358
  ) + "\n"
@@ -36805,6 +38381,9 @@ registerMcp(program2);
36805
38381
  registerKnowledge(program2);
36806
38382
  registerLeads(program2);
36807
38383
  registerConversations(program2);
38384
+ registerTranscripts(program2);
38385
+ registerAnalytics(program2);
38386
+ registerTest(program2);
36808
38387
  registerTests(program2);
36809
38388
  registerOps(program2);
36810
38389
  registerSetup(program2);
@@ -36812,6 +38391,7 @@ registerManifest(program2);
36812
38391
  registerSim(program2);
36813
38392
  registerDiag(program2);
36814
38393
  registerBug(program2);
38394
+ registerProve(program2);
36815
38395
  registerUpdate(program2);
36816
38396
  registerHome(program2);
36817
38397
  hideInternalApiUrlOptions(program2);
@@ -36849,13 +38429,19 @@ program2.parseAsync(process.argv).catch((e) => {
36849
38429
  if (jsonRequested()) {
36850
38430
  process.stderr.write(
36851
38431
  JSON.stringify(
36852
- {
36853
- error: {
36854
- step: "cli.unhandled",
36855
- message: e instanceof Error ? e.message : String(e),
36856
- remediation: `Report this as a bug at ${BUG_REPORT_URL}`
38432
+ cliEnvelope({
38433
+ status: "fail",
38434
+ reasonCode: "cli_unhandled_failed",
38435
+ summary: e instanceof Error ? e.message : String(e),
38436
+ nextCommands: [],
38437
+ extra: {
38438
+ error: {
38439
+ step: "cli.unhandled",
38440
+ message: e instanceof Error ? e.message : String(e),
38441
+ remediation: `Report this as a bug at ${BUG_REPORT_URL}`
38442
+ }
36857
38443
  }
36858
- },
38444
+ }),
36859
38445
  null,
36860
38446
  2
36861
38447
  ) + "\n"