@f-o-h/cli 0.1.72 → 0.1.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/foh.js +228 -38
  2. package/package.json +41 -42
package/dist/foh.js CHANGED
@@ -32790,7 +32790,7 @@ var StdioServerTransport = class {
32790
32790
  };
32791
32791
 
32792
32792
  // src/lib/cli-version.ts
32793
- var CLI_VERSION = "0.1.72";
32793
+ var CLI_VERSION = "0.1.74";
32794
32794
 
32795
32795
  // src/commands/mcp-serve.ts
32796
32796
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -35480,9 +35480,11 @@ function registerCertify(program3) {
35480
35480
  const certify = program3.command("certify").description("Produce release certification evidence for an agent");
35481
35481
  certify.command("run").description("Run certification for an exact agent draft/profile and emit release evidence").requiredOption("--agent <id>", "Agent ID to certify").option("--profile <profile>", "Certification profile: smoke, release, or stress", "release").option("--adaptive-runs <n>", "Adaptive runs override; release defaults to a budget-safe 5").option("--journeys <list>", "Comma-separated journey allowlist").option("--scenario-ids <list>", "Comma-separated scenario ID allowlist").option("--channel <channel>", "Channel filter: chat, voice, or mixed", "mixed").option("--out <path>", "Write certification run JSON to this file path").option("--api-url <url>", "API base URL override").option("--json", "Output as machine-readable JSON").action(async (opts) => {
35482
35482
  try {
35483
+ const totalStarted = Date.now();
35483
35484
  const profile = normalizeProfile(opts.profile);
35484
35485
  const mode = modeForProfile(profile);
35485
35486
  const adaptiveRuns = Math.max(1, Math.min(120, Number(opts.adaptiveRuns ?? defaultAdaptiveRuns(profile)) || defaultAdaptiveRuns(profile)));
35487
+ const apiStarted = Date.now();
35486
35488
  const response = await apiFetch(
35487
35489
  `/v1/console/agents/${opts.agent}/sim-certify`,
35488
35490
  {
@@ -35497,6 +35499,7 @@ function registerCertify(program3) {
35497
35499
  apiUrlOverride: opts.apiUrl
35498
35500
  }
35499
35501
  );
35502
+ const apiMs = Math.max(0, Date.now() - apiStarted);
35500
35503
  if (!response.ok || !response.certificate) {
35501
35504
  throw new FohError({
35502
35505
  step: "certify.run",
@@ -35512,6 +35515,10 @@ function registerCertify(program3) {
35512
35515
  reason_code: passed ? "certification_passed" : "certification_failed",
35513
35516
  profile,
35514
35517
  mode,
35518
+ timing: {
35519
+ api_ms: apiMs,
35520
+ total_ms: Math.max(0, Date.now() - totalStarted)
35521
+ },
35515
35522
  certificate: response.certificate,
35516
35523
  next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh certify run --agent ${opts.agent} --profile ${profile} --json`]
35517
35524
  };
@@ -37507,8 +37514,10 @@ function readFreshCache(filePath, maxAgeMs) {
37507
37514
  const payload = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
37508
37515
  const createdAt = Date.parse(String(payload.created_at || ""));
37509
37516
  if (!Number.isFinite(createdAt)) return null;
37510
- if (Date.now() - createdAt > maxAgeMs) return null;
37511
- return payload.value ?? null;
37517
+ const ageMs = Date.now() - createdAt;
37518
+ if (ageMs > maxAgeMs) return null;
37519
+ if (payload.value === void 0) return null;
37520
+ return { value: payload.value, ageMs };
37512
37521
  } catch {
37513
37522
  return null;
37514
37523
  }
@@ -37532,7 +37541,16 @@ async function withProofCache(options, run) {
37532
37541
  if (!resolvedDir) {
37533
37542
  return {
37534
37543
  value: await run(),
37535
- metadata: { hit: false, key: "disabled", cache_path: "", waited_ms: 0 }
37544
+ metadata: {
37545
+ hit: false,
37546
+ miss: true,
37547
+ key: "disabled",
37548
+ cache_path: "",
37549
+ waited_ms: 0,
37550
+ waited_for_peer: false,
37551
+ source: "disabled",
37552
+ age_ms: null
37553
+ }
37536
37554
  };
37537
37555
  }
37538
37556
  const key = cacheKey2(options.kind, options.keyParts);
@@ -37545,8 +37563,17 @@ async function withProofCache(options, run) {
37545
37563
  const existing = readFreshCache(cachePath, maxAgeMs);
37546
37564
  if (existing) {
37547
37565
  return {
37548
- value: existing,
37549
- metadata: { hit: true, key, cache_path: publicPath(cachePath), waited_ms: 0 }
37566
+ value: existing.value,
37567
+ metadata: {
37568
+ hit: true,
37569
+ miss: false,
37570
+ key,
37571
+ cache_path: publicPath(cachePath),
37572
+ waited_ms: 0,
37573
+ waited_for_peer: false,
37574
+ source: "cache",
37575
+ age_ms: existing.ageMs
37576
+ }
37550
37577
  };
37551
37578
  }
37552
37579
  let lockOwner = false;
@@ -37560,8 +37587,17 @@ async function withProofCache(options, run) {
37560
37587
  const waitedValue = readFreshCache(cachePath, maxAgeMs);
37561
37588
  if (waitedValue) {
37562
37589
  return {
37563
- value: waitedValue,
37564
- metadata: { hit: true, key, cache_path: publicPath(cachePath), waited_ms: Date.now() - started }
37590
+ value: waitedValue.value,
37591
+ metadata: {
37592
+ hit: true,
37593
+ miss: false,
37594
+ key,
37595
+ cache_path: publicPath(cachePath),
37596
+ waited_ms: Date.now() - started,
37597
+ waited_for_peer: true,
37598
+ source: "cache",
37599
+ age_ms: waitedValue.ageMs
37600
+ }
37565
37601
  };
37566
37602
  }
37567
37603
  }
@@ -37571,7 +37607,16 @@ async function withProofCache(options, run) {
37571
37607
  writeCache(cachePath, value);
37572
37608
  return {
37573
37609
  value,
37574
- metadata: { hit: false, key, cache_path: publicPath(cachePath), waited_ms: 0 }
37610
+ metadata: {
37611
+ hit: false,
37612
+ miss: true,
37613
+ key,
37614
+ cache_path: publicPath(cachePath),
37615
+ waited_ms: 0,
37616
+ waited_for_peer: false,
37617
+ source: "fresh_run",
37618
+ age_ms: null
37619
+ }
37575
37620
  };
37576
37621
  } finally {
37577
37622
  if (lockOwner) (0, import_node_fs3.rmSync)(lockPath, { recursive: true, force: true });
@@ -37579,6 +37624,37 @@ async function withProofCache(options, run) {
37579
37624
  }
37580
37625
 
37581
37626
  // src/commands/prove.ts
37627
+ function addTiming(timings, name, startedMs) {
37628
+ timings.set(name, (timings.get(name) ?? 0) + Math.max(0, Date.now() - startedMs));
37629
+ }
37630
+ async function timedCheck(timings, name, fn) {
37631
+ const startedMs = Date.now();
37632
+ try {
37633
+ return await fn();
37634
+ } finally {
37635
+ addTiming(timings, name, startedMs);
37636
+ }
37637
+ }
37638
+ function enrichCheckTimings(checks, timings) {
37639
+ return checks.map((check2) => ({
37640
+ ...check2,
37641
+ duration_ms: Math.max(0, timings.get(check2.name) ?? check2.duration_ms ?? 0)
37642
+ }));
37643
+ }
37644
+ function proofTimingSummary(checks, commandStartedMs) {
37645
+ const totalCheckDurationMs = checks.reduce((sum, check2) => sum + (check2.duration_ms ?? 0), 0);
37646
+ return {
37647
+ total_ms: Math.max(0, Date.now() - commandStartedMs),
37648
+ total_check_duration_ms: totalCheckDurationMs,
37649
+ slow_checks: checks.filter((check2) => (check2.duration_ms ?? 0) > 0).map((check2) => ({
37650
+ name: check2.name,
37651
+ category: check2.category,
37652
+ status: check2.status,
37653
+ reason_code: check2.reason_code,
37654
+ duration_ms: check2.duration_ms ?? 0
37655
+ })).sort((a, b) => b.duration_ms - a.duration_ms).slice(0, 10)
37656
+ };
37657
+ }
37582
37658
  function categoryForCheck(name) {
37583
37659
  if (name === "auth") return "auth";
37584
37660
  if (name === "contact_channel" || name === "voice_realtime_health") return "voice";
@@ -37674,7 +37750,9 @@ function isProviderCapacityBlocked(onboarding) {
37674
37750
  }
37675
37751
  function registerProve(program3) {
37676
37752
  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("--include-certification", "Run explicit simulation certification check (slow)").option("--cert-mode <m>", "Simulation cert mode when --include-certification is set: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification when included", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds in included cert loop (0-5)", "1").option("--mission <mission>", "Proof mission: setup, widget, voice, publish", "setup").option("--contact-path <mode>", "Voice contact path: auto, managed, or byon", "auto").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", "Deprecated compatibility flag; certification is skipped unless --include-certification is set").option("--skip-smoke", "Skip widget runtime smoke check").option("--skip-voice-health", "Skip realtime voice provider health check").option("--proof-cache-dir <path>", "Optional local proof cache directory for shared certification results").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 () => {
37753
+ const commandStartedMs = Date.now();
37677
37754
  const checks = [];
37755
+ const checkTimings = /* @__PURE__ */ new Map();
37678
37756
  const mission = normalizeMission(opts.mission);
37679
37757
  const contactPath = normalizeContactPath(opts.contactPath);
37680
37758
  const mutationMode = normalizeMutationMode(opts.mutationMode, Boolean(opts.repair));
@@ -37685,7 +37763,9 @@ function registerProve(program3) {
37685
37763
  correlationIds: []
37686
37764
  };
37687
37765
  try {
37766
+ const authStartedMs = Date.now();
37688
37767
  const creds = loadCredentials(opts.apiUrl);
37768
+ addTiming(checkTimings, "auth", authStartedMs);
37689
37769
  ctx.apiUrl = creds.apiUrl;
37690
37770
  ctx.tokenPresent = Boolean(creds.token);
37691
37771
  ctx.orgId = opts.org || creds.orgId;
@@ -37694,13 +37774,14 @@ function registerProve(program3) {
37694
37774
  org_id_from_credentials: creds.orgId ?? null
37695
37775
  }));
37696
37776
  } catch (error2) {
37777
+ if (!checkTimings.has("auth")) addTiming(checkTimings, "auth", commandStartedMs);
37697
37778
  checks.push(hold("auth", "auth_missing_or_expired", "CLI is not authenticated.", "foh auth login --web", {
37698
37779
  message: error2 instanceof Error ? error2.message : String(error2)
37699
37780
  }));
37700
37781
  }
37701
37782
  if (ctx.tokenPresent && !ctx.orgId) {
37702
37783
  try {
37703
- const orgs = await apiFetch("/v1/console/auth/my-orgs", { apiUrlOverride: opts.apiUrl });
37784
+ const orgs = await timedCheck(checkTimings, "org", () => apiFetch("/v1/console/auth/my-orgs", { apiUrlOverride: opts.apiUrl }));
37704
37785
  const resolved = firstUsableOrgId(orgs);
37705
37786
  if (resolved.orgId) {
37706
37787
  ctx.orgId = resolved.orgId;
@@ -37728,10 +37809,10 @@ function registerProve(program3) {
37728
37809
  checks.push(pass("agent_selection", "Using explicitly supplied agent.", { agent_id: ctx.agentId }));
37729
37810
  } else {
37730
37811
  try {
37731
- const list = await apiFetch("/v1/console/agents", {
37812
+ const list = await timedCheck(checkTimings, "agent_selection", () => apiFetch("/v1/console/agents", {
37732
37813
  orgId: ctx.orgId,
37733
37814
  apiUrlOverride: opts.apiUrl
37734
- });
37815
+ }));
37735
37816
  const resolved = agentIdFromList(list);
37736
37817
  if (resolved.agentId) {
37737
37818
  ctx.agentId = resolved.agentId;
@@ -37754,11 +37835,11 @@ function registerProve(program3) {
37754
37835
  }
37755
37836
  if (ctx.agentId) {
37756
37837
  try {
37757
- const validation = await apiFetch(`/v1/console/agents/${ctx.agentId}/validate`, {
37838
+ const validation = await timedCheck(checkTimings, "agent_validation", () => apiFetch(`/v1/console/agents/${ctx.agentId}/validate`, {
37758
37839
  method: "POST",
37759
37840
  orgId: ctx.orgId,
37760
37841
  apiUrlOverride: opts.apiUrl
37761
- });
37842
+ }));
37762
37843
  const issues = Array.isArray(validation.issues) ? validation.issues : [];
37763
37844
  validationFingerprint = validation;
37764
37845
  if (validation.ok === false || issues.length > 0) {
@@ -37771,10 +37852,10 @@ function registerProve(program3) {
37771
37852
  }
37772
37853
  if (ctx.orgId) {
37773
37854
  try {
37774
- const onboarding = await apiFetch(`/v1/console/org/${ctx.orgId}/onboarding`, {
37855
+ const onboarding = await timedCheck(checkTimings, "contact_channel", () => apiFetch(`/v1/console/org/${ctx.orgId}/onboarding`, {
37775
37856
  orgId: ctx.orgId,
37776
37857
  apiUrlOverride: opts.apiUrl
37777
- });
37858
+ }));
37778
37859
  const phoneNumber = typeof onboarding.phone_number === "string" && onboarding.phone_number.trim() ? onboarding.phone_number.trim() : null;
37779
37860
  const provisioningStatus = typeof onboarding.provisioning_status === "string" ? onboarding.provisioning_status : null;
37780
37861
  if (phoneNumber) {
@@ -37838,10 +37919,10 @@ function registerProve(program3) {
37838
37919
  checks.push(skipped("voice_realtime_health", "operator_skipped", "Skipped by --skip-voice-health.", "foh voice realtime-health --json"));
37839
37920
  } else {
37840
37921
  try {
37841
- const health = await apiFetch(
37922
+ const health = await timedCheck(checkTimings, "voice_realtime_health", () => apiFetch(
37842
37923
  "/v1/console/realtime/health",
37843
37924
  { apiUrlOverride: opts.apiUrl }
37844
- );
37925
+ ));
37845
37926
  const providers = Array.isArray(health.providers) ? health.providers : [];
37846
37927
  if (providers.length === 0) {
37847
37928
  checks.push(skipped("voice_realtime_health", "voice_health_no_providers", "Realtime voice health returned no providers.", "foh voice realtime-health --json"));
@@ -37859,13 +37940,14 @@ function registerProve(program3) {
37859
37940
  }
37860
37941
  }
37861
37942
  try {
37862
- const embed = await apiFetch("/v1/console/channels/widget/embed-snippet", {
37943
+ const embed = await timedCheck(checkTimings, "widget_embed", () => apiFetch("/v1/console/channels/widget/embed-snippet", {
37863
37944
  orgId: ctx.orgId,
37864
37945
  apiUrlOverride: opts.apiUrl,
37865
- headers: { "x-agent-id": ctx.agentId }
37866
- });
37946
+ headers: { "x-agent-id": String(ctx.agentId) }
37947
+ }));
37867
37948
  const publicKey = publicKeyFromEmbedResponse(embed);
37868
37949
  if (publicKey) {
37950
+ checkTimings.set("widget_channel", Math.max(checkTimings.get("widget_channel") ?? 0, checkTimings.get("widget_embed") ?? 0));
37869
37951
  ctx.widgetPublicKey = publicKey;
37870
37952
  checks.push(pass("widget_channel", "Widget channel is available in read-only proof mode.", {
37871
37953
  public_key_present: true,
@@ -37880,12 +37962,12 @@ function registerProve(program3) {
37880
37962
  } catch (error2) {
37881
37963
  if (mutationMode === "ensure") {
37882
37964
  try {
37883
- const ensure = await apiFetch("/v1/console/channels/widget/ensure", {
37965
+ const ensure = await timedCheck(checkTimings, "widget_channel", () => apiFetch("/v1/console/channels/widget/ensure", {
37884
37966
  method: "POST",
37885
37967
  body: JSON.stringify({ agentId: ctx.agentId }),
37886
37968
  orgId: ctx.orgId,
37887
37969
  apiUrlOverride: opts.apiUrl
37888
- });
37970
+ }));
37889
37971
  const publicKey = publicKeyFromEnsureResponse(ensure);
37890
37972
  if (!publicKey) {
37891
37973
  checks.push(hold("widget_channel", "widget_public_key_missing", "Widget channel ensure returned no public key.", `foh widget ensure --agent ${ctx.agentId} --json`, ensure));
@@ -37915,7 +37997,7 @@ function registerProve(program3) {
37915
37997
  checks.push(skipped("widget_smoke", "widget_public_key_required", "Skipped because widget public key is unavailable.", `foh widget ensure --agent ${ctx.agentId} --json`));
37916
37998
  } else {
37917
37999
  try {
37918
- const smoke = await runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl);
38000
+ const smoke = await timedCheck(checkTimings, "widget_smoke", () => runWidgetSmoke(ctx.widgetPublicKey, opts.apiUrl));
37919
38001
  ctx.conversationId = smoke.conversation_id;
37920
38002
  ctx.traceIds = smoke.trace_ids;
37921
38003
  ctx.correlationIds = smoke.correlation_ids;
@@ -37943,7 +38025,7 @@ function registerProve(program3) {
37943
38025
  const agentId = ctx.agentId;
37944
38026
  const adaptiveRuns = Math.max(1, Number(opts.certAdaptiveRuns ?? 30) || 30);
37945
38027
  const maxImprovementRounds = Math.max(0, Math.min(5, Number(opts.certMaxImprovementRounds ?? 1) || 1));
37946
- const cached2 = await withProofCache({
38028
+ const cached2 = await timedCheck(checkTimings, "simulation_certification", () => withProofCache({
37947
38029
  cacheDir: opts.proofCacheDir,
37948
38030
  kind: "simulation_certification",
37949
38031
  keyParts: {
@@ -37961,7 +38043,7 @@ function registerProve(program3) {
37961
38043
  maxImprovementRounds,
37962
38044
  orgId: ctx.orgId,
37963
38045
  apiUrlOverride: opts.apiUrl
37964
- }));
38046
+ })));
37965
38047
  const loop = cached2.value;
37966
38048
  const loopWithCache = {
37967
38049
  ...loop,
@@ -37991,8 +38073,9 @@ function registerProve(program3) {
37991
38073
  checks.push(skipped("widget_smoke", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37992
38074
  checks.push(skipped("simulation_certification", "agent_required", "Skipped until an agent is selected.", "foh agent list --json"));
37993
38075
  }
37994
- const status = hasBlockingChecks(checks) ? "hold" : "pass";
37995
- const nextCommands = Array.from(new Set(checks.map((check2) => check2.next_command).filter((command) => Boolean(command))));
38076
+ const timedChecks = enrichCheckTimings(checks, checkTimings);
38077
+ const status = hasBlockingChecks(timedChecks) ? "hold" : "pass";
38078
+ const nextCommands = Array.from(new Set(timedChecks.map((check2) => check2.next_command).filter((command) => Boolean(command))));
37996
38079
  if (status === "pass" && ctx.agentId) {
37997
38080
  nextCommands.push(`foh agent publish --agent ${ctx.agentId} --json`);
37998
38081
  }
@@ -38012,10 +38095,11 @@ function registerProve(program3) {
38012
38095
  trace_ids: ctx.traceIds,
38013
38096
  correlation_ids: ctx.correlationIds
38014
38097
  },
38015
- checks,
38098
+ checks: timedChecks,
38016
38099
  nextCommands,
38017
38100
  extra: {
38018
- generated_at: (/* @__PURE__ */ new Date()).toISOString()
38101
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
38102
+ timing: proofTimingSummary(timedChecks, commandStartedMs)
38019
38103
  }
38020
38104
  }));
38021
38105
  const artifactPath = opts.out ? writeSignedJsonArtifact(String(opts.out), report) : void 0;
@@ -39659,23 +39743,58 @@ function firstCommandTime(commands) {
39659
39743
  const times = commands.map((command) => String(command.started_at || command.recorded_at || command.completed_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => a.time - b.time);
39660
39744
  return times[0]?.raw ?? null;
39661
39745
  }
39746
+ function commandReasonCodes(commands) {
39747
+ const codes = /* @__PURE__ */ new Set();
39748
+ for (const command of commands) {
39749
+ if (command.reason_code) codes.add(String(command.reason_code));
39750
+ for (const reasonCode of toArray2(command.check_reason_codes)) {
39751
+ if (reasonCode) codes.add(String(reasonCode));
39752
+ }
39753
+ }
39754
+ return Array.from(codes);
39755
+ }
39756
+ function syntheticStatusFromCommands(commands) {
39757
+ const commandReasons = commandReasonCodes(commands);
39758
+ const failed = commands.find((command) => {
39759
+ const status = String(command.status || "").toLowerCase();
39760
+ return status === "fail" || typeof command.exit_code === "number" && command.exit_code !== 0 && status !== "hold";
39761
+ });
39762
+ if (failed) {
39763
+ return {
39764
+ status: "fail",
39765
+ reasonCode: String(failed.reason_code || commandReasons[0] || "external_agent_command_failed")
39766
+ };
39767
+ }
39768
+ const held = commands.find((command) => String(command.status || "").toLowerCase() === "hold");
39769
+ if (held) {
39770
+ return {
39771
+ status: "hold",
39772
+ reasonCode: String(held.reason_code || commandReasons[0] || "external_agent_command_held")
39773
+ };
39774
+ }
39775
+ if (commands.length === 0) {
39776
+ return { status: "hold", reasonCode: "external_agent_capture_empty" };
39777
+ }
39778
+ return { status: "pass", reasonCode: null };
39779
+ }
39662
39780
  function synthesizeRunFromCapture(runPath) {
39663
39781
  const runDir = (0, import_path15.dirname)(runPath);
39664
39782
  const commands = collapseCommandRecords(readNdjson((0, import_path15.join)(runDir, "commands.ndjson")));
39665
39783
  const metadata = asObject((0, import_fs16.existsSync)((0, import_path15.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path15.join)(runDir, "external-agent-metadata.json")) : {});
39666
39784
  const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
39667
- const holdReason = blockerCodes[0] || "external_agent_capture_unfinalized";
39785
+ const commandClassification = syntheticStatusFromCommands(commands);
39786
+ const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
39787
+ const reasonCode = commandClassification.status === "fail" ? commandClassification.reasonCode : blockerCodes[0] || commandClassification.reasonCode;
39668
39788
  const firstCommand = commands[0] || {};
39669
39789
  const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
39670
39790
  const endedAt = latestCommandTime(commands) || startedAt;
39671
- const status = blockerCodes.length > 0 ? "hold" : "pass";
39672
39791
  const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
39673
39792
  const runId = (0, import_path15.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
39674
39793
  return {
39675
39794
  schema_version: "external_agent_run.v1",
39676
39795
  run_id: runId,
39677
39796
  status,
39678
- failure_reason_code: status === "pass" ? null : holdReason,
39797
+ failure_reason_code: status === "pass" ? null : reasonCode || "external_agent_capture_unfinalized",
39679
39798
  model_provider: "unknown",
39680
39799
  model_name: "unknown",
39681
39800
  prompt_version: String(firstCommand.prompt_version || "unknown"),
@@ -39770,15 +39889,82 @@ function collapseCommandRecords(records) {
39770
39889
  }
39771
39890
  return order.map((id) => byId.get(id)).filter((record2) => Boolean(record2));
39772
39891
  }
39892
+ function readCommandOutputJson(runDir, command) {
39893
+ const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
39894
+ if (!artifact || artifact.includes("..") || artifact.includes("/") || artifact.includes("\\")) return null;
39895
+ const artifactPath = (0, import_path15.join)(runDir, artifact);
39896
+ if (!(0, import_fs16.existsSync)(artifactPath)) return null;
39897
+ try {
39898
+ const text = (0, import_fs16.readFileSync)(artifactPath, "utf8");
39899
+ const firstObject = text.indexOf("{");
39900
+ const lastObject = text.lastIndexOf("}");
39901
+ if (firstObject < 0 || lastObject <= firstObject) return null;
39902
+ return JSON.parse(text.slice(firstObject, lastObject + 1));
39903
+ } catch {
39904
+ return null;
39905
+ }
39906
+ }
39907
+ function commandTimingBreakdown(command, output) {
39908
+ const schemaVersion = String(output?.schema_version || "");
39909
+ if (schemaVersion === "foh_cli_proof_report.v1") {
39910
+ const timing = asObject(output?.timing) || {};
39911
+ const cacheSources = /* @__PURE__ */ new Map();
39912
+ let cacheWaitMs = 0;
39913
+ let cacheHitCount = 0;
39914
+ let cacheMissCount = 0;
39915
+ for (const check2 of toArray2(output?.checks)) {
39916
+ const detail = asObject(check2)?.detail;
39917
+ const proofCache = asObject(asObject(detail)?.proof_cache);
39918
+ if (!proofCache) continue;
39919
+ if (proofCache["hit"] === true) cacheHitCount += 1;
39920
+ if (proofCache["miss"] === true) cacheMissCount += 1;
39921
+ cacheWaitMs += Number(proofCache["waited_ms"] || 0);
39922
+ increment(cacheSources, proofCache["source"] || "unknown");
39923
+ }
39924
+ return {
39925
+ kind: "proof",
39926
+ command: command.command || "",
39927
+ total_ms: Number(timing.total_ms || command.duration_ms || 0),
39928
+ total_check_duration_ms: Number(timing.total_check_duration_ms || 0),
39929
+ slow_checks: toArray2(timing.slow_checks).slice(0, 5),
39930
+ cache_wait_ms: cacheWaitMs,
39931
+ cache_hit_count: cacheHitCount,
39932
+ cache_miss_count: cacheMissCount,
39933
+ cache_sources: ranked(cacheSources)
39934
+ };
39935
+ }
39936
+ if (schemaVersion === "foh_certification_run.v1") {
39937
+ const timing = asObject(output?.timing) || {};
39938
+ const certificate = asObject(output?.certificate) || {};
39939
+ return {
39940
+ kind: "certification",
39941
+ command: command.command || "",
39942
+ total_ms: Number(timing.total_ms || command.duration_ms || 0),
39943
+ api_ms: Number(timing.api_ms || 0),
39944
+ status: output?.status || null,
39945
+ reason_code: output?.reason_code || null,
39946
+ scenario_summary: certificate.scenario_summary || null
39947
+ };
39948
+ }
39949
+ return null;
39950
+ }
39773
39951
  function analyzeRunArtifacts(runPath, run, cwd) {
39774
39952
  const runDir = (0, import_path15.dirname)(runPath);
39775
39953
  const commands = collapseCommandRecords(readNdjson((0, import_path15.join)(runDir, "commands.ndjson")));
39776
39954
  const reasonCounts = /* @__PURE__ */ new Map();
39777
39955
  const slowSteps = [];
39956
+ const timingBreakdowns = [];
39778
39957
  let completed = 0;
39779
39958
  let withDuration = 0;
39780
39959
  let totalDuration = 0;
39781
39960
  for (const command of commands) {
39961
+ const output = readCommandOutputJson(runDir, command);
39962
+ const breakdown = commandTimingBreakdown(command, output);
39963
+ if (breakdown) timingBreakdowns.push({
39964
+ run_id: run.run_id,
39965
+ run_path: (0, import_path15.relative)(cwd, runPath).replaceAll("\\", "/"),
39966
+ ...breakdown
39967
+ });
39782
39968
  if (command.phase === "completed" || command.completed_at) completed += 1;
39783
39969
  if (typeof command.duration_ms === "number") {
39784
39970
  withDuration += 1;
@@ -39823,6 +40009,7 @@ function analyzeRunArtifacts(runPath, run, cwd) {
39823
40009
  total_command_duration_ms: totalDuration,
39824
40010
  command_reason_codes: ranked(reasonCounts),
39825
40011
  slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms) - Number(a.duration_ms)).slice(0, 10),
40012
+ timing_breakdowns: timingBreakdowns,
39826
40013
  docs_pages_observed: Array.from(docs).sort(),
39827
40014
  codex_command_execution_completed_count: codexCommandExecutions,
39828
40015
  codex_failed_exit_code_count: codexFailedExitCodes
@@ -39840,6 +40027,7 @@ function summarizeExternalAgentRuns(options) {
39840
40027
  const commandReasonCounts = /* @__PURE__ */ new Map();
39841
40028
  const docsCounts = /* @__PURE__ */ new Map();
39842
40029
  const slowSteps = [];
40030
+ const timingBreakdowns = [];
39843
40031
  let manualInterventions = 0;
39844
40032
  let commandCount = 0;
39845
40033
  let completedCommandCount = 0;
@@ -39865,6 +40053,7 @@ function summarizeExternalAgentRuns(options) {
39865
40053
  codexCommandExecutions += Number(artifactSummary.codex_command_execution_completed_count || 0);
39866
40054
  codexFailedExitCodes += Number(artifactSummary.codex_failed_exit_code_count || 0);
39867
40055
  for (const row of toArray2(artifactSummary.slow_steps)) slowSteps.push(row);
40056
+ for (const row of toArray2(artifactSummary.timing_breakdowns)) timingBreakdowns.push(row);
39868
40057
  for (const row of toArray2(artifactSummary.command_reason_codes)) {
39869
40058
  const entry = asObject(row);
39870
40059
  if (entry) increment(commandReasonCounts, entry.key, Number(entry.count || 1));
@@ -39872,7 +40061,7 @@ function summarizeExternalAgentRuns(options) {
39872
40061
  for (const page of toArray2(artifactSummary.docs_pages_observed)) increment(docsCounts, page);
39873
40062
  }
39874
40063
  const topFailures = ranked(failureCounts);
39875
- const commandReasonCodes = ranked(commandReasonCounts);
40064
+ const commandReasonCodes2 = ranked(commandReasonCounts);
39876
40065
  const recommendedFixes = topFailures.map((failure) => ({
39877
40066
  reason_code: failure.key,
39878
40067
  count: failure.count,
@@ -39900,8 +40089,9 @@ function summarizeExternalAgentRuns(options) {
39900
40089
  missing_completion_count: missingCompletionCount,
39901
40090
  commands_with_duration_count: commandsWithDurationCount,
39902
40091
  total_command_duration_ms: totalCommandDurationMs,
39903
- command_reason_codes: commandReasonCodes,
39904
- slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
40092
+ command_reason_codes: commandReasonCodes2,
40093
+ slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20),
40094
+ timing_breakdowns: timingBreakdowns.sort((a, b) => Number(b.total_ms || 0) - Number(a.total_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
39905
40095
  },
39906
40096
  codex_telemetry: {
39907
40097
  command_execution_completed_count: codexCommandExecutions,
@@ -39974,11 +40164,11 @@ function classifyExternalAgentRun(input) {
39974
40164
  if (observedVersions.some((version2) => version2 !== CLI_VERSION)) {
39975
40165
  return { status: "hold", reasonCode: "external_agent_cli_version_drift" };
39976
40166
  }
39977
- const commandReasonCodes = completedCommands.flatMap((record2) => [
40167
+ const commandReasonCodes2 = completedCommands.flatMap((record2) => [
39978
40168
  String(record2.reason_code || ""),
39979
40169
  ...Array.isArray(record2.check_reason_codes) ? record2.check_reason_codes.map((code) => String(code || "")) : []
39980
40170
  ]).filter(Boolean);
39981
- const hasCommandReason = (pattern) => commandReasonCodes.some((reason) => pattern.test(reason));
40171
+ const hasCommandReason = (pattern) => commandReasonCodes2.some((reason) => pattern.test(reason));
39982
40172
  if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
39983
40173
  return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
39984
40174
  }
package/package.json CHANGED
@@ -1,42 +1,41 @@
1
- {
2
- "name": "@f-o-h/cli",
3
- "version": "0.1.72",
4
- "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
- "license": "UNLICENSED",
6
- "bin": {
7
- "foh": "dist/foh.js"
8
- },
9
- "main": "dist/foh.js",
10
- "files": [
11
- "dist/",
12
- "examples/",
13
- "schemas/",
14
- "README.md",
15
- "package.json"
16
- ],
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "engines": {
21
- "node": ">=18"
22
- },
23
- "scripts": {
24
- "build": "node build.mjs",
25
- "test": "vitest run",
26
- "typecheck": "tsc --noEmit"
27
- },
28
- "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.29.0",
30
- "commander": "^12.1.0",
31
- "js-yaml": "^4.1.1",
32
- "picocolors": "^1.1.1",
33
- "zod": "^4.3.6"
34
- },
35
- "devDependencies": {
36
- "@types/js-yaml": "^4.0.9",
37
- "@types/node": "^22.0.0",
38
- "esbuild": "^0.24.0",
39
- "vitest": "^2.0.0"
40
- }
41
- }
42
-
1
+ {
2
+ "name": "@f-o-h/cli",
3
+ "version": "0.1.74",
4
+ "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
+ "license": "UNLICENSED",
6
+ "bin": {
7
+ "foh": "dist/foh.js"
8
+ },
9
+ "main": "dist/foh.js",
10
+ "files": [
11
+ "dist/",
12
+ "examples/",
13
+ "schemas/",
14
+ "README.md",
15
+ "package.json"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "build": "node build.mjs",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.29.0",
30
+ "commander": "^12.1.0",
31
+ "js-yaml": "^4.1.1",
32
+ "picocolors": "^1.1.1",
33
+ "zod": "^4.3.6"
34
+ },
35
+ "devDependencies": {
36
+ "@types/js-yaml": "^4.0.9",
37
+ "@types/node": "^22.0.0",
38
+ "esbuild": "^0.24.0",
39
+ "vitest": "^2.0.0"
40
+ }
41
+ }