@f-o-h/cli 0.1.5 → 0.1.7

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 = resolve7.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 resolve7(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 resolve7(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: resolve7,
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((resolve7) => {
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
- resolve7(defaultValue);
10115
+ resolve8(defaultValue);
10074
10116
  return;
10075
10117
  }
10076
10118
  if (!value && !allowEmpty) {
10077
- resolve7("");
10119
+ resolve8("");
10078
10120
  return;
10079
10121
  }
10080
- resolve7(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((resolve7) => {
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
- resolve7(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 resolve7("");
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((resolve7) => setTimeout(resolve7, 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((resolve7) => setTimeout(resolve7, 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: readFileSync7 } = await import("fs");
13787
- rule = JSON.parse(readFileSync7(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,23 +14402,39 @@ 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
  }));
14193
- 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").requiredOption("--agent <id>", "Agent ID").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14194
- if (!opts.trace && !opts.conversation) {
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) {
14195
14408
  throw new FohError({
14196
14409
  step: "agent.replay",
14197
14410
  error: "Missing replay source",
14198
- remediation: "Pass --trace <id> or --conversation <id>.",
14411
+ remediation: "Pass --trace <id>, --conversation <id>, or --file <transcript.json>.",
14199
14412
  statusCode: 400
14200
14413
  });
14201
14414
  }
14202
- if (opts.trace && opts.conversation) {
14415
+ if (sourceCount > 1) {
14203
14416
  throw new FohError({
14204
14417
  step: "agent.replay",
14205
14418
  error: "Ambiguous replay source",
14206
- remediation: "Pass only one of --trace or --conversation.",
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.",
14207
14428
  statusCode: 400
14208
14429
  });
14209
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
+ }
14210
14438
  if (opts.trace) {
14211
14439
  const data2 = await apiFetch(`/v1/console/traces/${opts.trace}/replay`, {
14212
14440
  method: "POST",
@@ -15806,8 +16034,8 @@ function registerVoice(program3) {
15806
16034
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
15807
16035
  const audio = Buffer.from(await res.arrayBuffer());
15808
16036
  const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync6 } = await import("fs");
15809
- const { dirname: dirname5, resolve: resolve7 } = await import("path");
15810
- const absolutePath = resolve7(outputPath);
16037
+ const { dirname: dirname5, resolve: resolve8 } = await import("path");
16038
+ const absolutePath = resolve8(outputPath);
15811
16039
  mkdirSync4(dirname5(absolutePath), { recursive: true });
15812
16040
  writeFileSync6(absolutePath, audio);
15813
16041
  format({
@@ -30290,7 +30518,7 @@ var Protocol = class {
30290
30518
  return;
30291
30519
  }
30292
30520
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
30293
- await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
30521
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
30294
30522
  options?.signal?.throwIfAborted();
30295
30523
  }
30296
30524
  } catch (error2) {
@@ -30307,7 +30535,7 @@ var Protocol = class {
30307
30535
  */
30308
30536
  request(request, resultSchema, options) {
30309
30537
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
30310
- return new Promise((resolve7, reject) => {
30538
+ return new Promise((resolve8, reject) => {
30311
30539
  const earlyReject = (error2) => {
30312
30540
  reject(error2);
30313
30541
  };
@@ -30385,7 +30613,7 @@ var Protocol = class {
30385
30613
  if (!parseResult.success) {
30386
30614
  reject(parseResult.error);
30387
30615
  } else {
30388
- resolve7(parseResult.data);
30616
+ resolve8(parseResult.data);
30389
30617
  }
30390
30618
  } catch (error2) {
30391
30619
  reject(error2);
@@ -30646,12 +30874,12 @@ var Protocol = class {
30646
30874
  }
30647
30875
  } catch {
30648
30876
  }
30649
- return new Promise((resolve7, reject) => {
30877
+ return new Promise((resolve8, reject) => {
30650
30878
  if (signal.aborted) {
30651
30879
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
30652
30880
  return;
30653
30881
  }
30654
- const timeoutId = setTimeout(resolve7, interval);
30882
+ const timeoutId = setTimeout(resolve8, interval);
30655
30883
  signal.addEventListener("abort", () => {
30656
30884
  clearTimeout(timeoutId);
30657
30885
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -31751,7 +31979,7 @@ var McpServer = class {
31751
31979
  let task = createTaskResult.task;
31752
31980
  const pollInterval = task.pollInterval ?? 5e3;
31753
31981
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
31754
- await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
31982
+ await new Promise((resolve8) => setTimeout(resolve8, pollInterval));
31755
31983
  const updatedTask = await extra.taskStore.getTask(taskId);
31756
31984
  if (!updatedTask) {
31757
31985
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -32400,19 +32628,19 @@ var StdioServerTransport = class {
32400
32628
  this.onclose?.();
32401
32629
  }
32402
32630
  send(message) {
32403
- return new Promise((resolve7) => {
32631
+ return new Promise((resolve8) => {
32404
32632
  const json3 = serializeMessage(message);
32405
32633
  if (this._stdout.write(json3)) {
32406
- resolve7();
32634
+ resolve8();
32407
32635
  } else {
32408
- this._stdout.once("drain", resolve7);
32636
+ this._stdout.once("drain", resolve8);
32409
32637
  }
32410
32638
  });
32411
32639
  }
32412
32640
  };
32413
32641
 
32414
32642
  // src/lib/cli-version.ts
32415
- var CLI_VERSION = "0.1.5";
32643
+ var CLI_VERSION = "0.1.7";
32416
32644
 
32417
32645
  // src/commands/mcp-serve.ts
32418
32646
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -32597,7 +32825,7 @@ async function runFohCli(params) {
32597
32825
  effectiveArgv.push("--json");
32598
32826
  }
32599
32827
  const command = `foh ${effectiveArgv.join(" ")}`;
32600
- return await new Promise((resolve7) => {
32828
+ return await new Promise((resolve8) => {
32601
32829
  const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
32602
32830
  stdio: ["ignore", "pipe", "pipe"],
32603
32831
  env: {
@@ -32622,7 +32850,7 @@ async function runFohCli(params) {
32622
32850
  });
32623
32851
  child.once("error", (error2) => {
32624
32852
  clearTimeout(timeoutHandle);
32625
- resolve7({
32853
+ resolve8({
32626
32854
  ok: false,
32627
32855
  command,
32628
32856
  argv: effectiveArgv,
@@ -32638,7 +32866,7 @@ async function runFohCli(params) {
32638
32866
  const stderrText = finalizeBoundedText(stderrBuffer);
32639
32867
  const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
32640
32868
  const stdoutJson = tryParseJson(stdoutText);
32641
- resolve7({
32869
+ resolve8({
32642
32870
  ok: !timedOut && exitCode === 0,
32643
32871
  command,
32644
32872
  argv: effectiveArgv,
@@ -33411,7 +33639,7 @@ function registerMcp(program3) {
33411
33639
  }
33412
33640
 
33413
33641
  // src/commands/knowledge.ts
33414
- var import_fs2 = require("fs");
33642
+ var import_fs3 = require("fs");
33415
33643
  var import_path2 = require("path");
33416
33644
 
33417
33645
  // src/lib/query-options.ts
@@ -33473,6 +33701,11 @@ function registerKnowledge(program3) {
33473
33701
  source: "agent_draft_knowledge",
33474
33702
  citation: `agent:${opts.agent}:chunk:${chunk.index + 1}`,
33475
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
+ },
33476
33709
  text: chunk.text.slice(0, 1200)
33477
33710
  })).filter((chunk) => chunk.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
33478
33711
  const topScore = matches[0]?.score ?? 0;
@@ -33485,6 +33718,11 @@ function registerKnowledge(program3) {
33485
33718
  reason_code: reasonCode,
33486
33719
  top_score: topScore,
33487
33720
  chunk_count: chunks.length,
33721
+ lineage: {
33722
+ source: "agent_draft_direct",
33723
+ agent_id: opts.agent,
33724
+ candidate_chunk_count: chunks.length
33725
+ },
33488
33726
  next_commands: [
33489
33727
  `foh knowledge ingest-file --agent ${opts.agent} --file <path> --json`,
33490
33728
  `foh knowledge query --agent ${opts.agent} --text "${query.replace(/"/g, '\\"')}" --explain --json`
@@ -33495,6 +33733,23 @@ function registerKnowledge(program3) {
33495
33733
  ok: status === "pass",
33496
33734
  status,
33497
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
+ ],
33498
33753
  agent_id: opts.agent,
33499
33754
  query,
33500
33755
  retrieval: {
@@ -33502,7 +33757,12 @@ function registerKnowledge(program3) {
33502
33757
  chunk_count: chunks.length,
33503
33758
  match_count: matches.length,
33504
33759
  top_score: topScore,
33505
- min_score: minScore
33760
+ min_score: minScore,
33761
+ lineage: {
33762
+ agent_id: opts.agent,
33763
+ source: "agent_draft_direct",
33764
+ candidate_chunk_count: chunks.length
33765
+ }
33506
33766
  },
33507
33767
  matches,
33508
33768
  failure_packet: packet,
@@ -33515,7 +33775,7 @@ function registerKnowledge(program3) {
33515
33775
  }, { json: opts.json ?? false });
33516
33776
  }));
33517
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 () => {
33518
- const content = (0, import_fs2.readFileSync)(opts.file, "utf-8");
33778
+ const content = (0, import_fs3.readFileSync)(opts.file, "utf-8");
33519
33779
  let data;
33520
33780
  if (opts.agent) {
33521
33781
  const draft = await apiFetch(`/v1/console/agents/${opts.agent}/draft`, {
@@ -33674,7 +33934,7 @@ var import_crypto3 = require("crypto");
33674
33934
 
33675
33935
  // src/lib/signed-report.ts
33676
33936
  var import_crypto2 = require("crypto");
33677
- var import_fs3 = require("fs");
33937
+ var import_fs4 = require("fs");
33678
33938
  var import_path3 = require("path");
33679
33939
  function canonicalize(value) {
33680
33940
  if (value === null || value === void 0) return null;
@@ -33706,13 +33966,13 @@ function signReport(reportPayload) {
33706
33966
  }
33707
33967
  function writeSignedJsonArtifact(path2, value) {
33708
33968
  const absolutePath = (0, import_path3.resolve)(path2);
33709
- (0, import_fs3.mkdirSync)((0, import_path3.dirname)(absolutePath), { recursive: true });
33710
- (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");
33711
33971
  return absolutePath;
33712
33972
  }
33713
33973
 
33714
33974
  // src/commands/manifest.ts
33715
- var import_fs4 = require("fs");
33975
+ var import_fs5 = require("fs");
33716
33976
  var import_picocolors3 = __toESM(require_picocolors());
33717
33977
  function formatDiff(diffs) {
33718
33978
  if (diffs.length === 0) return "No changes";
@@ -33748,7 +34008,7 @@ function formatDiff(diffs) {
33748
34008
  function loadManifestFile(filePath) {
33749
34009
  let raw;
33750
34010
  try {
33751
- raw = (0, import_fs4.readFileSync)(filePath, "utf-8");
34011
+ raw = (0, import_fs5.readFileSync)(filePath, "utf-8");
33752
34012
  } catch {
33753
34013
  throw new FohError({
33754
34014
  step: "manifest.load",
@@ -34016,27 +34276,32 @@ function optionNameToFlag(key) {
34016
34276
  function buildMissingOptionsPlan(missing, opts) {
34017
34277
  const missingFlags = missing.map(optionNameToFlag);
34018
34278
  const signInUrl = buildConsoleSignInUrl(resolveConsoleBaseUrl(opts.consoleUrl));
34019
- return {
34279
+ return cliEnvelope({
34020
34280
  status: "blocked",
34021
- code: "setup_required_options_missing",
34022
- missing_options: missingFlags,
34023
- reason: "setup requires an authenticated org, an agent template, and an agent name before it can mutate customer resources",
34024
- sign_in_url: signInUrl,
34025
- 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: [
34026
34285
  "foh auth signup --web --json",
34027
34286
  "foh auth login --web --json",
34028
34287
  ...buildCliAuthFallbackCommands(),
34029
34288
  "foh templates list --json",
34030
34289
  'foh setup --org <org-id> --agent-template <template-id> --agent-name "Demo Agent" --widget-domains <domain> --report-out setup-report.json --json'
34031
34290
  ],
34032
- text_fallback: buildCliAuthFallbackInstructions(signInUrl),
34033
- ai_agent_instruction: [
34034
- "Do not guess org IDs, template IDs, or customer domains.",
34035
- "If no browser is available, print sign_in_url and ask the user to sign in.",
34036
- "After auth, discover orgs and templates with the listed commands.",
34037
- "Rerun setup only after all missing_options are resolved."
34038
- ]
34039
- };
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
+ });
34040
34305
  }
34041
34306
  function emitMissingOptionsPlan(missing, opts) {
34042
34307
  const plan = buildMissingOptionsPlan(missing, { consoleUrl: opts.consoleUrl });
@@ -34527,7 +34792,15 @@ ${serialiseManifest(manifest)}`,
34527
34792
  });
34528
34793
  const reportMeta = emitSetupReport("success");
34529
34794
  const summary = {
34795
+ schema_version: "foh_cli_setup_summary.v1",
34796
+ ok: true,
34530
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
+ },
34531
34804
  org_id: opts.org,
34532
34805
  agent_id: agentId,
34533
34806
  phone_number: phoneNumber,
@@ -34539,7 +34812,15 @@ ${serialiseManifest(manifest)}`,
34539
34812
  manifest_written: manifestWritten ? "tenant.yaml" : null,
34540
34813
  resume_from: resumeState.resumeFrom,
34541
34814
  setup_report_hash: reportMeta.reportHash,
34542
- 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
+ ] : []
34543
34824
  };
34544
34825
  format(summary, { json: opts.json ?? false });
34545
34826
  } catch (error2) {
@@ -34552,13 +34833,25 @@ ${serialiseManifest(manifest)}`,
34552
34833
  });
34553
34834
  format(
34554
34835
  {
34836
+ schema_version: "foh_cli_setup_failure.v1",
34837
+ ok: false,
34555
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
+ },
34556
34845
  step: error2.step,
34557
34846
  completed_steps: completed.map((stepResult) => stepResult.step),
34558
34847
  error: error2.error,
34559
34848
  remediation: error2.remediation,
34560
34849
  setup_report_hash: reportMeta.reportHash,
34561
- 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)
34562
34855
  },
34563
34856
  { json: opts.json ?? false }
34564
34857
  );
@@ -34902,7 +35195,7 @@ function registerConversations(program3) {
34902
35195
  }
34903
35196
 
34904
35197
  // src/commands/transcripts.ts
34905
- var import_fs5 = require("fs");
35198
+ var import_fs6 = require("fs");
34906
35199
  var import_path4 = require("path");
34907
35200
  function listPath(agentId, opts) {
34908
35201
  const params = new URLSearchParams();
@@ -34916,6 +35209,27 @@ function listPath(agentId, opts) {
34916
35209
  function jsonl(rows) {
34917
35210
  return rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "");
34918
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
+ }
34919
35233
  function registerTranscripts(program3) {
34920
35234
  const transcripts = program3.command("transcripts").description("List, fetch, and export conversation transcripts");
34921
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 () => {
@@ -34929,7 +35243,7 @@ function registerTranscripts(program3) {
34929
35243
  const data = await apiFetch(path2, { orgId: opts.org, apiUrlOverride: opts.apiUrl });
34930
35244
  format(data, { json: opts.json ?? false });
34931
35245
  }));
34932
- 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("--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 () => {
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 () => {
34933
35247
  const exportFormat = String(opts.format || "jsonl").trim().toLowerCase();
34934
35248
  if (!["jsonl", "json"].includes(exportFormat)) {
34935
35249
  throw new FohError({
@@ -34943,16 +35257,74 @@ function registerTranscripts(program3) {
34943
35257
  orgId: opts.org,
34944
35258
  apiUrlOverride: opts.apiUrl
34945
35259
  });
34946
- const rows = Array.isArray(data.conversations) ? data.conversations : [];
34947
- const content = exportFormat === "json" ? stableStringify({ schema_version: "foh_transcript_export.v1", conversations: rows }) : jsonl(rows);
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);
34948
35288
  if (opts.out) {
34949
35289
  const outputPath = (0, import_path4.resolve)(String(opts.out));
34950
- (0, import_fs5.writeFileSync)(outputPath, content, "utf-8");
34951
- format({ status: "exported", format: exportFormat, count: rows.length, output_path: outputPath }, { json: opts.json ?? false });
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 });
34952
35309
  return;
34953
35310
  }
34954
35311
  if (opts.json || exportFormat === "json") {
34955
- format({ schema_version: "foh_transcript_export.v1", conversations: rows }, { json: opts.json ?? false });
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 });
34956
35328
  return;
34957
35329
  }
34958
35330
  process.stdout.write(content);
@@ -35017,6 +35389,14 @@ function registerAnalytics(program3) {
35017
35389
  format({
35018
35390
  schema_version: "foh_analytics_fetch.v1",
35019
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: {},
35020
35400
  agent_id: opts.agent,
35021
35401
  preset,
35022
35402
  window_days: windowDays,
@@ -35220,16 +35600,39 @@ function registerTests(program3) {
35220
35600
  }
35221
35601
 
35222
35602
  // src/commands/test.ts
35223
- var import_fs6 = require("fs");
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");
35224
35608
  var import_path5 = require("path");
35225
35609
  function asStringList(value) {
35226
35610
  if (typeof value === "string" && value.trim()) return [value.trim()];
35227
35611
  if (Array.isArray(value)) return value.map((entry) => String(entry || "").trim()).filter(Boolean);
35228
35612
  return [];
35229
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
+ }
35230
35634
  function parseSuiteFile(path2) {
35231
- const raw = (0, import_fs6.readFileSync)(path2, "utf-8");
35232
- const parsed = path2.toLowerCase().endsWith(".json") ? JSON.parse(raw) : load(raw);
35635
+ const parsed = parseStructuredFile(path2);
35233
35636
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
35234
35637
  throw new FohError({
35235
35638
  step: "test.run",
@@ -35240,7 +35643,33 @@ function parseSuiteFile(path2) {
35240
35643
  }
35241
35644
  return parsed;
35242
35645
  }
35243
- function validateSuite(suite) {
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) {
35244
35673
  const scenarios = Array.isArray(suite.scenarios) ? suite.scenarios : [];
35245
35674
  if (scenarios.length === 0) {
35246
35675
  throw new FohError({
@@ -35250,17 +35679,18 @@ function validateSuite(suite) {
35250
35679
  statusCode: 400
35251
35680
  });
35252
35681
  }
35253
- for (const scenario of scenarios) {
35254
- if (!Array.isArray(scenario.turns) || scenario.turns.length === 0) {
35682
+ return scenarios.map((scenario) => {
35683
+ const turns = resolveScenarioTurns(scenario, suitePath);
35684
+ if (turns.length === 0) {
35255
35685
  throw new FohError({
35256
35686
  step: "test.run",
35257
35687
  error: `Scenario "${scenario.id || scenario.name || "(unnamed)"}" has no turns`,
35258
- remediation: "Add turns with user/message and expect.contains assertions.",
35688
+ remediation: "Add inline turns or a fixture_transcript file with user messages.",
35259
35689
  statusCode: 400
35260
35690
  });
35261
35691
  }
35262
- }
35263
- return scenarios;
35692
+ return { ...scenario, turns };
35693
+ });
35264
35694
  }
35265
35695
  function evaluateReply(reply, expect) {
35266
35696
  const lowerReply = reply.toLowerCase();
@@ -35279,10 +35709,128 @@ function evaluateReply(reply, expect) {
35279
35709
  }
35280
35710
  return failures;
35281
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
35282
35830
  function registerTest(program3) {
35283
35831
  const test = program3.command("test").description("Run local scenario suites against runtime channels");
35284
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 () => {
35285
- const suitePath = (0, import_path5.resolve)(String(opts.suite));
35833
+ const suitePath = (0, import_path6.resolve)(String(opts.suite));
35286
35834
  const suite = parseSuiteFile(suitePath);
35287
35835
  const agentId = String(opts.agent || suite.agent || "").trim();
35288
35836
  if (!agentId) {
@@ -35293,7 +35841,7 @@ function registerTest(program3) {
35293
35841
  statusCode: 400
35294
35842
  });
35295
35843
  }
35296
- const scenarios = validateSuite(suite);
35844
+ const scenarios = validateSuite(suite, suitePath);
35297
35845
  const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
35298
35846
  method: "POST",
35299
35847
  body: JSON.stringify({ agentId }),
@@ -35316,7 +35864,7 @@ function registerTest(program3) {
35316
35864
  const scenario = scenarios[scenarioIndex];
35317
35865
  let conversationId;
35318
35866
  const turnResults = [];
35319
- for (let turnIndex = 0; turnIndex < (scenario.turns || []).length; turnIndex += 1) {
35867
+ for (let turnIndex = 0; turnIndex < scenario.turns.length; turnIndex += 1) {
35320
35868
  const turn = scenario.turns[turnIndex];
35321
35869
  const message = String(turn.user || turn.message || "").trim();
35322
35870
  if (!message) {
@@ -35324,6 +35872,7 @@ function registerTest(program3) {
35324
35872
  turnResults.push({ turn: turnIndex + 1, ok: false, failures: ["missing user/message"] });
35325
35873
  continue;
35326
35874
  }
35875
+ const start = Date.now();
35327
35876
  const response = await apiFetch("/v1/widget/inbound", {
35328
35877
  method: "POST",
35329
35878
  body: JSON.stringify({
@@ -35334,9 +35883,13 @@ function registerTest(program3) {
35334
35883
  }),
35335
35884
  apiUrlOverride: opts.apiUrl
35336
35885
  });
35886
+ const latencyMs = Date.now() - start;
35337
35887
  conversationId = response.conversationId || conversationId;
35338
35888
  const reply = String(response.reply || "");
35339
- const failures = reply ? evaluateReply(reply, turn.expect) : ["empty reply"];
35889
+ const failures = reply ? [
35890
+ ...evaluateReply(reply, turn.expect),
35891
+ ...evaluateStructuredExpectations(response, turn.expect, latencyMs)
35892
+ ] : ["empty reply"];
35340
35893
  if (failures.length === 0) passed += 1;
35341
35894
  else failed += 1;
35342
35895
  turnResults.push({
@@ -35345,9 +35898,12 @@ function registerTest(program3) {
35345
35898
  message,
35346
35899
  reply,
35347
35900
  failures,
35901
+ latency_ms: latencyMs,
35348
35902
  conversation_id: response.conversationId ?? null,
35349
35903
  trace_id: response.trace_id ?? null,
35350
- correlation_id: response.correlation_id ?? null
35904
+ correlation_id: response.correlation_id ?? null,
35905
+ action: response.action ?? null,
35906
+ handoff: response.handoff ?? null
35351
35907
  });
35352
35908
  }
35353
35909
  scenarioResults.push({
@@ -35359,16 +35915,24 @@ function registerTest(program3) {
35359
35915
  }
35360
35916
  const report = {
35361
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`],
35362
35927
  suite_path: suitePath,
35363
35928
  agent_id: agentId,
35364
- ok: failed === 0,
35365
35929
  passed,
35366
35930
  failed,
35367
35931
  scenarios: scenarioResults
35368
35932
  };
35369
35933
  if (opts.out) {
35370
- const out = (0, import_path5.resolve)(String(opts.out));
35371
- (0, import_fs6.writeFileSync)(out, stableStringify(report), "utf-8");
35934
+ const out = (0, import_path6.resolve)(String(opts.out));
35935
+ (0, import_fs8.writeFileSync)(out, stableStringify(report), "utf-8");
35372
35936
  format({ ...report, output_path: out }, { json: opts.json ?? false });
35373
35937
  } else {
35374
35938
  format(report, { json: opts.json ?? false });
@@ -35791,8 +36355,220 @@ function registerDiag(program3) {
35791
36355
  }
35792
36356
 
35793
36357
  // src/commands/bug.ts
35794
- var import_fs7 = require("fs");
35795
- var import_path6 = 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
+ "external_agent_run",
36366
+ "setup_failure",
36367
+ "proof_failure",
36368
+ "replay_failure",
36369
+ "knowledge_miss",
36370
+ "runtime_miss",
36371
+ "live_proof_failure"
36372
+ ];
36373
+ var IMPROVEMENT_DECISIONS = [
36374
+ "ignore",
36375
+ "fix_docs",
36376
+ "fix_config",
36377
+ "fix_cli",
36378
+ "fix_api",
36379
+ "fix_runtime",
36380
+ "add_test"
36381
+ ];
36382
+ var SECRET_LEAK_RE = /\b(?:sk|pk|xai|whsec|EAAN)[A-Za-z0-9_\-]{16,}\b/i;
36383
+ var MAX_REDACTED_SOURCE_BYTES = 4e3;
36384
+ function asRecord2(value) {
36385
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
36386
+ }
36387
+ function nonEmpty2(value) {
36388
+ const text = String(value ?? "").trim();
36389
+ return text.length > 0 ? text : void 0;
36390
+ }
36391
+ function getPath2(value, path2) {
36392
+ let current = value;
36393
+ for (const segment of path2.split(".")) {
36394
+ const record2 = asRecord2(current);
36395
+ if (!record2) return void 0;
36396
+ current = record2[segment];
36397
+ }
36398
+ return current;
36399
+ }
36400
+ function parseEnum(raw, allowed, label) {
36401
+ const value = nonEmpty2(raw);
36402
+ if (!value) return void 0;
36403
+ if (allowed.includes(value)) return value;
36404
+ throw new FohError({
36405
+ step: "bug.improve",
36406
+ error: `Invalid ${label} "${value}"`,
36407
+ remediation: `Use one of: ${allowed.join(", ")}.`,
36408
+ statusCode: 400
36409
+ });
36410
+ }
36411
+ function inferSourceType(artifact) {
36412
+ const schema2 = nonEmpty2(getPath2(artifact, "schema_version")) || "";
36413
+ const status = nonEmpty2(getPath2(artifact, "status")) || "";
36414
+ if (schema2 === "external_agent_run.v1") return "external_agent_run";
36415
+ if (schema2.includes("knowledge_query") || nonEmpty2(getPath2(artifact, "failure_packet.schema_version"))?.includes("knowledge_query")) {
36416
+ return "knowledge_miss";
36417
+ }
36418
+ if (schema2.includes("agent_replay") || status.includes("replay")) return "replay_failure";
36419
+ if (schema2.includes("proof") || schema2.includes("live_proof")) return schema2.includes("live") ? "live_proof_failure" : "proof_failure";
36420
+ if (schema2.includes("setup")) return "setup_failure";
36421
+ return "runtime_miss";
36422
+ }
36423
+ function inferReasonCode(artifact) {
36424
+ const direct = nonEmpty2(getPath2(artifact, "reason_code"));
36425
+ if (direct) return direct;
36426
+ const failureReason = nonEmpty2(getPath2(artifact, "failure_reason_code"));
36427
+ if (failureReason) return failureReason;
36428
+ const nested = nonEmpty2(getPath2(artifact, "failure_packet.reason_code"));
36429
+ if (nested) return nested;
36430
+ const checks = getPath2(artifact, "checks");
36431
+ if (Array.isArray(checks)) {
36432
+ for (const check2 of checks) {
36433
+ const reason = nonEmpty2(getPath2(check2, "reason_code"));
36434
+ const status = nonEmpty2(getPath2(check2, "status"));
36435
+ if (reason && status !== "pass" && status !== "success") return reason;
36436
+ }
36437
+ }
36438
+ return nonEmpty2(getPath2(artifact, "status"));
36439
+ }
36440
+ function inferPromotionDecision(sourceType) {
36441
+ if (sourceType === "external_agent_run") return "fix_docs";
36442
+ if (sourceType === "knowledge_miss") return "fix_docs";
36443
+ if (sourceType === "setup_failure" || sourceType === "proof_failure" || sourceType === "live_proof_failure") return "fix_config";
36444
+ if (sourceType === "replay_failure" || sourceType === "runtime_miss") return "add_test";
36445
+ return "fix_runtime";
36446
+ }
36447
+ function collectIds(artifact, explicit = {}) {
36448
+ const source = getPath2(artifact, "source");
36449
+ const ids = {
36450
+ org_id: explicit.org_id ?? nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id")),
36451
+ agent_id: explicit.agent_id ?? nonEmpty2(getPath2(source, "agent_id")) ?? nonEmpty2(getPath2(artifact, "ids.agent_id")) ?? nonEmpty2(getPath2(artifact, "agent_id")),
36452
+ conversation_id: explicit.conversation_id ?? nonEmpty2(getPath2(source, "conversation_id")) ?? nonEmpty2(getPath2(artifact, "ids.conversation_id")) ?? nonEmpty2(getPath2(artifact, "conversation_id")),
36453
+ trace_id: explicit.trace_id ?? nonEmpty2(getPath2(source, "trace_id")) ?? nonEmpty2(getPath2(artifact, "ids.trace_id")) ?? nonEmpty2(getPath2(artifact, "trace_id")),
36454
+ correlation_id: explicit.correlation_id ?? nonEmpty2(getPath2(artifact, "ids.correlation_id")) ?? nonEmpty2(getPath2(artifact, "correlation_id")),
36455
+ proof_artifact: explicit.proof_artifact
36456
+ };
36457
+ return Object.fromEntries(Object.entries(ids).filter(([, value]) => value !== void 0));
36458
+ }
36459
+ function compactSourceArtifact(artifact) {
36460
+ const redacted = redactObject(artifact);
36461
+ const text = JSON.stringify(redacted);
36462
+ if (text.length <= MAX_REDACTED_SOURCE_BYTES) {
36463
+ return { truncated: false, value: redacted };
36464
+ }
36465
+ return {
36466
+ truncated: true,
36467
+ bytes_before_truncate: text.length,
36468
+ value_preview: text.slice(0, MAX_REDACTED_SOURCE_BYTES)
36469
+ };
36470
+ }
36471
+ function defaultNextCommands(input) {
36472
+ const commands = [];
36473
+ if (input.ids.trace_id && input.ids.agent_id) {
36474
+ commands.push(`foh tests from-trace --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
36475
+ commands.push(`foh agent replay --agent ${input.ids.agent_id} --trace ${input.ids.trace_id} --json`);
36476
+ }
36477
+ if (input.ids.conversation_id && input.ids.agent_id) {
36478
+ commands.push(`foh agent replay --agent ${input.ids.agent_id} --conversation ${input.ids.conversation_id} --json`);
36479
+ }
36480
+ if (input.sourceType === "knowledge_miss" && input.ids.agent_id) {
36481
+ commands.push(`foh knowledge query --agent ${input.ids.agent_id} --text "<question>" --explain --json`);
36482
+ }
36483
+ if (input.sourceType === "external_agent_run" && input.sourceArtifactPath) {
36484
+ commands.push(`foh bug improve --from external-agent-run --file ${input.sourceArtifactPath} --json`);
36485
+ }
36486
+ if (input.sourceArtifactPath) {
36487
+ 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`);
36488
+ }
36489
+ return commands;
36490
+ }
36491
+ function assertOrgBoundary(artifact, explicitOrgId) {
36492
+ const artifactOrgId = nonEmpty2(getPath2(artifact, "ids.org_id")) ?? nonEmpty2(getPath2(artifact, "org_id"));
36493
+ if (explicitOrgId && artifactOrgId && explicitOrgId !== artifactOrgId) {
36494
+ throw new FohError({
36495
+ step: "bug.improve",
36496
+ error: "Org boundary check failed for improvement packet.",
36497
+ remediation: "Use the org id from the source artifact, or omit --org when building a local redacted packet.",
36498
+ statusCode: 403
36499
+ });
36500
+ }
36501
+ }
36502
+ function assertRedacted(value) {
36503
+ const text = JSON.stringify(value);
36504
+ if (SECRET_LEAK_RE.test(text)) {
36505
+ throw new FohError({
36506
+ step: "bug.improve",
36507
+ error: "Improvement packet still contains a secret-like value after redaction.",
36508
+ remediation: "Remove raw credentials from the source artifact and rebuild the packet.",
36509
+ statusCode: 400
36510
+ });
36511
+ }
36512
+ }
36513
+ function readSourceArtifact(path2) {
36514
+ if (!path2) return null;
36515
+ try {
36516
+ return JSON.parse((0, import_fs9.readFileSync)(path2, "utf-8"));
36517
+ } catch (error2) {
36518
+ throw new FohError({
36519
+ step: "bug.improve",
36520
+ error: `Failed to read source artifact: ${error2 instanceof Error ? error2.message : String(error2)}`,
36521
+ remediation: "Pass --from-file with a readable JSON artifact.",
36522
+ statusCode: 400
36523
+ });
36524
+ }
36525
+ }
36526
+ function buildImprovementPacket(input) {
36527
+ const artifact = input.sourceArtifact ?? null;
36528
+ const sourceType = parseEnum(input.sourceType, IMPROVEMENT_SOURCE_TYPES, "--source-type") ?? inferSourceType(artifact);
36529
+ const promotionDecision = parseEnum(input.promotionDecision, IMPROVEMENT_DECISIONS, "--recommendation") ?? inferPromotionDecision(sourceType);
36530
+ const ids = collectIds(artifact, input.ids);
36531
+ assertOrgBoundary(artifact, input.ids?.org_id);
36532
+ const reasonCode = nonEmpty2(input.reasonCode) ?? inferReasonCode(artifact);
36533
+ if (!reasonCode) {
36534
+ throw new FohError({
36535
+ step: "bug.improve",
36536
+ error: "Missing improvement reason code.",
36537
+ remediation: "Pass --reason-code <code> or build from a source artifact that includes reason_code/status.",
36538
+ statusCode: 400
36539
+ });
36540
+ }
36541
+ const evidenceSummary = redactString(
36542
+ nonEmpty2(input.evidenceSummary) ?? nonEmpty2(getPath2(artifact, "summary")) ?? `Improvement candidate generated from ${sourceType} with reason ${reasonCode}.`
36543
+ );
36544
+ const nextCommands = Array.from(new Set([
36545
+ ...input.nextCommands ?? [],
36546
+ ...defaultNextCommands({ sourceType, ids, sourceArtifactPath: input.sourceArtifactPath, reasonCode })
36547
+ ].map((command) => command.trim()).filter(Boolean)));
36548
+ const packet = {
36549
+ schema_version: "foh_improvement_packet.v1",
36550
+ created_at: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
36551
+ source_type: sourceType,
36552
+ reason_code: reasonCode,
36553
+ promotion_decision: promotionDecision,
36554
+ ids,
36555
+ evidence: {
36556
+ summary: evidenceSummary,
36557
+ source_artifact_path: input.sourceArtifactPath ?? null,
36558
+ source_command: input.sourceCommand ?? null,
36559
+ redaction: {
36560
+ enabled: true,
36561
+ fields: ["email", "phone", "secret-like-token"]
36562
+ },
36563
+ redacted_source: artifact ? compactSourceArtifact(artifact) : null
36564
+ },
36565
+ next_commands: nextCommands
36566
+ };
36567
+ assertRedacted(packet);
36568
+ return packet;
36569
+ }
36570
+
36571
+ // src/commands/bug.ts
35796
36572
  var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
35797
36573
  var MAX_BODY_PREVIEW_LENGTH = 200;
35798
36574
  function parseMethod(raw) {
@@ -35908,14 +36684,18 @@ function parseRequestBody(raw) {
35908
36684
  }
35909
36685
  }
35910
36686
  function writeJsonArtifact(path2, value) {
35911
- const absolutePath = (0, import_path6.resolve)(path2);
35912
- (0, import_fs7.mkdirSync)((0, import_path6.dirname)(absolutePath), { recursive: true });
35913
- (0, import_fs7.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
36687
+ const absolutePath = (0, import_path8.resolve)(path2);
36688
+ (0, import_fs10.mkdirSync)((0, import_path8.dirname)(absolutePath), { recursive: true });
36689
+ (0, import_fs10.writeFileSync)(absolutePath, stableStringify(value), "utf-8");
35914
36690
  return absolutePath;
35915
36691
  }
35916
36692
  function defaultArtifactPath() {
35917
36693
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
35918
- return (0, import_path6.resolve)(`test-results/bug-report.${timestamp2}.json`);
36694
+ return (0, import_path8.resolve)(`test-results/bug-report.${timestamp2}.json`);
36695
+ }
36696
+ function defaultImprovementArtifactPath() {
36697
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
36698
+ return (0, import_path8.resolve)(`test-results/improvement-packet.${timestamp2}.json`);
35919
36699
  }
35920
36700
  async function resolveBugReportWizardInputs(opts) {
35921
36701
  if (!opts.wizard) return opts;
@@ -36065,6 +36845,46 @@ function registerBug(program3) {
36065
36845
  throw e;
36066
36846
  }
36067
36847
  });
36848
+ bug.command("improve").description("Write a redacted failure-to-improvement packet from an external-agent/setup/proof/replay/knowledge/runtime artifact").option("--from <type>", "Source artifact type alias, for example external-agent-run").option("--file <path>", "Source JSON artifact alias for --from-file").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>", "external_agent_run|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_cli|fix_api|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(
36849
+ "--next-command <text>",
36850
+ "Deterministic next command (repeat this flag for multiple commands)",
36851
+ (value, previous = []) => [...previous, value],
36852
+ []
36853
+ ).option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
36854
+ const sourcePath = opts.fromFile ?? opts.file;
36855
+ const sourceType = opts.sourceType ?? (typeof opts.from === "string" ? opts.from.replaceAll("-", "_") : void 0);
36856
+ const sourceArtifact = readSourceArtifact(sourcePath);
36857
+ const outPath = String(opts.out || defaultImprovementArtifactPath());
36858
+ const packet = buildImprovementPacket({
36859
+ sourceArtifact,
36860
+ sourceArtifactPath: sourcePath,
36861
+ sourceType,
36862
+ reasonCode: opts.reasonCode,
36863
+ evidenceSummary: opts.evidenceSummary,
36864
+ promotionDecision: opts.recommendation,
36865
+ sourceCommand: opts.sourceCommand,
36866
+ ids: {
36867
+ org_id: opts.org,
36868
+ agent_id: opts.agent,
36869
+ conversation_id: opts.conversation,
36870
+ trace_id: opts.trace,
36871
+ correlation_id: opts.correlation,
36872
+ proof_artifact: opts.proofArtifact
36873
+ },
36874
+ nextCommands: Array.isArray(opts.nextCommand) ? opts.nextCommand : []
36875
+ });
36876
+ const artifactPath = writeJsonArtifact(outPath, packet);
36877
+ format(cliEnvelope({
36878
+ schemaVersion: "foh_improvement_packet_result.v1",
36879
+ status: "exported",
36880
+ reasonCode: "improvement_packet_created",
36881
+ summary: "Improvement packet created.",
36882
+ ids: packet.ids,
36883
+ artifacts: { improvement_packet: artifactPath },
36884
+ nextCommands: packet.next_commands,
36885
+ extra: { packet }
36886
+ }), { json: opts.json ?? false });
36887
+ }));
36068
36888
  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) => {
36069
36889
  try {
36070
36890
  const params = new URLSearchParams();
@@ -36108,18 +36928,26 @@ function registerBug(program3) {
36108
36928
  }
36109
36929
 
36110
36930
  // src/commands/prove.ts
36931
+ function categoryForCheck(name) {
36932
+ if (name === "auth") return "auth";
36933
+ if (name === "contact_channel" || name === "voice_realtime_health") return "voice";
36934
+ if (name.startsWith("widget_")) return "widget";
36935
+ if (name === "simulation_certification") return "certification";
36936
+ if (name === "agent_validation") return "publish_readiness";
36937
+ return "setup";
36938
+ }
36111
36939
  function pass(name, summary, detail) {
36112
- return { name, status: "pass", reason_code: `${name}_ok`, summary, detail };
36940
+ return { name, category: categoryForCheck(name), status: "pass", reason_code: `${name}_ok`, summary, detail };
36113
36941
  }
36114
36942
  function hold(name, reasonCode, summary, nextCommand, detail) {
36115
- return { name, status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
36943
+ return { name, category: categoryForCheck(name), status: "hold", reason_code: reasonCode, summary, next_command: nextCommand, detail };
36116
36944
  }
36117
36945
  function fail(name, reasonCode, error2, nextCommand) {
36118
36946
  const message = error2 instanceof Error ? error2.message : String(error2);
36119
- return { name, status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
36947
+ return { name, category: categoryForCheck(name), status: "fail", reason_code: reasonCode, summary: message, next_command: nextCommand };
36120
36948
  }
36121
36949
  function skipped(name, reasonCode, summary, nextCommand) {
36122
- return { name, status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
36950
+ return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
36123
36951
  }
36124
36952
  function hasBlockingChecks(checks) {
36125
36953
  return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
@@ -36130,6 +36958,21 @@ function publicKeyFromEnsureResponse(response) {
36130
36958
  const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
36131
36959
  return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
36132
36960
  }
36961
+ function publicKeyFromEmbedResponse(response) {
36962
+ const record2 = response && typeof response === "object" ? response : {};
36963
+ const publicKey = record2.widget_public_key ?? record2.public_key;
36964
+ return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
36965
+ }
36966
+ function normalizeMission(raw) {
36967
+ const value = String(raw || "setup").trim().toLowerCase();
36968
+ if (value === "setup" || value === "widget" || value === "voice" || value === "publish") return value;
36969
+ return "setup";
36970
+ }
36971
+ function normalizeMutationMode(raw, repair) {
36972
+ if (repair) return "ensure";
36973
+ const value = String(raw || "read-only").trim().toLowerCase();
36974
+ return value === "ensure" ? "ensure" : "read-only";
36975
+ }
36133
36976
  function agentIdFromList(response) {
36134
36977
  const agents = Array.isArray(response.agents) ? response.agents : [];
36135
36978
  const usable = agents.filter((agent) => typeof agent.id === "string" && agent.id.trim());
@@ -36144,8 +36987,10 @@ function firstUsableOrgId(response) {
36144
36987
  return { orgId: usable.length === 1 ? usable[0] : void 0, count: usable.length };
36145
36988
  }
36146
36989
  function registerProve(program3) {
36147
- 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("--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 () => {
36990
+ 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 () => {
36148
36991
  const checks = [];
36992
+ const mission = normalizeMission(opts.mission);
36993
+ const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
36149
36994
  const ctx = {
36150
36995
  tokenPresent: false,
36151
36996
  traceIds: [],
@@ -36247,9 +37092,10 @@ function registerProve(program3) {
36247
37092
  phone_number_present: true,
36248
37093
  provisioning_status: onboarding.provisioning_status ?? null
36249
37094
  }));
36250
- } else if (opts.requirePhone) {
37095
+ } else if (opts.requirePhone || mission === "voice") {
36251
37096
  checks.push(hold("contact_channel", "contact_phone_missing", "No phone/contact number is provisioned for this org.", `foh provision buy --org ${ctx.orgId} --json`, {
36252
- provisioning_status: onboarding.provisioning_status ?? null
37097
+ provisioning_status: onboarding.provisioning_status ?? null,
37098
+ mission
36253
37099
  }));
36254
37100
  } else {
36255
37101
  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`));
@@ -36282,36 +37128,56 @@ function registerProve(program3) {
36282
37128
  checks.push(fail("voice_realtime_health", "voice_realtime_health_failed", error2, "foh voice realtime-health --json"));
36283
37129
  }
36284
37130
  }
36285
- try {
36286
- const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
36287
- method: "POST",
36288
- body: JSON.stringify({ agentId: ctx.agentId }),
36289
- orgId: ctx.orgId,
36290
- apiUrlOverride: opts.apiUrl
36291
- });
36292
- const publicKey = publicKeyFromEnsureResponse(ensure);
36293
- if (!publicKey) {
36294
- checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel exists but no public key was returned.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
36295
- } else {
36296
- ctx.widgetPublicKey = publicKey;
36297
- checks.push(pass("widget_channel", "Widget channel is available.", { public_key_present: true }));
36298
- }
36299
- } catch (error2) {
36300
- checks.push(fail("widget_channel", "widget_channel_failed", error2, `foh widget ensure --agent ${ctx.agentId} --json`));
36301
- }
36302
37131
  try {
36303
37132
  const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
36304
37133
  orgId: ctx.orgId,
36305
37134
  apiUrlOverride: opts.apiUrl,
36306
37135
  headers: { "x-agent-id": ctx.agentId }
36307
37136
  });
37137
+ const publicKey = publicKeyFromEmbedResponse(embed);
37138
+ if (publicKey) {
37139
+ ctx.widgetPublicKey = publicKey;
37140
+ checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
37141
+ public_key_present: true,
37142
+ mutation_mode: mutationMode
37143
+ }));
37144
+ }
36308
37145
  if (typeof embed.snippet === "string" && embed.snippet.trim()) {
36309
37146
  checks.push(pass("widget_embed", "Widget embed snippet is available.", { snippet_present: true }));
36310
37147
  } else {
36311
37148
  checks.push(hold("widget_embed", "widget_embed_missing", "Widget embed snippet is missing.", `foh widget embed-snippet --agent ${ctx.agentId}`));
36312
37149
  }
36313
37150
  } catch (error2) {
36314
- checks.push(fail("widget_embed", "widget_embed_failed", error2, `foh widget embed-snippet --agent ${ctx.agentId}`));
37151
+ if (mutationMode === "ensure") {
37152
+ try {
37153
+ const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
37154
+ method: "POST",
37155
+ body: JSON.stringify({ agentId: ctx.agentId }),
37156
+ orgId: ctx.orgId,
37157
+ apiUrlOverride: opts.apiUrl
37158
+ });
37159
+ const publicKey = publicKeyFromEnsureResponse(ensure);
37160
+ if (!publicKey) {
37161
+ checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel ensure returned no public key.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
37162
+ } else {
37163
+ ctx.widgetPublicKey = publicKey;
37164
+ checks.push(pass("widget_channel", "Widget channel was ensured explicitly.", {
37165
+ public_key_present: true,
37166
+ mutation_mode: mutationMode
37167
+ }));
37168
+ }
37169
+ 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`));
37170
+ } catch (ensureError) {
37171
+ checks.push(fail("widget_channel", "widget_channel_ensure_failed", ensureError, `foh widget ensure --agent ${ctx.agentId} --json`));
37172
+ checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel could not be ensured.", `foh widget ensure --agent ${ctx.agentId} --json`));
37173
+ }
37174
+ } else {
37175
+ 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`, {
37176
+ message: error2 instanceof Error ? error2.message : String(error2),
37177
+ mutation_mode: mutationMode
37178
+ }));
37179
+ checks.push(skipped("widget_embed", "widget_channel_required", "Skipped because widget channel was not observable.", `foh widget ensure --agent ${ctx.agentId} --json`));
37180
+ }
36315
37181
  }
36316
37182
  if (opts.skipSmoke) {
36317
37183
  checks.push(skipped("widget_smoke", "operator_skipped", "Skipped by --skip-smoke.", `foh widget smoke --agent ${ctx.agentId} --json`));
@@ -36372,22 +37238,27 @@ function registerProve(program3) {
36372
37238
  if (status === "pass" && ctx.agentId) {
36373
37239
  nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
36374
37240
  }
36375
- const report = signReport({
36376
- schema_version: "foh_cli_proof_report.v1",
36377
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
36378
- ok: status === "pass",
37241
+ const report = signReport(cliEnvelope({
37242
+ schemaVersion: "foh_cli_proof_report.v1",
36379
37243
  status,
37244
+ reasonCode: status === "pass" ? "proof_passed" : "proof_held",
37245
+ summary: status === "pass" ? "All non-skipped proof checks passed." : "One or more proof checks require operator action.",
36380
37246
  ids: {
36381
37247
  org_id: ctx.orgId ?? null,
36382
37248
  agent_id: ctx.agentId ?? null,
37249
+ mission,
37250
+ mutation_mode: mutationMode,
36383
37251
  widget_public_key_present: Boolean(ctx.widgetPublicKey),
36384
37252
  conversation_id: ctx.conversationId ?? null,
36385
37253
  trace_ids: ctx.traceIds,
36386
37254
  correlation_ids: ctx.correlationIds
36387
37255
  },
36388
37256
  checks,
36389
- next_commands: nextCommands
36390
- });
37257
+ nextCommands,
37258
+ extra: {
37259
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
37260
+ }
37261
+ }));
36391
37262
  const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
36392
37263
  format(artifactPath ? { ...report, artifact_path: artifactPath } : report, { json: opts.json ?? false });
36393
37264
  if (opts.strict && status !== "pass") markCommandFailed(1);
@@ -36839,7 +37710,7 @@ async function runSelf(args, apiUrlOverride) {
36839
37710
  if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
36840
37711
  spawnArgs.push("--api-url", apiUrlOverride);
36841
37712
  }
36842
- return await new Promise((resolve7, reject) => {
37713
+ return await new Promise((resolve8, reject) => {
36843
37714
  const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
36844
37715
  stdio: "inherit",
36845
37716
  env: {
@@ -36849,7 +37720,7 @@ async function runSelf(args, apiUrlOverride) {
36849
37720
  }
36850
37721
  });
36851
37722
  child.once("error", reject);
36852
- child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
37723
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
36853
37724
  });
36854
37725
  }
36855
37726
  function shouldUseInteractiveHome(argv) {
@@ -37139,8 +38010,8 @@ function maybeDefaultToHome(argv = process.argv) {
37139
38010
  }
37140
38011
 
37141
38012
  // src/lib/update.ts
37142
- var import_fs8 = require("fs");
37143
- var import_path7 = require("path");
38013
+ var import_fs11 = require("fs");
38014
+ var import_path9 = require("path");
37144
38015
  var import_child_process3 = require("child_process");
37145
38016
  var import_crypto5 = require("crypto");
37146
38017
  function parseSemver(version2) {
@@ -37161,7 +38032,7 @@ function compareSemver(a, b) {
37161
38032
  }
37162
38033
  function readPackageJsonVersion(path2) {
37163
38034
  try {
37164
- const raw = (0, import_fs8.readFileSync)(path2, "utf-8");
38035
+ const raw = (0, import_fs11.readFileSync)(path2, "utf-8");
37165
38036
  const parsed = JSON.parse(raw);
37166
38037
  const version2 = String(parsed.version ?? "").trim();
37167
38038
  return version2 || void 0;
@@ -37170,13 +38041,13 @@ function readPackageJsonVersion(path2) {
37170
38041
  }
37171
38042
  }
37172
38043
  function findRepoRoot(startCwd = process.cwd()) {
37173
- let current = (0, import_path7.resolve)(startCwd);
38044
+ let current = (0, import_path9.resolve)(startCwd);
37174
38045
  while (true) {
37175
- const rootPackageJsonPath = (0, import_path7.join)(current, "package.json");
37176
- const cliPackageJsonPath = (0, import_path7.join)(current, "packages", "cli", "package.json");
37177
- if ((0, import_fs8.existsSync)(rootPackageJsonPath) && (0, import_fs8.existsSync)(cliPackageJsonPath)) {
38046
+ const rootPackageJsonPath = (0, import_path9.join)(current, "package.json");
38047
+ const cliPackageJsonPath = (0, import_path9.join)(current, "packages", "cli", "package.json");
38048
+ if ((0, import_fs11.existsSync)(rootPackageJsonPath) && (0, import_fs11.existsSync)(cliPackageJsonPath)) {
37178
38049
  try {
37179
- const raw = (0, import_fs8.readFileSync)(rootPackageJsonPath, "utf-8");
38050
+ const raw = (0, import_fs11.readFileSync)(rootPackageJsonPath, "utf-8");
37180
38051
  const parsed = JSON.parse(raw);
37181
38052
  if (String(parsed.name ?? "").trim() === "front-of-house") {
37182
38053
  return current;
@@ -37184,7 +38055,7 @@ function findRepoRoot(startCwd = process.cwd()) {
37184
38055
  } catch {
37185
38056
  }
37186
38057
  }
37187
- const parent = (0, import_path7.dirname)(current);
38058
+ const parent = (0, import_path9.dirname)(current);
37188
38059
  if (parent === current) return void 0;
37189
38060
  current = parent;
37190
38061
  }
@@ -37198,7 +38069,7 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
37198
38069
  remediation: "Run this command from the Front Of House repo root to compare/install the latest CLI."
37199
38070
  };
37200
38071
  }
37201
- const cliPackageJsonPath = (0, import_path7.join)(repoRoot, "packages", "cli", "package.json");
38072
+ const cliPackageJsonPath = (0, import_path9.join)(repoRoot, "packages", "cli", "package.json");
37202
38073
  const latestVersion = readPackageJsonVersion(cliPackageJsonPath);
37203
38074
  if (!latestVersion) {
37204
38075
  return {
@@ -37225,19 +38096,19 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
37225
38096
  };
37226
38097
  }
37227
38098
  async function applyRepoUpdate(repoRoot) {
37228
- const scriptPath = (0, import_path7.join)(repoRoot, "scripts", "Install-FohCli.ps1");
38099
+ const scriptPath = (0, import_path9.join)(repoRoot, "scripts", "Install-FohCli.ps1");
37229
38100
  if (process.platform === "win32") {
37230
- return await new Promise((resolve7, reject) => {
38101
+ return await new Promise((resolve8, reject) => {
37231
38102
  const child = (0, import_child_process3.spawn)(
37232
38103
  "powershell",
37233
38104
  ["-ExecutionPolicy", "Bypass", "-File", scriptPath],
37234
38105
  { stdio: "inherit" }
37235
38106
  );
37236
38107
  child.once("error", reject);
37237
- child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
38108
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
37238
38109
  });
37239
38110
  }
37240
- return await new Promise((resolve7, reject) => {
38111
+ return await new Promise((resolve8, reject) => {
37241
38112
  const child = (0, import_child_process3.spawn)(
37242
38113
  "corepack",
37243
38114
  ["pnpm", "cli:install:global"],
@@ -37247,7 +38118,7 @@ async function applyRepoUpdate(repoRoot) {
37247
38118
  }
37248
38119
  );
37249
38120
  child.once("error", reject);
37250
- child.once("close", (code) => resolve7(typeof code === "number" ? code : 1));
38121
+ child.once("close", (code) => resolve8(typeof code === "number" ? code : 1));
37251
38122
  });
37252
38123
  }
37253
38124
  function shouldShowUpdateNotice(argv = process.argv) {
@@ -37261,7 +38132,7 @@ function shouldShowUpdateNotice(argv = process.argv) {
37261
38132
  }
37262
38133
  function hashFileSha256(filePath) {
37263
38134
  try {
37264
- const bytes = (0, import_fs8.readFileSync)(filePath);
38135
+ const bytes = (0, import_fs11.readFileSync)(filePath);
37265
38136
  return (0, import_crypto5.createHash)("sha256").update(bytes).digest("hex");
37266
38137
  } catch {
37267
38138
  return void 0;
@@ -37271,10 +38142,10 @@ function verifyCliArtifactIntegrity(params = {}) {
37271
38142
  const cwd = params.cwd ?? process.cwd();
37272
38143
  const argv = params.argv ?? process.argv;
37273
38144
  const expectedSha256 = String(params.expectedSha256 ?? "").trim().toLowerCase() || void 0;
37274
- const runtimePath = (0, import_path7.resolve)(String(argv[1] || ""));
38145
+ const runtimePath = (0, import_path9.resolve)(String(argv[1] || ""));
37275
38146
  const runtimeHash = runtimePath ? hashFileSha256(runtimePath) : void 0;
37276
38147
  const warnings = [];
37277
- if (!runtimePath || !(0, import_fs8.existsSync)(runtimePath)) {
38148
+ if (!runtimePath || !(0, import_fs11.existsSync)(runtimePath)) {
37278
38149
  warnings.push("runtime_path_unreadable");
37279
38150
  }
37280
38151
  if (!runtimeHash) {
@@ -37292,8 +38163,8 @@ function verifyCliArtifactIntegrity(params = {}) {
37292
38163
  let repoDistHash;
37293
38164
  let runtimeMatchesRepoDist;
37294
38165
  if (repoRoot) {
37295
- repoDistPath = (0, import_path7.join)(repoRoot, "packages", "cli", "dist", "foh.js");
37296
- if ((0, import_fs8.existsSync)(repoDistPath)) {
38166
+ repoDistPath = (0, import_path9.join)(repoRoot, "packages", "cli", "dist", "foh.js");
38167
+ if ((0, import_fs11.existsSync)(repoDistPath)) {
37297
38168
  repoDistHash = hashFileSha256(repoDistPath);
37298
38169
  if (runtimeHash && repoDistHash) {
37299
38170
  runtimeMatchesRepoDist = runtimeHash === repoDistHash;
@@ -37481,13 +38352,19 @@ function writeKnownError(message, remediation) {
37481
38352
  if (jsonRequested()) {
37482
38353
  process.stderr.write(
37483
38354
  JSON.stringify(
37484
- {
37485
- error: {
37486
- step: "cli.run",
37487
- message,
37488
- remediation
38355
+ cliEnvelope({
38356
+ status: "fail",
38357
+ reasonCode: reasonCodeFromStep(message, "cli_run_failed"),
38358
+ summary: message,
38359
+ nextCommands: remediation.startsWith("Run: ") ? [remediation.slice("Run: ".length)] : [],
38360
+ extra: {
38361
+ error: {
38362
+ step: "cli.run",
38363
+ message,
38364
+ remediation
38365
+ }
37489
38366
  }
37490
- },
38367
+ }),
37491
38368
  null,
37492
38369
  2
37493
38370
  ) + "\n"
@@ -37564,13 +38441,19 @@ program2.parseAsync(process.argv).catch((e) => {
37564
38441
  if (jsonRequested()) {
37565
38442
  process.stderr.write(
37566
38443
  JSON.stringify(
37567
- {
37568
- error: {
37569
- step: "cli.unhandled",
37570
- message: e instanceof Error ? e.message : String(e),
37571
- remediation: `Report this as a bug at ${BUG_REPORT_URL}`
38444
+ cliEnvelope({
38445
+ status: "fail",
38446
+ reasonCode: "cli_unhandled_failed",
38447
+ summary: e instanceof Error ? e.message : String(e),
38448
+ nextCommands: [],
38449
+ extra: {
38450
+ error: {
38451
+ step: "cli.unhandled",
38452
+ message: e instanceof Error ? e.message : String(e),
38453
+ remediation: `Report this as a bug at ${BUG_REPORT_URL}`
38454
+ }
37572
38455
  }
37573
- },
38456
+ }),
37574
38457
  null,
37575
38458
  2
37576
38459
  ) + "\n"