@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 +1 -1
- package/dist/foh.js +425 -176
- package/package.json +1 -1
- package/schemas/foh-template.schema.json +52 -1
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.
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
35768
|
-
scenario_ids:
|
|
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
|
|
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
|
|
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(
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
38743
|
-
const 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:
|
|
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(
|
|
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 ?
|
|
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 =
|
|
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:
|
|
38896
|
+
business_objective: normalizeString2(opts.businessObjective) || null,
|
|
38827
38897
|
requested_tool_surface: tools,
|
|
38828
|
-
target_exposure_mode:
|
|
38898
|
+
target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
|
|
38829
38899
|
};
|
|
38830
|
-
if (opts.sourceUrl) body.source_url =
|
|
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",
|
|
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",
|
|
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",
|
|
38843
|
-
if (opts.targetMode) params.set("target_mode",
|
|
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
|
|
38917
|
+
return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
|
|
38848
38918
|
}
|
|
38849
38919
|
function resolveLocation(opts) {
|
|
38850
|
-
return
|
|
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 =
|
|
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:
|
|
39164
|
+
business_objective: normalizeString2(input.opts.businessObjective) || null,
|
|
38938
39165
|
business_name: resolveBusinessName(input.opts) || null,
|
|
38939
|
-
agency_name:
|
|
38940
|
-
|
|
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:
|
|
38943
|
-
target_mode:
|
|
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) =>
|
|
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
|
|
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({
|
|
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: "
|
|
39427
|
-
commandPath: ["channel", "whatsapp", "
|
|
39428
|
-
label: "whatsapp
|
|
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:
|
|
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
|
|
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
|
|
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
|
@@ -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,
|