@f-o-h/cli 0.1.64 → 0.1.66

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 (3) hide show
  1. package/README.md +16 -11
  2. package/dist/foh.js +190 -141
  3. package/package.json +41 -41
package/README.md CHANGED
@@ -86,19 +86,23 @@ prints the fallback URL. `auth login --web` starts browser device
86
86
  authorization, opens `/cli-auth`, waits for console approval, and stores the
87
87
  returned short-lived token. Credential auth remains available as fallback.
88
88
 
89
- `foh prove` produces a compact signed proof report across auth, org context,
90
- agent validation, contact phone readiness, voice provider health, widget
91
- channel/embed readiness, widget smoke, and simulation certification. It is
92
- read-only by default; pass `--mutation-mode ensure` or `--repair` only when you
93
- explicitly want proof to ensure missing widget state. Use `--strict` in
94
- automation when holds should fail the command, and `--mission voice` or
95
- `--require-phone` when a voice/contact number is mandatory for the demo.
89
+ `foh prove` produces a compact signed proof report across auth, org context,
90
+ agent validation, contact phone readiness, voice provider health, widget
91
+ channel/embed readiness, and widget smoke. It does not run release
92
+ certification by default; run `foh certify run --agent <id> --profile release`
93
+ before publish, or pass `--include-certification` only when you intentionally
94
+ want the slower certification check inside proof. It is read-only by default;
95
+ pass `--mutation-mode ensure` or `--repair` only when you explicitly want proof
96
+ to ensure missing widget state. Use `--strict` in automation when holds should
97
+ fail the command, and `--mission voice` or `--require-phone` when a
98
+ voice/contact number is mandatory for the demo.
96
99
  Use `--contact-path byon` when the proof is meant to validate a
97
100
  customer-owned/BYON phone route; missing BYON config then reports
98
101
  `byon_voice_number_not_configured` instead of suggesting FOH number purchase.
99
- For repeated or parallel proof missions in AI-agent evals, pass
100
- `--proof-cache-dir .foh/proof-cache` so simulation certification runs once and
101
- sibling proofs reuse the same signed certification detail.
102
+ For repeated or parallel explicit-certification proof missions in AI-agent
103
+ evals, pass `--include-certification --proof-cache-dir .foh/proof-cache` so
104
+ simulation certification runs once and sibling proofs reuse the same signed
105
+ certification detail.
102
106
 
103
107
  For mass AI-agent evals and repeated demo rehearsals, keep setup on the free
104
108
  scaffold lane:
@@ -176,8 +180,9 @@ than creating a second bronze-tier agent.
176
180
  | Start | `foh start` |
177
181
  | Setup | `foh setup --phone-mode observe --json` |
178
182
  | Prove | `foh prove --agent <agent_id> --mission widget --json` |
183
+ | Certify | `foh certify run --agent <agent_id> --profile release --json` |
179
184
  | Debug | `foh debug --out test-results/foh-cli-diag.latest.json --json` |
180
- | Improve | `foh bug improve --from-file <artifact.json> --json` |
185
+ | Platform feedback | `foh bug improve --from-file <artifact.json> --json` |
181
186
  | Publish | `foh agent publish --agent <agent_id> --json` |
182
187
 
183
188
  For a planted knowledge-miss benchmark:
package/dist/foh.js CHANGED
@@ -14311,12 +14311,6 @@ function registerAgentPreviewCommands(agent) {
14311
14311
  }));
14312
14312
  }
14313
14313
 
14314
- // src/lib/cert-mode.ts
14315
- var agentCertModeValues = ["quick", "full", "stress"];
14316
- function normalizeAgentCertMode(value) {
14317
- return agentCertModeValues.includes(value) ? value : "quick";
14318
- }
14319
-
14320
14314
  // src/lib/setup-api.ts
14321
14315
  function resolvePublishOptions(options) {
14322
14316
  if (typeof options === "string") return { apiUrlOverride: options };
@@ -14389,61 +14383,7 @@ async function runSetupCertifyLoop(agentId, params) {
14389
14383
  }
14390
14384
 
14391
14385
  // src/lib/agent-publish-gate.ts
14392
- function boundedInt(value, params) {
14393
- const parsed = Number(value);
14394
- if (!Number.isFinite(parsed)) return params.fallback;
14395
- return Math.max(params.min, Math.min(params.max, Math.trunc(parsed)));
14396
- }
14397
- function certModeFlag(mode) {
14398
- if (mode === "full") return ["--full"];
14399
- if (mode === "stress") return ["--stress"];
14400
- return [];
14401
- }
14402
- function buildCertificationFailureCommands(params) {
14403
- const modeFlags = certModeFlag(params.certMode);
14404
- const orgFlags = params.orgId ? ["--org", params.orgId] : [];
14405
- const scenarioFlags = params.topBlocker?.scenario_id ? ["--scenario-ids", params.topBlocker.scenario_id] : [];
14406
- const command = [
14407
- "foh",
14408
- "sim",
14409
- "certify-loop",
14410
- "--agent",
14411
- params.agentId,
14412
- ...orgFlags,
14413
- ...modeFlags,
14414
- ...scenarioFlags,
14415
- "--json"
14416
- ].join(" ");
14417
- return [
14418
- command,
14419
- `foh bug improve --from external-agent-run --file <run_dir>/run.json --json`
14420
- ];
14421
- }
14422
- function resolveCertifiedPublishOptions(opts) {
14423
- const rawMode = String(opts.certMode || "quick").toLowerCase();
14424
- const certMode = normalizeAgentCertMode(rawMode);
14425
- if (certMode !== rawMode) {
14426
- throw new FohError({
14427
- step: "agent.publish",
14428
- error: `Invalid cert mode: ${opts.certMode}`,
14429
- remediation: "Use --cert-mode quick, full, or stress.",
14430
- statusCode: 400
14431
- });
14432
- }
14433
- return {
14434
- certMode,
14435
- adaptiveRuns: boundedInt(opts.adaptiveRuns, { min: 1, max: 120, fallback: 30 }),
14436
- maxImprovementRounds: boundedInt(opts.maxImprovementRounds, { min: 0, max: 5, fallback: 1 }),
14437
- scenarioIds: Array.isArray(opts.scenarioIds) ? opts.scenarioIds.map((item) => String(item).trim()).filter(Boolean) : String(opts.scenarioIds || "").split(",").map((item) => item.trim()).filter(Boolean)
14438
- };
14439
- }
14440
14386
  async function validateCertifyAndPublishAgent(opts) {
14441
- const { certMode, adaptiveRuns, maxImprovementRounds, scenarioIds } = resolveCertifiedPublishOptions({
14442
- certMode: opts.certMode,
14443
- adaptiveRuns: opts.adaptiveRuns,
14444
- maxImprovementRounds: opts.maxImprovementRounds,
14445
- scenarioIds: opts.scenarioIds
14446
- });
14447
14387
  const validation = await apiFetch(
14448
14388
  `/v1/console/agents/${opts.agentId}/validate`,
14449
14389
  {
@@ -14459,44 +14399,18 @@ async function validateCertifyAndPublishAgent(opts) {
14459
14399
  remediation: `Run: foh agent validate --agent ${opts.agentId} to see details.`
14460
14400
  });
14461
14401
  }
14462
- const certification = await runSetupCertifyLoop(opts.agentId, {
14463
- mode: certMode,
14464
- adaptiveRuns,
14465
- maxImprovementRounds,
14466
- scenarioIds,
14467
- orgId: opts.orgId,
14468
- apiUrlOverride: opts.apiUrlOverride
14469
- });
14470
- const certificate = certification.certificate;
14471
- if (!certification.ok || !certification.overall_pass || !certificate) {
14472
- const topBlocker = certificate?.blockers?.[0];
14473
- const blockerLabel = topBlocker?.invariant && topBlocker?.scenario_id ? `${topBlocker.invariant} in ${topBlocker.scenario_id}` : "unknown";
14474
- const nextCommands = buildCertificationFailureCommands({
14475
- agentId: opts.agentId,
14476
- orgId: opts.orgId,
14477
- certMode,
14478
- topBlocker
14479
- });
14480
- throw new FohError({
14481
- step: "agent.publish",
14482
- error: `Simulation certification failed before publish: ${certificate?.scenario_summary?.failed ?? "unknown"}/${certificate?.scenario_summary?.total ?? "unknown"} scenario(s) failed. Top blocker: ${blockerLabel}.`,
14483
- remediation: [
14484
- topBlocker?.suggested_fix ?? certificate?.recommendations?.[0] ?? "Fix the top simulation blocker before publishing.",
14485
- `Re-run: ${nextCommands[0]}`
14486
- ].filter(Boolean).join(" "),
14487
- reasonCode: "simulation_certification_failed",
14488
- nextCommands,
14489
- detail: {
14490
- certification,
14491
- top_blocker: topBlocker ?? null
14492
- }
14493
- });
14494
- }
14495
14402
  await publishAgentFromCurrentDraft(opts.agentId, {
14496
14403
  apiUrlOverride: opts.apiUrlOverride,
14497
14404
  orgId: opts.orgId
14498
14405
  });
14499
- return { validation, certification, publish: { ok: true } };
14406
+ return {
14407
+ validation,
14408
+ certification: {
14409
+ status: "not_run",
14410
+ reason_code: "publish_consumes_existing_certification_evidence"
14411
+ },
14412
+ publish: { ok: true }
14413
+ };
14500
14414
  }
14501
14415
 
14502
14416
  // src/commands/agent-validation.ts
@@ -14545,7 +14459,7 @@ function registerAgentValidationCommands(agent) {
14545
14459
  format(data, { json: opts.json ?? false });
14546
14460
  if (Array.isArray(data.issues) && data.issues.length > 0) markCommandFailed(1);
14547
14461
  }));
14548
- agent.command("publish").description("Validate, simulation-certify, then publish an agent").requiredOption("--agent <id>", "Agent ID").option("--cert-mode <m>", "Simulation cert mode before publish: quick, full, stress", "quick").option("--cert-scenario-ids <csv>", "Comma-separated scenario IDs to certify before publish").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification (default: 30)", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds before publish (0-5)", "1").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--break-glass-reason <reason>", "Break-glass reason for publish override").option("--break-glass-incident <id>", "Break-glass incident ID for publish override").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14462
+ agent.command("publish").description("Validate and publish an agent using existing certification evidence").requiredOption("--agent <id>", "Agent ID").option("--cert-mode <m>", "Deprecated compatibility flag; publish consumes existing certification evidence", "quick").option("--cert-scenario-ids <csv>", "Deprecated compatibility flag; run foh sim certify before publish").option("--cert-adaptive-runs <n>", "Deprecated compatibility flag; run foh sim certify before publish", "30").option("--cert-max-improvement-rounds <n>", "Deprecated compatibility flag; run foh sim certify before publish", "1").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--break-glass-reason <reason>", "Break-glass reason for publish override").option("--break-glass-incident <id>", "Break-glass incident ID for publish override").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14549
14463
  if (opts.breakGlassReason || opts.breakGlassIncident) {
14550
14464
  if (!opts.breakGlassReason || !opts.breakGlassIncident) {
14551
14465
  throw new FohError({
@@ -14568,7 +14482,7 @@ function registerAgentValidationCommands(agent) {
14568
14482
  });
14569
14483
  format({
14570
14484
  status: "published_with_break_glass",
14571
- warning: "break-glass bypassed the normal validate -> sim-certify/loop -> publish CLI sequence",
14485
+ warning: "break-glass bypassed the normal validate -> publish evidence gate CLI sequence",
14572
14486
  publish: data2
14573
14487
  }, { json: opts.json ?? false });
14574
14488
  return;
@@ -14583,13 +14497,8 @@ function registerAgentValidationCommands(agent) {
14583
14497
  maxImprovementRounds: Number(opts.certMaxImprovementRounds)
14584
14498
  });
14585
14499
  format({
14586
- status: "validated_certified_published",
14587
- certification: {
14588
- mode: data.certification.mode,
14589
- overall_pass: data.certification.overall_pass,
14590
- attempts: data.certification.attempts?.length ?? 0,
14591
- improvement_runs: data.certification.improvement_runs
14592
- },
14500
+ status: "validated_published",
14501
+ certification: data.certification,
14593
14502
  publish: data.publish
14594
14503
  }, { json: opts.json ?? false });
14595
14504
  }));
@@ -14810,9 +14719,9 @@ function registerAgent(program3) {
14810
14719
  process.stdout.write(yaml);
14811
14720
  return;
14812
14721
  }
14813
- const { writeFileSync: writeFileSync11 } = await import("fs");
14722
+ const { writeFileSync: writeFileSync12 } = await import("fs");
14814
14723
  const outputPath = opts.output ?? "tenant.yaml";
14815
- writeFileSync11(
14724
+ writeFileSync12(
14816
14725
  outputPath,
14817
14726
  `# tenant.yaml - Front Of House agent manifest
14818
14727
  # Edit this file and run: foh plan tenant.yaml
@@ -14954,7 +14863,7 @@ function registerTemplates(program3) {
14954
14863
  const data = await apiFetch(`/v1/console/templates/${opts.template}`, { apiUrlOverride: opts.apiUrl });
14955
14864
  format(data, { json: opts.json ?? false });
14956
14865
  }));
14957
- templates.command("apply").description("Create a new agent in your org from a template (clones draft config, leaves unpublished unless --publish)").requiredOption("--template <id>", "Template ID").requiredOption("--name <name>", "Name for the new agent").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--publish", "Validate, simulation-certify, then publish the agent after creating").option("--cert-mode <m>", "Simulation cert mode before publish: quick, full, stress", "full").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification (default: 30)", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds before publish (0-5)", "1").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14866
+ templates.command("apply").description("Create a new agent in your org from a template (clones draft config, leaves unpublished unless --publish)").requiredOption("--template <id>", "Template ID").requiredOption("--name <name>", "Name for the new agent").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--publish", "Validate and publish using existing certification evidence after creating").option("--cert-mode <m>", "Deprecated compatibility flag; run foh sim certify before publish", "full").option("--cert-adaptive-runs <n>", "Deprecated compatibility flag; run foh sim certify before publish", "30").option("--cert-max-improvement-rounds <n>", "Deprecated compatibility flag; run foh sim certify before publish", "1").option("--api-url <url>", "API base URL override").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
14958
14867
  const result = await apiFetch(`/v1/console/templates/${opts.template}/apply`, {
14959
14868
  method: "POST",
14960
14869
  body: JSON.stringify({ name: opts.name }),
@@ -16025,9 +15934,9 @@ function buildCommonOptions(command) {
16025
15934
  return command.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");
16026
15935
  }
16027
15936
  function registerChannel(program3) {
16028
- const channel = program3.command("channel").description("Manage external channel onboarding and readiness");
16029
- const whatsapp = channel.command("whatsapp").description("Manage WhatsApp channel onboarding");
16030
- const instagram = channel.command("instagram").description("Manage Instagram channel onboarding");
15937
+ const channel2 = program3.command("channel").description("Manage external channel onboarding and readiness");
15938
+ const whatsapp = channel2.command("whatsapp").description("Manage WhatsApp channel onboarding");
15939
+ const instagram = channel2.command("instagram").description("Manage Instagram channel onboarding");
16031
15940
  registerWhatsAppChannelCommands(whatsapp, buildCommonOptions);
16032
15941
  registerInstagramChannelCommands(instagram, buildCommonOptions);
16033
15942
  }
@@ -16260,11 +16169,11 @@ function registerVoice(program3) {
16260
16169
  }
16261
16170
  const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
16262
16171
  const audio = Buffer.from(await res.arrayBuffer());
16263
- const { mkdirSync: mkdirSync8, writeFileSync: writeFileSync11 } = await import("fs");
16172
+ const { mkdirSync: mkdirSync8, writeFileSync: writeFileSync12 } = await import("fs");
16264
16173
  const { dirname: dirname8, resolve: resolve13 } = await import("path");
16265
16174
  const absolutePath = resolve13(outputPath);
16266
16175
  mkdirSync8(dirname8(absolutePath), { recursive: true });
16267
- writeFileSync11(absolutePath, audio);
16176
+ writeFileSync12(absolutePath, audio);
16268
16177
  format({
16269
16178
  status: "ok",
16270
16179
  provider,
@@ -16285,7 +16194,7 @@ function registerVoice(program3) {
16285
16194
  const allReady = providers.length > 0 && providers.every((provider) => provider?.ready === true);
16286
16195
  if (!allReady) markCommandFailed(1);
16287
16196
  }));
16288
- voice.command("configure").description("Configure voice settings for an agent (does not publish unless --publish)").requiredOption("--agent <id>", "Agent ID").requiredOption("--provider <p>", "TTS provider: openai, azure, twilio").requiredOption("--voice <id>", "Voice ID").option("--stt-provider <p>", "STT provider (default: best available, preferring deepgram)").option("--publish", "Validate, simulation-certify, then publish after configuring").option("--cert-mode <m>", "Simulation cert mode before publish: quick, full, stress", "full").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification (default: 30)", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds before publish (0-5)", "1").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 () => {
16197
+ voice.command("configure").description("Configure voice settings for an agent (does not publish unless --publish)").requiredOption("--agent <id>", "Agent ID").requiredOption("--provider <p>", "TTS provider: openai, azure, twilio").requiredOption("--voice <id>", "Voice ID").option("--stt-provider <p>", "STT provider (default: best available, preferring deepgram)").option("--publish", "Validate and publish using existing certification evidence after configuring").option("--cert-mode <m>", "Deprecated compatibility flag; run foh sim certify before publish", "full").option("--cert-adaptive-runs <n>", "Deprecated compatibility flag; run foh sim certify before publish", "30").option("--cert-max-improvement-rounds <n>", "Deprecated compatibility flag; run foh sim certify before publish", "1").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 () => {
16289
16198
  const provider = String(opts.provider || "").trim().toLowerCase();
16290
16199
  const voiceId = String(opts.voice || "").trim();
16291
16200
  const catalog = await getSpeechCatalog(opts.apiUrl);
@@ -16330,7 +16239,7 @@ function registerVoice(program3) {
16330
16239
  apiUrlOverride: opts.apiUrl
16331
16240
  });
16332
16241
  if (!opts.publish) {
16333
- format({ status: "configured", note: "Run: foh agent publish --agent " + opts.agent + " to validate, certify, and make live." }, { json: opts.json ?? false });
16242
+ format({ status: "configured", note: "Run: foh sim certify --agent " + opts.agent + " --full, then foh agent publish --agent " + opts.agent + " to make live." }, { json: opts.json ?? false });
16334
16243
  return;
16335
16244
  }
16336
16245
  const pub = await validateCertifyAndPublishAgent({
@@ -32877,7 +32786,7 @@ var StdioServerTransport = class {
32877
32786
  };
32878
32787
 
32879
32788
  // src/lib/cli-version.ts
32880
- var CLI_VERSION = "0.1.64";
32789
+ var CLI_VERSION = "0.1.66";
32881
32790
 
32882
32791
  // src/commands/mcp-serve.ts
32883
32792
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -33266,7 +33175,7 @@ var TYPED_TOOL_SPECS = [
33266
33175
  {
33267
33176
  name: "foh_agent_publish",
33268
33177
  title: "FOH Agent Publish",
33269
- description: "Publish validated agent draft.",
33178
+ description: "Validate and publish an agent draft using existing certification evidence.",
33270
33179
  commandKey: "agent publish",
33271
33180
  risk: "write",
33272
33181
  inputSchema: {
@@ -34364,7 +34273,7 @@ function registerManifest(program3) {
34364
34273
  throw e;
34365
34274
  }
34366
34275
  });
34367
- program3.command("apply").description("Apply a tenant.yaml manifest to an agent").argument("[file]", "Path to manifest file", "tenant.yaml").option("--agent <id>", "Agent ID (overrides agent_id in manifest)").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--publish", "Validate, simulation-certify, and publish after applying").option("--cert-mode <m>", "Simulation cert mode before publish: quick, full, stress", "full").option("--cert-adaptive-runs <n>", "Adaptive runs for full/stress certification (default: 30)", "30").option("--cert-max-improvement-rounds <n>", "Max prompt improvement rounds before publish (0-5)", "1").option("--dry-run", "Show diff without making any changes (same as: foh plan)").option("--api-url <url>", "API base URL override").option("--json", "Output result as JSON").action(async (file2, opts) => {
34276
+ program3.command("apply").description("Apply a tenant.yaml manifest to an agent").argument("[file]", "Path to manifest file", "tenant.yaml").option("--agent <id>", "Agent ID (overrides agent_id in manifest)").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--publish", "Validate and publish using existing certification evidence after applying").option("--cert-mode <m>", "Deprecated compatibility flag; run foh sim certify before publish", "full").option("--cert-adaptive-runs <n>", "Deprecated compatibility flag; run foh sim certify before publish", "30").option("--cert-max-improvement-rounds <n>", "Deprecated compatibility flag; run foh sim certify before publish", "1").option("--dry-run", "Show diff without making any changes (same as: foh plan)").option("--api-url <url>", "API base URL override").option("--json", "Output result as JSON").action(async (file2, opts) => {
34368
34277
  try {
34369
34278
  const manifest = loadManifestFile(file2);
34370
34279
  const agentId = resolveAgentId(manifest, opts.agent);
@@ -34463,6 +34372,14 @@ function registerManifest(program3) {
34463
34372
 
34464
34373
  // src/commands/setup.ts
34465
34374
  var import_picocolors4 = __toESM(require_picocolors());
34375
+
34376
+ // src/lib/cert-mode.ts
34377
+ var agentCertModeValues = ["quick", "full", "stress"];
34378
+ function normalizeAgentCertMode(value) {
34379
+ return agentCertModeValues.includes(value) ? value : "quick";
34380
+ }
34381
+
34382
+ // src/commands/setup.ts
34466
34383
  var SETUP_STEP_ORDER = [
34467
34384
  "check_credentials",
34468
34385
  "check_org_access",
@@ -34567,6 +34484,32 @@ function isAgentLimitReachedError(error2) {
34567
34484
  function shouldReuseSingleAgentForEval() {
34568
34485
  return Boolean(process.env.FOH_EXTERNAL_AGENT_RUN_DIR) && isNoSpendPolicy();
34569
34486
  }
34487
+ async function rebaseEvalAgentDraftFromTemplate(params) {
34488
+ const preview = await apiFetch(`/v1/console/templates/${params.templateId}`, {
34489
+ orgId: params.orgId,
34490
+ apiUrlOverride: params.apiUrlOverride
34491
+ });
34492
+ const draft = preview.template?.draft_config;
34493
+ if (!draft || typeof draft !== "object" || Array.isArray(draft)) {
34494
+ throw new FohError({
34495
+ step: "create_agent",
34496
+ error: "Template preview did not return a draft_config for eval reuse.",
34497
+ remediation: `Run: foh templates show --template ${params.templateId} --json, then retry setup.`,
34498
+ reasonCode: "eval_agent_template_rebase_failed"
34499
+ });
34500
+ }
34501
+ await apiFetch(`/v1/console/agents/${params.agentId}/draft`, {
34502
+ method: "PATCH",
34503
+ body: JSON.stringify({ ...draft, name: params.agentName }),
34504
+ orgId: params.orgId,
34505
+ apiUrlOverride: params.apiUrlOverride
34506
+ });
34507
+ return {
34508
+ template_rebased: true,
34509
+ template_id: params.templateId,
34510
+ draft_keys: Object.keys(draft).sort()
34511
+ };
34512
+ }
34570
34513
  function buildMissingOptionsPlan(missing, opts) {
34571
34514
  const missingFlags = missing.map(optionNameToFlag);
34572
34515
  const signInUrl = buildConsoleSignInUrl(resolveConsoleBaseUrl(opts.consoleUrl));
@@ -34948,6 +34891,13 @@ function registerSetup(program3) {
34948
34891
  if (!(error2 instanceof FohError)) throw error2;
34949
34892
  if (isAgentLimitReachedError(error2) && shouldReuseSingleAgentForEval() && existingAgents.length === 1) {
34950
34893
  const reusable = existingAgents[0];
34894
+ const rebase = await rebaseEvalAgentDraftFromTemplate({
34895
+ agentId: reusable.id,
34896
+ agentName: opts.agentName,
34897
+ templateId: opts.agentTemplate,
34898
+ orgId: opts.org,
34899
+ apiUrlOverride: opts.apiUrl
34900
+ });
34951
34901
  agentId = reusable.id;
34952
34902
  return {
34953
34903
  step: "create_agent",
@@ -34959,7 +34909,8 @@ function registerSetup(program3) {
34959
34909
  desired_agent_name: opts.agentName,
34960
34910
  reused_agent_id: reusable.id,
34961
34911
  reused_agent_name: reusable.name,
34962
- operator_note: "External-agent no-spend eval reused the single existing agent instead of creating a second bronze-tier agent."
34912
+ ...rebase,
34913
+ operator_note: "External-agent no-spend eval reused the single existing agent and rebased its draft onto the requested template instead of creating a second bronze-tier agent."
34963
34914
  }
34964
34915
  };
34965
34916
  }
@@ -35189,8 +35140,8 @@ function registerSetup(program3) {
35189
35140
  }
35190
35141
  try {
35191
35142
  const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
35192
- const { writeFileSync: writeFileSync11 } = await import("fs");
35193
- writeFileSync11(
35143
+ const { writeFileSync: writeFileSync12 } = await import("fs");
35144
+ writeFileSync12(
35194
35145
  "tenant.yaml",
35195
35146
  `# tenant.yaml - Front Of House agent manifest
35196
35147
  # Edit this file and run: foh plan tenant.yaml
@@ -35340,14 +35291,14 @@ function registerSim(program3) {
35340
35291
  const adaptiveRuns = Math.max(1, Math.min(120, Number(opts.adaptiveRuns ?? 30)));
35341
35292
  const journeys = opts.journeys ? String(opts.journeys).split(",").map((j) => j.trim()).filter(Boolean) : void 0;
35342
35293
  const scenarioIds = opts.scenarioIds ? String(opts.scenarioIds).split(",").map((id) => id.trim()).filter(Boolean) : void 0;
35343
- const channel = ["chat", "voice", "mixed"].includes(String(opts.channel ?? "")) ? opts.channel : "mixed";
35294
+ const channel2 = ["chat", "voice", "mixed"].includes(String(opts.channel ?? "")) ? opts.channel : "mixed";
35344
35295
  process.stderr.write(` Running ${mode} simulation certification for agent ${opts.agent}...
35345
35296
  `);
35346
35297
  const response = await apiFetch(
35347
35298
  `/v1/console/agents/${opts.agent}/sim-certify`,
35348
35299
  {
35349
35300
  method: "POST",
35350
- body: JSON.stringify({ mode, adaptive_runs: adaptiveRuns, journeys, scenario_ids: scenarioIds, channel }),
35301
+ body: JSON.stringify({ mode, adaptive_runs: adaptiveRuns, journeys, scenario_ids: scenarioIds, channel: channel2 }),
35351
35302
  apiUrlOverride: opts.apiUrl
35352
35303
  }
35353
35304
  );
@@ -35360,8 +35311,8 @@ function registerSim(program3) {
35360
35311
  }
35361
35312
  const cert = response.certificate;
35362
35313
  if (opts.out) {
35363
- const { writeFileSync: writeFileSync11 } = await import("fs");
35364
- writeFileSync11(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35314
+ const { writeFileSync: writeFileSync12 } = await import("fs");
35315
+ writeFileSync12(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
35365
35316
  process.stderr.write(` Certificate written to ${opts.out}
35366
35317
  `);
35367
35318
  }
@@ -35385,7 +35336,7 @@ function registerSim(program3) {
35385
35336
  const maxImprovementRounds = Math.max(0, Math.min(5, Number(opts.maxImprovementRounds ?? 1)));
35386
35337
  const journeys = opts.journeys ? String(opts.journeys).split(",").map((j) => j.trim()).filter(Boolean) : void 0;
35387
35338
  const scenarioIds = opts.scenarioIds ? String(opts.scenarioIds).split(",").map((id) => id.trim()).filter(Boolean) : void 0;
35388
- const channel = ["chat", "voice", "mixed"].includes(String(opts.channel ?? "")) ? opts.channel : "mixed";
35339
+ const channel2 = ["chat", "voice", "mixed"].includes(String(opts.channel ?? "")) ? opts.channel : "mixed";
35389
35340
  process.stderr.write(` Running ${mode} certification loop for agent ${opts.agent}...
35390
35341
  `);
35391
35342
  const response = await apiFetch(
@@ -35398,7 +35349,7 @@ function registerSim(program3) {
35398
35349
  max_improvement_rounds: maxImprovementRounds,
35399
35350
  journeys,
35400
35351
  scenario_ids: scenarioIds,
35401
- channel
35352
+ channel: channel2
35402
35353
  }),
35403
35354
  apiUrlOverride: opts.apiUrl
35404
35355
  }
@@ -35411,8 +35362,8 @@ function registerSim(program3) {
35411
35362
  });
35412
35363
  }
35413
35364
  if (opts.out) {
35414
- const { writeFileSync: writeFileSync11 } = await import("fs");
35415
- writeFileSync11(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35365
+ const { writeFileSync: writeFileSync12 } = await import("fs");
35366
+ writeFileSync12(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
35416
35367
  process.stderr.write(` Final certificate written to ${opts.out}
35417
35368
  `);
35418
35369
  }
@@ -35447,6 +35398,95 @@ ${passIcon} Certification loop summary
35447
35398
  });
35448
35399
  }
35449
35400
 
35401
+ // src/commands/certify.ts
35402
+ var import_node_fs2 = require("node:fs");
35403
+ function normalizeProfile(raw) {
35404
+ const value = String(raw || "release").trim().toLowerCase();
35405
+ if (value === "smoke" || value === "release" || value === "stress") return value;
35406
+ throw new FohError({
35407
+ step: "certify.run",
35408
+ error: `Invalid certification profile: ${raw}`,
35409
+ remediation: "Use --profile smoke, release, or stress.",
35410
+ statusCode: 400
35411
+ });
35412
+ }
35413
+ function modeForProfile(profile) {
35414
+ if (profile === "smoke") return "quick";
35415
+ if (profile === "stress") return "stress";
35416
+ return "full";
35417
+ }
35418
+ function csv(raw) {
35419
+ if (!raw) return void 0;
35420
+ const values = String(raw).split(",").map((value) => value.trim()).filter(Boolean);
35421
+ return values.length > 0 ? values : void 0;
35422
+ }
35423
+ function channel(raw) {
35424
+ const value = String(raw || "mixed").trim().toLowerCase();
35425
+ if (value === "chat" || value === "voice" || value === "mixed") return value;
35426
+ return "mixed";
35427
+ }
35428
+ function registerCertify(program3) {
35429
+ const certify = program3.command("certify").description("Produce release certification evidence for an agent");
35430
+ 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 for release/stress profiles", "30").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) => {
35431
+ try {
35432
+ const profile = normalizeProfile(opts.profile);
35433
+ const mode = modeForProfile(profile);
35434
+ const adaptiveRuns = Math.max(1, Math.min(120, Number(opts.adaptiveRuns ?? 30) || 30));
35435
+ const response = await apiFetch(
35436
+ `/v1/console/agents/${opts.agent}/sim-certify`,
35437
+ {
35438
+ method: "POST",
35439
+ body: JSON.stringify({
35440
+ mode,
35441
+ adaptive_runs: adaptiveRuns,
35442
+ journeys: csv(opts.journeys),
35443
+ scenario_ids: csv(opts.scenarioIds),
35444
+ channel: channel(opts.channel)
35445
+ }),
35446
+ apiUrlOverride: opts.apiUrl
35447
+ }
35448
+ );
35449
+ if (!response.ok || !response.certificate) {
35450
+ throw new FohError({
35451
+ step: "certify.run",
35452
+ error: "API did not return a certificate",
35453
+ remediation: "Check that the agent ID is valid and the API is reachable."
35454
+ });
35455
+ }
35456
+ const passed = response.certificate.overall_pass === true;
35457
+ const result = {
35458
+ schema_version: "foh_certification_run.v1",
35459
+ ok: passed,
35460
+ status: passed ? "pass" : "hold",
35461
+ reason_code: passed ? "certification_passed" : "certification_failed",
35462
+ profile,
35463
+ mode,
35464
+ certificate: response.certificate,
35465
+ next_commands: passed ? [`foh agent publish --agent ${opts.agent} --json`] : [`foh sim certify --agent ${opts.agent} --${mode === "quick" ? "full" : mode} --json`]
35466
+ };
35467
+ if (opts.out) {
35468
+ (0, import_node_fs2.writeFileSync)(opts.out, JSON.stringify(result, null, 2) + "\n", "utf-8");
35469
+ }
35470
+ if (opts.json ?? false) {
35471
+ format(result, { json: true });
35472
+ } else {
35473
+ const summary = response.certificate.scenario_summary;
35474
+ process.stderr.write(`${passed ? "PASS" : "HOLD"} certification ${profile} (${mode})`);
35475
+ if (summary) process.stderr.write(`: ${summary.passed}/${summary.total} scenarios passed`);
35476
+ process.stderr.write("\n");
35477
+ }
35478
+ if (!passed) markCommandFailed(1);
35479
+ } catch (error2) {
35480
+ if (error2 instanceof FohError) {
35481
+ formatError(error2, { json: opts.json ?? false });
35482
+ markCommandFailed(1);
35483
+ return;
35484
+ }
35485
+ throw error2;
35486
+ }
35487
+ });
35488
+ }
35489
+
35450
35490
  // src/commands/conversations.ts
35451
35491
  var import_crypto4 = require("crypto");
35452
35492
  function registerConversations(program3) {
@@ -37389,7 +37429,7 @@ function registerBug(program3) {
37389
37429
 
37390
37430
  // src/lib/proof-cache.ts
37391
37431
  var import_node_crypto2 = require("node:crypto");
37392
- var import_node_fs2 = require("node:fs");
37432
+ var import_node_fs3 = require("node:fs");
37393
37433
  var import_node_path = require("node:path");
37394
37434
  var DEFAULT_MAX_AGE_MS = 15 * 60 * 1e3;
37395
37435
  var DEFAULT_WAIT_MS = 180 * 1e3;
@@ -37413,7 +37453,7 @@ function publicPath(filePath) {
37413
37453
  }
37414
37454
  function readFreshCache(filePath, maxAgeMs) {
37415
37455
  try {
37416
- const payload = JSON.parse((0, import_node_fs2.readFileSync)(filePath, "utf8"));
37456
+ const payload = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
37417
37457
  const createdAt = Date.parse(String(payload.created_at || ""));
37418
37458
  if (!Number.isFinite(createdAt)) return null;
37419
37459
  if (Date.now() - createdAt > maxAgeMs) return null;
@@ -37423,8 +37463,8 @@ function readFreshCache(filePath, maxAgeMs) {
37423
37463
  }
37424
37464
  }
37425
37465
  function writeCache(filePath, value) {
37426
- (0, import_node_fs2.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37427
- (0, import_node_fs2.writeFileSync)(filePath, `${JSON.stringify({
37466
+ (0, import_node_fs3.mkdirSync)((0, import_node_path.dirname)(filePath), { recursive: true });
37467
+ (0, import_node_fs3.writeFileSync)(filePath, `${JSON.stringify({
37428
37468
  schema_version: "foh_proof_cache_entry.v1",
37429
37469
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
37430
37470
  value
@@ -37450,7 +37490,7 @@ async function withProofCache(options, run) {
37450
37490
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
37451
37491
  const waitMs = options.waitMs ?? Number(process.env.FOH_PROOF_CACHE_WAIT_MS || DEFAULT_WAIT_MS);
37452
37492
  const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
37453
- (0, import_node_fs2.mkdirSync)(resolvedDir, { recursive: true });
37493
+ (0, import_node_fs3.mkdirSync)(resolvedDir, { recursive: true });
37454
37494
  const existing = readFreshCache(cachePath, maxAgeMs);
37455
37495
  if (existing) {
37456
37496
  return {
@@ -37460,7 +37500,7 @@ async function withProofCache(options, run) {
37460
37500
  }
37461
37501
  let lockOwner = false;
37462
37502
  try {
37463
- (0, import_node_fs2.mkdirSync)(lockPath);
37503
+ (0, import_node_fs3.mkdirSync)(lockPath);
37464
37504
  lockOwner = true;
37465
37505
  } catch {
37466
37506
  const started = Date.now();
@@ -37483,7 +37523,7 @@ async function withProofCache(options, run) {
37483
37523
  metadata: { hit: false, key, cache_path: publicPath(cachePath), waited_ms: 0 }
37484
37524
  };
37485
37525
  } finally {
37486
- if (lockOwner) (0, import_node_fs2.rmSync)(lockPath, { recursive: true, force: true });
37526
+ if (lockOwner) (0, import_node_fs3.rmSync)(lockPath, { recursive: true, force: true });
37487
37527
  }
37488
37528
  }
37489
37529
 
@@ -37514,8 +37554,8 @@ function hasBlockingChecks(checks) {
37514
37554
  }
37515
37555
  function publicKeyFromEnsureResponse(response) {
37516
37556
  const record2 = response && typeof response === "object" ? response : {};
37517
- const channel = record2.channel && typeof record2.channel === "object" ? record2.channel : {};
37518
- const publicKey = channel.public_key ?? record2.widget_public_key ?? record2.public_key;
37557
+ const channel2 = record2.channel && typeof record2.channel === "object" ? record2.channel : {};
37558
+ const publicKey = channel2.public_key ?? record2.widget_public_key ?? record2.public_key;
37519
37559
  return typeof publicKey === "string" && publicKey.trim() ? publicKey.trim() : void 0;
37520
37560
  }
37521
37561
  function publicKeyFromEmbedResponse(response) {
@@ -37582,7 +37622,7 @@ function isProviderCapacityBlocked(onboarding) {
37582
37622
  return /maximum number of subaccounts|subaccount limit|reserve[- ]number pool|reserve pool exhausted|global safety limit/.test(message);
37583
37623
  }
37584
37624
  function registerProve(program3) {
37585
- 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("--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", "Skip simulation certification check").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 () => {
37625
+ 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 () => {
37586
37626
  const checks = [];
37587
37627
  const mission = normalizeMission(opts.mission);
37588
37628
  const contactPath = normalizeContactPath(opts.contactPath);
@@ -37838,7 +37878,14 @@ function registerProve(program3) {
37838
37878
  }
37839
37879
  }
37840
37880
  if (opts.skipCert) {
37841
- checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh sim certify-loop --agent ${ctx.agentId} --json`));
37881
+ checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh sim certify --agent ${ctx.agentId} --full --json`));
37882
+ } else if (!opts.includeCertification) {
37883
+ checks.push(skipped(
37884
+ "simulation_certification",
37885
+ "certification_explicitly_required",
37886
+ "Runtime proof does not run release certification by default.",
37887
+ `foh sim certify --agent ${ctx.agentId} --full --json`
37888
+ ));
37842
37889
  } else {
37843
37890
  try {
37844
37891
  const certMode = normalizeAgentCertMode(opts.certMode);
@@ -37870,7 +37917,7 @@ function registerProve(program3) {
37870
37917
  proof_cache: cached2.metadata
37871
37918
  };
37872
37919
  if (!loop.overall_pass) {
37873
- checks.push(hold("simulation_certification", "simulation_certification_failed", "Simulation certification did not pass.", `foh sim certify-loop --agent ${agentId} --${certMode === "quick" ? "full" : certMode} --json`, loopWithCache));
37920
+ checks.push(hold("simulation_certification", "simulation_certification_failed", "Simulation certification did not pass.", `foh sim certify --agent ${agentId} --${certMode === "quick" ? "full" : certMode} --json`, loopWithCache));
37874
37921
  } else {
37875
37922
  checks.push(pass("simulation_certification", "Simulation certification passed.", {
37876
37923
  mode: loop.mode,
@@ -37881,7 +37928,7 @@ function registerProve(program3) {
37881
37928
  }));
37882
37929
  }
37883
37930
  } catch (error2) {
37884
- checks.push(fail("simulation_certification", "simulation_certification_failed", error2, `foh sim certify-loop --agent ${ctx.agentId} --json`));
37931
+ checks.push(fail("simulation_certification", "simulation_certification_failed", error2, `foh sim certify --agent ${ctx.agentId} --full --json`));
37885
37932
  }
37886
37933
  }
37887
37934
  } else {
@@ -40367,7 +40414,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
40367
40414
  var DEFAULT_PROMPT_VERSION = "blank-setup.v1";
40368
40415
  var DEFAULT_BATCH_MODELS = "openai/codex,anthropic/claude,cursor/agent";
40369
40416
  var PROMPTS = {
40370
- "blank-setup.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, because cached older packages can produce invalid evidence. Install or verify the FOH CLI, authenticate or reach a deterministic auth blocker, then create or configure a Front Of House voice agent and website widget. Mass evals reuse existing eval state: run `npx --yes @f-o-h/cli@latest org status --json` and `npx --yes @f-o-h/cli@latest agent list --json` before trying to create a fresh agent; if an existing eval agent is present, configure and prove that agent instead of creating a second bronze-tier agent. Prefer the certification-oriented buyer templates: run `npx --yes @f-o-h/cli@latest templates list --category buyer --json` and use `UK Buyer Qualification` or `Viewing Booking` when available; do not use a greeting-only template for proof/certification. Prefer `npx --yes @f-o-h/cli@latest setup --phone-mode observe` for the free scaffold path: agent, widget, voice config, smoke test, certification, and publish readiness together. Treat phone-number purchasing as an explicit paid/scarce contact-path step, not part of high-volume eval setup. If `FOH_CLI_SPEND_POLICY=no_spend` is active and a command returns `paid_resource_blocked_by_spend_policy`, do not try to bypass it; continue widget/setup proof and report that exact reason code for the phone path. If the customer/operator explicitly owns a number and asks for real PSTN proof, use `npx --yes @f-o-h/cli@latest provision byon attach --phone-number <e164> --confirm-owned --json`; do not invent ownership or buy a FOH-owned number. Run proof/smoke/certification where available, including widget proof and voice proof. When running more than one `foh prove` mission for the same agent, pass `--proof-cache-dir .foh/proof-cache` so simulation certification can be shared instead of recomputed. If voice proof returns `contact_phone_missing` or `voice_contact_expected_no_spend_hold`, report that exact reason code unless a BYON/customer-approved phone path already exists. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, and any blocker reason codes. Do not assume access to the private source repository.",
40417
+ "blank-setup.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, because cached older packages can produce invalid evidence. Install or verify the FOH CLI, authenticate or reach a deterministic auth blocker, then create or configure a Front Of House voice agent and website widget. Mass evals reuse existing eval state: run `npx --yes @f-o-h/cli@latest org status --json` and `npx --yes @f-o-h/cli@latest agent list --json` before trying to create a fresh agent; if an existing eval agent is present, configure and prove that agent instead of creating a second bronze-tier agent. Prefer the certification-oriented buyer templates: run `npx --yes @f-o-h/cli@latest templates list --category buyer --json` and use `UK Buyer Qualification` or `Viewing Booking` when available; do not use a greeting-only template for proof/certification. Prefer `npx --yes @f-o-h/cli@latest setup --phone-mode observe` for the free scaffold path: agent, widget, voice config, smoke test, certification, and publish readiness together. Treat phone-number purchasing as an explicit paid/scarce contact-path step, not part of high-volume eval setup. If `FOH_CLI_SPEND_POLICY=no_spend` is active and a command returns `paid_resource_blocked_by_spend_policy`, do not try to bypass it; continue widget/setup proof and report that exact reason code for the phone path. If the customer/operator explicitly owns a number and asks for real PSTN proof, use `npx --yes @f-o-h/cli@latest provision byon attach --phone-number <e164> --confirm-owned --json`; do not invent ownership or buy a FOH-owned number. Run proof/smoke/certification where available, including widget proof, voice proof, and one explicit `foh sim certify --agent <id> --full --json` before publish. `foh prove` does not run release certification by default; only pass `--include-certification --proof-cache-dir .foh/proof-cache` when an explicit combined proof/certification run is required. If voice proof returns `contact_phone_missing` or `voice_contact_expected_no_spend_hold`, report that exact reason code unless a BYON/customer-approved phone path already exists. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, and any blocker reason codes. Do not assume access to the private source repository.",
40371
40418
  "debug-proof-failure.v1": "You are given a FOH proof or debug artifact. Use public docs and FOH CLI/API behavior to classify whether the blocker is docs, auth, org setup, agent config, widget, channel, runtime, or product bug. Produce a redacted improvement packet or the exact command needed to produce one. Do not ask the human to interpret logs manually unless no machine-readable artifact exists.",
40372
40419
  "knowledge-miss.v1": "A FOH agent failed to answer a business question. Use CLI/API/docs to determine whether this is a knowledge-ingestion issue, retrieval issue, config issue, prompt/behavior issue, or runtime issue. Prefer foh knowledge query, transcript export, replay, and foh bug improve artifacts over screenshots.",
40373
40420
  "replay-failure.v1": "You are given a FOH transcript or replay artifact. Use CLI/API/docs to replay or inspect the failed interaction, identify expected vs actual behavior, and produce a scenario-test or improvement-packet candidate."
@@ -40944,6 +40991,7 @@ var CLI_MISSION_EXAMPLES = [
40944
40991
  { mission: "Start", command: "foh start", description: "guided setup and next action selector" },
40945
40992
  { mission: "Setup", command: "foh setup --phone-mode observe --json", description: "create or update agent, widget, voice config, and proof scaffold" },
40946
40993
  { mission: "Prove", command: "foh prove --agent <agent_id> --mission widget --json", description: "produce a machine-readable proof report" },
40994
+ { mission: "Certify", command: "foh certify run --agent <agent_id> --profile release --json", description: "produce release evidence before publish" },
40947
40995
  { mission: "Debug", command: "foh debug --out test-results/foh-cli-diag.latest.json --json", description: "collect auth/org/API diagnostics" },
40948
40996
  { mission: "Improve", command: "foh bug improve --from-file <artifact.json> --json", description: "convert a failure artifact into a redacted improvement packet" },
40949
40997
  { mission: "Publish", command: "foh agent publish --agent <agent_id> --json", description: "publish after proof gates pass" }
@@ -41043,6 +41091,7 @@ registerOps(program2);
41043
41091
  registerSetup(program2);
41044
41092
  registerManifest(program2);
41045
41093
  registerSim(program2);
41094
+ registerCertify(program2);
41046
41095
  registerDiag(program2);
41047
41096
  registerBug(program2);
41048
41097
  registerProve(program2);
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "@f-o-h/cli",
3
- "version": "0.1.64",
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
- }
1
+ {
2
+ "name": "@f-o-h/cli",
3
+ "version": "0.1.66",
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
+ }