@f-o-h/cli 0.1.87 → 0.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ AI-operator provisioning CLI for Front Of House.
4
4
 
5
5
  Public mirror: https://github.com/iiko38/front-of-house-cli
6
6
 
7
- Current published baseline: `@f-o-h/cli@0.1.79`
7
+ Current published baseline: `@f-o-h/cli@0.1.87`
8
8
 
9
9
  This mirror is a generated release artifact. The private product monorepo is not
10
10
  published here, and no open-source license is granted unless stated separately.
package/dist/foh.js CHANGED
@@ -15186,6 +15186,10 @@ var SMOKE_TURNS = [
15186
15186
  "I am interested in buying a 3-bedroom house in the area",
15187
15187
  "Can I book a viewing for this week?"
15188
15188
  ];
15189
+ function isGenericTroubleReply(reply) {
15190
+ const normalized = reply.trim().toLowerCase();
15191
+ return /\bi'?m sorry\b/.test(normalized) && /having trouble|try again|something went wrong|unable to help right now/.test(normalized);
15192
+ }
15189
15193
  async function resolveChannelPublicKey(agentId, orgId, apiUrlOverride) {
15190
15194
  const data = await apiFetch(
15191
15195
  "/v1/console/channels/widget/ensure",
@@ -15227,12 +15231,17 @@ async function runWidgetSmoke(publicKey, apiUrlOverride) {
15227
15231
  conversationId = data.conversationId;
15228
15232
  if (data.trace_id) traceIds.push(data.trace_id);
15229
15233
  if (data.correlation_id) correlationIds.push(data.correlation_id);
15234
+ const genericTroubleReply = isGenericTroubleReply(data.reply);
15230
15235
  turns.push({
15231
15236
  turn: i + 1,
15232
15237
  message,
15233
- ok: true,
15238
+ ok: !genericTroubleReply,
15234
15239
  latency_ms: latencyMs,
15235
15240
  reply: data.reply,
15241
+ ...genericTroubleReply ? {
15242
+ reason_code: "widget_generic_trouble_reply",
15243
+ error: "Widget returned a generic trouble reply instead of advancing the customer request."
15244
+ } : {},
15236
15245
  conversation_id: data.conversationId,
15237
15246
  trace_id: data.trace_id ?? null,
15238
15247
  correlation_id: data.correlation_id ?? null,
@@ -15400,7 +15409,7 @@ function buildReasonedNextSteps({
15400
15409
  const verifyTokenValue = verifyToken || "<verify_token>";
15401
15410
  if (has("whatsapp_channel_not_ready") || has("whatsapp_access_token_missing") || has("whatsapp_verify_token_missing") || has("whatsapp_app_secret_missing")) {
15402
15411
  steps.push(
15403
- "Connect/update channel credentials: foh channel whatsapp connect --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
15412
+ "Connect/update channel credentials: foh channel whatsapp onboard --phone-number-id <meta_phone_number_id> --access-token <meta_access_token> --verify-token <verify_token> --app-secret <meta_app_secret>"
15404
15413
  );
15405
15414
  }
15406
15415
  if (has("whatsapp_verify_check_failed") || has("whatsapp_webhook_challenge_failed")) {
@@ -15676,24 +15685,6 @@ async function runWhatsAppOnboardingSession(params) {
15676
15685
  apiUrlOverride: params.apiUrl
15677
15686
  });
15678
15687
  }
15679
- function emitLegacyCommandNotice({
15680
- command,
15681
- canonical,
15682
- jsonMode
15683
- }) {
15684
- if (!jsonMode) {
15685
- process.stderr.write(
15686
- `[deprecated] foh channel whatsapp ${command} is a compatibility wrapper.
15687
- Use: ${canonical}
15688
- `
15689
- );
15690
- }
15691
- return {
15692
- command,
15693
- canonical,
15694
- status: "deprecated_compat_wrapper"
15695
- };
15696
- }
15697
15688
  function parseBatchManifest(manifestPathRaw) {
15698
15689
  const manifestPath = String(manifestPathRaw || "").trim();
15699
15690
  if (!manifestPath) {
@@ -15843,51 +15834,10 @@ async function runWhatsAppOnboardingWizard(opts) {
15843
15834
  throw new FohError({
15844
15835
  step: "channel.whatsapp.onboard",
15845
15836
  error: "Unable to complete WhatsApp onboarding after guided retries.",
15846
- remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp start` for deterministic next steps."
15837
+ remediation: "Run again with --phone-number-id and --waba-id, or use `foh channel whatsapp guide` for deterministic next steps."
15847
15838
  });
15848
15839
  }
15849
15840
  function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
15850
- addCommonOptions(
15851
- whatsapp.command("start").description("[Deprecated wrapper] Start onboarding using the canonical session flow")
15852
- ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
15853
- const accessToken = String(opts.accessToken || "").trim();
15854
- const useWizard = Boolean(opts.wizard) || !accessToken;
15855
- const legacy = emitLegacyCommandNotice({
15856
- command: "start",
15857
- canonical: "foh channel whatsapp onboard --wizard",
15858
- jsonMode: Boolean(opts.json)
15859
- });
15860
- const data = useWizard ? await runWhatsAppOnboardingWizard({
15861
- ...opts,
15862
- dryRun: true
15863
- }) : await runWhatsAppOnboardingSession({
15864
- orgId: opts.org,
15865
- apiUrl: opts.apiUrl,
15866
- accessToken,
15867
- wabaId: String(opts.wabaId || "").trim() || void 0,
15868
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
15869
- verifyToken: String(opts.verifyToken || "").trim() || void 0,
15870
- appSecret: String(opts.appSecret || "").trim() || void 0,
15871
- agentId: opts.agentId,
15872
- businessSlug: opts.businessSlug,
15873
- audioEnabled: parseBooleanOption({
15874
- value: opts.audioEnabled,
15875
- fallback: true,
15876
- optionName: "--audio-enabled",
15877
- step: "channel.whatsapp.start"
15878
- }),
15879
- dryRun: true
15880
- });
15881
- format({
15882
- ...data,
15883
- legacy_wrapper: legacy,
15884
- next_steps: dedupeSteps([
15885
- "Run canonical onboarding apply flow: foh channel whatsapp onboard --wizard",
15886
- "Run deterministic closure: foh channel whatsapp proof --strict",
15887
- "Capture live provider evidence: corepack pnpm ops:whatsapp:proof:live"
15888
- ])
15889
- }, { json: opts.json ?? false });
15890
- }));
15891
15841
  addCommonOptions(
15892
15842
  whatsapp.command("onboard").description("Run one-session WhatsApp onboarding (discover -> bind -> verify -> prove)")
15893
15843
  ).option("--access-token <token>", "Meta access token with WhatsApp Business access").option("--waba-id <id>", "Optional explicit WhatsApp Business Account id when /me discovery is restricted").option("--phone-number-id <id>", "Optional explicit phone number id when discovery returns multiple candidates").option("--verify-token <token>", "Optional webhook verify token (auto-generated when omitted)").option("--app-secret <secret>", "Optional Meta app secret (required for full signature-ready closure)").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").option("--dry-run", "Run discovery/binding preflight without writing channel config").option("--wizard", "Run guided onboarding with automatic recovery prompts").action(async (opts) => withCommandErrorHandling(async () => {
@@ -15993,75 +15943,6 @@ function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
15993
15943
  results
15994
15944
  }, { json: opts.json ?? false });
15995
15945
  }));
15996
- addCommonOptions(
15997
- whatsapp.command("setup").description("[Deprecated wrapper] Use canonical onboarding session flow")
15998
- ).option("--phone-number-id <id>", "Meta WhatsApp phone number id").option("--access-token <token>", "Meta access token").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)").option("--generate-verify-token", "Generate webhook verify token automatically when missing").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--wizard", "Run guided setup wizard prompts").action(async (opts) => withCommandErrorHandling(async () => {
15999
- const legacy = emitLegacyCommandNotice({
16000
- command: "setup",
16001
- canonical: "foh channel whatsapp onboard --wizard",
16002
- jsonMode: Boolean(opts.json)
16003
- });
16004
- const useWizard = Boolean(opts.wizard) || !String(opts.accessToken || "").trim();
16005
- const generatedVerifyToken = String(opts.verifyToken || "").trim() || (Boolean(opts.generateVerifyToken) ? generateVerifyToken() : "");
16006
- const data = useWizard ? await runWhatsAppOnboardingWizard({
16007
- ...opts,
16008
- verifyToken: generatedVerifyToken || void 0,
16009
- dryRun: false
16010
- }) : await runWhatsAppOnboardingSession({
16011
- orgId: opts.org,
16012
- apiUrl: opts.apiUrl,
16013
- accessToken: String(opts.accessToken || "").trim(),
16014
- wabaId: void 0,
16015
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16016
- verifyToken: generatedVerifyToken || void 0,
16017
- appSecret: String(opts.appSecret || "").trim() || void 0,
16018
- agentId: opts.agentId,
16019
- businessSlug: String(opts.businessSlug || "").trim() || void 0,
16020
- audioEnabled: parseBooleanOption({
16021
- value: opts.audioEnabled,
16022
- fallback: true,
16023
- optionName: "--audio-enabled",
16024
- step: "channel.whatsapp.setup"
16025
- }),
16026
- dryRun: false
16027
- });
16028
- format({
16029
- ...data,
16030
- legacy_wrapper: legacy,
16031
- generated_verify_token: generatedVerifyToken && !opts.verifyToken ? generatedVerifyToken : null
16032
- }, { json: opts.json ?? false });
16033
- }));
16034
- addCommonOptions(
16035
- whatsapp.command("connect").description("[Deprecated wrapper] Use canonical onboarding session flow")
16036
- ).requiredOption("--phone-number-id <id>", "Meta WhatsApp phone number id").requiredOption("--access-token <token>", "Meta access token").option("--agent-id <id>", "Agent to bind to the WhatsApp channel").option("--verify-token <token>", "Webhook verify token").option("--app-secret <secret>", "Meta app secret for signature verification").option("--business-slug <slug>", "Business identifier for operator-level targeting").option("--audio-enabled <bool>", "Enable inbound WhatsApp audio fallback auto-reply (true/false)", "true").action(async (opts) => withCommandErrorHandling(async () => {
16037
- const legacy = emitLegacyCommandNotice({
16038
- command: "connect",
16039
- canonical: "foh channel whatsapp onboard --access-token <token> --phone-number-id <id>",
16040
- jsonMode: Boolean(opts.json)
16041
- });
16042
- const data = await runWhatsAppOnboardingSession({
16043
- orgId: opts.org,
16044
- apiUrl: opts.apiUrl,
16045
- accessToken: String(opts.accessToken || "").trim(),
16046
- wabaId: void 0,
16047
- phoneNumberId: String(opts.phoneNumberId || "").trim() || void 0,
16048
- verifyToken: String(opts.verifyToken || "").trim() || void 0,
16049
- appSecret: String(opts.appSecret || "").trim() || void 0,
16050
- agentId: opts.agentId,
16051
- businessSlug: String(opts.businessSlug || "").trim() || void 0,
16052
- audioEnabled: parseBooleanOption({
16053
- value: opts.audioEnabled,
16054
- fallback: true,
16055
- optionName: "--audio-enabled",
16056
- step: "channel.whatsapp.connect"
16057
- }),
16058
- dryRun: false
16059
- });
16060
- format({
16061
- ...data,
16062
- legacy_wrapper: legacy
16063
- }, { json: opts.json ?? false });
16064
- }));
16065
15946
  }
16066
15947
 
16067
15948
  // src/commands/channel-whatsapp.ts
@@ -33060,7 +32941,7 @@ var StdioServerTransport = class {
33060
32941
  };
33061
32942
 
33062
32943
  // src/lib/cli-version.ts
33063
- var injectedVersion = true ? String("0.1.87").trim() : "";
32944
+ var injectedVersion = true ? String("0.1.88").trim() : "";
33064
32945
  var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
33065
32946
  var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
33066
32947
 
@@ -34882,8 +34763,190 @@ function writeSetupRunReport(reportPayload, reportOut) {
34882
34763
  }
34883
34764
 
34884
34765
  // src/commands/setup.ts
34766
+ var OBJECTIVE_SETUP_INDUSTRIES = ["real_estate", "restaurant", "general"];
34767
+ function normalizeString(value) {
34768
+ return typeof value === "string" ? value.trim() : "";
34769
+ }
34770
+ function csv(value, fallback) {
34771
+ return String(value || fallback).split(",").map((entry) => entry.trim()).filter(Boolean);
34772
+ }
34773
+ function objectiveChannels(tools) {
34774
+ const allowed = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
34775
+ const channels = tools.filter((tool) => allowed.has(tool));
34776
+ return channels.length > 0 ? channels : ["widget"];
34777
+ }
34778
+ function objectiveOptimizationTarget(industry, objective) {
34779
+ const text = objective.toLowerCase();
34780
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(text)) return "booking_rate";
34781
+ if (/support|resolve|resolution/.test(text)) return "support_resolution";
34782
+ if (/speed|lead|callback|call back/.test(text)) return "speed_to_lead";
34783
+ return "lead_quality";
34784
+ }
34785
+ function objectiveSetupCommand(opts) {
34786
+ const parts = [
34787
+ "npx --yes @f-o-h/cli@latest setup",
34788
+ `--objective ${JSON.stringify(normalizeString(opts.objective) || "<front-of-house objective>")}`,
34789
+ `--business-name ${JSON.stringify(normalizeString(opts.businessName) || "<business name>")}`,
34790
+ `--industry ${normalizeString(opts.industry) || "<real_estate|restaurant>"}`,
34791
+ `--source-url ${JSON.stringify(normalizeString(opts.sourceUrl) || "<official url>")}`,
34792
+ "--json"
34793
+ ];
34794
+ return parts.join(" ");
34795
+ }
34796
+ function selectedTemplateSummary(selection) {
34797
+ const candidate = Array.isArray(selection?.candidates) ? selection.candidates[0] : null;
34798
+ const template = candidate?.template;
34799
+ const contract = template?.template_contract ?? {};
34800
+ if (!template) return null;
34801
+ return {
34802
+ template_id: String(template.id || contract.template_id || ""),
34803
+ template_name: String(template.name || contract.name || ""),
34804
+ template_slug: String(contract.slug || ""),
34805
+ industry: String(contract.industry || ""),
34806
+ use_case: String(contract.use_case || ""),
34807
+ match_score: Number.isFinite(Number(candidate.match_score)) ? Number(candidate.match_score) : null,
34808
+ matched_reasons: Array.isArray(candidate.matched_reasons) ? candidate.matched_reasons.map(String) : []
34809
+ };
34810
+ }
34811
+ async function emitObjectiveSetupBootstrap(opts) {
34812
+ const businessName = normalizeString(opts.businessName);
34813
+ const objective = normalizeString(opts.objective);
34814
+ const industry = normalizeString(opts.industry);
34815
+ const sourceUrl = normalizeString(opts.sourceUrl);
34816
+ const tools = csv(opts.tools, "widget,voice,whatsapp");
34817
+ let credentials = null;
34818
+ try {
34819
+ credentials = loadCredentials(opts.apiUrl);
34820
+ if (!opts.org) opts.org = credentials.orgId;
34821
+ } catch {
34822
+ credentials = null;
34823
+ }
34824
+ if (!credentials && !opts.org) {
34825
+ format({
34826
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34827
+ ok: false,
34828
+ status: "blocked",
34829
+ reason_code: "auth_required",
34830
+ summary: "Authenticate before objective setup so template selection and setup planning can use the correct org.",
34831
+ spend_policy: resolveCliSpendPolicy(),
34832
+ next_commands: [
34833
+ "npx --yes @f-o-h/cli@latest auth signup --web --json",
34834
+ "npx --yes @f-o-h/cli@latest auth login --web --json",
34835
+ "npx --yes @f-o-h/cli@latest org list --json",
34836
+ objectiveSetupCommand(opts)
34837
+ ]
34838
+ }, { json: opts.json ?? false });
34839
+ markCommandFailed(1);
34840
+ return;
34841
+ }
34842
+ const missing = [];
34843
+ if (!businessName) missing.push("--business-name");
34844
+ if (!industry) missing.push("--industry");
34845
+ if (!objective) missing.push("--objective");
34846
+ if (!sourceUrl) missing.push("--source-url");
34847
+ if (industry && !OBJECTIVE_SETUP_INDUSTRIES.includes(industry)) missing.push("--industry:supported_value");
34848
+ if (missing.length > 0) {
34849
+ format({
34850
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34851
+ ok: false,
34852
+ status: "blocked",
34853
+ reason_code: "objective_setup_required_options_missing",
34854
+ summary: "Objective setup needs an explicit business name, industry, objective, and official source URL.",
34855
+ missing_options: missing,
34856
+ org_id: opts.org ?? null,
34857
+ next_commands: [
34858
+ objectiveSetupCommand(opts),
34859
+ "npx --yes @f-o-h/cli@latest templates list --category general --json",
34860
+ "npx --yes @f-o-h/cli@latest templates list --category buyer --json"
34861
+ ],
34862
+ claim_boundaries: {
34863
+ customer_live_claim_allowed: false,
34864
+ production_claim_allowed: false
34865
+ }
34866
+ }, { json: opts.json ?? false });
34867
+ markCommandFailed(1);
34868
+ return;
34869
+ }
34870
+ const brief = {
34871
+ schema_version: "business_requirement_brief.v1",
34872
+ business_name: businessName,
34873
+ industry,
34874
+ desired_use_cases: [objective],
34875
+ channels: objectiveChannels(tools),
34876
+ knowledge_sources: [{ type: "website", label: `${businessName} official website`, uri: sourceUrl }],
34877
+ required_outcomes: [objective],
34878
+ handoff_rules: ["handoff when approved facts, credentials, or action availability are missing"],
34879
+ constraints: ["do not invent business facts or claim live readiness without accepted evidence"],
34880
+ optimization_target: objectiveOptimizationTarget(industry, objective)
34881
+ };
34882
+ const templateSelection = await apiFetch("/v1/console/templates/select", {
34883
+ method: "POST",
34884
+ body: JSON.stringify(brief),
34885
+ apiUrlOverride: opts.apiUrl
34886
+ });
34887
+ const selectedTemplate = selectedTemplateSummary(templateSelection);
34888
+ if (!selectedTemplate) {
34889
+ format({
34890
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34891
+ ok: false,
34892
+ status: "blocked",
34893
+ reason_code: "objective_template_selection_empty",
34894
+ summary: "No supported template matched this objective. Do not continue with a guessed template.",
34895
+ requirement_brief: brief,
34896
+ template_selection: templateSelection,
34897
+ next_commands: [objectiveSetupCommand(opts)]
34898
+ }, { json: opts.json ?? false });
34899
+ markCommandFailed(1);
34900
+ return;
34901
+ }
34902
+ const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
34903
+ method: "POST",
34904
+ body: JSON.stringify({
34905
+ agency_name: businessName,
34906
+ business_objective: objective,
34907
+ requested_tool_surface: tools,
34908
+ target_exposure_mode: normalizeString(opts.targetMode) || "customer_owned_voice_trial",
34909
+ source_url: sourceUrl,
34910
+ ...normalizeString(opts.location) ? { branch_location: normalizeString(opts.location) } : {}
34911
+ }),
34912
+ orgId: opts.org,
34913
+ apiUrlOverride: opts.apiUrl
34914
+ });
34915
+ format({
34916
+ schema_version: "foh_cli_objective_setup_bootstrap.v1",
34917
+ ok: true,
34918
+ status: "hold",
34919
+ reason_code: "objective_setup_plan_ready",
34920
+ summary: "Objective setup plan is ready. Treat this as dry-run planning until evidence is applied and status explicitly allows live claims.",
34921
+ dry_run: opts.apply === true ? false : true,
34922
+ apply_requested: opts.apply === true,
34923
+ org_id: opts.org ?? null,
34924
+ requirement_brief: brief,
34925
+ selected_template: selectedTemplate,
34926
+ setup_workflow: {
34927
+ decision: setupWorkflow?.decision ?? setupWorkflow?.status ?? null,
34928
+ reason_codes: Array.isArray(setupWorkflow?.reason_codes) ? setupWorkflow.reason_codes : [],
34929
+ evidence_packet: setupWorkflow?.evidence_packet ?? null,
34930
+ operator_status: setupWorkflow?.operator_status ?? null
34931
+ },
34932
+ claim_boundaries: {
34933
+ customer_live_claim_allowed: false,
34934
+ production_claim_allowed: false,
34935
+ fully_autonomous_claim_allowed: false
34936
+ },
34937
+ next_commands: [
34938
+ `npx --yes @f-o-h/cli@latest objective status --business-name ${JSON.stringify(businessName)} --industry ${industry} --business-objective ${JSON.stringify(objective)} --source-url ${JSON.stringify(sourceUrl)} --out test-results/objective-status.latest.json --json`,
34939
+ "npx --yes @f-o-h/cli@latest objective debug --from test-results/objective-status.latest.json --json",
34940
+ `npx --yes @f-o-h/cli@latest templates show --template ${selectedTemplate.template_id} --json`
34941
+ ]
34942
+ }, { json: opts.json ?? false });
34943
+ }
34885
34944
  function registerSetup(program3) {
34886
- program3.command("setup").description("Fully provision a new agency customer in one command").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
34945
+ program3.command("setup").description("Fully provision a customer or plan objective-first setup").option("--objective <text>", "Objective-first setup mode: plain-English front-of-house objective").option("--business-name <name>", "Business trading name for objective-first setup").option("--industry <industry>", "Business industry for objective-first setup: real_estate, restaurant, general").option("--source-url <url>", "Official business source URL for objective-first setup").option("--location <value>", "Location or branch represented by this setup").option("--tools <csv>", "Requested surfaces for objective-first setup", "widget,voice,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode for objective-first setup", "customer_owned_voice_trial").option("--apply", "Explicitly request apply mode for objective setup; default is dry-run planning").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--agent-template <id>", "Agent template ID (e.g. viewing-request)").option("--agent-name <name>", "Name for the new agent").option("--phone-country <cc>", "Phone number country code", "GB").option("--phone-area-code <code>", "Phone area code preference").option("--phone-mode <mode>", "Phone path: observe, skip, or purchase", "purchase").option("--widget-domains <domains>", "Comma-separated widget domain allowlist").option("--voice-provider <p>", "TTS provider: openai, azure, twilio").option("--voice-id <id>", "Voice ID").option("--skip-compliance", "Skip compliance submission and wait").option("--skip-voice", "Skip voice configuration").option("--skip-tests", "Skip smoke tests").option("--cert-mode <m>", "Simulation cert mode: quick, full, stress", "quick").option("--cert-adaptive-runs <n>", "Adaptive run count for certification loop", "30").option("--cert-max-improvement-rounds <n>", "Max instruction improvement rounds in cert loop (0-5)", "1").option("--resume-from <step>", `Resume from a setup step (${SETUP_STEP_ORDER.join(", ")})`).option("--report-out <path>", "Optional path to write signed setup run report JSON").option("--dry-run", "Print all steps that would run without making any API calls").option("--api-url <url>", "API base URL override").option("--console-url <url>", "Console sign-in URL override").option("--json", "Output as JSON").action(async (opts) => {
34946
+ if (opts.objective) {
34947
+ await emitObjectiveSetupBootstrap(opts);
34948
+ return;
34949
+ }
34887
34950
  if (!opts.org) {
34888
34951
  try {
34889
34952
  opts.org = loadCredentials(opts.apiUrl).orgId;
@@ -35738,7 +35801,7 @@ function defaultAdaptiveRuns(profile) {
35738
35801
  if (profile === "stress") return 30;
35739
35802
  return 5;
35740
35803
  }
35741
- function csv(raw) {
35804
+ function csv2(raw) {
35742
35805
  if (!raw) return void 0;
35743
35806
  const values = String(raw).split(",").map((value) => value.trim()).filter(Boolean);
35744
35807
  return values.length > 0 ? values : void 0;
@@ -35764,8 +35827,8 @@ function registerCertify(program3) {
35764
35827
  body: JSON.stringify({
35765
35828
  mode,
35766
35829
  adaptive_runs: adaptiveRuns,
35767
- journeys: csv(opts.journeys),
35768
- scenario_ids: csv(opts.scenarioIds),
35830
+ journeys: csv2(opts.journeys),
35831
+ scenario_ids: csv2(opts.scenarioIds),
35769
35832
  channel: channel(opts.channel)
35770
35833
  }),
35771
35834
  apiUrlOverride: opts.apiUrl
@@ -38114,6 +38177,9 @@ function fail(name, reasonCode, error2, nextCommand) {
38114
38177
  function skipped(name, reasonCode, summary, nextCommand) {
38115
38178
  return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
38116
38179
  }
38180
+ function defaultCertificationProfileForMission(mission) {
38181
+ return mission === "publish" ? "release" : "smoke";
38182
+ }
38117
38183
  function hasBlockingChecks(checks) {
38118
38184
  return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
38119
38185
  }
@@ -38451,11 +38517,12 @@ function registerProve(program3) {
38451
38517
  if (opts.skipCert) {
38452
38518
  checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh certify run --agent ${ctx.agentId} --profile release --json`));
38453
38519
  } else if (!opts.includeCertification) {
38520
+ const certificationProfile = defaultCertificationProfileForMission(mission);
38454
38521
  checks.push(skipped(
38455
38522
  "simulation_certification",
38456
38523
  "certification_explicitly_required",
38457
- "Runtime proof does not run release certification by default.",
38458
- `foh certify run --agent ${ctx.agentId} --profile release --json`
38524
+ certificationProfile === "release" ? "Runtime proof does not run release certification by default." : "Runtime proof does not run certification by default; use smoke certification for bounded external-agent verification.",
38525
+ `foh certify run --agent ${ctx.agentId} --profile ${certificationProfile} --json`
38459
38526
  ));
38460
38527
  } else {
38461
38528
  try {
@@ -38551,14 +38618,16 @@ function registerProve(program3) {
38551
38618
  var import_node_fs5 = require("node:fs");
38552
38619
  var import_node_path2 = require("node:path");
38553
38620
  var DEFAULT_OBJECTIVE_REPORT_PATH = "test-results/objective-status.latest.json";
38621
+ var VALID_OBJECTIVE_INDUSTRIES = ["real_estate", "restaurant", "general"];
38622
+ var BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION = "business_requirement_brief.v1";
38554
38623
  function asRecord3(value) {
38555
38624
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
38556
38625
  }
38557
- function normalizeString(value) {
38626
+ function normalizeString2(value) {
38558
38627
  return typeof value === "string" ? value.trim() : "";
38559
38628
  }
38560
38629
  function uniqueStrings(values) {
38561
- return Array.from(new Set(values.map(normalizeString).filter(Boolean)));
38630
+ return Array.from(new Set(values.map(normalizeString2).filter(Boolean)));
38562
38631
  }
38563
38632
  function asArray(value) {
38564
38633
  return Array.isArray(value) ? value : [];
@@ -38630,7 +38699,7 @@ function normalizeCustomerEvidenceActions(value) {
38630
38699
  ]),
38631
38700
  required_evidence: firstString(action, ["required_evidence"]) || null,
38632
38701
  unlocks: firstString(action, ["unlocks"]) || null
38633
- })).filter((action) => normalizeString(action.id));
38702
+ })).filter((action) => normalizeString2(action.id));
38634
38703
  }
38635
38704
  function normalizeCustomerEvidenceActionPacket(value) {
38636
38705
  const packet = asRecord3(value);
@@ -38716,7 +38785,7 @@ async function resolveEvidenceInput(opts) {
38716
38785
  }
38717
38786
  function firstString(record2, keys) {
38718
38787
  for (const key of keys) {
38719
- const value = normalizeString(record2[key]);
38788
+ const value = normalizeString2(record2[key]);
38720
38789
  if (value) return value;
38721
38790
  }
38722
38791
  return "";
@@ -38734,13 +38803,13 @@ function statusFromDecision(value) {
38734
38803
  return "hold";
38735
38804
  }
38736
38805
  function firstCustomerEvidenceAction(packet) {
38737
- const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString(action.id));
38806
+ const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString2(action.id));
38738
38807
  return actions[0] ?? null;
38739
38808
  }
38740
38809
  function buildDeveloperReadinessPacket(input) {
38741
38810
  const businessName = resolveBusinessName(input.opts);
38742
- const sourceUrl = normalizeString(input.opts.sourceUrl);
38743
- const businessObjective = normalizeString(input.opts.businessObjective);
38811
+ const sourceUrl = normalizeString2(input.opts.sourceUrl);
38812
+ const businessObjective = normalizeString2(input.opts.businessObjective);
38744
38813
  const location = resolveLocation(input.opts);
38745
38814
  const tools = parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
38746
38815
  const setupDecision = firstString(input.setupWorkflow, ["decision", "current_decision", "status"]);
@@ -38757,9 +38826,10 @@ function buildDeveloperReadinessPacket(input) {
38757
38826
  business_context: {
38758
38827
  business_name: businessName || null,
38759
38828
  business_objective: businessObjective || null,
38829
+ industry: objectiveIndustryOrNull(input.opts),
38760
38830
  source_url: sourceUrl || null,
38761
38831
  location: location || null,
38762
- target_mode: normalizeString(input.opts.targetMode) || "customer_owned_voice_trial",
38832
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
38763
38833
  tools
38764
38834
  },
38765
38835
  input_completeness: {
@@ -38767,7 +38837,7 @@ function buildDeveloperReadinessPacket(input) {
38767
38837
  source_url: Boolean(sourceUrl),
38768
38838
  business_objective: Boolean(businessObjective),
38769
38839
  requested_tools: tools.length > 0,
38770
- target_mode: Boolean(normalizeString(input.opts.targetMode) || "customer_owned_voice_trial")
38840
+ target_mode: Boolean(normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial")
38771
38841
  },
38772
38842
  readiness_dimensions: {
38773
38843
  setup_workflow: {
@@ -38781,7 +38851,7 @@ function buildDeveloperReadinessPacket(input) {
38781
38851
  customer_evidence_actions: {
38782
38852
  status: firstAction ? "hold" : "pass",
38783
38853
  action_count: finiteNumber(input.customerEvidenceActionPacket?.action_count) ?? asArray(input.customerEvidenceActionPacket?.actions).length,
38784
- first_action_id: firstAction ? normalizeString(firstAction.id) : null,
38854
+ first_action_id: firstAction ? normalizeString2(firstAction.id) : null,
38785
38855
  first_validator_command: firstActionValidator
38786
38856
  },
38787
38857
  agent_operability: {
@@ -38805,7 +38875,7 @@ function buildDeveloperReadinessPacket(input) {
38805
38875
  };
38806
38876
  }
38807
38877
  function resolveObjectiveReportPath(value) {
38808
- const raw = normalizeString(value);
38878
+ const raw = normalizeString2(value);
38809
38879
  if (!raw || raw === "latest") return (0, import_node_path2.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
38810
38880
  return (0, import_node_path2.resolve)(raw);
38811
38881
  }
@@ -38823,11 +38893,11 @@ function buildSetupBody(opts) {
38823
38893
  const location = resolveLocation(opts);
38824
38894
  const body = {
38825
38895
  agency_name: businessName,
38826
- business_objective: normalizeString(opts.businessObjective) || null,
38896
+ business_objective: normalizeString2(opts.businessObjective) || null,
38827
38897
  requested_tool_surface: tools,
38828
- target_exposure_mode: normalizeString(opts.targetMode) || "customer_owned_voice_trial"
38898
+ target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
38829
38899
  };
38830
- if (opts.sourceUrl) body.source_url = normalizeString(opts.sourceUrl);
38900
+ if (opts.sourceUrl) body.source_url = normalizeString2(opts.sourceUrl);
38831
38901
  if (location) body.branch_location = location;
38832
38902
  return body;
38833
38903
  }
@@ -38835,19 +38905,176 @@ function buildStatusParams(opts) {
38835
38905
  const params = new URLSearchParams();
38836
38906
  const businessName = resolveBusinessName(opts);
38837
38907
  const location = resolveLocation(opts);
38838
- if (opts.environment) params.set("environment", normalizeString(opts.environment));
38908
+ if (opts.environment) params.set("environment", normalizeString2(opts.environment));
38839
38909
  if (businessName) params.set("agency_name", businessName);
38840
- if (opts.sourceUrl) params.set("source_url", normalizeString(opts.sourceUrl));
38910
+ if (opts.sourceUrl) params.set("source_url", normalizeString2(opts.sourceUrl));
38841
38911
  if (location) params.set("branch_location", location);
38842
- if (opts.tools) params.set("tools", normalizeString(opts.tools));
38843
- if (opts.targetMode) params.set("target_mode", normalizeString(opts.targetMode));
38912
+ if (opts.tools) params.set("tools", normalizeString2(opts.tools));
38913
+ if (opts.targetMode) params.set("target_mode", normalizeString2(opts.targetMode));
38844
38914
  return params;
38845
38915
  }
38846
38916
  function resolveBusinessName(opts) {
38847
- return normalizeString(opts.businessName) || normalizeString(opts.agencyName);
38917
+ return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
38848
38918
  }
38849
38919
  function resolveLocation(opts) {
38850
- return normalizeString(opts.location) || normalizeString(opts.branchLocation);
38920
+ return normalizeString2(opts.location) || normalizeString2(opts.branchLocation);
38921
+ }
38922
+ function hasAnyToken(value, tokens) {
38923
+ const normalized = ` ${value.toLowerCase().replace(/[^a-z0-9]+/g, " ")} `;
38924
+ return tokens.some((token) => normalized.includes(` ${token} `));
38925
+ }
38926
+ function inferObjectiveIndustry(opts) {
38927
+ const explicit = normalizeString2(opts.industry);
38928
+ if (explicit) {
38929
+ if (VALID_OBJECTIVE_INDUSTRIES.includes(explicit)) return explicit;
38930
+ throw new FohError({
38931
+ step: "objective.brief",
38932
+ error: `Unsupported industry: ${explicit}.`,
38933
+ remediation: `Use one of: ${VALID_OBJECTIVE_INDUSTRIES.join(", ")}.`,
38934
+ statusCode: 400,
38935
+ reasonCode: "objective_industry_unsupported"
38936
+ });
38937
+ }
38938
+ const objectiveText = [
38939
+ opts.businessObjective,
38940
+ opts.businessName,
38941
+ opts.agencyName,
38942
+ opts.sourceUrl
38943
+ ].map(normalizeString2).join(" ");
38944
+ const realEstate = hasAnyToken(objectiveText, [
38945
+ "estate",
38946
+ "property",
38947
+ "properties",
38948
+ "viewing",
38949
+ "valuation",
38950
+ "buyer",
38951
+ "seller",
38952
+ "landlord",
38953
+ "tenant",
38954
+ "lettings"
38955
+ ]);
38956
+ const restaurant = hasAnyToken(objectiveText, [
38957
+ "restaurant",
38958
+ "table",
38959
+ "booking",
38960
+ "reservation",
38961
+ "reservations",
38962
+ "diner",
38963
+ "diners",
38964
+ "menu",
38965
+ "allergy",
38966
+ "hospitality"
38967
+ ]);
38968
+ if (realEstate && !restaurant) return "real_estate";
38969
+ if (restaurant && !realEstate) return "restaurant";
38970
+ throw new FohError({
38971
+ step: "objective.brief",
38972
+ error: realEstate && restaurant ? "Objective matches multiple industries." : "Objective is too ambiguous to select a business template.",
38973
+ remediation: "Pass --industry real_estate or --industry restaurant, and keep --business-objective specific to one front-of-house outcome.",
38974
+ statusCode: 400,
38975
+ reasonCode: realEstate && restaurant ? "objective_industry_ambiguous" : "objective_industry_missing",
38976
+ nextCommands: [
38977
+ 'foh objective plan --business-name <name> --industry restaurant --business-objective "Book tables from website and voice enquiries" --source-url <official_url> --json',
38978
+ 'foh objective plan --business-name <name> --industry real_estate --business-objective "Qualify buyers and book viewings" --source-url <official_url> --json'
38979
+ ]
38980
+ });
38981
+ }
38982
+ function objectiveIndustryOrNull(opts) {
38983
+ try {
38984
+ return inferObjectiveIndustry(opts);
38985
+ } catch {
38986
+ return null;
38987
+ }
38988
+ }
38989
+ function channelsFromTools(tools) {
38990
+ const supported = /* @__PURE__ */ new Set(["widget", "voice", "whatsapp", "instagram", "sms"]);
38991
+ const channels = tools.filter((tool) => supported.has(tool));
38992
+ return channels.length > 0 ? channels : ["widget"];
38993
+ }
38994
+ function inferOptimizationTarget(industry, objective) {
38995
+ const normalized = objective.toLowerCase();
38996
+ if (industry === "restaurant" || /book|booking|reservation|table/.test(normalized)) return "booking_rate";
38997
+ if (/speed|fast|callback|call back|lead/.test(normalized)) return "speed_to_lead";
38998
+ if (/support|help|resolve|resolution/.test(normalized)) return "support_resolution";
38999
+ return "lead_quality";
39000
+ }
39001
+ function buildRequirementBrief(opts) {
39002
+ const businessName = resolveBusinessName(opts);
39003
+ const businessObjective = normalizeString2(opts.businessObjective);
39004
+ if (!businessObjective) {
39005
+ throw new FohError({
39006
+ step: "objective.brief",
39007
+ error: "Missing business objective.",
39008
+ remediation: "Pass --business-objective with the concrete front-of-house outcome to configure.",
39009
+ statusCode: 400,
39010
+ reasonCode: "objective_business_objective_missing"
39011
+ });
39012
+ }
39013
+ const industry = inferObjectiveIndustry(opts);
39014
+ const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
39015
+ const knowledgeSources = [];
39016
+ const sourceUrl = normalizeString2(opts.sourceUrl);
39017
+ if (sourceUrl) {
39018
+ knowledgeSources.push({
39019
+ type: "website",
39020
+ label: `${businessName} official website`,
39021
+ uri: sourceUrl
39022
+ });
39023
+ }
39024
+ return {
39025
+ schema_version: BUSINESS_REQUIREMENT_BRIEF_SCHEMA_VERSION,
39026
+ business_name: businessName,
39027
+ industry,
39028
+ desired_use_cases: [businessObjective],
39029
+ channels: channelsFromTools(tools),
39030
+ knowledge_sources: knowledgeSources,
39031
+ required_outcomes: [businessObjective],
39032
+ handoff_rules: ["handoff when customer facts, credentials, or booking/action availability are missing"],
39033
+ constraints: ["do not invent business facts or confirm actions without configured evidence/tool results"],
39034
+ optimization_target: inferOptimizationTarget(industry, businessObjective)
39035
+ };
39036
+ }
39037
+ function selectedTemplateSummary2(templateSelection) {
39038
+ const candidates = asArray(asRecord3(templateSelection).candidates).map(asRecord3);
39039
+ const top = candidates[0];
39040
+ const template = asRecord3(top?.template);
39041
+ if (!template || Object.keys(template).length === 0) return null;
39042
+ const contract = asRecord3(template.template_contract);
39043
+ return {
39044
+ template_id: normalizeString2(template.id) || normalizeString2(contract.template_id) || null,
39045
+ template_name: normalizeString2(template.name) || normalizeString2(contract.name) || null,
39046
+ industry: normalizeString2(contract.industry) || null,
39047
+ use_case: normalizeString2(contract.use_case) || null,
39048
+ match_score: finiteNumber(top.match_score),
39049
+ matched_reasons: uniqueStrings(asArray(top.matched_reasons))
39050
+ };
39051
+ }
39052
+ function assertTemplateSelected(templateSelection) {
39053
+ if (selectedTemplateSummary2(templateSelection)) return;
39054
+ throw new FohError({
39055
+ step: "objective.template_selection",
39056
+ error: "No supported template matched this business objective.",
39057
+ remediation: "Use a more specific --business-objective or choose a supported --industry before setup. Do not continue with a guessed template.",
39058
+ statusCode: 422,
39059
+ reasonCode: "objective_template_selection_empty",
39060
+ nextCommands: [
39061
+ "foh templates select --brief @business-brief.json --json"
39062
+ ]
39063
+ });
39064
+ }
39065
+ async function selectTemplateForObjective(opts) {
39066
+ const brief = buildRequirementBrief(opts);
39067
+ const selection = await apiFetch("/v1/console/templates/select", {
39068
+ method: "POST",
39069
+ body: JSON.stringify(brief),
39070
+ apiUrlOverride: opts.apiUrl
39071
+ });
39072
+ assertTemplateSelected(selection);
39073
+ return {
39074
+ brief,
39075
+ selection,
39076
+ selected: selectedTemplateSummary2(selection)
39077
+ };
38851
39078
  }
38852
39079
  function assertBusinessName(opts, step) {
38853
39080
  if (resolveBusinessName(opts)) return;
@@ -38894,7 +39121,7 @@ function buildObjectiveReport(input) {
38894
39121
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
38895
39122
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
38896
39123
  ]);
38897
- const debugSource = normalizeString(input.opts.out) || "latest";
39124
+ const debugSource = normalizeString2(input.opts.out) || "latest";
38898
39125
  const nextCommands = dedupeCommands([
38899
39126
  ...collectStringArrays(setup, /* @__PURE__ */ new Set(["next_commands"])),
38900
39127
  ...collectStringArrays(live, /* @__PURE__ */ new Set(["next_commands"])),
@@ -38934,15 +39161,19 @@ function buildObjectiveReport(input) {
38934
39161
  safeToRetry: true,
38935
39162
  extra: {
38936
39163
  objective: {
38937
- business_objective: normalizeString(input.opts.businessObjective) || null,
39164
+ business_objective: normalizeString2(input.opts.businessObjective) || null,
38938
39165
  business_name: resolveBusinessName(input.opts) || null,
38939
- agency_name: normalizeString(input.opts.agencyName) || resolveBusinessName(input.opts) || null,
38940
- source_url: normalizeString(input.opts.sourceUrl) || null,
39166
+ agency_name: normalizeString2(input.opts.agencyName) || resolveBusinessName(input.opts) || null,
39167
+ industry: objectiveIndustryOrNull(input.opts),
39168
+ source_url: normalizeString2(input.opts.sourceUrl) || null,
38941
39169
  location: resolveLocation(input.opts) || null,
38942
- branch_location: normalizeString(input.opts.branchLocation) || resolveLocation(input.opts) || null,
38943
- target_mode: normalizeString(input.opts.targetMode) || "customer_owned_voice_trial",
39170
+ branch_location: normalizeString2(input.opts.branchLocation) || resolveLocation(input.opts) || null,
39171
+ target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
38944
39172
  tools: parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? []
38945
39173
  },
39174
+ requirement_brief: firstNonEmptyObject(input.requirementBrief) ?? null,
39175
+ selected_template: firstNonEmptyObject(input.selectedTemplate) ?? null,
39176
+ template_selection: firstNonEmptyObject(input.templateSelection) ?? null,
38946
39177
  allowed_mode: allowedMode,
38947
39178
  blocked_modes: blockedModes,
38948
39179
  reason_codes: reasonCodes,
@@ -38996,7 +39227,7 @@ function stripDiagnosticField(target, fieldPath) {
38996
39227
  function buildObjectiveNormalPathOutput(report) {
38997
39228
  const output = JSON.parse(JSON.stringify(report));
38998
39229
  const artifactPolicy = asRecord3(output.artifact_policy);
38999
- const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString(value)));
39230
+ const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString2(value)));
39000
39231
  for (const fieldPath of diagnosticFields) stripDiagnosticField(output, fieldPath);
39001
39232
  return output;
39002
39233
  }
@@ -39072,8 +39303,9 @@ function buildDebugReport(sourcePath, objectiveReport) {
39072
39303
  }
39073
39304
  function registerObjective(program3) {
39074
39305
  const objective = program3.command("objective").description("Agent-native objective workflow: plan, apply, prove, status, debug");
39075
- objective.command("plan").description("Generate an objective setup/workflow plan").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective plan JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39306
+ objective.command("plan").description("Generate an objective setup/workflow plan").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective plan JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39076
39307
  assertBusinessName(opts, "objective.plan");
39308
+ const templateSelection = await selectTemplateForObjective(opts);
39077
39309
  const report = await apiFetch("/v1/console/agency-setup/workflow", {
39078
39310
  method: "POST",
39079
39311
  body: JSON.stringify(buildSetupBody(opts)),
@@ -39081,7 +39313,13 @@ function registerObjective(program3) {
39081
39313
  apiUrlOverride: opts.apiUrl
39082
39314
  });
39083
39315
  const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
39084
- const output = outPath ? { ...asRecord3(report), artifact_path: outPath } : report;
39316
+ const objectiveSetup = {
39317
+ ...asRecord3(report),
39318
+ requirement_brief: templateSelection.brief,
39319
+ template_selection: templateSelection.selection,
39320
+ selected_template: templateSelection.selected
39321
+ };
39322
+ const output = outPath ? { ...objectiveSetup, artifact_path: outPath } : objectiveSetup;
39085
39323
  if (outPath) writeJsonArtifact2(outPath, output);
39086
39324
  format(output, { json: opts.json ?? false });
39087
39325
  }));
@@ -39102,7 +39340,7 @@ function registerObjective(program3) {
39102
39340
  if (outPath) writeJsonArtifact2(outPath, output);
39103
39341
  format(output, { json: opts.json ?? false });
39104
39342
  }));
39105
- objective.command("prove").description("Run objective proof status against customer-live gate").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective proof JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39343
+ objective.command("prove").description("Run objective proof status against customer-live gate").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective proof JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39106
39344
  assertBusinessName(opts, "objective.prove");
39107
39345
  const status = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
39108
39346
  orgId: opts.org,
@@ -39113,8 +39351,9 @@ function registerObjective(program3) {
39113
39351
  if (outPath) writeJsonArtifact2(outPath, output);
39114
39352
  format(output, { json: opts.json ?? false });
39115
39353
  }));
39116
- objective.command("status").description("Compose setup and launch evidence into one agent workbench status envelope").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective report JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39354
+ objective.command("status").description("Compose setup and launch evidence into one agent workbench status envelope").option("--business-name <name>", "Business trading name").option("--agency-name <name>", "Legacy alias for --business-name").option("--industry <industry>", "Business industry: real_estate, restaurant, general").option("--business-objective <text>", "Natural-language business objective").option("--source-url <url>", "Official source URL").option("--location <value>", "Location or branch this setup represents").option("--branch-location <value>", "Legacy alias for --location").option("--tools <csv>", "Requested tool surfaces", "widget,voice,email,callback,calendar,crm,webhook,whatsapp").option("--target-mode <mode>", "Target launch/exposure mode", "customer_owned_voice_trial").option("--environment <value>", "Environment filter").option("--org <id>", "Org ID (default: stored org from foh org use)").option("--api-url <url>", "API base URL override").option("--out <path>", "Write objective report JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
39117
39355
  assertBusinessName(opts, "objective.status");
39356
+ const templateSelection = await selectTemplateForObjective(opts);
39118
39357
  const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
39119
39358
  method: "POST",
39120
39359
  body: JSON.stringify(buildSetupBody(opts)),
@@ -39125,7 +39364,14 @@ function registerObjective(program3) {
39125
39364
  orgId: opts.org,
39126
39365
  apiUrlOverride: opts.apiUrl
39127
39366
  });
39128
- const report = buildObjectiveReport({ opts, setupWorkflow, customerLiveStatus });
39367
+ const report = buildObjectiveReport({
39368
+ opts,
39369
+ setupWorkflow,
39370
+ customerLiveStatus,
39371
+ requirementBrief: templateSelection.brief,
39372
+ templateSelection: templateSelection.selection,
39373
+ selectedTemplate: templateSelection.selected
39374
+ });
39129
39375
  const artifactPath = resolveObjectiveReportPath(opts.out);
39130
39376
  const fullOutput = withObjectiveArtifactPath(report, artifactPath);
39131
39377
  writeJsonArtifact2(artifactPath, fullOutput);
@@ -39423,9 +39669,9 @@ var COMMAND_SURFACE_DEFINITIONS = [
39423
39669
  includeInSuggestions: false
39424
39670
  },
39425
39671
  {
39426
- id: "whatsapp_start",
39427
- commandPath: ["channel", "whatsapp", "start"],
39428
- label: "whatsapp start",
39672
+ id: "whatsapp_onboard",
39673
+ commandPath: ["channel", "whatsapp", "onboard"],
39674
+ label: "whatsapp onboard",
39429
39675
  descriptionFallback: "WhatsApp readiness path",
39430
39676
  mutatesState: "write",
39431
39677
  shellSlash: "/whatsapp",
@@ -42321,10 +42567,11 @@ function createExternalAgentExecutorPlan(options) {
42321
42567
  ].join("\n"),
42322
42568
  "utf8"
42323
42569
  );
42570
+ const explicitPromptVersion = typeof run.prompt_version === "string" && run.prompt_version.trim() ? run.prompt_version.trim() : typeof batch.prompt_version === "string" && batch.prompt_version.trim() ? batch.prompt_version.trim() : promptVersionFromPath(promptPath);
42324
42571
  const env = buildCodexExecutorEnv({
42325
42572
  sourceEnv: options.env,
42326
42573
  runDir,
42327
- promptVersion: promptVersionFromPath(promptPath)
42574
+ promptVersion: explicitPromptVersion
42328
42575
  });
42329
42576
  const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
42330
42577
  const outputStem = runner === "gemini" ? "gemini" : "codex";
@@ -43091,6 +43338,7 @@ var DEFAULT_PROMPT_VERSION = "blank-setup.v1";
43091
43338
  var DEFAULT_BATCH_MODELS = "openai/codex,anthropic/claude,cursor/agent";
43092
43339
  var PROMPTS = {
43093
43340
  "blank-setup.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`, because cached older packages can produce invalid evidence. Install or verify the FOH CLI, authenticate or reach a deterministic auth blocker, then create or configure a Front Of House voice agent and website widget. Mass evals reuse existing eval state: run `npx --yes @f-o-h/cli@latest org status --json` and `npx --yes @f-o-h/cli@latest agent list --json` before trying to create a fresh agent; if an existing eval agent is present, configure and prove that agent instead of creating a second bronze-tier agent. Prefer the certification-oriented buyer templates: run `npx --yes @f-o-h/cli@latest templates list --category buyer --json` and use `UK Buyer Qualification` or `Viewing Booking` when available; do not use a greeting-only template for proof/certification. Prefer `npx --yes @f-o-h/cli@latest setup --phone-mode observe` for the free scaffold path: agent, widget, voice config, smoke test, certification, and publish readiness together. Treat phone-number purchasing as an explicit paid/scarce contact-path step, not part of high-volume eval setup. If `FOH_CLI_SPEND_POLICY=no_spend` is active and a command returns `paid_resource_blocked_by_spend_policy`, do not try to bypass it; continue widget/setup proof and report that exact reason code for the phone path. If the customer/operator explicitly owns a number and asks for real PSTN proof, use `npx --yes @f-o-h/cli@latest provision byon attach --phone-number <e164> --confirm-owned --json`; do not invent ownership or buy a FOH-owned number. Run proof/smoke/certification where available, including widget proof, voice proof, and one explicit `foh certify run --agent <id> --profile release --json` before publish. `foh prove` does not run release certification by default; only pass `--include-certification --proof-cache-dir .foh/proof-cache` when an explicit combined proof/certification run is required. If voice proof returns `contact_phone_missing` or `voice_contact_expected_no_spend_hold`, report that exact reason code unless a BYON/customer-approved phone path already exists. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `docs_pages_used`, key decisions, and blocker reason codes before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, and any blocker reason codes. Do not assume access to the private source repository.",
43341
+ "restaurant-table-booking-blackbox.v1": "Set up a front of house system for a real restaurant that takes table bookings. Find Front Of House online and use only public web pages, public docs, public API docs, and public package installs. Do not use or inspect any private repository, local source checkout, unpublished local commands, or hidden runbooks. Pick a real restaurant name and official website from public information, then try to reach a deterministic setup/proof result for a table-booking front-of-house agent. Use the authenticated environment if the public CLI discovers it, but do not expose credentials or secrets. Do not buy phone numbers or paid resources. If the system blocks because customer-owned credentials, booking-system access, approval, or live validation are missing, treat that as a valid hold only when the command returns machine-readable reason codes and exact next commands. If `FOH_EXTERNAL_AGENT_RUN_DIR` is set, write `${FOH_EXTERNAL_AGENT_RUN_DIR}/external-agent-metadata.json` with `schema_version`, `restaurant_name`, `restaurant_source_url`, `docs_pages_used`, `commands_run`, `artifacts_created`, `final_status`, `blocker_reason_codes`, and `friction_points` before finishing. Produce a final evidence summary with commands run, docs used, artifacts created, final pass/hold/fail status, blocker reason codes, and every friction point encountered.",
43094
43342
  "real-estate-buyer-enquiry.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a buyer-enquiry real-estate agent (UK Buyer Qualification preferred), prove widget behavior, and prove voice behavior in no-spend mode. Required path: 1) verify auth/org scope and reuse existing eval org/agent where possible; 2) select/apply buyer template; 3) configure widget + voice; 4) run widget smoke and `foh certify run --profile release`; 5) run voice proof and treat no-spend contact holds as expected only when all non-contact gates pass. Do not buy numbers; if spend policy blocks purchase, record `paid_resource_blocked_by_spend_policy` and continue no-spend proof path. Final artifact must include: selected template id/slug, commands executed, pass/hold/fail per gate, reason codes, docs_pages_used, and next fix commands.",
43095
43343
  "real-estate-seller-valuation.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a seller-valuation real-estate agent, prove valuation lead capture on widget and voice, and keep strict no-spend behavior. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply seller valuation template; 3) configure widget + voice; 4) run widget smoke and release certification; 5) run voice proof and classify holds. Do not claim precise valuation output without approved tooling; safe fallback or handoff must remain explicit. Do not buy numbers. Final artifact must include: selected template id/slug, lead fields observed, tool/action behavior, reason codes, docs_pages_used, and next fix commands.",
43096
43344
  "real-estate-viewing-and-qa.v1": "Go to https://frontofhouse.okii.uk. Use only public docs, public API docs, and the public npm CLI package. Always invoke the CLI with `npx --yes @f-o-h/cli@latest ...`; do not use unpinned `npx @f-o-h/cli ...`. Do not assume access to the private source repository. Mission: configure a viewing-booking/property-QA real-estate agent and prove booking-safe behavior under no-spend policy. Required path: 1) verify auth/org scope and reuse existing eval state; 2) select/apply viewing template; 3) configure widget + voice; 4) run widget smoke, release certification, and voice proof; 5) explicitly check no-duplicate-side-effect behavior and safe fallback/handoff when booking tooling or contact path is unavailable. Do not buy numbers. Final artifact must include: selected template id/slug, booking/fallback evidence, reason codes, docs_pages_used, and next fix commands.",
@@ -43768,11 +44016,12 @@ function installSoftExitTrap() {
43768
44016
  // src/lib/mission-help.ts
43769
44017
  var CLI_MISSION_EXAMPLES = [
43770
44018
  { mission: "Start", command: "foh start", description: "guided setup and next action selector" },
43771
- { mission: "Objective Plan", command: "foh objective plan --business-name <name> --source-url <url> --out test-results/objective-plan.latest.json --json", description: "generate setup and onboarding context" },
44019
+ { mission: "Setup Objective", command: 'foh setup --objective "<goal>" --business-name <name> --industry <industry> --source-url <url> --json', description: "select template and create/update the agent from a business goal" },
44020
+ { mission: "Objective Plan", command: "foh objective plan --business-name <name> --source-url <url> --out test-results/objective-plan.latest.json --json", description: "preview setup and onboarding context without applying" },
43772
44021
  { mission: "Objective Apply", command: "foh objective apply --evidence <json|@file> --out test-results/objective-apply.latest.json --json", description: "submit verified evidence into customer-live gating path" },
43773
44022
  { mission: "Objective Prove", command: "foh objective prove --business-name <name> --source-url <url> --out test-results/objective-live.latest.json --json", description: "run customer-live proof check directly" },
43774
44023
  { mission: "Objective Status", command: "foh objective status --business-name <name> --source-url <url> --out test-results/objective-status.latest.json --json", description: "compose setup and live status into one report envelope" },
43775
- { mission: "Setup", command: "foh setup --phone-mode observe --json", description: "create or update agent, widget, voice config, and proof scaffold" },
44024
+ { mission: "Setup Legacy", command: "foh setup --phone-mode observe --json", description: "create or update agent from an explicit template" },
43776
44025
  { mission: "Prove", command: "foh prove --agent <agent_id> --mission widget --json", description: "produce a machine-readable proof report" },
43777
44026
  { mission: "Publish", command: "foh publish --agent <agent_id> --json", description: "publish when proof and release evidence pass" },
43778
44027
  { mission: "Debug", command: "foh debug --out test-results/foh-cli-diag.latest.json --json", description: "collect auth/org/API diagnostics" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f-o-h/cli",
3
- "version": "0.1.87",
3
+ "version": "0.1.88",
4
4
  "description": "FOH CLI - AI-operator provisioning tool for Front Of House",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -34,10 +34,61 @@
34
34
  "name": { "type": "string", "minLength": 1 },
35
35
  "version": { "type": "string", "minLength": 1 },
36
36
  "status": { "enum": ["draft", "certification_ready", "published", "deprecated"] },
37
- "industry": { "enum": ["real_estate", "general"] },
37
+ "industry": { "enum": ["real_estate", "restaurant", "general"] },
38
38
  "category": { "enum": ["buyer", "seller", "landlord", "tenant", "commercial", "support", "sales", "general"] },
39
39
  "use_case": { "type": "string", "minLength": 1 },
40
40
  "summary": { "type": "string", "minLength": 1 },
41
+ "business_blueprint": {
42
+ "type": "object",
43
+ "required": [
44
+ "schema_version",
45
+ "blueprint_id",
46
+ "name",
47
+ "industry",
48
+ "business_model",
49
+ "primary_audiences",
50
+ "supported_objectives",
51
+ "knowledge_source_types",
52
+ "default_channels"
53
+ ],
54
+ "properties": {
55
+ "schema_version": { "const": "business_blueprint.v1" },
56
+ "blueprint_id": { "type": "string", "minLength": 1 },
57
+ "name": { "type": "string", "minLength": 1 },
58
+ "industry": { "enum": ["real_estate", "restaurant", "general"] },
59
+ "business_model": { "type": "string", "minLength": 1 },
60
+ "primary_audiences": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
61
+ "supported_objectives": {
62
+ "type": "array",
63
+ "minItems": 1,
64
+ "items": {
65
+ "type": "object",
66
+ "required": ["id", "label", "outcome"],
67
+ "properties": {
68
+ "id": { "type": "string", "minLength": 1 },
69
+ "label": { "type": "string", "minLength": 1 },
70
+ "outcome": { "type": "string", "minLength": 1 },
71
+ "required_fields": { "type": "array", "items": { "type": "string", "minLength": 1 } },
72
+ "default_tools": { "type": "array", "items": { "type": "string", "minLength": 1 } },
73
+ "scenario_tags": { "type": "array", "items": { "type": "string", "minLength": 1 } }
74
+ }
75
+ }
76
+ },
77
+ "knowledge_source_types": {
78
+ "type": "array",
79
+ "minItems": 1,
80
+ "items": { "enum": ["website", "document", "manual_text", "crm", "property_feed", "booking_system", "menu"] }
81
+ },
82
+ "default_channels": {
83
+ "type": "array",
84
+ "minItems": 1,
85
+ "items": { "enum": ["widget", "voice", "whatsapp", "instagram", "sms"] }
86
+ },
87
+ "hard_constraints": { "type": "array", "items": { "type": "string", "minLength": 1 } },
88
+ "handoff_triggers": { "type": "array", "items": { "type": "string", "minLength": 1 } },
89
+ "evidence_requirements": { "type": "array", "items": { "type": "string", "minLength": 1 } }
90
+ }
91
+ },
41
92
  "channel_support": {
42
93
  "type": "array",
43
94
  "minItems": 1,