@f-o-h/cli 0.1.86 → 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 +1977 -1021
- package/package.json +1 -1
- package/schemas/foh-template.schema.json +52 -1
package/dist/foh.js
CHANGED
|
@@ -6046,7 +6046,7 @@ var require_compile = __commonJS({
|
|
|
6046
6046
|
const schOrFunc = root.refs[ref];
|
|
6047
6047
|
if (schOrFunc)
|
|
6048
6048
|
return schOrFunc;
|
|
6049
|
-
let _sch =
|
|
6049
|
+
let _sch = resolve16.call(this, root, ref);
|
|
6050
6050
|
if (_sch === void 0) {
|
|
6051
6051
|
const schema2 = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
|
|
6052
6052
|
const { schemaId } = this.opts;
|
|
@@ -6073,7 +6073,7 @@ var require_compile = __commonJS({
|
|
|
6073
6073
|
function sameSchemaEnv(s1, s2) {
|
|
6074
6074
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
6075
6075
|
}
|
|
6076
|
-
function
|
|
6076
|
+
function resolve16(root, ref) {
|
|
6077
6077
|
let sch;
|
|
6078
6078
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
6079
6079
|
ref = sch;
|
|
@@ -6648,7 +6648,7 @@ var require_fast_uri = __commonJS({
|
|
|
6648
6648
|
}
|
|
6649
6649
|
return uri;
|
|
6650
6650
|
}
|
|
6651
|
-
function
|
|
6651
|
+
function resolve16(baseURI, relativeURI, options) {
|
|
6652
6652
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
6653
6653
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
6654
6654
|
schemelessOptions.skipEscape = true;
|
|
@@ -6875,7 +6875,7 @@ var require_fast_uri = __commonJS({
|
|
|
6875
6875
|
var fastUri = {
|
|
6876
6876
|
SCHEMES,
|
|
6877
6877
|
normalize,
|
|
6878
|
-
resolve:
|
|
6878
|
+
resolve: resolve16,
|
|
6879
6879
|
resolveComponent,
|
|
6880
6880
|
equal,
|
|
6881
6881
|
serialize,
|
|
@@ -10183,21 +10183,21 @@ async function promptLine(label, {
|
|
|
10183
10183
|
allowEmpty = false,
|
|
10184
10184
|
defaultValue
|
|
10185
10185
|
} = {}) {
|
|
10186
|
-
return await new Promise((
|
|
10186
|
+
return await new Promise((resolve16) => {
|
|
10187
10187
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
10188
10188
|
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
|
|
10189
10189
|
rl.question(`${label}${suffix}: `, (answer) => {
|
|
10190
10190
|
rl.close();
|
|
10191
10191
|
const value = String(answer ?? "").trim();
|
|
10192
10192
|
if (!value && typeof defaultValue === "string") {
|
|
10193
|
-
|
|
10193
|
+
resolve16(defaultValue);
|
|
10194
10194
|
return;
|
|
10195
10195
|
}
|
|
10196
10196
|
if (!value && !allowEmpty) {
|
|
10197
|
-
|
|
10197
|
+
resolve16("");
|
|
10198
10198
|
return;
|
|
10199
10199
|
}
|
|
10200
|
-
|
|
10200
|
+
resolve16(value);
|
|
10201
10201
|
});
|
|
10202
10202
|
});
|
|
10203
10203
|
}
|
|
@@ -10205,7 +10205,7 @@ async function promptSecret(label) {
|
|
|
10205
10205
|
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
10206
10206
|
return await promptLine(label);
|
|
10207
10207
|
}
|
|
10208
|
-
return await new Promise((
|
|
10208
|
+
return await new Promise((resolve16) => {
|
|
10209
10209
|
const stdin = process.stdin;
|
|
10210
10210
|
const stdout = process.stdout;
|
|
10211
10211
|
const wasRaw = Boolean(stdin.isRaw);
|
|
@@ -10219,7 +10219,7 @@ async function promptSecret(label) {
|
|
|
10219
10219
|
const finish = () => {
|
|
10220
10220
|
cleanup();
|
|
10221
10221
|
stdout.write("\n");
|
|
10222
|
-
|
|
10222
|
+
resolve16(value);
|
|
10223
10223
|
};
|
|
10224
10224
|
const onData = (chunk) => {
|
|
10225
10225
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
@@ -10228,7 +10228,7 @@ async function promptSecret(label) {
|
|
|
10228
10228
|
cleanup();
|
|
10229
10229
|
process.exitCode = 130;
|
|
10230
10230
|
stdout.write("\n");
|
|
10231
|
-
return
|
|
10231
|
+
return resolve16("");
|
|
10232
10232
|
}
|
|
10233
10233
|
if (char === "\r" || char === "\n") {
|
|
10234
10234
|
finish();
|
|
@@ -10583,7 +10583,7 @@ async function storeAuthenticatedSession(params) {
|
|
|
10583
10583
|
return output;
|
|
10584
10584
|
}
|
|
10585
10585
|
function sleep(ms) {
|
|
10586
|
-
return new Promise((
|
|
10586
|
+
return new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
10587
10587
|
}
|
|
10588
10588
|
function hasExplicitTimeoutFlag(argv = process.argv) {
|
|
10589
10589
|
return argv.some((arg) => arg === "--timeout-seconds" || arg.startsWith("--timeout-seconds="));
|
|
@@ -11141,7 +11141,7 @@ async function pollUntil(check2, opts) {
|
|
|
11141
11141
|
}
|
|
11142
11142
|
}
|
|
11143
11143
|
function sleep2(ms) {
|
|
11144
|
-
return new Promise((
|
|
11144
|
+
return new Promise((resolve16) => setTimeout(resolve16, ms));
|
|
11145
11145
|
}
|
|
11146
11146
|
|
|
11147
11147
|
// src/commands/compliance.ts
|
|
@@ -14265,8 +14265,8 @@ function registerAgentGuardrailCommands(agent) {
|
|
|
14265
14265
|
try {
|
|
14266
14266
|
rule = JSON.parse(opts.rule);
|
|
14267
14267
|
} catch {
|
|
14268
|
-
const { readFileSync:
|
|
14269
|
-
rule = JSON.parse(
|
|
14268
|
+
const { readFileSync: readFileSync20 } = await import("fs");
|
|
14269
|
+
rule = JSON.parse(readFileSync20(opts.rule, "utf-8"));
|
|
14270
14270
|
}
|
|
14271
14271
|
const data = await apiFetch(`/v1/console/agents/${opts.agent}/guardrails`, {
|
|
14272
14272
|
method: "POST",
|
|
@@ -14835,9 +14835,9 @@ function registerAgent(program3) {
|
|
|
14835
14835
|
process.stdout.write(yaml);
|
|
14836
14836
|
return;
|
|
14837
14837
|
}
|
|
14838
|
-
const { writeFileSync:
|
|
14838
|
+
const { writeFileSync: writeFileSync16 } = await import("fs");
|
|
14839
14839
|
const outputPath = opts.output ?? "tenant.yaml";
|
|
14840
|
-
|
|
14840
|
+
writeFileSync16(
|
|
14841
14841
|
outputPath,
|
|
14842
14842
|
`# tenant.yaml - Front Of House agent manifest
|
|
14843
14843
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -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,
|
|
@@ -15368,9 +15377,6 @@ function registerInstagramChannelCommands(instagram, addCommonOptions) {
|
|
|
15368
15377
|
}));
|
|
15369
15378
|
}
|
|
15370
15379
|
|
|
15371
|
-
// src/commands/channel-whatsapp.ts
|
|
15372
|
-
var import_node_fs2 = require("node:fs");
|
|
15373
|
-
|
|
15374
15380
|
// src/commands/channel-whatsapp-helpers.ts
|
|
15375
15381
|
function parsePositiveNumber(value, fallback) {
|
|
15376
15382
|
if (value === void 0 || value === null || String(value).trim() === "") return fallback;
|
|
@@ -15403,7 +15409,7 @@ function buildReasonedNextSteps({
|
|
|
15403
15409
|
const verifyTokenValue = verifyToken || "<verify_token>";
|
|
15404
15410
|
if (has("whatsapp_channel_not_ready") || has("whatsapp_access_token_missing") || has("whatsapp_verify_token_missing") || has("whatsapp_app_secret_missing")) {
|
|
15405
15411
|
steps.push(
|
|
15406
|
-
"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>"
|
|
15407
15413
|
);
|
|
15408
15414
|
}
|
|
15409
15415
|
if (has("whatsapp_verify_check_failed") || has("whatsapp_webhook_challenge_failed")) {
|
|
@@ -15549,6 +15555,9 @@ function resolveLiveProof({
|
|
|
15549
15555
|
};
|
|
15550
15556
|
}
|
|
15551
15557
|
|
|
15558
|
+
// src/commands/channel-whatsapp-onboarding.ts
|
|
15559
|
+
var import_node_fs2 = require("node:fs");
|
|
15560
|
+
|
|
15552
15561
|
// src/commands/channel-whatsapp-setup.ts
|
|
15553
15562
|
var import_node_crypto = require("node:crypto");
|
|
15554
15563
|
var WHATSAPP_WEBHOOK_CHALLENGE_TIMEOUT_MS = 1e4;
|
|
@@ -15657,7 +15666,7 @@ function assertProofPass(strict, reasons) {
|
|
|
15657
15666
|
}
|
|
15658
15667
|
}
|
|
15659
15668
|
|
|
15660
|
-
// src/commands/channel-whatsapp.ts
|
|
15669
|
+
// src/commands/channel-whatsapp-onboarding.ts
|
|
15661
15670
|
async function runWhatsAppOnboardingSession(params) {
|
|
15662
15671
|
return await apiFetch("/v1/console/channels/whatsapp/onboarding-session", {
|
|
15663
15672
|
method: "POST",
|
|
@@ -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) {
|
|
@@ -15765,11 +15756,11 @@ async function runWhatsAppOnboardingWizard(opts) {
|
|
|
15765
15756
|
}
|
|
15766
15757
|
]
|
|
15767
15758
|
});
|
|
15768
|
-
|
|
15759
|
+
const accessToken = String(wizardState.accessToken || "").trim();
|
|
15769
15760
|
let wabaId = String(opts.wabaId || "").trim();
|
|
15770
15761
|
let phoneNumberId = String(opts.phoneNumberId || "").trim();
|
|
15771
|
-
|
|
15772
|
-
|
|
15762
|
+
const verifyToken = String(opts.verifyToken || "").trim();
|
|
15763
|
+
const appSecret = String(wizardState.appSecret || "").trim();
|
|
15773
15764
|
if (!accessToken) {
|
|
15774
15765
|
throw new FohError({
|
|
15775
15766
|
step: "channel.whatsapp.onboard",
|
|
@@ -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
|
-
function
|
|
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
|
-
}));
|
|
15840
|
+
function registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions) {
|
|
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,11 @@ function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
|
|
|
15993
15943
|
results
|
|
15994
15944
|
}, { json: opts.json ?? false });
|
|
15995
15945
|
}));
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
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
|
-
}));
|
|
15946
|
+
}
|
|
15947
|
+
|
|
15948
|
+
// src/commands/channel-whatsapp.ts
|
|
15949
|
+
function registerWhatsAppChannelCommands(whatsapp, addCommonOptions) {
|
|
15950
|
+
registerWhatsAppOnboardingCommands(whatsapp, addCommonOptions);
|
|
16065
15951
|
addCommonOptions(
|
|
16066
15952
|
whatsapp.command("status").description("Get WhatsApp channel status")
|
|
16067
15953
|
).action(async (opts) => withCommandErrorHandling(async () => {
|
|
@@ -16438,11 +16324,11 @@ function registerVoice(program3) {
|
|
|
16438
16324
|
}
|
|
16439
16325
|
const outputPath = String(opts.out || `foh-voice-preview-${provider}-${voiceId}.mp3`).trim();
|
|
16440
16326
|
const audio = Buffer.from(await res.arrayBuffer());
|
|
16441
|
-
const { mkdirSync:
|
|
16442
|
-
const { dirname: dirname13, resolve:
|
|
16443
|
-
const absolutePath =
|
|
16444
|
-
|
|
16445
|
-
|
|
16327
|
+
const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync16 } = await import("fs");
|
|
16328
|
+
const { dirname: dirname13, resolve: resolve16 } = await import("path");
|
|
16329
|
+
const absolutePath = resolve16(outputPath);
|
|
16330
|
+
mkdirSync11(dirname13(absolutePath), { recursive: true });
|
|
16331
|
+
writeFileSync16(absolutePath, audio);
|
|
16446
16332
|
format({
|
|
16447
16333
|
status: "ok",
|
|
16448
16334
|
provider,
|
|
@@ -30933,7 +30819,7 @@ var Protocol = class {
|
|
|
30933
30819
|
return;
|
|
30934
30820
|
}
|
|
30935
30821
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
30936
|
-
await new Promise((
|
|
30822
|
+
await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
|
|
30937
30823
|
options?.signal?.throwIfAborted();
|
|
30938
30824
|
}
|
|
30939
30825
|
} catch (error2) {
|
|
@@ -30950,7 +30836,7 @@ var Protocol = class {
|
|
|
30950
30836
|
*/
|
|
30951
30837
|
request(request, resultSchema, options) {
|
|
30952
30838
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
30953
|
-
return new Promise((
|
|
30839
|
+
return new Promise((resolve16, reject) => {
|
|
30954
30840
|
const earlyReject = (error2) => {
|
|
30955
30841
|
reject(error2);
|
|
30956
30842
|
};
|
|
@@ -31028,7 +30914,7 @@ var Protocol = class {
|
|
|
31028
30914
|
if (!parseResult.success) {
|
|
31029
30915
|
reject(parseResult.error);
|
|
31030
30916
|
} else {
|
|
31031
|
-
|
|
30917
|
+
resolve16(parseResult.data);
|
|
31032
30918
|
}
|
|
31033
30919
|
} catch (error2) {
|
|
31034
30920
|
reject(error2);
|
|
@@ -31289,12 +31175,12 @@ var Protocol = class {
|
|
|
31289
31175
|
}
|
|
31290
31176
|
} catch {
|
|
31291
31177
|
}
|
|
31292
|
-
return new Promise((
|
|
31178
|
+
return new Promise((resolve16, reject) => {
|
|
31293
31179
|
if (signal.aborted) {
|
|
31294
31180
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
31295
31181
|
return;
|
|
31296
31182
|
}
|
|
31297
|
-
const timeoutId = setTimeout(
|
|
31183
|
+
const timeoutId = setTimeout(resolve16, interval);
|
|
31298
31184
|
signal.addEventListener("abort", () => {
|
|
31299
31185
|
clearTimeout(timeoutId);
|
|
31300
31186
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -32394,7 +32280,7 @@ var McpServer = class {
|
|
|
32394
32280
|
let task = createTaskResult.task;
|
|
32395
32281
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
32396
32282
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
32397
|
-
await new Promise((
|
|
32283
|
+
await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
|
|
32398
32284
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
32399
32285
|
if (!updatedTask) {
|
|
32400
32286
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -33043,19 +32929,19 @@ var StdioServerTransport = class {
|
|
|
33043
32929
|
this.onclose?.();
|
|
33044
32930
|
}
|
|
33045
32931
|
send(message) {
|
|
33046
|
-
return new Promise((
|
|
32932
|
+
return new Promise((resolve16) => {
|
|
33047
32933
|
const json3 = serializeMessage(message);
|
|
33048
32934
|
if (this._stdout.write(json3)) {
|
|
33049
|
-
|
|
32935
|
+
resolve16();
|
|
33050
32936
|
} else {
|
|
33051
|
-
this._stdout.once("drain",
|
|
32937
|
+
this._stdout.once("drain", resolve16);
|
|
33052
32938
|
}
|
|
33053
32939
|
});
|
|
33054
32940
|
}
|
|
33055
32941
|
};
|
|
33056
32942
|
|
|
33057
32943
|
// src/lib/cli-version.ts
|
|
33058
|
-
var injectedVersion = true ? String("0.1.
|
|
32944
|
+
var injectedVersion = true ? String("0.1.88").trim() : "";
|
|
33059
32945
|
var envVersion = String(process.env.FOH_CLI_VERSION || process.env.npm_package_version || "").trim();
|
|
33060
32946
|
var CLI_VERSION = injectedVersion || envVersion || "0.0.0-dev";
|
|
33061
32947
|
|
|
@@ -33242,7 +33128,7 @@ async function runFohCli(params) {
|
|
|
33242
33128
|
effectiveArgv.push("--json");
|
|
33243
33129
|
}
|
|
33244
33130
|
const command = `foh ${effectiveArgv.join(" ")}`;
|
|
33245
|
-
return await new Promise((
|
|
33131
|
+
return await new Promise((resolve16) => {
|
|
33246
33132
|
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, ...effectiveArgv], {
|
|
33247
33133
|
stdio: ["ignore", "pipe", "pipe"],
|
|
33248
33134
|
env: {
|
|
@@ -33267,7 +33153,7 @@ async function runFohCli(params) {
|
|
|
33267
33153
|
});
|
|
33268
33154
|
child.once("error", (error2) => {
|
|
33269
33155
|
clearTimeout(timeoutHandle);
|
|
33270
|
-
|
|
33156
|
+
resolve16({
|
|
33271
33157
|
ok: false,
|
|
33272
33158
|
command,
|
|
33273
33159
|
argv: effectiveArgv,
|
|
@@ -33283,7 +33169,7 @@ async function runFohCli(params) {
|
|
|
33283
33169
|
const stderrText = finalizeBoundedText(stderrBuffer);
|
|
33284
33170
|
const exitCode = Number.isFinite(code ?? NaN) ? Number(code) : 1;
|
|
33285
33171
|
const stdoutJson = tryParseJson(stdoutText);
|
|
33286
|
-
|
|
33172
|
+
resolve16({
|
|
33287
33173
|
ok: !timedOut && exitCode === 0,
|
|
33288
33174
|
command,
|
|
33289
33175
|
argv: effectiveArgv,
|
|
@@ -34877,8 +34763,190 @@ function writeSetupRunReport(reportPayload, reportOut) {
|
|
|
34877
34763
|
}
|
|
34878
34764
|
|
|
34879
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
|
+
}
|
|
34880
34944
|
function registerSetup(program3) {
|
|
34881
|
-
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
|
+
}
|
|
34882
34950
|
if (!opts.org) {
|
|
34883
34951
|
try {
|
|
34884
34952
|
opts.org = loadCredentials(opts.apiUrl).orgId;
|
|
@@ -35453,8 +35521,8 @@ function registerSetup(program3) {
|
|
|
35453
35521
|
}
|
|
35454
35522
|
try {
|
|
35455
35523
|
const manifest = await agentExport(resolvedAgentId, { apiUrlOverride: opts.apiUrl });
|
|
35456
|
-
const { writeFileSync:
|
|
35457
|
-
|
|
35524
|
+
const { writeFileSync: writeFileSync16 } = await import("fs");
|
|
35525
|
+
writeFileSync16(
|
|
35458
35526
|
"tenant.yaml",
|
|
35459
35527
|
`# tenant.yaml - Front Of House agent manifest
|
|
35460
35528
|
# Edit this file and run: foh plan tenant.yaml
|
|
@@ -35624,8 +35692,8 @@ function registerSim(program3) {
|
|
|
35624
35692
|
}
|
|
35625
35693
|
const cert = response.certificate;
|
|
35626
35694
|
if (opts.out) {
|
|
35627
|
-
const { writeFileSync:
|
|
35628
|
-
|
|
35695
|
+
const { writeFileSync: writeFileSync16 } = await import("fs");
|
|
35696
|
+
writeFileSync16(opts.out, JSON.stringify(cert, null, 2) + "\n", "utf-8");
|
|
35629
35697
|
process.stderr.write(` Certificate written to ${opts.out}
|
|
35630
35698
|
`);
|
|
35631
35699
|
}
|
|
@@ -35675,8 +35743,8 @@ function registerSim(program3) {
|
|
|
35675
35743
|
});
|
|
35676
35744
|
}
|
|
35677
35745
|
if (opts.out) {
|
|
35678
|
-
const { writeFileSync:
|
|
35679
|
-
|
|
35746
|
+
const { writeFileSync: writeFileSync16 } = await import("fs");
|
|
35747
|
+
writeFileSync16(opts.out, JSON.stringify(response.certificate, null, 2) + "\n", "utf-8");
|
|
35680
35748
|
process.stderr.write(` Final certificate written to ${opts.out}
|
|
35681
35749
|
`);
|
|
35682
35750
|
}
|
|
@@ -35733,7 +35801,7 @@ function defaultAdaptiveRuns(profile) {
|
|
|
35733
35801
|
if (profile === "stress") return 30;
|
|
35734
35802
|
return 5;
|
|
35735
35803
|
}
|
|
35736
|
-
function
|
|
35804
|
+
function csv2(raw) {
|
|
35737
35805
|
if (!raw) return void 0;
|
|
35738
35806
|
const values = String(raw).split(",").map((value) => value.trim()).filter(Boolean);
|
|
35739
35807
|
return values.length > 0 ? values : void 0;
|
|
@@ -35759,8 +35827,8 @@ function registerCertify(program3) {
|
|
|
35759
35827
|
body: JSON.stringify({
|
|
35760
35828
|
mode,
|
|
35761
35829
|
adaptive_runs: adaptiveRuns,
|
|
35762
|
-
journeys:
|
|
35763
|
-
scenario_ids:
|
|
35830
|
+
journeys: csv2(opts.journeys),
|
|
35831
|
+
scenario_ids: csv2(opts.scenarioIds),
|
|
35764
35832
|
channel: channel(opts.channel)
|
|
35765
35833
|
}),
|
|
35766
35834
|
apiUrlOverride: opts.apiUrl
|
|
@@ -38109,6 +38177,9 @@ function fail(name, reasonCode, error2, nextCommand) {
|
|
|
38109
38177
|
function skipped(name, reasonCode, summary, nextCommand) {
|
|
38110
38178
|
return { name, category: categoryForCheck(name), status: "skipped", reason_code: reasonCode, summary, next_command: nextCommand };
|
|
38111
38179
|
}
|
|
38180
|
+
function defaultCertificationProfileForMission(mission) {
|
|
38181
|
+
return mission === "publish" ? "release" : "smoke";
|
|
38182
|
+
}
|
|
38112
38183
|
function hasBlockingChecks(checks) {
|
|
38113
38184
|
return checks.some((check2) => check2.status === "hold" || check2.status === "fail");
|
|
38114
38185
|
}
|
|
@@ -38446,11 +38517,12 @@ function registerProve(program3) {
|
|
|
38446
38517
|
if (opts.skipCert) {
|
|
38447
38518
|
checks.push(skipped("simulation_certification", "operator_skipped", "Skipped by --skip-cert.", `foh certify run --agent ${ctx.agentId} --profile release --json`));
|
|
38448
38519
|
} else if (!opts.includeCertification) {
|
|
38520
|
+
const certificationProfile = defaultCertificationProfileForMission(mission);
|
|
38449
38521
|
checks.push(skipped(
|
|
38450
38522
|
"simulation_certification",
|
|
38451
38523
|
"certification_explicitly_required",
|
|
38452
|
-
"Runtime proof does not run release certification by default.",
|
|
38453
|
-
`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`
|
|
38454
38526
|
));
|
|
38455
38527
|
} else {
|
|
38456
38528
|
try {
|
|
@@ -38542,6 +38614,808 @@ function registerProve(program3) {
|
|
|
38542
38614
|
}));
|
|
38543
38615
|
}
|
|
38544
38616
|
|
|
38617
|
+
// src/commands/objective.ts
|
|
38618
|
+
var import_node_fs5 = require("node:fs");
|
|
38619
|
+
var import_node_path2 = require("node:path");
|
|
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";
|
|
38623
|
+
function asRecord3(value) {
|
|
38624
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
38625
|
+
}
|
|
38626
|
+
function normalizeString2(value) {
|
|
38627
|
+
return typeof value === "string" ? value.trim() : "";
|
|
38628
|
+
}
|
|
38629
|
+
function uniqueStrings(values) {
|
|
38630
|
+
return Array.from(new Set(values.map(normalizeString2).filter(Boolean)));
|
|
38631
|
+
}
|
|
38632
|
+
function asArray(value) {
|
|
38633
|
+
return Array.isArray(value) ? value : [];
|
|
38634
|
+
}
|
|
38635
|
+
function collectStringArrays(value, keys, output = []) {
|
|
38636
|
+
if (!value || typeof value !== "object") return output;
|
|
38637
|
+
if (Array.isArray(value)) {
|
|
38638
|
+
for (const item of value) collectStringArrays(item, keys, output);
|
|
38639
|
+
return output;
|
|
38640
|
+
}
|
|
38641
|
+
for (const [key, child] of Object.entries(value)) {
|
|
38642
|
+
if (keys.has(key) && Array.isArray(child)) {
|
|
38643
|
+
for (const item of child) {
|
|
38644
|
+
if (typeof item === "string") output.push(item);
|
|
38645
|
+
}
|
|
38646
|
+
} else if (child && typeof child === "object") {
|
|
38647
|
+
collectStringArrays(child, keys, output);
|
|
38648
|
+
}
|
|
38649
|
+
}
|
|
38650
|
+
return output;
|
|
38651
|
+
}
|
|
38652
|
+
function collectArtifactRefs(value, output = []) {
|
|
38653
|
+
if (!value || typeof value !== "object") return output;
|
|
38654
|
+
if (Array.isArray(value)) {
|
|
38655
|
+
for (const item of value) collectArtifactRefs(item, output);
|
|
38656
|
+
return output;
|
|
38657
|
+
}
|
|
38658
|
+
for (const [key, child] of Object.entries(value)) {
|
|
38659
|
+
if (typeof child === "string") {
|
|
38660
|
+
const normalized = child.trim();
|
|
38661
|
+
if (normalized && (key.toLowerCase().includes("artifact") || key.toLowerCase().includes("report") || normalized.includes("test-results/") || normalized.includes("test-results\\"))) {
|
|
38662
|
+
output.push(normalized);
|
|
38663
|
+
}
|
|
38664
|
+
} else if (child && typeof child === "object") {
|
|
38665
|
+
collectArtifactRefs(child, output);
|
|
38666
|
+
}
|
|
38667
|
+
}
|
|
38668
|
+
return output;
|
|
38669
|
+
}
|
|
38670
|
+
function summarizeSourceReport(value) {
|
|
38671
|
+
const report = asRecord3(value);
|
|
38672
|
+
return {
|
|
38673
|
+
decision: firstString(report, ["decision", "current_decision", "status"]) || null,
|
|
38674
|
+
go_live_allowed: report.go_live_allowed === true || report.customer_live_ready === true || report.production_ready === true,
|
|
38675
|
+
allowed_mode: firstString(report, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || null,
|
|
38676
|
+
blocked_modes: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["blocked_modes"]))),
|
|
38677
|
+
reason_codes: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))),
|
|
38678
|
+
next_commands: uniqueStrings(collectStringArrays(report, /* @__PURE__ */ new Set(["next_commands"]))),
|
|
38679
|
+
evidence_refs: uniqueStrings(collectArtifactRefs(report))
|
|
38680
|
+
};
|
|
38681
|
+
}
|
|
38682
|
+
function finiteNumber(value) {
|
|
38683
|
+
const number3 = Number(value);
|
|
38684
|
+
return Number.isFinite(number3) ? number3 : null;
|
|
38685
|
+
}
|
|
38686
|
+
function normalizeCustomerEvidenceActions(value) {
|
|
38687
|
+
return asArray(value).map(asRecord3).map((action) => ({
|
|
38688
|
+
id: firstString(action, ["id", "action_id"]),
|
|
38689
|
+
title: firstString(action, ["title"]) || null,
|
|
38690
|
+
owner: firstString(action, ["owner"]) || null,
|
|
38691
|
+
blocker_count: finiteNumber(action.blocker_count) ?? (uniqueStrings(asArray(action.reason_codes)).length || null),
|
|
38692
|
+
target_evidence_paths: uniqueStrings([
|
|
38693
|
+
...asArray(action.target_evidence_paths),
|
|
38694
|
+
firstString(action, ["target_evidence_path"])
|
|
38695
|
+
]),
|
|
38696
|
+
validator_commands: uniqueStrings([
|
|
38697
|
+
...asArray(action.validator_commands),
|
|
38698
|
+
...asArray(action.next_commands)
|
|
38699
|
+
]),
|
|
38700
|
+
required_evidence: firstString(action, ["required_evidence"]) || null,
|
|
38701
|
+
unlocks: firstString(action, ["unlocks"]) || null
|
|
38702
|
+
})).filter((action) => normalizeString2(action.id));
|
|
38703
|
+
}
|
|
38704
|
+
function normalizeCustomerEvidenceActionPacket(value) {
|
|
38705
|
+
const packet = asRecord3(value);
|
|
38706
|
+
const actions = normalizeCustomerEvidenceActions(packet.actions);
|
|
38707
|
+
if (actions.length === 0) return void 0;
|
|
38708
|
+
return {
|
|
38709
|
+
packet_role: firstString(packet, ["packet_role"]) || "compressed_customer_operator_action_plan",
|
|
38710
|
+
action_count: finiteNumber(packet.action_count) ?? actions.length,
|
|
38711
|
+
external_hold_count: finiteNumber(packet.external_hold_count),
|
|
38712
|
+
raw_reason_code_count: finiteNumber(packet.raw_reason_code_count),
|
|
38713
|
+
compression_ratio: finiteNumber(packet.compression_ratio),
|
|
38714
|
+
actions,
|
|
38715
|
+
validation_commands: uniqueStrings(asArray(packet.validation_commands)),
|
|
38716
|
+
instructions: uniqueStrings(asArray(packet.instructions))
|
|
38717
|
+
};
|
|
38718
|
+
}
|
|
38719
|
+
function customerEvidenceActionPacketFromSources(...sources) {
|
|
38720
|
+
for (const source of sources) {
|
|
38721
|
+
const report = asRecord3(source);
|
|
38722
|
+
const requestPackets = asRecord3(report.request_packets);
|
|
38723
|
+
const operatorStatus = asRecord3(report.operator_status);
|
|
38724
|
+
const candidates = [
|
|
38725
|
+
requestPackets.customer_operator_action_packet,
|
|
38726
|
+
report.customer_operator_action_packet,
|
|
38727
|
+
report.customer_evidence_action_packet,
|
|
38728
|
+
operatorStatus.customer_operator_action_packet,
|
|
38729
|
+
operatorStatus.customer_evidence_action_packet,
|
|
38730
|
+
operatorStatus.customer_actions ? {
|
|
38731
|
+
packet_role: "compressed_customer_operator_action_plan",
|
|
38732
|
+
action_count: asArray(operatorStatus.customer_actions).length,
|
|
38733
|
+
actions: operatorStatus.customer_actions,
|
|
38734
|
+
validation_commands: collectStringArrays(operatorStatus, /* @__PURE__ */ new Set(["next_commands"]))
|
|
38735
|
+
} : null,
|
|
38736
|
+
report.customer_actions ? { actions: report.customer_actions } : null
|
|
38737
|
+
];
|
|
38738
|
+
for (const candidate of candidates) {
|
|
38739
|
+
const normalized = normalizeCustomerEvidenceActionPacket(candidate);
|
|
38740
|
+
if (normalized) return normalized;
|
|
38741
|
+
}
|
|
38742
|
+
}
|
|
38743
|
+
return void 0;
|
|
38744
|
+
}
|
|
38745
|
+
function normalizeObjectiveArtifactPath(value) {
|
|
38746
|
+
return value.trim() ? (0, import_node_path2.resolve)(value.trim()) : "";
|
|
38747
|
+
}
|
|
38748
|
+
function resolveObjectiveArtifactPath(value) {
|
|
38749
|
+
return normalizeObjectiveArtifactPath(value);
|
|
38750
|
+
}
|
|
38751
|
+
function firstNonEmptyObject(value) {
|
|
38752
|
+
const record2 = asRecord3(value);
|
|
38753
|
+
return Object.keys(record2).length > 0 ? record2 : void 0;
|
|
38754
|
+
}
|
|
38755
|
+
function pickEvidencePacket(value) {
|
|
38756
|
+
const record2 = firstNonEmptyObject(value);
|
|
38757
|
+
if (!record2) return void 0;
|
|
38758
|
+
const payload = firstNonEmptyObject(record2.payload);
|
|
38759
|
+
return payload ?? record2;
|
|
38760
|
+
}
|
|
38761
|
+
function readEvidencePacketFromPlan(path2) {
|
|
38762
|
+
const plan = asRecord3(readJsonArtifact(path2));
|
|
38763
|
+
const sourceReports = asRecord3(plan.source_reports);
|
|
38764
|
+
return pickEvidencePacket(plan.evidence) ?? pickEvidencePacket(plan.evidence_packet) ?? pickEvidencePacket(sourceReports.customer_live_status && asRecord3(sourceReports.customer_live_status).evidence) ?? pickEvidencePacket(asRecord3(sourceReports.setup_workflow).evidence_packet) ?? {};
|
|
38765
|
+
}
|
|
38766
|
+
function inferEvidenceFromProofPaths(evidence) {
|
|
38767
|
+
const record2 = asRecord3(evidence);
|
|
38768
|
+
if (!record2 || Object.keys(record2).length === 0) return evidence;
|
|
38769
|
+
return record2.evidence ?? evidence;
|
|
38770
|
+
}
|
|
38771
|
+
async function resolveEvidenceInput(opts) {
|
|
38772
|
+
if (opts.evidence) {
|
|
38773
|
+
return inferEvidenceFromProofPaths(await parseJsonOption(opts.evidence, "--evidence"));
|
|
38774
|
+
}
|
|
38775
|
+
if (!opts.plan) {
|
|
38776
|
+
throw new FohError({
|
|
38777
|
+
step: "objective.apply",
|
|
38778
|
+
error: "Missing objective evidence input: pass --evidence or --plan.",
|
|
38779
|
+
remediation: "Provide either --evidence <json|@file> or --plan <path-to-plan-json>.",
|
|
38780
|
+
statusCode: 400
|
|
38781
|
+
});
|
|
38782
|
+
}
|
|
38783
|
+
const planPath = resolveObjectiveArtifactPath(opts.plan);
|
|
38784
|
+
return inferEvidenceFromProofPaths(readEvidencePacketFromPlan(planPath));
|
|
38785
|
+
}
|
|
38786
|
+
function firstString(record2, keys) {
|
|
38787
|
+
for (const key of keys) {
|
|
38788
|
+
const value = normalizeString2(record2[key]);
|
|
38789
|
+
if (value) return value;
|
|
38790
|
+
}
|
|
38791
|
+
return "";
|
|
38792
|
+
}
|
|
38793
|
+
function firstStringFromUnknown(value, keys) {
|
|
38794
|
+
return firstString(asRecord3(value), keys);
|
|
38795
|
+
}
|
|
38796
|
+
function truthy(record2, keys) {
|
|
38797
|
+
return keys.some((key) => record2[key] === true);
|
|
38798
|
+
}
|
|
38799
|
+
function statusFromDecision(value) {
|
|
38800
|
+
const normalized = value.toLowerCase();
|
|
38801
|
+
if (/fail|failed|error/.test(normalized)) return "fail";
|
|
38802
|
+
if (/ready|pass|passed|success|go_live/.test(normalized)) return "pass";
|
|
38803
|
+
return "hold";
|
|
38804
|
+
}
|
|
38805
|
+
function firstCustomerEvidenceAction(packet) {
|
|
38806
|
+
const actions = asArray(packet?.actions).map(asRecord3).filter((action) => normalizeString2(action.id));
|
|
38807
|
+
return actions[0] ?? null;
|
|
38808
|
+
}
|
|
38809
|
+
function buildDeveloperReadinessPacket(input) {
|
|
38810
|
+
const businessName = resolveBusinessName(input.opts);
|
|
38811
|
+
const sourceUrl = normalizeString2(input.opts.sourceUrl);
|
|
38812
|
+
const businessObjective = normalizeString2(input.opts.businessObjective);
|
|
38813
|
+
const location = resolveLocation(input.opts);
|
|
38814
|
+
const tools = parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
|
|
38815
|
+
const setupDecision = firstString(input.setupWorkflow, ["decision", "current_decision", "status"]);
|
|
38816
|
+
const liveDecision = firstString(input.customerLiveStatus, ["decision", "current_decision", "status"]);
|
|
38817
|
+
const explicitGoLive = truthy(input.customerLiveStatus, ["go_live_allowed", "customer_live_ready", "production_ready"]) || truthy(input.setupWorkflow, ["go_live_allowed", "customer_live_ready", "production_ready"]);
|
|
38818
|
+
const firstAction = firstCustomerEvidenceAction(input.customerEvidenceActionPacket);
|
|
38819
|
+
const firstActionValidator = firstAction ? uniqueStrings(asArray(firstAction.validator_commands))[0] ?? null : null;
|
|
38820
|
+
const firstNextCommand = input.nextCommands[0] ?? null;
|
|
38821
|
+
return {
|
|
38822
|
+
schema_version: "developer_objective_readiness_packet.v1",
|
|
38823
|
+
packet_role: "coding_agent_objective_readiness",
|
|
38824
|
+
decision: input.classification.status,
|
|
38825
|
+
reason_code: input.classification.reasonCode,
|
|
38826
|
+
business_context: {
|
|
38827
|
+
business_name: businessName || null,
|
|
38828
|
+
business_objective: businessObjective || null,
|
|
38829
|
+
industry: objectiveIndustryOrNull(input.opts),
|
|
38830
|
+
source_url: sourceUrl || null,
|
|
38831
|
+
location: location || null,
|
|
38832
|
+
target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
|
|
38833
|
+
tools
|
|
38834
|
+
},
|
|
38835
|
+
input_completeness: {
|
|
38836
|
+
business_name: Boolean(businessName),
|
|
38837
|
+
source_url: Boolean(sourceUrl),
|
|
38838
|
+
business_objective: Boolean(businessObjective),
|
|
38839
|
+
requested_tools: tools.length > 0,
|
|
38840
|
+
target_mode: Boolean(normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial")
|
|
38841
|
+
},
|
|
38842
|
+
readiness_dimensions: {
|
|
38843
|
+
setup_workflow: {
|
|
38844
|
+
status: statusFromDecision(setupDecision),
|
|
38845
|
+
decision: setupDecision || null
|
|
38846
|
+
},
|
|
38847
|
+
customer_live_status: {
|
|
38848
|
+
status: explicitGoLive ? "pass" : statusFromDecision(liveDecision),
|
|
38849
|
+
decision: liveDecision || null
|
|
38850
|
+
},
|
|
38851
|
+
customer_evidence_actions: {
|
|
38852
|
+
status: firstAction ? "hold" : "pass",
|
|
38853
|
+
action_count: finiteNumber(input.customerEvidenceActionPacket?.action_count) ?? asArray(input.customerEvidenceActionPacket?.actions).length,
|
|
38854
|
+
first_action_id: firstAction ? normalizeString2(firstAction.id) : null,
|
|
38855
|
+
first_validator_command: firstActionValidator
|
|
38856
|
+
},
|
|
38857
|
+
agent_operability: {
|
|
38858
|
+
status: firstNextCommand ? "pass" : "fail",
|
|
38859
|
+
first_next_command: firstNextCommand,
|
|
38860
|
+
normal_path_requires_raw_artifacts: false
|
|
38861
|
+
}
|
|
38862
|
+
},
|
|
38863
|
+
next_command: firstNextCommand,
|
|
38864
|
+
claim_boundaries: {
|
|
38865
|
+
developer_setup_loop_ready: Boolean(firstNextCommand),
|
|
38866
|
+
customer_live_claim_allowed: explicitGoLive && input.classification.status === "pass",
|
|
38867
|
+
production_claim_allowed: explicitGoLive && input.classification.status === "pass",
|
|
38868
|
+
do_not_claim: explicitGoLive && input.classification.status === "pass" ? [] : ["customer-live", "production-ready", "fully autonomous customer setup"]
|
|
38869
|
+
},
|
|
38870
|
+
agent_instructions: [
|
|
38871
|
+
"Use this packet before source_reports or raw artifacts.",
|
|
38872
|
+
"Execute next_command first, then rerun objective status.",
|
|
38873
|
+
"Treat customer evidence actions as request work, not proof."
|
|
38874
|
+
]
|
|
38875
|
+
};
|
|
38876
|
+
}
|
|
38877
|
+
function resolveObjectiveReportPath(value) {
|
|
38878
|
+
const raw = normalizeString2(value);
|
|
38879
|
+
if (!raw || raw === "latest") return (0, import_node_path2.resolve)(DEFAULT_OBJECTIVE_REPORT_PATH);
|
|
38880
|
+
return (0, import_node_path2.resolve)(raw);
|
|
38881
|
+
}
|
|
38882
|
+
function writeJsonArtifact2(path2, value) {
|
|
38883
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path2.dirname)(path2), { recursive: true });
|
|
38884
|
+
(0, import_node_fs5.writeFileSync)(path2, `${JSON.stringify(value, null, 2)}
|
|
38885
|
+
`, "utf8");
|
|
38886
|
+
}
|
|
38887
|
+
function readJsonArtifact(path2) {
|
|
38888
|
+
return JSON.parse((0, import_node_fs5.readFileSync)(path2, "utf8"));
|
|
38889
|
+
}
|
|
38890
|
+
function buildSetupBody(opts) {
|
|
38891
|
+
const tools = parseCsvOption(opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? [];
|
|
38892
|
+
const businessName = resolveBusinessName(opts);
|
|
38893
|
+
const location = resolveLocation(opts);
|
|
38894
|
+
const body = {
|
|
38895
|
+
agency_name: businessName,
|
|
38896
|
+
business_objective: normalizeString2(opts.businessObjective) || null,
|
|
38897
|
+
requested_tool_surface: tools,
|
|
38898
|
+
target_exposure_mode: normalizeString2(opts.targetMode) || "customer_owned_voice_trial"
|
|
38899
|
+
};
|
|
38900
|
+
if (opts.sourceUrl) body.source_url = normalizeString2(opts.sourceUrl);
|
|
38901
|
+
if (location) body.branch_location = location;
|
|
38902
|
+
return body;
|
|
38903
|
+
}
|
|
38904
|
+
function buildStatusParams(opts) {
|
|
38905
|
+
const params = new URLSearchParams();
|
|
38906
|
+
const businessName = resolveBusinessName(opts);
|
|
38907
|
+
const location = resolveLocation(opts);
|
|
38908
|
+
if (opts.environment) params.set("environment", normalizeString2(opts.environment));
|
|
38909
|
+
if (businessName) params.set("agency_name", businessName);
|
|
38910
|
+
if (opts.sourceUrl) params.set("source_url", normalizeString2(opts.sourceUrl));
|
|
38911
|
+
if (location) params.set("branch_location", location);
|
|
38912
|
+
if (opts.tools) params.set("tools", normalizeString2(opts.tools));
|
|
38913
|
+
if (opts.targetMode) params.set("target_mode", normalizeString2(opts.targetMode));
|
|
38914
|
+
return params;
|
|
38915
|
+
}
|
|
38916
|
+
function resolveBusinessName(opts) {
|
|
38917
|
+
return normalizeString2(opts.businessName) || normalizeString2(opts.agencyName);
|
|
38918
|
+
}
|
|
38919
|
+
function resolveLocation(opts) {
|
|
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
|
+
};
|
|
39078
|
+
}
|
|
39079
|
+
function assertBusinessName(opts, step) {
|
|
39080
|
+
if (resolveBusinessName(opts)) return;
|
|
39081
|
+
throw new FohError({
|
|
39082
|
+
step,
|
|
39083
|
+
error: "Missing business name.",
|
|
39084
|
+
remediation: "Pass --business-name <name>. Legacy --agency-name <name> is still accepted.",
|
|
39085
|
+
statusCode: 400
|
|
39086
|
+
});
|
|
39087
|
+
}
|
|
39088
|
+
function classifyStatus(input) {
|
|
39089
|
+
const setup = asRecord3(input.setupWorkflow);
|
|
39090
|
+
const live = asRecord3(input.customerLiveStatus);
|
|
39091
|
+
const allReasonCodes = uniqueStrings([
|
|
39092
|
+
...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
|
|
39093
|
+
...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
|
|
39094
|
+
]);
|
|
39095
|
+
const setupDecision = firstString(setup, ["decision", "current_decision", "status"]);
|
|
39096
|
+
const liveDecision = firstString(live, ["decision", "current_decision", "status"]);
|
|
39097
|
+
const explicitGoLive = truthy(live, ["go_live_allowed", "customer_live_ready", "production_ready"]) || truthy(setup, ["go_live_allowed", "customer_live_ready", "production_ready"]);
|
|
39098
|
+
const hasBlockingReason = allReasonCodes.some((code) => !/_ok$|passed|ready|no_findings/.test(code));
|
|
39099
|
+
const hasHoldDecision = [setupDecision, liveDecision].some(
|
|
39100
|
+
(decision) => /hold|blocked|fail|failed|no_go|developer_supervised|founder_assisted/.test(decision.toLowerCase())
|
|
39101
|
+
);
|
|
39102
|
+
if (explicitGoLive && !hasBlockingReason && !hasHoldDecision) {
|
|
39103
|
+
return {
|
|
39104
|
+
status: "pass",
|
|
39105
|
+
reasonCode: "objective_ready",
|
|
39106
|
+
summary: "Objective evidence is ready for the requested launch mode."
|
|
39107
|
+
};
|
|
39108
|
+
}
|
|
39109
|
+
return {
|
|
39110
|
+
status: "hold",
|
|
39111
|
+
reasonCode: allReasonCodes[0] || "objective_needs_next_action",
|
|
39112
|
+
summary: "Objective is not yet proven for the requested mode; follow next_commands exactly."
|
|
39113
|
+
};
|
|
39114
|
+
}
|
|
39115
|
+
function buildObjectiveReport(input) {
|
|
39116
|
+
const setup = asRecord3(input.setupWorkflow);
|
|
39117
|
+
const live = asRecord3(input.customerLiveStatus);
|
|
39118
|
+
const classification = classifyStatus(input);
|
|
39119
|
+
const reasonCodes = uniqueStrings([
|
|
39120
|
+
classification.reasonCode,
|
|
39121
|
+
...collectStringArrays(setup, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"])),
|
|
39122
|
+
...collectStringArrays(live, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
|
|
39123
|
+
]);
|
|
39124
|
+
const debugSource = normalizeString2(input.opts.out) || "latest";
|
|
39125
|
+
const nextCommands = dedupeCommands([
|
|
39126
|
+
...collectStringArrays(setup, /* @__PURE__ */ new Set(["next_commands"])),
|
|
39127
|
+
...collectStringArrays(live, /* @__PURE__ */ new Set(["next_commands"])),
|
|
39128
|
+
`foh objective debug --from ${debugSource} --json`
|
|
39129
|
+
]);
|
|
39130
|
+
const allowedMode = firstString(live, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || firstString(setup, ["allowed_mode", "highest_allowed_mode", "current_mode"]) || null;
|
|
39131
|
+
const blockedModes = uniqueStrings([
|
|
39132
|
+
...collectStringArrays(setup, /* @__PURE__ */ new Set(["blocked_modes"])),
|
|
39133
|
+
...collectStringArrays(live, /* @__PURE__ */ new Set(["blocked_modes"]))
|
|
39134
|
+
]);
|
|
39135
|
+
const evidenceRefs = uniqueStrings([
|
|
39136
|
+
...collectArtifactRefs(setup),
|
|
39137
|
+
...collectArtifactRefs(live)
|
|
39138
|
+
]);
|
|
39139
|
+
const customerEvidenceActionPacket = customerEvidenceActionPacketFromSources(live, setup);
|
|
39140
|
+
const developerReadinessPacket = buildDeveloperReadinessPacket({
|
|
39141
|
+
opts: input.opts,
|
|
39142
|
+
setupWorkflow: setup,
|
|
39143
|
+
customerLiveStatus: live,
|
|
39144
|
+
classification,
|
|
39145
|
+
nextCommands,
|
|
39146
|
+
customerEvidenceActionPacket
|
|
39147
|
+
});
|
|
39148
|
+
return cliEnvelope({
|
|
39149
|
+
schemaVersion: "agent_workbench_report.v1",
|
|
39150
|
+
status: classification.status,
|
|
39151
|
+
reasonCode: classification.reasonCode,
|
|
39152
|
+
summary: classification.summary,
|
|
39153
|
+
ids: {
|
|
39154
|
+
org_id: input.opts.org ?? null
|
|
39155
|
+
},
|
|
39156
|
+
artifacts: {
|
|
39157
|
+
evidence_refs: evidenceRefs
|
|
39158
|
+
},
|
|
39159
|
+
nextCommands,
|
|
39160
|
+
spendClass: "free",
|
|
39161
|
+
safeToRetry: true,
|
|
39162
|
+
extra: {
|
|
39163
|
+
objective: {
|
|
39164
|
+
business_objective: normalizeString2(input.opts.businessObjective) || null,
|
|
39165
|
+
business_name: resolveBusinessName(input.opts) || 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,
|
|
39169
|
+
location: resolveLocation(input.opts) || null,
|
|
39170
|
+
branch_location: normalizeString2(input.opts.branchLocation) || resolveLocation(input.opts) || null,
|
|
39171
|
+
target_mode: normalizeString2(input.opts.targetMode) || "customer_owned_voice_trial",
|
|
39172
|
+
tools: parseCsvOption(input.opts.tools ?? "widget,voice,email,callback,calendar,crm,webhook,whatsapp") ?? []
|
|
39173
|
+
},
|
|
39174
|
+
requirement_brief: firstNonEmptyObject(input.requirementBrief) ?? null,
|
|
39175
|
+
selected_template: firstNonEmptyObject(input.selectedTemplate) ?? null,
|
|
39176
|
+
template_selection: firstNonEmptyObject(input.templateSelection) ?? null,
|
|
39177
|
+
allowed_mode: allowedMode,
|
|
39178
|
+
blocked_modes: blockedModes,
|
|
39179
|
+
reason_codes: reasonCodes,
|
|
39180
|
+
next_action: nextCommands[0] ?? null,
|
|
39181
|
+
developer_readiness_packet: developerReadinessPacket,
|
|
39182
|
+
customer_evidence_action_packet: customerEvidenceActionPacket ?? null,
|
|
39183
|
+
source_report_summaries: {
|
|
39184
|
+
setup_workflow: summarizeSourceReport(setup),
|
|
39185
|
+
customer_live_status: summarizeSourceReport(live)
|
|
39186
|
+
},
|
|
39187
|
+
artifact_policy: {
|
|
39188
|
+
normal_path_fields: ["status", "reason_code", "reason_codes", "next_action", "next_commands", "developer_readiness_packet", "customer_evidence_action_packet", "source_report_summaries", "agent_guidance"],
|
|
39189
|
+
diagnostic_fields: ["source_reports", "artifacts.evidence_refs"],
|
|
39190
|
+
raw_artifact_reads_required: false,
|
|
39191
|
+
mutable_latest_json_policy: "diagnostic_only",
|
|
39192
|
+
immutable_evidence_refs_required: true
|
|
39193
|
+
},
|
|
39194
|
+
source_reports: {
|
|
39195
|
+
setup_workflow: setup,
|
|
39196
|
+
customer_live_status: live
|
|
39197
|
+
},
|
|
39198
|
+
agent_guidance: {
|
|
39199
|
+
normal_user_path: "Use objective commands first; inspect source_reports only when debug asks for it.",
|
|
39200
|
+
do_not_infer_from_raw_artifacts: true
|
|
39201
|
+
}
|
|
39202
|
+
}
|
|
39203
|
+
});
|
|
39204
|
+
}
|
|
39205
|
+
function withObjectiveArtifactPath(report, artifactPath) {
|
|
39206
|
+
return {
|
|
39207
|
+
...report,
|
|
39208
|
+
artifacts: {
|
|
39209
|
+
...asRecord3(report.artifacts),
|
|
39210
|
+
objective_report: artifactPath
|
|
39211
|
+
},
|
|
39212
|
+
artifact_path: artifactPath
|
|
39213
|
+
};
|
|
39214
|
+
}
|
|
39215
|
+
function stripDiagnosticField(target, fieldPath) {
|
|
39216
|
+
const parts = fieldPath.split(".").map((part) => part.trim()).filter(Boolean);
|
|
39217
|
+
if (parts.length === 0) return;
|
|
39218
|
+
let current = target;
|
|
39219
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
39220
|
+
const next = asRecord3(current?.[parts[index]]);
|
|
39221
|
+
if (Object.keys(next).length === 0) return;
|
|
39222
|
+
current = next;
|
|
39223
|
+
}
|
|
39224
|
+
if (!current) return;
|
|
39225
|
+
delete current[parts[parts.length - 1]];
|
|
39226
|
+
}
|
|
39227
|
+
function buildObjectiveNormalPathOutput(report) {
|
|
39228
|
+
const output = JSON.parse(JSON.stringify(report));
|
|
39229
|
+
const artifactPolicy = asRecord3(output.artifact_policy);
|
|
39230
|
+
const diagnosticFields = uniqueStrings(asArray(artifactPolicy.diagnostic_fields).map((value) => normalizeString2(value)));
|
|
39231
|
+
for (const fieldPath of diagnosticFields) stripDiagnosticField(output, fieldPath);
|
|
39232
|
+
return output;
|
|
39233
|
+
}
|
|
39234
|
+
function classifyReasonCode(reasonCode) {
|
|
39235
|
+
const normalized = reasonCode.toLowerCase();
|
|
39236
|
+
if (/credential|token|auth|login|permission|scope|key|secret/.test(normalized)) return "credentials";
|
|
39237
|
+
if (/source|official|fact|approval|conflict|provenance|unknown|intake/.test(normalized)) return "source_facts";
|
|
39238
|
+
if (/customer.*evidence|evidence|signoff|ownership|customer_owned|live.*validation/.test(normalized)) return "customer_evidence";
|
|
39239
|
+
if (/voice|stt|tts|turn|audio|latency|tool|webhook|calendar|crm|email|callback|whatsapp/.test(normalized)) return "voice_or_tool_quality";
|
|
39240
|
+
if (/deploy|production|backup|rollback|runtime|env|release|drift/.test(normalized)) return "deploy_or_runtime";
|
|
39241
|
+
if (/docs|api|route|command|registry|schema|contract/.test(normalized)) return "docs_api_mismatch";
|
|
39242
|
+
if (/proof|simulation|cert|widget|artifact|gate|readiness/.test(normalized)) return "runtime_proof";
|
|
39243
|
+
return "uncategorized";
|
|
39244
|
+
}
|
|
39245
|
+
function buildDebugReport(sourcePath, objectiveReport) {
|
|
39246
|
+
const report = asRecord3(objectiveReport);
|
|
39247
|
+
const reasonCodes = uniqueStrings([
|
|
39248
|
+
firstString(report, ["reason_code"]),
|
|
39249
|
+
...collectStringArrays(report, /* @__PURE__ */ new Set(["reason_codes", "blocker_reason_codes"]))
|
|
39250
|
+
]);
|
|
39251
|
+
const nextCommands = dedupeCommands([
|
|
39252
|
+
...collectStringArrays(report, /* @__PURE__ */ new Set(["next_commands"])),
|
|
39253
|
+
"foh objective status --business-name <name> --source-url <official_url> --out test-results/objective-status.latest.json --json"
|
|
39254
|
+
]);
|
|
39255
|
+
const categories = uniqueStrings(reasonCodes.map(classifyReasonCode));
|
|
39256
|
+
const status = firstString(report, ["status"]);
|
|
39257
|
+
const isReady = report.ok === true || status === "pass" || status === "success";
|
|
39258
|
+
const dominantCategory = categories.find((category) => category !== "uncategorized") ?? categories[0] ?? "uncategorized";
|
|
39259
|
+
const sourceReports = asRecord3(report.source_reports);
|
|
39260
|
+
const customerEvidenceActionPacket = firstNonEmptyObject(report.customer_evidence_action_packet);
|
|
39261
|
+
const developerReadinessPacket = firstNonEmptyObject(report.developer_readiness_packet);
|
|
39262
|
+
if (isReady && reasonCodes.length <= 1 && reasonCodes[0] === "objective_ready") {
|
|
39263
|
+
return cliEnvelope({
|
|
39264
|
+
schemaVersion: "agent_workbench_debug.v1",
|
|
39265
|
+
status: "pass",
|
|
39266
|
+
reasonCode: "objective_debug_no_blockers",
|
|
39267
|
+
summary: "Objective report is ready; no repair packet is required.",
|
|
39268
|
+
artifacts: { objective_report: sourcePath },
|
|
39269
|
+
nextCommands,
|
|
39270
|
+
spendClass: "free",
|
|
39271
|
+
safeToRetry: true,
|
|
39272
|
+
extra: {
|
|
39273
|
+
blocker_categories: [],
|
|
39274
|
+
dominant_category: null,
|
|
39275
|
+
repair_packet: null
|
|
39276
|
+
}
|
|
39277
|
+
});
|
|
39278
|
+
}
|
|
39279
|
+
return cliEnvelope({
|
|
39280
|
+
schemaVersion: "agent_workbench_debug.v1",
|
|
39281
|
+
status: "hold",
|
|
39282
|
+
reasonCode: "objective_debug_repair_packet_ready",
|
|
39283
|
+
summary: `Objective is held by ${dominantCategory}; execute the first repair command before rerunning status.`,
|
|
39284
|
+
artifacts: { objective_report: sourcePath },
|
|
39285
|
+
nextCommands,
|
|
39286
|
+
spendClass: "free",
|
|
39287
|
+
safeToRetry: true,
|
|
39288
|
+
extra: {
|
|
39289
|
+
blocker_categories: categories,
|
|
39290
|
+
dominant_category: dominantCategory,
|
|
39291
|
+
repair_packet: {
|
|
39292
|
+
category: dominantCategory,
|
|
39293
|
+
reason_codes: reasonCodes,
|
|
39294
|
+
first_repair_command: nextCommands[0] ?? null,
|
|
39295
|
+
developer_readiness_packet: developerReadinessPacket ?? null,
|
|
39296
|
+
customer_evidence_action_packet: customerEvidenceActionPacket ?? null,
|
|
39297
|
+
setup_decision: firstStringFromUnknown(sourceReports.setup_workflow, ["decision", "current_decision", "status"]) || null,
|
|
39298
|
+
customer_live_decision: firstStringFromUnknown(sourceReports.customer_live_status, ["decision", "current_decision", "status"]) || null,
|
|
39299
|
+
principle: "Do not infer readiness from raw artifacts; repair the named blocker and rerun objective status."
|
|
39300
|
+
}
|
|
39301
|
+
}
|
|
39302
|
+
});
|
|
39303
|
+
}
|
|
39304
|
+
function registerObjective(program3) {
|
|
39305
|
+
const objective = program3.command("objective").description("Agent-native objective workflow: plan, apply, prove, status, debug");
|
|
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 () => {
|
|
39307
|
+
assertBusinessName(opts, "objective.plan");
|
|
39308
|
+
const templateSelection = await selectTemplateForObjective(opts);
|
|
39309
|
+
const report = await apiFetch("/v1/console/agency-setup/workflow", {
|
|
39310
|
+
method: "POST",
|
|
39311
|
+
body: JSON.stringify(buildSetupBody(opts)),
|
|
39312
|
+
orgId: opts.org,
|
|
39313
|
+
apiUrlOverride: opts.apiUrl
|
|
39314
|
+
});
|
|
39315
|
+
const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
|
|
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;
|
|
39323
|
+
if (outPath) writeJsonArtifact2(outPath, output);
|
|
39324
|
+
format(output, { json: opts.json ?? false });
|
|
39325
|
+
}));
|
|
39326
|
+
objective.command("apply").description("Apply objective evidence to release launch packet gating").option("--evidence <json|@file>", "Redacted evidence payload JSON, or @/path/to/file.json").option("--plan <path>", "Optional objective plan file to derive evidence payload from").option("--dry-run", "Validate evidence payload without persisting it").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 apply result JSON to this file path").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
39327
|
+
const evidence = await resolveEvidenceInput(opts);
|
|
39328
|
+
const body = {
|
|
39329
|
+
...evidence && typeof evidence === "object" && !Array.isArray(evidence) ? evidence : { evidence }
|
|
39330
|
+
};
|
|
39331
|
+
if (!opts.dryRun) body.apply = true;
|
|
39332
|
+
const report = await apiFetch("/v1/console/release-launch-packet/evidence-dry-run", {
|
|
39333
|
+
method: "POST",
|
|
39334
|
+
body: JSON.stringify(body),
|
|
39335
|
+
orgId: opts.org,
|
|
39336
|
+
apiUrlOverride: opts.apiUrl
|
|
39337
|
+
});
|
|
39338
|
+
const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
|
|
39339
|
+
const output = outPath ? { ...asRecord3(report), artifact_path: outPath } : report;
|
|
39340
|
+
if (outPath) writeJsonArtifact2(outPath, output);
|
|
39341
|
+
format(output, { json: opts.json ?? false });
|
|
39342
|
+
}));
|
|
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 () => {
|
|
39344
|
+
assertBusinessName(opts, "objective.prove");
|
|
39345
|
+
const status = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
|
|
39346
|
+
orgId: opts.org,
|
|
39347
|
+
apiUrlOverride: opts.apiUrl
|
|
39348
|
+
});
|
|
39349
|
+
const outPath = opts.out ? resolveObjectiveReportPath(opts.out) : null;
|
|
39350
|
+
const output = outPath ? { ...asRecord3(status), artifact_path: outPath } : status;
|
|
39351
|
+
if (outPath) writeJsonArtifact2(outPath, output);
|
|
39352
|
+
format(output, { json: opts.json ?? false });
|
|
39353
|
+
}));
|
|
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 () => {
|
|
39355
|
+
assertBusinessName(opts, "objective.status");
|
|
39356
|
+
const templateSelection = await selectTemplateForObjective(opts);
|
|
39357
|
+
const setupWorkflow = await apiFetch("/v1/console/agency-setup/workflow", {
|
|
39358
|
+
method: "POST",
|
|
39359
|
+
body: JSON.stringify(buildSetupBody(opts)),
|
|
39360
|
+
orgId: opts.org,
|
|
39361
|
+
apiUrlOverride: opts.apiUrl
|
|
39362
|
+
});
|
|
39363
|
+
const customerLiveStatus = await apiFetch(withQuery("/v1/console/customer-live-status", buildStatusParams(opts)), {
|
|
39364
|
+
orgId: opts.org,
|
|
39365
|
+
apiUrlOverride: opts.apiUrl
|
|
39366
|
+
});
|
|
39367
|
+
const report = buildObjectiveReport({
|
|
39368
|
+
opts,
|
|
39369
|
+
setupWorkflow,
|
|
39370
|
+
customerLiveStatus,
|
|
39371
|
+
requirementBrief: templateSelection.brief,
|
|
39372
|
+
templateSelection: templateSelection.selection,
|
|
39373
|
+
selectedTemplate: templateSelection.selected
|
|
39374
|
+
});
|
|
39375
|
+
const artifactPath = resolveObjectiveReportPath(opts.out);
|
|
39376
|
+
const fullOutput = withObjectiveArtifactPath(report, artifactPath);
|
|
39377
|
+
writeJsonArtifact2(artifactPath, fullOutput);
|
|
39378
|
+
format(buildObjectiveNormalPathOutput(fullOutput), { json: opts.json ?? false });
|
|
39379
|
+
}));
|
|
39380
|
+
objective.command("debug").description("Explain the next debugging action for an objective report").option("--from <source>", "Report source alias or path", "latest").option("--json", "Output as JSON").action(async (opts) => withCommandErrorHandling(async () => {
|
|
39381
|
+
const sourcePath = resolveObjectiveReportPath(opts.from);
|
|
39382
|
+
if (!(0, import_node_fs5.existsSync)(sourcePath)) {
|
|
39383
|
+
format(cliEnvelope({
|
|
39384
|
+
schemaVersion: "agent_workbench_debug.v1",
|
|
39385
|
+
status: "fail",
|
|
39386
|
+
reasonCode: "objective_report_not_found",
|
|
39387
|
+
summary: `Objective report not found at ${sourcePath}.`,
|
|
39388
|
+
artifacts: { objective_report: sourcePath },
|
|
39389
|
+
nextCommands: [
|
|
39390
|
+
`foh objective status --business-name <name> --source-url <official_url> --out ${DEFAULT_OBJECTIVE_REPORT_PATH} --json`
|
|
39391
|
+
],
|
|
39392
|
+
spendClass: "free",
|
|
39393
|
+
safeToRetry: true
|
|
39394
|
+
}), { json: opts.json ?? false });
|
|
39395
|
+
return;
|
|
39396
|
+
}
|
|
39397
|
+
let objectiveReport;
|
|
39398
|
+
try {
|
|
39399
|
+
objectiveReport = readJsonArtifact(sourcePath);
|
|
39400
|
+
} catch {
|
|
39401
|
+
format(cliEnvelope({
|
|
39402
|
+
schemaVersion: "agent_workbench_debug.v1",
|
|
39403
|
+
status: "fail",
|
|
39404
|
+
reasonCode: "objective_report_invalid",
|
|
39405
|
+
summary: `Objective report at ${sourcePath} is not valid JSON.`,
|
|
39406
|
+
artifacts: { objective_report: sourcePath },
|
|
39407
|
+
nextCommands: [
|
|
39408
|
+
`foh objective status --business-name <name> --source-url <official_url> --out ${DEFAULT_OBJECTIVE_REPORT_PATH} --json`
|
|
39409
|
+
],
|
|
39410
|
+
spendClass: "free",
|
|
39411
|
+
safeToRetry: true
|
|
39412
|
+
}), { json: opts.json ?? false });
|
|
39413
|
+
return;
|
|
39414
|
+
}
|
|
39415
|
+
format(buildDebugReport(sourcePath, objectiveReport), { json: opts.json ?? false });
|
|
39416
|
+
}));
|
|
39417
|
+
}
|
|
39418
|
+
|
|
38545
39419
|
// src/commands/interactive.ts
|
|
38546
39420
|
var import_readline2 = require("readline");
|
|
38547
39421
|
var import_child_process2 = require("child_process");
|
|
@@ -38651,6 +39525,66 @@ var COMMAND_SURFACE_DEFINITIONS = [
|
|
|
38651
39525
|
shellSlash: "/status",
|
|
38652
39526
|
includeInSuggestions: true
|
|
38653
39527
|
},
|
|
39528
|
+
{
|
|
39529
|
+
id: "objective_status",
|
|
39530
|
+
commandPath: ["objective", "status"],
|
|
39531
|
+
invokeArgs: ["objective", "status", "--help"],
|
|
39532
|
+
label: "objective status",
|
|
39533
|
+
descriptionFallback: "Compose setup and launch evidence into one agent workbench status envelope.",
|
|
39534
|
+
requiresAuth: true,
|
|
39535
|
+
requiresOrg: true,
|
|
39536
|
+
mutatesState: "read",
|
|
39537
|
+
examples: ['foh objective status --business-name "Acme" --source-url https://example.com --out test-results/objective-status.latest.json --json'],
|
|
39538
|
+
homeContexts: ["authenticated_with_org"],
|
|
39539
|
+
homeAliases: ["objective", "status objective"],
|
|
39540
|
+
shellSlash: "/objective",
|
|
39541
|
+
includeInSuggestions: true
|
|
39542
|
+
},
|
|
39543
|
+
{
|
|
39544
|
+
id: "objective_plan",
|
|
39545
|
+
commandPath: ["objective", "plan"],
|
|
39546
|
+
invokeArgs: ["objective", "plan", "--help"],
|
|
39547
|
+
label: "objective plan",
|
|
39548
|
+
descriptionFallback: "Generate an objective setup/workflow plan.",
|
|
39549
|
+
requiresAuth: true,
|
|
39550
|
+
requiresOrg: true,
|
|
39551
|
+
mutatesState: "read",
|
|
39552
|
+
examples: ['foh objective plan --business-name "Acme" --business-objective "Qualify leads" --source-url https://example.com --out test-results/objective-plan.latest.json --json'],
|
|
39553
|
+
homeContexts: ["authenticated_with_org"],
|
|
39554
|
+
homeAliases: ["objective plan"],
|
|
39555
|
+
shellSlash: "/objective-plan",
|
|
39556
|
+
includeInSuggestions: true
|
|
39557
|
+
},
|
|
39558
|
+
{
|
|
39559
|
+
id: "objective_apply",
|
|
39560
|
+
commandPath: ["objective", "apply"],
|
|
39561
|
+
invokeArgs: ["objective", "apply", "--help"],
|
|
39562
|
+
label: "objective apply",
|
|
39563
|
+
descriptionFallback: "Apply objective evidence packets against customer-live launch packet gates.",
|
|
39564
|
+
requiresAuth: true,
|
|
39565
|
+
requiresOrg: true,
|
|
39566
|
+
mutatesState: "write",
|
|
39567
|
+
examples: ["foh objective apply --evidence @test-results/evidence.json --org <org-id> --out test-results/objective-apply.latest.json --json"],
|
|
39568
|
+
homeContexts: ["authenticated_with_org"],
|
|
39569
|
+
homeAliases: ["objective apply"],
|
|
39570
|
+
shellSlash: "/objective-apply",
|
|
39571
|
+
includeInSuggestions: true
|
|
39572
|
+
},
|
|
39573
|
+
{
|
|
39574
|
+
id: "objective_prove",
|
|
39575
|
+
commandPath: ["objective", "prove"],
|
|
39576
|
+
invokeArgs: ["objective", "prove", "--help"],
|
|
39577
|
+
label: "objective prove",
|
|
39578
|
+
descriptionFallback: "Check one objective against customer-live proof gates.",
|
|
39579
|
+
requiresAuth: true,
|
|
39580
|
+
requiresOrg: true,
|
|
39581
|
+
mutatesState: "read",
|
|
39582
|
+
examples: ['foh objective prove --business-name "Acme" --source-url https://example.com --environment staging --json'],
|
|
39583
|
+
homeContexts: ["authenticated_with_org"],
|
|
39584
|
+
homeAliases: ["objective prove"],
|
|
39585
|
+
shellSlash: "/objective-prove",
|
|
39586
|
+
includeInSuggestions: true
|
|
39587
|
+
},
|
|
38654
39588
|
{
|
|
38655
39589
|
id: "templates_list",
|
|
38656
39590
|
commandPath: ["templates", "list"],
|
|
@@ -38735,9 +39669,9 @@ var COMMAND_SURFACE_DEFINITIONS = [
|
|
|
38735
39669
|
includeInSuggestions: false
|
|
38736
39670
|
},
|
|
38737
39671
|
{
|
|
38738
|
-
id: "
|
|
38739
|
-
commandPath: ["channel", "whatsapp", "
|
|
38740
|
-
label: "whatsapp
|
|
39672
|
+
id: "whatsapp_onboard",
|
|
39673
|
+
commandPath: ["channel", "whatsapp", "onboard"],
|
|
39674
|
+
label: "whatsapp onboard",
|
|
38741
39675
|
descriptionFallback: "WhatsApp readiness path",
|
|
38742
39676
|
mutatesState: "write",
|
|
38743
39677
|
shellSlash: "/whatsapp",
|
|
@@ -38791,6 +39725,7 @@ var HOME_QUICK_ACTION_ORDER = {
|
|
|
38791
39725
|
unauthenticated: ["auth_login", "interactive_shell", "full_help", "auth_login_help"],
|
|
38792
39726
|
authenticated_no_org: ["org_list", "interactive_shell", "org_use_help", "auth_whoami", "full_help"],
|
|
38793
39727
|
authenticated_with_org: [
|
|
39728
|
+
"objective_status",
|
|
38794
39729
|
"tenant_status",
|
|
38795
39730
|
"interactive_shell",
|
|
38796
39731
|
"templates_list",
|
|
@@ -39133,7 +40068,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
39133
40068
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
39134
40069
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
39135
40070
|
}
|
|
39136
|
-
return await new Promise((
|
|
40071
|
+
return await new Promise((resolve16, reject) => {
|
|
39137
40072
|
const child = (0, import_child_process2.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
39138
40073
|
stdio: "inherit",
|
|
39139
40074
|
env: {
|
|
@@ -39143,7 +40078,7 @@ async function runSelf(args, apiUrlOverride) {
|
|
|
39143
40078
|
}
|
|
39144
40079
|
});
|
|
39145
40080
|
child.once("error", reject);
|
|
39146
|
-
child.once("close", (code) =>
|
|
40081
|
+
child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
|
|
39147
40082
|
});
|
|
39148
40083
|
}
|
|
39149
40084
|
function resolveInteractiveState(apiUrlOverride) {
|
|
@@ -39330,10 +40265,10 @@ async function confirmMutationIfNeeded(rl, args) {
|
|
|
39330
40265
|
const commandEntry = getCommandGraphEntryForArgs(args);
|
|
39331
40266
|
const label = commandEntry ? `foh ${commandEntry.args.join(" ")}` : `foh ${args.join(" ")}`;
|
|
39332
40267
|
const warning = mutationState === "high_risk" ? "high-risk" : "write";
|
|
39333
|
-
const approved = await new Promise((
|
|
40268
|
+
const approved = await new Promise((resolve16) => {
|
|
39334
40269
|
rl.question(`Confirm ${warning} command ${label}? [y/N]: `, (answer) => {
|
|
39335
40270
|
const normalized = answer.trim().toLowerCase();
|
|
39336
|
-
|
|
40271
|
+
resolve16(normalized === "y" || normalized === "yes");
|
|
39337
40272
|
});
|
|
39338
40273
|
});
|
|
39339
40274
|
if (approved) return { approved: true };
|
|
@@ -39501,7 +40436,7 @@ function registerInteractive(program3) {
|
|
|
39501
40436
|
rl.on("SIGINT", () => {
|
|
39502
40437
|
rl.close();
|
|
39503
40438
|
});
|
|
39504
|
-
await new Promise((
|
|
40439
|
+
await new Promise((resolve16) => {
|
|
39505
40440
|
rl.on("close", () => {
|
|
39506
40441
|
process.stdout.write("\n");
|
|
39507
40442
|
memory.history = rl.history.slice(0, 500);
|
|
@@ -39509,7 +40444,7 @@ function registerInteractive(program3) {
|
|
|
39509
40444
|
flushInteractiveSessionArtifact(sessionArtifact);
|
|
39510
40445
|
flushInteractiveSessionReport(sessionArtifact);
|
|
39511
40446
|
saveInteractiveShellMemory(memory);
|
|
39512
|
-
|
|
40447
|
+
resolve16();
|
|
39513
40448
|
});
|
|
39514
40449
|
});
|
|
39515
40450
|
});
|
|
@@ -39841,7 +40776,7 @@ async function runSelf2(args, apiUrlOverride) {
|
|
|
39841
40776
|
if (apiUrlOverride && !spawnArgs.includes("--api-url")) {
|
|
39842
40777
|
spawnArgs.push("--api-url", apiUrlOverride);
|
|
39843
40778
|
}
|
|
39844
|
-
return await new Promise((
|
|
40779
|
+
return await new Promise((resolve16, reject) => {
|
|
39845
40780
|
const child = (0, import_child_process3.spawn)(process.execPath, [process.argv[1], ...spawnArgs], {
|
|
39846
40781
|
stdio: "inherit",
|
|
39847
40782
|
env: {
|
|
@@ -39851,7 +40786,7 @@ async function runSelf2(args, apiUrlOverride) {
|
|
|
39851
40786
|
}
|
|
39852
40787
|
});
|
|
39853
40788
|
child.once("error", reject);
|
|
39854
|
-
child.once("close", (code) =>
|
|
40789
|
+
child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
|
|
39855
40790
|
});
|
|
39856
40791
|
}
|
|
39857
40792
|
function shouldUseInteractiveHome(argv) {
|
|
@@ -40200,17 +41135,17 @@ function detectUpdateAvailability(currentVersion, cwd = process.cwd()) {
|
|
|
40200
41135
|
async function applyRepoUpdate(repoRoot) {
|
|
40201
41136
|
const scriptPath = (0, import_path10.join)(repoRoot, "scripts", "Install-FohCli.ps1");
|
|
40202
41137
|
if (process.platform === "win32") {
|
|
40203
|
-
return await new Promise((
|
|
41138
|
+
return await new Promise((resolve16, reject) => {
|
|
40204
41139
|
const child = (0, import_child_process4.spawn)(
|
|
40205
41140
|
"powershell",
|
|
40206
41141
|
["-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
40207
41142
|
{ stdio: "inherit" }
|
|
40208
41143
|
);
|
|
40209
41144
|
child.once("error", reject);
|
|
40210
|
-
child.once("close", (code) =>
|
|
41145
|
+
child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
|
|
40211
41146
|
});
|
|
40212
41147
|
}
|
|
40213
|
-
return await new Promise((
|
|
41148
|
+
return await new Promise((resolve16, reject) => {
|
|
40214
41149
|
const child = (0, import_child_process4.spawn)(
|
|
40215
41150
|
"corepack",
|
|
40216
41151
|
["pnpm", "cli:install:global"],
|
|
@@ -40220,7 +41155,7 @@ async function applyRepoUpdate(repoRoot) {
|
|
|
40220
41155
|
}
|
|
40221
41156
|
);
|
|
40222
41157
|
child.once("error", reject);
|
|
40223
|
-
child.once("close", (code) =>
|
|
41158
|
+
child.once("close", (code) => resolve16(typeof code === "number" ? code : 1));
|
|
40224
41159
|
});
|
|
40225
41160
|
}
|
|
40226
41161
|
function shouldShowUpdateNotice(argv = process.argv) {
|
|
@@ -40692,9 +41627,9 @@ function readCommandRecords(runDir) {
|
|
|
40692
41627
|
}
|
|
40693
41628
|
|
|
40694
41629
|
// src/lib/external-agent-executor.ts
|
|
40695
|
-
var
|
|
41630
|
+
var import_fs19 = require("fs");
|
|
40696
41631
|
var import_os3 = require("os");
|
|
40697
|
-
var
|
|
41632
|
+
var import_path18 = require("path");
|
|
40698
41633
|
var import_child_process6 = require("child_process");
|
|
40699
41634
|
|
|
40700
41635
|
// src/lib/external-agent-executor-env.ts
|
|
@@ -40865,824 +41800,303 @@ function copyExternalAgentCommandCaptureArtifacts(input) {
|
|
|
40865
41800
|
}
|
|
40866
41801
|
|
|
40867
41802
|
// src/lib/external-agent-executor-classification.ts
|
|
40868
|
-
var import_fs18 = require("fs");
|
|
40869
|
-
var import_path17 = require("path");
|
|
40870
|
-
|
|
40871
|
-
// src/lib/external-agent-run-summary.ts
|
|
40872
41803
|
var import_fs17 = require("fs");
|
|
40873
41804
|
var import_path16 = require("path");
|
|
40874
|
-
|
|
40875
|
-
"
|
|
40876
|
-
|
|
40877
|
-
|
|
40878
|
-
|
|
40879
|
-
|
|
40880
|
-
|
|
40881
|
-
|
|
40882
|
-
|
|
40883
|
-
"environment",
|
|
40884
|
-
"public_entrypoints",
|
|
40885
|
-
"commands_run",
|
|
40886
|
-
"docs_pages_used",
|
|
40887
|
-
"artifacts"
|
|
40888
|
-
];
|
|
40889
|
-
var VALID_STATUSES = /* @__PURE__ */ new Set(["pass", "hold", "fail"]);
|
|
40890
|
-
var DOC_URL_RE = /https:\/\/frontofhouse\.okii\.uk\/[^\s"'`)<>,;\\\]}]*/g;
|
|
40891
|
-
function quoteShellArg(value) {
|
|
40892
|
-
const text = String(value);
|
|
40893
|
-
if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
|
|
40894
|
-
return `"${text.replace(/(["$`])/g, "\\$1")}"`;
|
|
41805
|
+
function proofArtifactPasses(runDir) {
|
|
41806
|
+
const proofPath = (0, import_path16.join)(runDir, "proof.json");
|
|
41807
|
+
if (!(0, import_fs17.existsSync)(proofPath)) return false;
|
|
41808
|
+
try {
|
|
41809
|
+
const parsed = JSON.parse((0, import_fs17.readFileSync)(proofPath, "utf8"));
|
|
41810
|
+
return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
|
|
41811
|
+
} catch {
|
|
41812
|
+
return false;
|
|
41813
|
+
}
|
|
40895
41814
|
}
|
|
40896
|
-
function
|
|
40897
|
-
|
|
40898
|
-
|
|
41815
|
+
function readIfExists(path2) {
|
|
41816
|
+
return (0, import_fs17.existsSync)(path2) ? (0, import_fs17.readFileSync)(path2, "utf8") : "";
|
|
41817
|
+
}
|
|
41818
|
+
function relativeArtifactName(path2) {
|
|
41819
|
+
return (0, import_path16.basename)(path2);
|
|
41820
|
+
}
|
|
41821
|
+
function externalAgentSummaryTemplateCommand() {
|
|
40899
41822
|
return [
|
|
40900
41823
|
"foh",
|
|
40901
41824
|
"eval",
|
|
40902
41825
|
"external-agent",
|
|
40903
41826
|
"summary",
|
|
40904
41827
|
"--root",
|
|
40905
|
-
|
|
41828
|
+
"<batch_dir>",
|
|
40906
41829
|
"--out",
|
|
40907
|
-
|
|
41830
|
+
"<batch_dir>/latest-summary.json",
|
|
40908
41831
|
"--report",
|
|
40909
|
-
|
|
41832
|
+
"<batch_dir>/summary.report.json",
|
|
40910
41833
|
"--json"
|
|
40911
41834
|
].join(" ");
|
|
40912
41835
|
}
|
|
40913
|
-
function
|
|
40914
|
-
|
|
40915
|
-
}
|
|
40916
|
-
|
|
40917
|
-
|
|
40918
|
-
|
|
40919
|
-
|
|
40920
|
-
const parsed = JSON.parse(line);
|
|
40921
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
40922
|
-
} catch {
|
|
40923
|
-
return null;
|
|
40924
|
-
}
|
|
40925
|
-
}).filter((record2) => Boolean(record2));
|
|
40926
|
-
}
|
|
40927
|
-
function asObject(value) {
|
|
40928
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
40929
|
-
}
|
|
40930
|
-
function toArray2(value) {
|
|
40931
|
-
return Array.isArray(value) ? value : [];
|
|
40932
|
-
}
|
|
40933
|
-
function increment(map3, key, amount = 1) {
|
|
40934
|
-
const normalized = String(key || "unknown");
|
|
40935
|
-
map3.set(normalized, (map3.get(normalized) || 0) + amount);
|
|
40936
|
-
}
|
|
40937
|
-
function ranked(map3) {
|
|
40938
|
-
return Array.from(map3.entries()).map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
|
|
40939
|
-
}
|
|
40940
|
-
function collectDocUrls(text) {
|
|
40941
|
-
return Array.from(new Set((String(text || "").match(DOC_URL_RE) || []).map((url2) => url2.replace(/[.?!:]+$/g, "")).filter((url2) => url2.startsWith("https://frontofhouse.okii.uk/")))).sort();
|
|
40942
|
-
}
|
|
40943
|
-
function findRunCandidates(root) {
|
|
40944
|
-
if (!(0, import_fs17.existsSync)(root)) return [];
|
|
40945
|
-
const candidates = [];
|
|
40946
|
-
const seenRunDirs = /* @__PURE__ */ new Set();
|
|
40947
|
-
const captureDirs = [];
|
|
40948
|
-
const stack = [root];
|
|
40949
|
-
while (stack.length > 0) {
|
|
40950
|
-
const current = stack.pop();
|
|
40951
|
-
if (!current) continue;
|
|
40952
|
-
for (const entry of (0, import_fs17.readdirSync)(current, { withFileTypes: true })) {
|
|
40953
|
-
const absolute = (0, import_path16.join)(current, entry.name);
|
|
40954
|
-
if (entry.isDirectory()) {
|
|
40955
|
-
stack.push(absolute);
|
|
40956
|
-
} else if (entry.isFile() && entry.name === "run.json") {
|
|
40957
|
-
candidates.push({ path: absolute, synthetic: false });
|
|
40958
|
-
seenRunDirs.add((0, import_path16.dirname)(absolute));
|
|
40959
|
-
} else if (entry.isFile() && entry.name === "commands.ndjson") {
|
|
40960
|
-
captureDirs.push(current);
|
|
40961
|
-
}
|
|
40962
|
-
}
|
|
41836
|
+
function classifyExternalAgentRun(input) {
|
|
41837
|
+
if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
|
|
41838
|
+
if (!input.artifactSafetyOk) return { status: "fail", reasonCode: "external_agent_artifact_safety_blocked" };
|
|
41839
|
+
const completedCommands = readCommandRecords(input.run.run_dir).filter((record2) => record2.phase === "completed");
|
|
41840
|
+
const observedVersions = completedCommands.map((record2) => String(record2.cli_version || "").trim()).filter((version2) => /^\d+\.\d+\.\d+$/.test(version2));
|
|
41841
|
+
if (observedVersions.some((version2) => version2 !== CLI_VERSION)) {
|
|
41842
|
+
return { status: "hold", reasonCode: "external_agent_cli_version_drift" };
|
|
40963
41843
|
}
|
|
40964
|
-
|
|
40965
|
-
|
|
40966
|
-
|
|
41844
|
+
const commandReasonCodes2 = completedCommands.flatMap((record2) => [
|
|
41845
|
+
String(record2.reason_code || ""),
|
|
41846
|
+
...Array.isArray(record2.check_reason_codes) ? record2.check_reason_codes.map((code) => String(code || "")) : []
|
|
41847
|
+
]).filter(Boolean);
|
|
41848
|
+
const hasCommandReason = (pattern) => commandReasonCodes2.some((reason) => pattern.test(reason));
|
|
41849
|
+
if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
|
|
41850
|
+
return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
|
|
40967
41851
|
}
|
|
40968
|
-
|
|
40969
|
-
}
|
|
40970
|
-
function validateExternalAgentRun(value) {
|
|
40971
|
-
const findings = [];
|
|
40972
|
-
const run = asObject(value);
|
|
40973
|
-
if (!run) return [{ id: "run_not_object", detail: "run artifact must be an object" }];
|
|
40974
|
-
for (const field of REQUIRED_RUN_FIELDS) {
|
|
40975
|
-
if (!(field in run)) findings.push({ id: "required_field_missing", field });
|
|
41852
|
+
if (hasCommandReason(/provider_capacity_blocked/i)) {
|
|
41853
|
+
return { status: "hold", reasonCode: "provider_capacity_blocked" };
|
|
40976
41854
|
}
|
|
40977
|
-
if (
|
|
40978
|
-
|
|
41855
|
+
if (hasCommandReason(/byon_voice_number_not_configured/i)) {
|
|
41856
|
+
return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
|
|
40979
41857
|
}
|
|
40980
|
-
if (
|
|
40981
|
-
|
|
41858
|
+
if (hasCommandReason(/contact_phone_provisioning_failed/i)) {
|
|
41859
|
+
return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
|
|
40982
41860
|
}
|
|
40983
|
-
if ((
|
|
40984
|
-
|
|
41861
|
+
if (hasCommandReason(/voice_contact_expected_no_spend_hold/i)) {
|
|
41862
|
+
return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
|
|
40985
41863
|
}
|
|
40986
|
-
if (
|
|
40987
|
-
|
|
41864
|
+
if (hasCommandReason(/contact_phone_missing/i)) {
|
|
41865
|
+
return { status: "hold", reasonCode: "voice_contact_phone_missing" };
|
|
40988
41866
|
}
|
|
40989
|
-
if (
|
|
40990
|
-
|
|
40991
|
-
if (!asObject(run.environment)) findings.push({ id: "environment_invalid" });
|
|
40992
|
-
if (!asObject(run.artifacts)) findings.push({ id: "artifacts_invalid" });
|
|
40993
|
-
if (toArray2(run.public_entrypoints).length === 0) findings.push({ id: "public_entrypoints_missing" });
|
|
40994
|
-
return findings;
|
|
40995
|
-
}
|
|
40996
|
-
function runSortTime(run) {
|
|
40997
|
-
const raw = String(run.ended_at || run.started_at || "");
|
|
40998
|
-
const time3 = Date.parse(raw);
|
|
40999
|
-
return Number.isFinite(time3) ? time3 : 0;
|
|
41000
|
-
}
|
|
41001
|
-
function latestCommandTime(commands) {
|
|
41002
|
-
const times = commands.map((command) => String(command.completed_at || command.started_at || command.recorded_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => b.time - a.time);
|
|
41003
|
-
return times[0]?.raw ?? null;
|
|
41004
|
-
}
|
|
41005
|
-
function firstCommandTime(commands) {
|
|
41006
|
-
const times = commands.map((command) => String(command.started_at || command.recorded_at || command.completed_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => a.time - b.time);
|
|
41007
|
-
return times[0]?.raw ?? null;
|
|
41008
|
-
}
|
|
41009
|
-
function commandReasonCodes(commands) {
|
|
41010
|
-
const codes = /* @__PURE__ */ new Set();
|
|
41011
|
-
for (const command of commands) {
|
|
41012
|
-
if (command.reason_code) codes.add(String(command.reason_code));
|
|
41013
|
-
for (const reasonCode of toArray2(command.check_reason_codes)) {
|
|
41014
|
-
if (reasonCode) codes.add(String(reasonCode));
|
|
41015
|
-
}
|
|
41867
|
+
if (hasCommandReason(/sim(?:ulation)?[_-]?cert(?:ify|ification)?.*failed|simulation_certification_failed/i)) {
|
|
41868
|
+
return { status: "hold", reasonCode: "simulation_certification_failed" };
|
|
41016
41869
|
}
|
|
41017
|
-
|
|
41018
|
-
}
|
|
41019
|
-
function syntheticStatusFromCommands(commands) {
|
|
41020
|
-
const commandReasons = commandReasonCodes(commands);
|
|
41021
|
-
const failed = commands.find((command) => {
|
|
41022
|
-
const status = String(command.status || "").toLowerCase();
|
|
41023
|
-
return status === "fail" || typeof command.exit_code === "number" && command.exit_code !== 0 && status !== "hold";
|
|
41024
|
-
});
|
|
41025
|
-
if (failed) {
|
|
41026
|
-
return {
|
|
41027
|
-
status: "fail",
|
|
41028
|
-
reasonCode: String(failed.reason_code || commandReasons[0] || "external_agent_command_failed")
|
|
41029
|
-
};
|
|
41870
|
+
if (hasCommandReason(/proof_held/i)) {
|
|
41871
|
+
return { status: "hold", reasonCode: "external_agent_proof_held" };
|
|
41030
41872
|
}
|
|
41031
|
-
|
|
41032
|
-
|
|
41033
|
-
return {
|
|
41034
|
-
status: "hold",
|
|
41035
|
-
reasonCode: String(held.reason_code || commandReasons[0] || "external_agent_command_held")
|
|
41036
|
-
};
|
|
41873
|
+
if (hasCommandReason(/agent_limit_reached/i)) {
|
|
41874
|
+
return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
|
|
41037
41875
|
}
|
|
41038
|
-
|
|
41039
|
-
|
|
41876
|
+
const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
|
|
41877
|
+
const metadataBlockerCodes = agentMetadata.blocker_reason_codes;
|
|
41878
|
+
const hasMetadataBlocker = (pattern) => metadataBlockerCodes.some((reason) => pattern.test(reason));
|
|
41879
|
+
if (hasMetadataBlocker(/^customer_owned_requirement_unverified:/i)) {
|
|
41880
|
+
return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
|
|
41040
41881
|
}
|
|
41041
|
-
|
|
41882
|
+
if (hasMetadataBlocker(/^api_health:/i) && metadataBlockerCodes.some((reason) => /^customer_owned_requirement_unverified:/i.test(reason))) {
|
|
41883
|
+
return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
|
|
41884
|
+
}
|
|
41885
|
+
const firstCommandReasonCode = commandReasonCodes2.find((reason) => reason.trim().length > 0) ?? null;
|
|
41886
|
+
if (firstCommandReasonCode) {
|
|
41887
|
+
return { status: "hold", reasonCode: firstCommandReasonCode };
|
|
41888
|
+
}
|
|
41889
|
+
const lastMessage = readIfExists(input.run.outputs.last_message);
|
|
41890
|
+
const stderr = readIfExists(input.run.outputs.stderr);
|
|
41891
|
+
const combined = `${lastMessage}
|
|
41892
|
+
${stderr}`;
|
|
41893
|
+
if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
|
|
41894
|
+
return { status: "fail", reasonCode: "private_repo_assumption_detected" };
|
|
41895
|
+
}
|
|
41896
|
+
if (/(?:blocked|rejected|declined) by policy|EXEC_POLICY_BLOCKED|command execution was rejected|shell commands were rejected/i.test(combined)) {
|
|
41897
|
+
return { status: "hold", reasonCode: "codex_exec_policy_blocked" };
|
|
41898
|
+
}
|
|
41899
|
+
if (/bwrap:.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|bubblewrap.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|Failed RTM_NEWADDR|ENV_SANDBOX_EXEC_BLOCKED|permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock|legacy[_ -]?landlock.*incompatible/i.test(combined)) {
|
|
41900
|
+
return { status: "hold", reasonCode: "codex_sandbox_exec_blocked" };
|
|
41901
|
+
}
|
|
41902
|
+
if (/ENV_NETWORK_DNS_BLOCK|Could not resolve host|npm ping.*timeout|NO_EXECUTABLE_INSTALL/i.test(combined)) {
|
|
41903
|
+
return { status: "hold", reasonCode: "codex_network_dns_blocked" };
|
|
41904
|
+
}
|
|
41905
|
+
if (new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i").test(combined)) {
|
|
41906
|
+
return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
|
|
41907
|
+
}
|
|
41908
|
+
if (/provider_capacity_blocked/i.test(combined)) {
|
|
41909
|
+
return { status: "hold", reasonCode: "provider_capacity_blocked" };
|
|
41910
|
+
}
|
|
41911
|
+
if (/byon_voice_number_not_configured/i.test(combined)) {
|
|
41912
|
+
return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
|
|
41913
|
+
}
|
|
41914
|
+
if (/contact_phone_provisioning_failed/i.test(combined)) {
|
|
41915
|
+
return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
|
|
41916
|
+
}
|
|
41917
|
+
if (/voice_contact_expected_no_spend_hold/i.test(combined)) {
|
|
41918
|
+
return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
|
|
41919
|
+
}
|
|
41920
|
+
if (/contact_phone_missing/i.test(combined)) {
|
|
41921
|
+
return { status: "hold", reasonCode: "voice_contact_phone_missing" };
|
|
41922
|
+
}
|
|
41923
|
+
if (/simulation_certification_failed/i.test(combined)) {
|
|
41924
|
+
return { status: "hold", reasonCode: "simulation_certification_failed" };
|
|
41925
|
+
}
|
|
41926
|
+
if (/proof_held/i.test(combined)) {
|
|
41927
|
+
return { status: "hold", reasonCode: "external_agent_proof_held" };
|
|
41928
|
+
}
|
|
41929
|
+
if (/agent_limit_reached/i.test(combined)) {
|
|
41930
|
+
return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
|
|
41931
|
+
}
|
|
41932
|
+
if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
|
|
41933
|
+
return { status: "hold", reasonCode: "auth_browser_approval_required" };
|
|
41934
|
+
}
|
|
41935
|
+
if (input.exitCode !== 0) return { status: "hold", reasonCode: `${input.run.command}_runner_nonzero_exit` };
|
|
41936
|
+
if (proofArtifactPasses(input.run.run_dir)) return { status: "pass", reasonCode: null };
|
|
41937
|
+
return { status: "hold", reasonCode: "external_agent_proof_artifact_missing" };
|
|
41042
41938
|
}
|
|
41043
|
-
function
|
|
41044
|
-
const
|
|
41045
|
-
const
|
|
41046
|
-
const metadata = asObject((0, import_fs17.existsSync)((0, import_path16.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path16.join)(runDir, "external-agent-metadata.json")) : {});
|
|
41047
|
-
const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
|
|
41048
|
-
const commandClassification = syntheticStatusFromCommands(commands);
|
|
41049
|
-
const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
|
|
41050
|
-
const reasonCode = commandClassification.status === "fail" ? commandClassification.reasonCode : blockerCodes[0] || commandClassification.reasonCode;
|
|
41051
|
-
const firstCommand = commands[0] || {};
|
|
41052
|
-
const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
|
|
41053
|
-
const endedAt = latestCommandTime(commands) || startedAt;
|
|
41054
|
-
const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
|
|
41055
|
-
const runId = (0, import_path16.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
|
|
41939
|
+
function buildExecutedExternalAgentRunArtifact(input) {
|
|
41940
|
+
const commands = readCommandRecords(input.run.run_dir);
|
|
41941
|
+
const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
|
|
41056
41942
|
return {
|
|
41057
41943
|
schema_version: "external_agent_run.v1",
|
|
41058
|
-
run_id:
|
|
41059
|
-
status,
|
|
41060
|
-
failure_reason_code:
|
|
41061
|
-
model_provider:
|
|
41062
|
-
model_name:
|
|
41063
|
-
|
|
41064
|
-
|
|
41065
|
-
|
|
41944
|
+
run_id: input.run.run_id,
|
|
41945
|
+
status: input.status,
|
|
41946
|
+
failure_reason_code: input.reasonCode,
|
|
41947
|
+
model_provider: input.run.model_provider,
|
|
41948
|
+
model_name: input.run.model_name,
|
|
41949
|
+
runner_model: input.run.runner_model,
|
|
41950
|
+
agent_shell: `${input.run.command}-exec`,
|
|
41951
|
+
workspace_type: "clean-no-repo-programmatic",
|
|
41952
|
+
prompt_version: input.run.prompt_version,
|
|
41953
|
+
prompt_path: "prompt.txt",
|
|
41954
|
+
started_at: input.startedAt,
|
|
41955
|
+
ended_at: input.endedAt,
|
|
41066
41956
|
manual_intervention_count: 0,
|
|
41957
|
+
manual_interventions: [],
|
|
41067
41958
|
environment: {
|
|
41068
|
-
|
|
41069
|
-
|
|
41959
|
+
os: process.platform,
|
|
41960
|
+
node_version: process.version,
|
|
41961
|
+
npm_version: null,
|
|
41962
|
+
foh_cli_version: CLI_VERSION,
|
|
41963
|
+
runner_exit_code: input.exitCode,
|
|
41964
|
+
runner_timed_out: input.timedOut,
|
|
41965
|
+
duration_ms: input.durationMs
|
|
41070
41966
|
},
|
|
41071
41967
|
public_entrypoints: [
|
|
41072
41968
|
"https://frontofhouse.okii.uk",
|
|
41969
|
+
"https://frontofhouse.okii.uk/llms.txt",
|
|
41970
|
+
"https://frontofhouse.okii.uk/openapi.yaml",
|
|
41073
41971
|
"npx --yes @f-o-h/cli@latest"
|
|
41074
41972
|
],
|
|
41075
|
-
commands_run: commands.map((command) =>
|
|
41076
|
-
docs_pages_used:
|
|
41973
|
+
commands_run: commands.map((command) => command.command),
|
|
41974
|
+
docs_pages_used: agentMetadata.docs_pages_used,
|
|
41975
|
+
eval_state: {
|
|
41976
|
+
lifecycle_strategy: "reuse_existing_eval_state",
|
|
41977
|
+
org_reuse_expected: true,
|
|
41978
|
+
agent_reuse_expected: true,
|
|
41979
|
+
widget_reuse_expected: true,
|
|
41980
|
+
fresh_org_expected: false,
|
|
41981
|
+
ephemeral_org_expected: false,
|
|
41982
|
+
fresh_agent_expected: false,
|
|
41983
|
+
phone_purchase_expected: false,
|
|
41984
|
+
paid_resource_creation_expected: false,
|
|
41985
|
+
spend_policy_expected: NO_SPEND_POLICY,
|
|
41986
|
+
cleanup_expected: false,
|
|
41987
|
+
cleanup_strategy: "no_cleanup_for_reused_eval_state",
|
|
41988
|
+
paid_resource_strategy: "blocked_unless_explicit_byon_or_operator_approved",
|
|
41989
|
+
rationale: "Mass external-agent evals benchmark public docs/CLI/API clarity; reuse avoids paid phone and Twilio inventory churn."
|
|
41990
|
+
},
|
|
41077
41991
|
artifacts: {
|
|
41078
|
-
|
|
41079
|
-
|
|
41080
|
-
|
|
41992
|
+
terminal_transcript: relativeArtifactName(input.run.outputs.jsonl),
|
|
41993
|
+
command_log: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
|
|
41994
|
+
proof_bundle: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
|
|
41995
|
+
replay_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
|
|
41996
|
+
knowledge_packet: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
|
|
41997
|
+
improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
|
|
41998
|
+
agent_metadata: agentMetadata.path,
|
|
41999
|
+
notes: (0, import_fs17.existsSync)((0, import_path16.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
|
|
42000
|
+
runner_last_message: relativeArtifactName(input.run.outputs.last_message),
|
|
42001
|
+
runner_stderr: relativeArtifactName(input.run.outputs.stderr),
|
|
42002
|
+
codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
|
|
42003
|
+
codex_stderr: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.stderr) : null,
|
|
42004
|
+
artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
|
|
41081
42005
|
},
|
|
41082
|
-
summary: "
|
|
42006
|
+
summary: input.status === "pass" ? `Controlled ${input.run.command} external-agent run produced passing proof evidence.` : `Controlled ${input.run.command} external-agent run ended as ${input.status} with reason ${input.reasonCode}.`,
|
|
42007
|
+
next_commands: input.status === "pass" ? [externalAgentSummaryTemplateCommand()] : [
|
|
42008
|
+
"foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
|
|
42009
|
+
"foh bug improve --from external-agent-run --file <run_dir>/run.json --out <run_dir>/improvement-packet.json --json",
|
|
42010
|
+
externalAgentSummaryTemplateCommand()
|
|
42011
|
+
]
|
|
41083
42012
|
};
|
|
41084
42013
|
}
|
|
41085
|
-
|
|
41086
|
-
|
|
41087
|
-
|
|
41088
|
-
|
|
41089
|
-
|
|
41090
|
-
|
|
42014
|
+
|
|
42015
|
+
// src/lib/external-agent-runner-execution.ts
|
|
42016
|
+
var import_child_process5 = require("child_process");
|
|
42017
|
+
var import_fs18 = require("fs");
|
|
42018
|
+
var import_path17 = require("path");
|
|
42019
|
+
function buildCommandInvocation(command, args) {
|
|
42020
|
+
if (command.toLowerCase().endsWith(".cmd")) {
|
|
42021
|
+
const binDir = (0, import_path17.dirname)(command);
|
|
42022
|
+
const codexEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
42023
|
+
if ((0, import_fs18.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
|
|
42024
|
+
const geminiEntrypoint = (0, import_path17.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
|
|
42025
|
+
if ((0, import_fs18.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
|
|
42026
|
+
}
|
|
42027
|
+
return { command, args };
|
|
41091
42028
|
}
|
|
41092
|
-
function
|
|
41093
|
-
|
|
41094
|
-
|
|
41095
|
-
|
|
41096
|
-
const
|
|
41097
|
-
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
42029
|
+
function spawnExternalAgentRunner(input) {
|
|
42030
|
+
return new Promise((resolveRun) => {
|
|
42031
|
+
const started = Date.now();
|
|
42032
|
+
const commandInvocation = buildCommandInvocation(input.command, input.args);
|
|
42033
|
+
const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
|
|
42034
|
+
cwd: input.cwd,
|
|
42035
|
+
env: input.env,
|
|
42036
|
+
shell: false,
|
|
42037
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
42038
|
+
windowsHide: true
|
|
42039
|
+
});
|
|
42040
|
+
const stdout = (0, import_fs18.createWriteStream)(input.stdoutPath, { flags: "w" });
|
|
42041
|
+
const stderr = (0, import_fs18.createWriteStream)(input.stderrPath, { flags: "w" });
|
|
42042
|
+
child.stdout.pipe(stdout);
|
|
42043
|
+
child.stderr.pipe(stderr);
|
|
42044
|
+
child.stdin.end(input.prompt);
|
|
42045
|
+
let timedOut = false;
|
|
42046
|
+
const timer = setTimeout(() => {
|
|
42047
|
+
timedOut = true;
|
|
42048
|
+
if (child.pid && process.platform === "win32") {
|
|
42049
|
+
(0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
|
|
42050
|
+
} else {
|
|
42051
|
+
child.kill("SIGKILL");
|
|
41103
42052
|
}
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41108
|
-
|
|
41109
|
-
|
|
42053
|
+
}, input.timeoutMs);
|
|
42054
|
+
child.on("close", (exitCode) => {
|
|
42055
|
+
clearTimeout(timer);
|
|
42056
|
+
stdout.end();
|
|
42057
|
+
stderr.end();
|
|
42058
|
+
resolveRun({
|
|
42059
|
+
exitCode,
|
|
42060
|
+
timedOut,
|
|
42061
|
+
durationMs: Date.now() - started
|
|
41110
42062
|
});
|
|
41111
|
-
}
|
|
41112
|
-
|
|
41113
|
-
|
|
41114
|
-
|
|
42063
|
+
});
|
|
42064
|
+
child.on("error", () => {
|
|
42065
|
+
clearTimeout(timer);
|
|
42066
|
+
stdout.end();
|
|
42067
|
+
stderr.end();
|
|
42068
|
+
resolveRun({
|
|
42069
|
+
exitCode: null,
|
|
42070
|
+
timedOut,
|
|
42071
|
+
durationMs: Date.now() - started
|
|
41115
42072
|
});
|
|
41116
|
-
}
|
|
41117
|
-
}
|
|
41118
|
-
return { records, invalid_runs };
|
|
41119
|
-
}
|
|
41120
|
-
function latestCohortId(records) {
|
|
41121
|
-
return records.slice().sort((a, b) => b.sort_time - a.sort_time || b.path.localeCompare(a.path))[0]?.cohort_id ?? null;
|
|
41122
|
-
}
|
|
41123
|
-
function ownerSubsystemFor(reasonCode) {
|
|
41124
|
-
const reason = String(reasonCode || "").toLowerCase();
|
|
41125
|
-
if (reason.includes("simulation") || reason.includes("certification") || reason.includes("scenario")) return "dojo_certification";
|
|
41126
|
-
if (reason.includes("contact_phone") || reason.includes("voice_contact") || reason.includes("provider_capacity") || reason.includes("byon")) return "voice_contact";
|
|
41127
|
-
if (reason.includes("exec_policy") || reason.includes("policy_blocked") || reason.includes("sandbox") || reason.includes("runner") || reason.includes("codex")) return "infra_runner";
|
|
41128
|
-
if (reason.includes("api") || reason.includes("http_4") || reason.includes("http_5") || reason.includes("404") || reason.includes("500") || reason.includes("roundtrip")) return "api_contract";
|
|
41129
|
-
if (reason.includes("cli") || reason.includes("command") || reason.includes("flag")) return "cli";
|
|
41130
|
-
if (reason.includes("docs") || reason.includes("unclear") || reason.includes("not_found")) return "docs";
|
|
41131
|
-
if (reason.includes("auth") || reason.includes("org") || reason.includes("config")) return "infra_runner";
|
|
41132
|
-
if (reason.includes("runtime") || reason.includes("widget") || reason.includes("proof")) return "runtime";
|
|
41133
|
-
return "product_ux";
|
|
41134
|
-
}
|
|
41135
|
-
function recommendedFixFor(reasonCode) {
|
|
41136
|
-
const owner = ownerSubsystemFor(reasonCode);
|
|
41137
|
-
if (owner === "api_contract") return "fix_api";
|
|
41138
|
-
if (owner === "cli") return "fix_cli";
|
|
41139
|
-
if (owner === "docs") return "fix_docs";
|
|
41140
|
-
if (owner === "runtime") return "fix_runtime";
|
|
41141
|
-
if (owner === "dojo_certification") return "add_test";
|
|
41142
|
-
return "fix_config";
|
|
42073
|
+
});
|
|
42074
|
+
});
|
|
41143
42075
|
}
|
|
41144
|
-
|
|
41145
|
-
|
|
41146
|
-
|
|
41147
|
-
|
|
41148
|
-
|
|
41149
|
-
|
|
41150
|
-
|
|
41151
|
-
|
|
42076
|
+
|
|
42077
|
+
// src/lib/external-agent-executor.ts
|
|
42078
|
+
var GEMINI_HEADLESS_PROBE_TIMEOUT_MS = 15e3;
|
|
42079
|
+
var DEFAULT_FOH_API_URL2 = "https://api.frontofhouse.okii.uk";
|
|
42080
|
+
var ExternalAgentExecutorError = class extends Error {
|
|
42081
|
+
reasonCode;
|
|
42082
|
+
constructor(reasonCode, message) {
|
|
42083
|
+
super(message);
|
|
42084
|
+
this.name = "ExternalAgentExecutorError";
|
|
42085
|
+
this.reasonCode = reasonCode;
|
|
41152
42086
|
}
|
|
41153
|
-
|
|
42087
|
+
};
|
|
42088
|
+
function readExternalAgentEvalAuthEnv(env = process.env) {
|
|
42089
|
+
return {
|
|
42090
|
+
token: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || "").trim(),
|
|
42091
|
+
orgId: String(env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || "").trim(),
|
|
42092
|
+
apiUrl: String(env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_API_URL || DEFAULT_FOH_API_URL2).trim() || DEFAULT_FOH_API_URL2,
|
|
42093
|
+
expiresAt: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || "").trim()
|
|
42094
|
+
};
|
|
41154
42095
|
}
|
|
41155
|
-
function
|
|
41156
|
-
|
|
41157
|
-
|
|
41158
|
-
|
|
41159
|
-
if (!(0, import_fs17.existsSync)(artifactPath)) return null;
|
|
41160
|
-
try {
|
|
41161
|
-
const text = (0, import_fs17.readFileSync)(artifactPath, "utf8");
|
|
41162
|
-
const firstObject = text.indexOf("{");
|
|
41163
|
-
const lastObject = text.lastIndexOf("}");
|
|
41164
|
-
if (firstObject < 0 || lastObject <= firstObject) return null;
|
|
41165
|
-
return JSON.parse(text.slice(firstObject, lastObject + 1));
|
|
41166
|
-
} catch {
|
|
41167
|
-
return null;
|
|
41168
|
-
}
|
|
41169
|
-
}
|
|
41170
|
-
function commandTimingBreakdown(command, output) {
|
|
41171
|
-
const schemaVersion = String(output?.schema_version || "");
|
|
41172
|
-
if (schemaVersion === "foh_cli_proof_report.v1") {
|
|
41173
|
-
const timing = asObject(output?.timing) || {};
|
|
41174
|
-
const cacheSources = /* @__PURE__ */ new Map();
|
|
41175
|
-
let cacheWaitMs = 0;
|
|
41176
|
-
let cacheHitCount = 0;
|
|
41177
|
-
let cacheMissCount = 0;
|
|
41178
|
-
for (const check2 of toArray2(output?.checks)) {
|
|
41179
|
-
const detail = asObject(check2)?.detail;
|
|
41180
|
-
const proofCache = asObject(asObject(detail)?.proof_cache);
|
|
41181
|
-
if (!proofCache) continue;
|
|
41182
|
-
if (proofCache["hit"] === true) cacheHitCount += 1;
|
|
41183
|
-
if (proofCache["miss"] === true) cacheMissCount += 1;
|
|
41184
|
-
cacheWaitMs += Number(proofCache["waited_ms"] || 0);
|
|
41185
|
-
increment(cacheSources, proofCache["source"] || "unknown");
|
|
41186
|
-
}
|
|
41187
|
-
return {
|
|
41188
|
-
kind: "proof",
|
|
41189
|
-
command: command.command || "",
|
|
41190
|
-
total_ms: Number(timing.total_ms || command.duration_ms || 0),
|
|
41191
|
-
total_check_duration_ms: Number(timing.total_check_duration_ms || 0),
|
|
41192
|
-
slow_checks: toArray2(timing.slow_checks).slice(0, 5),
|
|
41193
|
-
cache_wait_ms: cacheWaitMs,
|
|
41194
|
-
cache_hit_count: cacheHitCount,
|
|
41195
|
-
cache_miss_count: cacheMissCount,
|
|
41196
|
-
cache_sources: ranked(cacheSources)
|
|
41197
|
-
};
|
|
41198
|
-
}
|
|
41199
|
-
if (schemaVersion === "foh_certification_run.v1") {
|
|
41200
|
-
const timing = asObject(output?.timing) || {};
|
|
41201
|
-
const certificate = asObject(output?.certificate) || {};
|
|
41202
|
-
return {
|
|
41203
|
-
kind: "certification",
|
|
41204
|
-
command: command.command || "",
|
|
41205
|
-
total_ms: Number(timing.total_ms || command.duration_ms || 0),
|
|
41206
|
-
api_ms: Number(timing.api_ms || 0),
|
|
41207
|
-
status: output?.status || null,
|
|
41208
|
-
reason_code: output?.reason_code || null,
|
|
41209
|
-
scenario_summary: certificate.scenario_summary || null
|
|
41210
|
-
};
|
|
41211
|
-
}
|
|
41212
|
-
return null;
|
|
41213
|
-
}
|
|
41214
|
-
function analyzeRunArtifacts(runPath, run, cwd) {
|
|
41215
|
-
const runDir = (0, import_path16.dirname)(runPath);
|
|
41216
|
-
const commands = collapseCommandRecords(readNdjson((0, import_path16.join)(runDir, "commands.ndjson")));
|
|
41217
|
-
const reasonCounts = /* @__PURE__ */ new Map();
|
|
41218
|
-
const slowSteps = [];
|
|
41219
|
-
const timingBreakdowns = [];
|
|
41220
|
-
let completed = 0;
|
|
41221
|
-
let withDuration = 0;
|
|
41222
|
-
let totalDuration = 0;
|
|
41223
|
-
for (const command of commands) {
|
|
41224
|
-
const output = readCommandOutputJson(runDir, command);
|
|
41225
|
-
const breakdown = commandTimingBreakdown(command, output);
|
|
41226
|
-
if (breakdown) timingBreakdowns.push({
|
|
41227
|
-
run_id: run.run_id,
|
|
41228
|
-
run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
|
|
41229
|
-
...breakdown
|
|
41230
|
-
});
|
|
41231
|
-
if (command.phase === "completed" || command.completed_at) completed += 1;
|
|
41232
|
-
if (typeof command.duration_ms === "number") {
|
|
41233
|
-
withDuration += 1;
|
|
41234
|
-
totalDuration += command.duration_ms;
|
|
41235
|
-
slowSteps.push({
|
|
41236
|
-
run_id: run.run_id,
|
|
41237
|
-
run_path: (0, import_path16.relative)(cwd, runPath).replaceAll("\\", "/"),
|
|
41238
|
-
command: command.command || "",
|
|
41239
|
-
duration_ms: command.duration_ms,
|
|
41240
|
-
status: command.status || null,
|
|
41241
|
-
reason_code: command.reason_code || null,
|
|
41242
|
-
check_reason_codes: Array.isArray(command.check_reason_codes) ? command.check_reason_codes : []
|
|
41243
|
-
});
|
|
41244
|
-
}
|
|
41245
|
-
if (command.reason_code) increment(reasonCounts, command.reason_code);
|
|
41246
|
-
for (const reasonCode of toArray2(command.check_reason_codes)) {
|
|
41247
|
-
if (reasonCode) increment(reasonCounts, reasonCode);
|
|
41248
|
-
}
|
|
41249
|
-
}
|
|
41250
|
-
const codexEvents = readNdjson((0, import_path16.join)(runDir, "codex-exec.jsonl"));
|
|
41251
|
-
const codexDocs = /* @__PURE__ */ new Set();
|
|
41252
|
-
let codexCommandExecutions = 0;
|
|
41253
|
-
let codexFailedExitCodes = 0;
|
|
41254
|
-
for (const event of codexEvents) {
|
|
41255
|
-
const item = asObject(event.item) || event;
|
|
41256
|
-
if (item.type === "command_execution" && item.status === "completed") {
|
|
41257
|
-
codexCommandExecutions += 1;
|
|
41258
|
-
if (typeof item.exit_code === "number" && item.exit_code !== 0) codexFailedExitCodes += 1;
|
|
41259
|
-
}
|
|
41260
|
-
for (const url2 of collectDocUrls(JSON.stringify(event))) codexDocs.add(url2);
|
|
41261
|
-
}
|
|
41262
|
-
const docs = /* @__PURE__ */ new Set([
|
|
41263
|
-
...toArray2(run.docs_pages_used).map(String),
|
|
41264
|
-
...Array.from(codexDocs)
|
|
41265
|
-
]);
|
|
41266
|
-
return {
|
|
41267
|
-
command_log_present: (0, import_fs17.existsSync)((0, import_path16.join)(runDir, "commands.ndjson")),
|
|
41268
|
-
command_count: commands.length,
|
|
41269
|
-
completed_command_count: completed,
|
|
41270
|
-
missing_completion_count: Math.max(0, commands.length - completed),
|
|
41271
|
-
commands_with_duration_count: withDuration,
|
|
41272
|
-
total_command_duration_ms: totalDuration,
|
|
41273
|
-
command_reason_codes: ranked(reasonCounts),
|
|
41274
|
-
slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms) - Number(a.duration_ms)).slice(0, 10),
|
|
41275
|
-
timing_breakdowns: timingBreakdowns,
|
|
41276
|
-
docs_pages_observed: Array.from(docs).sort(),
|
|
41277
|
-
codex_command_execution_completed_count: codexCommandExecutions,
|
|
41278
|
-
codex_failed_exit_code_count: codexFailedExitCodes
|
|
41279
|
-
};
|
|
41280
|
-
}
|
|
41281
|
-
function summarizeExternalAgentRuns(options) {
|
|
41282
|
-
const cwd = (0, import_path16.resolve)(options.cwd || process.cwd());
|
|
41283
|
-
const root = (0, import_path16.resolve)(cwd, options.root);
|
|
41284
|
-
const loaded = readRunRecords(root, cwd);
|
|
41285
|
-
const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
|
|
41286
|
-
const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
|
|
41287
|
-
const statusCounts = /* @__PURE__ */ new Map();
|
|
41288
|
-
const modelCounts = /* @__PURE__ */ new Map();
|
|
41289
|
-
const failureCounts = /* @__PURE__ */ new Map();
|
|
41290
|
-
const commandReasonCounts = /* @__PURE__ */ new Map();
|
|
41291
|
-
const docsCounts = /* @__PURE__ */ new Map();
|
|
41292
|
-
const slowSteps = [];
|
|
41293
|
-
const timingBreakdowns = [];
|
|
41294
|
-
let manualInterventions = 0;
|
|
41295
|
-
let commandCount = 0;
|
|
41296
|
-
let completedCommandCount = 0;
|
|
41297
|
-
let missingCompletionCount = 0;
|
|
41298
|
-
let commandsWithDurationCount = 0;
|
|
41299
|
-
let totalCommandDurationMs = 0;
|
|
41300
|
-
let commandLogRunCount = 0;
|
|
41301
|
-
let codexCommandExecutions = 0;
|
|
41302
|
-
let codexFailedExitCodes = 0;
|
|
41303
|
-
for (const record2 of records) {
|
|
41304
|
-
const run = record2.run;
|
|
41305
|
-
increment(statusCounts, run.status);
|
|
41306
|
-
increment(modelCounts, `${run.model_provider}/${run.model_name}`);
|
|
41307
|
-
manualInterventions += Number(run.manual_intervention_count || 0);
|
|
41308
|
-
if (run.status !== "pass") increment(failureCounts, run.failure_reason_code || "unknown");
|
|
41309
|
-
const artifactSummary = analyzeRunArtifacts(record2.path, run, cwd);
|
|
41310
|
-
if (artifactSummary.command_log_present) commandLogRunCount += 1;
|
|
41311
|
-
commandCount += Number(artifactSummary.command_count || 0);
|
|
41312
|
-
completedCommandCount += Number(artifactSummary.completed_command_count || 0);
|
|
41313
|
-
missingCompletionCount += Number(artifactSummary.missing_completion_count || 0);
|
|
41314
|
-
commandsWithDurationCount += Number(artifactSummary.commands_with_duration_count || 0);
|
|
41315
|
-
totalCommandDurationMs += Number(artifactSummary.total_command_duration_ms || 0);
|
|
41316
|
-
codexCommandExecutions += Number(artifactSummary.codex_command_execution_completed_count || 0);
|
|
41317
|
-
codexFailedExitCodes += Number(artifactSummary.codex_failed_exit_code_count || 0);
|
|
41318
|
-
for (const row of toArray2(artifactSummary.slow_steps)) slowSteps.push(row);
|
|
41319
|
-
for (const row of toArray2(artifactSummary.timing_breakdowns)) timingBreakdowns.push(row);
|
|
41320
|
-
for (const row of toArray2(artifactSummary.command_reason_codes)) {
|
|
41321
|
-
const entry = asObject(row);
|
|
41322
|
-
if (entry) increment(commandReasonCounts, entry.key, Number(entry.count || 1));
|
|
41323
|
-
}
|
|
41324
|
-
for (const page of toArray2(artifactSummary.docs_pages_observed)) increment(docsCounts, page);
|
|
41325
|
-
}
|
|
41326
|
-
const topFailures = ranked(failureCounts);
|
|
41327
|
-
const commandReasonCodes2 = ranked(commandReasonCounts);
|
|
41328
|
-
const recommendedFixes = topFailures.map((failure) => ({
|
|
41329
|
-
reason_code: failure.key,
|
|
41330
|
-
count: failure.count,
|
|
41331
|
-
recommended_fix: recommendedFixFor(failure.key),
|
|
41332
|
-
owner_subsystem: ownerSubsystemFor(failure.key)
|
|
41333
|
-
}));
|
|
41334
|
-
const nextRecommendedFix = recommendedFixes[0] || null;
|
|
41335
|
-
return {
|
|
41336
|
-
schema_version: "external_agent_run_summary.v1",
|
|
41337
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41338
|
-
root: (0, import_path16.relative)(cwd, root).replaceAll("\\", "/") || ".",
|
|
41339
|
-
cohort_id: selectedCohortId,
|
|
41340
|
-
current_baseline_only: Boolean(selectedCohortId),
|
|
41341
|
-
run_count: records.length,
|
|
41342
|
-
invalid_run_count: selectedCohortId ? 0 : loaded.invalid_runs.length,
|
|
41343
|
-
status_counts: Object.fromEntries(statusCounts),
|
|
41344
|
-
model_counts: ranked(modelCounts),
|
|
41345
|
-
manual_intervention_count: manualInterventions,
|
|
41346
|
-
top_failure_reason_codes: topFailures,
|
|
41347
|
-
docs_pages_observed: ranked(docsCounts),
|
|
41348
|
-
command_telemetry: {
|
|
41349
|
-
run_count_with_command_log: commandLogRunCount,
|
|
41350
|
-
command_count: commandCount,
|
|
41351
|
-
completed_command_count: completedCommandCount,
|
|
41352
|
-
missing_completion_count: missingCompletionCount,
|
|
41353
|
-
commands_with_duration_count: commandsWithDurationCount,
|
|
41354
|
-
total_command_duration_ms: totalCommandDurationMs,
|
|
41355
|
-
command_reason_codes: commandReasonCodes2,
|
|
41356
|
-
slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20),
|
|
41357
|
-
timing_breakdowns: timingBreakdowns.sort((a, b) => Number(b.total_ms || 0) - Number(a.total_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
|
|
41358
|
-
},
|
|
41359
|
-
codex_telemetry: {
|
|
41360
|
-
command_execution_completed_count: codexCommandExecutions,
|
|
41361
|
-
failed_exit_code_count: codexFailedExitCodes
|
|
41362
|
-
},
|
|
41363
|
-
recommended_fixes: recommendedFixes,
|
|
41364
|
-
next_recommended_fix: nextRecommendedFix,
|
|
41365
|
-
fix_selection_policy: {
|
|
41366
|
-
mode: "coherent_failure_cluster_first",
|
|
41367
|
-
rule: "Fix the highest-impact owner subsystem locally with focused proof, then rerun the same prompt once externally.",
|
|
41368
|
-
run_failure_weight: 3,
|
|
41369
|
-
command_reason_weight: 1
|
|
41370
|
-
},
|
|
41371
|
-
next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
|
|
41372
|
-
invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
|
|
41373
|
-
run_paths: records.map((record2) => (0, import_path16.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
|
|
41374
|
-
};
|
|
41375
|
-
}
|
|
41376
|
-
function runExternalAgentRunSummary(options) {
|
|
41377
|
-
const summary = summarizeExternalAgentRuns(options);
|
|
41378
|
-
const invalidRuns = toArray2(summary.invalid_runs);
|
|
41379
|
-
const status = invalidRuns.length > 0 ? "failed" : "passed";
|
|
41380
|
-
const report = {
|
|
41381
|
-
report_schema_version: "script_report.v1",
|
|
41382
|
-
script: "foh eval external-agent summary",
|
|
41383
|
-
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41384
|
-
status,
|
|
41385
|
-
errors: invalidRuns.map((entry) => {
|
|
41386
|
-
const object3 = asObject(entry);
|
|
41387
|
-
return `${object3?.path || "unknown"}: ${JSON.stringify(object3?.findings || [])}`;
|
|
41388
|
-
}),
|
|
41389
|
-
warnings: Number(summary.run_count || 0) === 0 ? ["no external-agent run artifacts found"] : [],
|
|
41390
|
-
report: summary
|
|
41391
|
-
};
|
|
41392
|
-
if (options.out) {
|
|
41393
|
-
(0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
|
|
41394
|
-
(0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
|
|
41395
|
-
`, "utf8");
|
|
41396
|
-
}
|
|
41397
|
-
if (options.report) {
|
|
41398
|
-
(0, import_fs17.mkdirSync)((0, import_path16.dirname)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
|
|
41399
|
-
(0, import_fs17.writeFileSync)((0, import_path16.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
|
|
41400
|
-
`, "utf8");
|
|
41401
|
-
}
|
|
41402
|
-
return { summary, report };
|
|
41403
|
-
}
|
|
41404
|
-
|
|
41405
|
-
// src/lib/external-agent-executor-classification.ts
|
|
41406
|
-
function proofArtifactPasses(runDir) {
|
|
41407
|
-
const proofPath = (0, import_path17.join)(runDir, "proof.json");
|
|
41408
|
-
if (!(0, import_fs18.existsSync)(proofPath)) return false;
|
|
41409
|
-
try {
|
|
41410
|
-
const parsed = JSON.parse((0, import_fs18.readFileSync)(proofPath, "utf8"));
|
|
41411
|
-
return parsed.ok === true || parsed.status === "pass" || parsed.status === "passed";
|
|
41412
|
-
} catch {
|
|
41413
|
-
return false;
|
|
41414
|
-
}
|
|
41415
|
-
}
|
|
41416
|
-
function readIfExists(path2) {
|
|
41417
|
-
return (0, import_fs18.existsSync)(path2) ? (0, import_fs18.readFileSync)(path2, "utf8") : "";
|
|
41418
|
-
}
|
|
41419
|
-
function relativeArtifactName(path2) {
|
|
41420
|
-
return (0, import_path17.basename)(path2);
|
|
41421
|
-
}
|
|
41422
|
-
function classifyExternalAgentRun(input) {
|
|
41423
|
-
if (input.timedOut) return { status: "hold", reasonCode: `${input.run.command}_runner_timeout` };
|
|
41424
|
-
if (!input.artifactSafetyOk) return { status: "fail", reasonCode: "external_agent_artifact_safety_blocked" };
|
|
41425
|
-
const completedCommands = readCommandRecords(input.run.run_dir).filter((record2) => record2.phase === "completed");
|
|
41426
|
-
const observedVersions = completedCommands.map((record2) => String(record2.cli_version || "").trim()).filter((version2) => /^\d+\.\d+\.\d+$/.test(version2));
|
|
41427
|
-
if (observedVersions.some((version2) => version2 !== CLI_VERSION)) {
|
|
41428
|
-
return { status: "hold", reasonCode: "external_agent_cli_version_drift" };
|
|
41429
|
-
}
|
|
41430
|
-
const commandReasonCodes2 = completedCommands.flatMap((record2) => [
|
|
41431
|
-
String(record2.reason_code || ""),
|
|
41432
|
-
...Array.isArray(record2.check_reason_codes) ? record2.check_reason_codes.map((code) => String(code || "")) : []
|
|
41433
|
-
]).filter(Boolean);
|
|
41434
|
-
const hasCommandReason = (pattern) => commandReasonCodes2.some((reason) => pattern.test(reason));
|
|
41435
|
-
if (hasCommandReason(new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i"))) {
|
|
41436
|
-
return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
|
|
41437
|
-
}
|
|
41438
|
-
if (hasCommandReason(/provider_capacity_blocked/i)) {
|
|
41439
|
-
return { status: "hold", reasonCode: "provider_capacity_blocked" };
|
|
41440
|
-
}
|
|
41441
|
-
if (hasCommandReason(/byon_voice_number_not_configured/i)) {
|
|
41442
|
-
return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
|
|
41443
|
-
}
|
|
41444
|
-
if (hasCommandReason(/contact_phone_provisioning_failed/i)) {
|
|
41445
|
-
return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
|
|
41446
|
-
}
|
|
41447
|
-
if (hasCommandReason(/voice_contact_expected_no_spend_hold/i)) {
|
|
41448
|
-
return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
|
|
41449
|
-
}
|
|
41450
|
-
if (hasCommandReason(/contact_phone_missing/i)) {
|
|
41451
|
-
return { status: "hold", reasonCode: "voice_contact_phone_missing" };
|
|
41452
|
-
}
|
|
41453
|
-
if (hasCommandReason(/sim(?:ulation)?[_-]?cert(?:ify|ification)?.*failed|simulation_certification_failed/i)) {
|
|
41454
|
-
return { status: "hold", reasonCode: "simulation_certification_failed" };
|
|
41455
|
-
}
|
|
41456
|
-
if (hasCommandReason(/proof_held/i)) {
|
|
41457
|
-
return { status: "hold", reasonCode: "external_agent_proof_held" };
|
|
41458
|
-
}
|
|
41459
|
-
if (hasCommandReason(/agent_limit_reached/i)) {
|
|
41460
|
-
return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
|
|
41461
|
-
}
|
|
41462
|
-
const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
|
|
41463
|
-
const metadataBlockerCodes = agentMetadata.blocker_reason_codes;
|
|
41464
|
-
const hasMetadataBlocker = (pattern) => metadataBlockerCodes.some((reason) => pattern.test(reason));
|
|
41465
|
-
if (hasMetadataBlocker(/^customer_owned_requirement_unverified:/i)) {
|
|
41466
|
-
return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
|
|
41467
|
-
}
|
|
41468
|
-
if (hasMetadataBlocker(/^api_health:/i) && metadataBlockerCodes.some((reason) => /^customer_owned_requirement_unverified:/i.test(reason))) {
|
|
41469
|
-
return { status: "hold", reasonCode: "customer_owned_requirements_unverified_expected_hold" };
|
|
41470
|
-
}
|
|
41471
|
-
const firstCommandReasonCode = commandReasonCodes2.find((reason) => reason.trim().length > 0) ?? null;
|
|
41472
|
-
if (firstCommandReasonCode) {
|
|
41473
|
-
return { status: "hold", reasonCode: firstCommandReasonCode };
|
|
41474
|
-
}
|
|
41475
|
-
const lastMessage = readIfExists(input.run.outputs.last_message);
|
|
41476
|
-
const stderr = readIfExists(input.run.outputs.stderr);
|
|
41477
|
-
const combined = `${lastMessage}
|
|
41478
|
-
${stderr}`;
|
|
41479
|
-
if (/need[^.\n]*(?:private|source)[^.\n]*repo|cannot[^.\n]*without[^.\n]*(?:private|source)[^.\n]*repo|clone[^.\n]*(?:private|source)[^.\n]*repo/i.test(combined)) {
|
|
41480
|
-
return { status: "fail", reasonCode: "private_repo_assumption_detected" };
|
|
41481
|
-
}
|
|
41482
|
-
if (/(?:blocked|rejected|declined) by policy|EXEC_POLICY_BLOCKED|command execution was rejected|shell commands were rejected/i.test(combined)) {
|
|
41483
|
-
return { status: "hold", reasonCode: "codex_exec_policy_blocked" };
|
|
41484
|
-
}
|
|
41485
|
-
if (/bwrap:.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|bubblewrap.*(?:RTM_NEWADDR|Operation not permitted|setting up uid map: Permission denied)|Failed RTM_NEWADDR|ENV_SANDBOX_EXEC_BLOCKED|permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock|legacy[_ -]?landlock.*incompatible/i.test(combined)) {
|
|
41486
|
-
return { status: "hold", reasonCode: "codex_sandbox_exec_blocked" };
|
|
41487
|
-
}
|
|
41488
|
-
if (/ENV_NETWORK_DNS_BLOCK|Could not resolve host|npm ping.*timeout|NO_EXECUTABLE_INSTALL/i.test(combined)) {
|
|
41489
|
-
return { status: "hold", reasonCode: "codex_network_dns_blocked" };
|
|
41490
|
-
}
|
|
41491
|
-
if (new RegExp(PAID_RESOURCE_BLOCKED_REASON_CODE, "i").test(combined)) {
|
|
41492
|
-
return { status: "hold", reasonCode: PAID_RESOURCE_BLOCKED_REASON_CODE };
|
|
41493
|
-
}
|
|
41494
|
-
if (/provider_capacity_blocked/i.test(combined)) {
|
|
41495
|
-
return { status: "hold", reasonCode: "provider_capacity_blocked" };
|
|
41496
|
-
}
|
|
41497
|
-
if (/byon_voice_number_not_configured/i.test(combined)) {
|
|
41498
|
-
return { status: "hold", reasonCode: "byon_voice_number_not_configured" };
|
|
41499
|
-
}
|
|
41500
|
-
if (/contact_phone_provisioning_failed/i.test(combined)) {
|
|
41501
|
-
return { status: "hold", reasonCode: "voice_contact_phone_provisioning_failed" };
|
|
41502
|
-
}
|
|
41503
|
-
if (/voice_contact_expected_no_spend_hold/i.test(combined)) {
|
|
41504
|
-
return { status: "hold", reasonCode: "voice_contact_expected_no_spend_hold" };
|
|
41505
|
-
}
|
|
41506
|
-
if (/contact_phone_missing/i.test(combined)) {
|
|
41507
|
-
return { status: "hold", reasonCode: "voice_contact_phone_missing" };
|
|
41508
|
-
}
|
|
41509
|
-
if (/simulation_certification_failed/i.test(combined)) {
|
|
41510
|
-
return { status: "hold", reasonCode: "simulation_certification_failed" };
|
|
41511
|
-
}
|
|
41512
|
-
if (/proof_held/i.test(combined)) {
|
|
41513
|
-
return { status: "hold", reasonCode: "external_agent_proof_held" };
|
|
41514
|
-
}
|
|
41515
|
-
if (/agent_limit_reached/i.test(combined)) {
|
|
41516
|
-
return { status: "hold", reasonCode: "eval_org_agent_limit_reached" };
|
|
41517
|
-
}
|
|
41518
|
-
if (/browser|approve|approval|login|auth|sign in/i.test(combined) && !proofArtifactPasses(input.run.run_dir)) {
|
|
41519
|
-
return { status: "hold", reasonCode: "auth_browser_approval_required" };
|
|
41520
|
-
}
|
|
41521
|
-
if (input.exitCode !== 0) return { status: "hold", reasonCode: `${input.run.command}_runner_nonzero_exit` };
|
|
41522
|
-
if (proofArtifactPasses(input.run.run_dir)) return { status: "pass", reasonCode: null };
|
|
41523
|
-
return { status: "hold", reasonCode: "external_agent_proof_artifact_missing" };
|
|
41524
|
-
}
|
|
41525
|
-
function buildExecutedExternalAgentRunArtifact(input) {
|
|
41526
|
-
const commands = readCommandRecords(input.run.run_dir);
|
|
41527
|
-
const agentMetadata = readExternalAgentMetadata(input.run.run_dir);
|
|
41528
|
-
return {
|
|
41529
|
-
schema_version: "external_agent_run.v1",
|
|
41530
|
-
run_id: input.run.run_id,
|
|
41531
|
-
status: input.status,
|
|
41532
|
-
failure_reason_code: input.reasonCode,
|
|
41533
|
-
model_provider: input.run.model_provider,
|
|
41534
|
-
model_name: input.run.model_name,
|
|
41535
|
-
runner_model: input.run.runner_model,
|
|
41536
|
-
agent_shell: `${input.run.command}-exec`,
|
|
41537
|
-
workspace_type: "clean-no-repo-programmatic",
|
|
41538
|
-
prompt_version: input.run.prompt_version,
|
|
41539
|
-
prompt_path: "prompt.txt",
|
|
41540
|
-
started_at: input.startedAt,
|
|
41541
|
-
ended_at: input.endedAt,
|
|
41542
|
-
manual_intervention_count: 0,
|
|
41543
|
-
manual_interventions: [],
|
|
41544
|
-
environment: {
|
|
41545
|
-
os: process.platform,
|
|
41546
|
-
node_version: process.version,
|
|
41547
|
-
npm_version: null,
|
|
41548
|
-
foh_cli_version: CLI_VERSION,
|
|
41549
|
-
runner_exit_code: input.exitCode,
|
|
41550
|
-
runner_timed_out: input.timedOut,
|
|
41551
|
-
duration_ms: input.durationMs
|
|
41552
|
-
},
|
|
41553
|
-
public_entrypoints: [
|
|
41554
|
-
"https://frontofhouse.okii.uk",
|
|
41555
|
-
"https://frontofhouse.okii.uk/llms.txt",
|
|
41556
|
-
"https://frontofhouse.okii.uk/openapi.yaml",
|
|
41557
|
-
"npx --yes @f-o-h/cli@latest"
|
|
41558
|
-
],
|
|
41559
|
-
commands_run: commands.map((command) => command.command),
|
|
41560
|
-
docs_pages_used: agentMetadata.docs_pages_used,
|
|
41561
|
-
eval_state: {
|
|
41562
|
-
lifecycle_strategy: "reuse_existing_eval_state",
|
|
41563
|
-
org_reuse_expected: true,
|
|
41564
|
-
agent_reuse_expected: true,
|
|
41565
|
-
widget_reuse_expected: true,
|
|
41566
|
-
fresh_org_expected: false,
|
|
41567
|
-
ephemeral_org_expected: false,
|
|
41568
|
-
fresh_agent_expected: false,
|
|
41569
|
-
phone_purchase_expected: false,
|
|
41570
|
-
paid_resource_creation_expected: false,
|
|
41571
|
-
spend_policy_expected: NO_SPEND_POLICY,
|
|
41572
|
-
cleanup_expected: false,
|
|
41573
|
-
cleanup_strategy: "no_cleanup_for_reused_eval_state",
|
|
41574
|
-
paid_resource_strategy: "blocked_unless_explicit_byon_or_operator_approved",
|
|
41575
|
-
rationale: "Mass external-agent evals benchmark public docs/CLI/API clarity; reuse avoids paid phone and Twilio inventory churn."
|
|
41576
|
-
},
|
|
41577
|
-
artifacts: {
|
|
41578
|
-
terminal_transcript: relativeArtifactName(input.run.outputs.jsonl),
|
|
41579
|
-
command_log: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "commands.ndjson")) ? "commands.ndjson" : null,
|
|
41580
|
-
proof_bundle: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "proof.json")) ? "proof.json" : null,
|
|
41581
|
-
replay_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "replay.json")) ? "replay.json" : null,
|
|
41582
|
-
knowledge_packet: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "knowledge.json")) ? "knowledge.json" : null,
|
|
41583
|
-
improvement_packet: input.status === "pass" ? null : "improvement-packet.json",
|
|
41584
|
-
agent_metadata: agentMetadata.path,
|
|
41585
|
-
notes: (0, import_fs18.existsSync)((0, import_path17.join)(input.run.run_dir, "notes.md")) ? "notes.md" : null,
|
|
41586
|
-
runner_last_message: relativeArtifactName(input.run.outputs.last_message),
|
|
41587
|
-
runner_stderr: relativeArtifactName(input.run.outputs.stderr),
|
|
41588
|
-
codex_last_message: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.last_message) : null,
|
|
41589
|
-
codex_stderr: input.run.command === "codex" ? relativeArtifactName(input.run.outputs.stderr) : null,
|
|
41590
|
-
artifact_safety: relativeArtifactName(input.run.outputs.artifact_safety)
|
|
41591
|
-
},
|
|
41592
|
-
summary: input.status === "pass" ? `Controlled ${input.run.command} external-agent run produced passing proof evidence.` : `Controlled ${input.run.command} external-agent run ended as ${input.status} with reason ${input.reasonCode}.`,
|
|
41593
|
-
next_commands: input.status === "pass" ? [externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))] : [
|
|
41594
|
-
"foh eval external-agent scan-artifacts --run-dir <run_dir> --private-repo-root <private_repo_root> --write-redacted --json",
|
|
41595
|
-
"foh bug improve --from external-agent-run --file <run_dir>/run.json --out <run_dir>/improvement-packet.json --json",
|
|
41596
|
-
externalAgentSummaryCommand((0, import_path17.dirname)(input.run.run_dir))
|
|
41597
|
-
]
|
|
41598
|
-
};
|
|
41599
|
-
}
|
|
41600
|
-
|
|
41601
|
-
// src/lib/external-agent-runner-execution.ts
|
|
41602
|
-
var import_child_process5 = require("child_process");
|
|
41603
|
-
var import_fs19 = require("fs");
|
|
41604
|
-
var import_path18 = require("path");
|
|
41605
|
-
function buildCommandInvocation(command, args) {
|
|
41606
|
-
if (process.platform === "win32" && command.toLowerCase().endsWith(".cmd")) {
|
|
41607
|
-
const binDir = (0, import_path18.dirname)(command);
|
|
41608
|
-
const codexEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
41609
|
-
if ((0, import_fs19.existsSync)(codexEntrypoint)) return { command: process.execPath, args: [codexEntrypoint, ...args] };
|
|
41610
|
-
const geminiEntrypoint = (0, import_path18.join)(binDir, "node_modules", "@google", "gemini-cli", "bundle", "gemini.js");
|
|
41611
|
-
if ((0, import_fs19.existsSync)(geminiEntrypoint)) return { command: process.execPath, args: ["--no-warnings=DEP0040", geminiEntrypoint, ...args] };
|
|
41612
|
-
}
|
|
41613
|
-
return { command, args };
|
|
41614
|
-
}
|
|
41615
|
-
function spawnExternalAgentRunner(input) {
|
|
41616
|
-
return new Promise((resolveRun) => {
|
|
41617
|
-
const started = Date.now();
|
|
41618
|
-
const commandInvocation = buildCommandInvocation(input.command, input.args);
|
|
41619
|
-
const child = (0, import_child_process5.spawn)(commandInvocation.command, commandInvocation.args, {
|
|
41620
|
-
cwd: input.cwd,
|
|
41621
|
-
env: input.env,
|
|
41622
|
-
shell: false,
|
|
41623
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
41624
|
-
windowsHide: true
|
|
41625
|
-
});
|
|
41626
|
-
const stdout = (0, import_fs19.createWriteStream)(input.stdoutPath, { flags: "w" });
|
|
41627
|
-
const stderr = (0, import_fs19.createWriteStream)(input.stderrPath, { flags: "w" });
|
|
41628
|
-
child.stdout.pipe(stdout);
|
|
41629
|
-
child.stderr.pipe(stderr);
|
|
41630
|
-
child.stdin.end(input.prompt);
|
|
41631
|
-
let timedOut = false;
|
|
41632
|
-
const timer = setTimeout(() => {
|
|
41633
|
-
timedOut = true;
|
|
41634
|
-
if (child.pid && process.platform === "win32") {
|
|
41635
|
-
(0, import_child_process5.spawnSync)("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" });
|
|
41636
|
-
} else {
|
|
41637
|
-
child.kill("SIGKILL");
|
|
41638
|
-
}
|
|
41639
|
-
}, input.timeoutMs);
|
|
41640
|
-
child.on("close", (exitCode) => {
|
|
41641
|
-
clearTimeout(timer);
|
|
41642
|
-
stdout.end();
|
|
41643
|
-
stderr.end();
|
|
41644
|
-
resolveRun({
|
|
41645
|
-
exitCode,
|
|
41646
|
-
timedOut,
|
|
41647
|
-
durationMs: Date.now() - started
|
|
41648
|
-
});
|
|
41649
|
-
});
|
|
41650
|
-
child.on("error", () => {
|
|
41651
|
-
clearTimeout(timer);
|
|
41652
|
-
stdout.end();
|
|
41653
|
-
stderr.end();
|
|
41654
|
-
resolveRun({
|
|
41655
|
-
exitCode: null,
|
|
41656
|
-
timedOut,
|
|
41657
|
-
durationMs: Date.now() - started
|
|
41658
|
-
});
|
|
41659
|
-
});
|
|
41660
|
-
});
|
|
41661
|
-
}
|
|
41662
|
-
|
|
41663
|
-
// src/lib/external-agent-executor.ts
|
|
41664
|
-
var GEMINI_HEADLESS_PROBE_TIMEOUT_MS = 15e3;
|
|
41665
|
-
var DEFAULT_FOH_API_URL2 = "https://api.frontofhouse.okii.uk";
|
|
41666
|
-
var ExternalAgentExecutorError = class extends Error {
|
|
41667
|
-
reasonCode;
|
|
41668
|
-
constructor(reasonCode, message) {
|
|
41669
|
-
super(message);
|
|
41670
|
-
this.name = "ExternalAgentExecutorError";
|
|
41671
|
-
this.reasonCode = reasonCode;
|
|
41672
|
-
}
|
|
41673
|
-
};
|
|
41674
|
-
function readExternalAgentEvalAuthEnv(env = process.env) {
|
|
41675
|
-
return {
|
|
41676
|
-
token: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || "").trim(),
|
|
41677
|
-
orgId: String(env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || "").trim(),
|
|
41678
|
-
apiUrl: String(env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_API_URL || DEFAULT_FOH_API_URL2).trim() || DEFAULT_FOH_API_URL2,
|
|
41679
|
-
expiresAt: String(env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || "").trim()
|
|
41680
|
-
};
|
|
41681
|
-
}
|
|
41682
|
-
function shouldRunExternalAgentEvalAuthPreflight(env = process.env) {
|
|
41683
|
-
return Boolean(
|
|
41684
|
-
env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || env.FOH_SERVICE_TOKEN || env.FOH_ORG_ID || env.FOH_API_URL || env.FOH_TOKEN_EXPIRES_AT
|
|
41685
|
-
);
|
|
42096
|
+
function shouldRunExternalAgentEvalAuthPreflight(env = process.env) {
|
|
42097
|
+
return Boolean(
|
|
42098
|
+
env.FOH_EXTERNAL_AGENT_EVAL_TOKEN || env.FOH_EXTERNAL_AGENT_EVAL_ORG_ID || env.FOH_EXTERNAL_AGENT_EVAL_API_URL || env.FOH_EXTERNAL_AGENT_EVAL_TOKEN_EXPIRES_AT || env.FOH_SERVICE_TOKEN || env.FOH_ORG_ID || env.FOH_API_URL || env.FOH_TOKEN_EXPIRES_AT
|
|
42099
|
+
);
|
|
41686
42100
|
}
|
|
41687
42101
|
async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}) {
|
|
41688
42102
|
const hasExplicitEvalAuth = Boolean(
|
|
@@ -41736,14 +42150,14 @@ async function runExternalAgentEvalAuthPreflight(env = process.env, options = {}
|
|
|
41736
42150
|
};
|
|
41737
42151
|
}
|
|
41738
42152
|
function normalizeForCompare(path2) {
|
|
41739
|
-
const resolved = (0,
|
|
42153
|
+
const resolved = (0, import_path18.resolve)(path2);
|
|
41740
42154
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
41741
42155
|
}
|
|
41742
42156
|
function isPathInside(childPath, parentPath) {
|
|
41743
42157
|
const child = normalizeForCompare(childPath);
|
|
41744
42158
|
const parent = normalizeForCompare(parentPath);
|
|
41745
|
-
const rel = (0,
|
|
41746
|
-
return rel === "" || !!rel && !rel.startsWith("..") && !(0,
|
|
42159
|
+
const rel = (0, import_path18.relative)(parent, child);
|
|
42160
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !(0, import_path18.isAbsolute)(rel);
|
|
41747
42161
|
}
|
|
41748
42162
|
function requireString(value, field) {
|
|
41749
42163
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -41752,10 +42166,10 @@ function requireString(value, field) {
|
|
|
41752
42166
|
return value;
|
|
41753
42167
|
}
|
|
41754
42168
|
function readBatch(batchPath) {
|
|
41755
|
-
if (!(0,
|
|
42169
|
+
if (!(0, import_fs19.existsSync)(batchPath)) {
|
|
41756
42170
|
throw new ExternalAgentExecutorError("external_agent_batch_not_found", `Batch file not found: ${batchPath}`);
|
|
41757
42171
|
}
|
|
41758
|
-
const parsed = JSON.parse((0,
|
|
42172
|
+
const parsed = JSON.parse((0, import_fs19.readFileSync)(batchPath, "utf8"));
|
|
41759
42173
|
if (parsed.schema_version !== "external_agent_batch_plan.v1") {
|
|
41760
42174
|
throw new ExternalAgentExecutorError("invalid_external_agent_batch", "Batch schema_version must be external_agent_batch_plan.v1.");
|
|
41761
42175
|
}
|
|
@@ -41792,8 +42206,8 @@ function resolveCodexProbeCommand() {
|
|
|
41792
42206
|
if (process.platform !== "win32") return "codex";
|
|
41793
42207
|
const appData = process.env.APPDATA;
|
|
41794
42208
|
if (appData) {
|
|
41795
|
-
const appDataShim = (0,
|
|
41796
|
-
if ((0,
|
|
42209
|
+
const appDataShim = (0, import_path18.join)(appData, "npm", "codex.cmd");
|
|
42210
|
+
if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
|
|
41797
42211
|
}
|
|
41798
42212
|
return "codex.cmd";
|
|
41799
42213
|
}
|
|
@@ -41804,8 +42218,8 @@ function resolveGeminiProbeCommand() {
|
|
|
41804
42218
|
if (process.platform !== "win32") return "gemini";
|
|
41805
42219
|
const appData = process.env.APPDATA;
|
|
41806
42220
|
if (appData) {
|
|
41807
|
-
const appDataShim = (0,
|
|
41808
|
-
if ((0,
|
|
42221
|
+
const appDataShim = (0, import_path18.join)(appData, "npm", "gemini.cmd");
|
|
42222
|
+
if ((0, import_fs19.existsSync)(appDataShim)) return appDataShim;
|
|
41809
42223
|
}
|
|
41810
42224
|
return "gemini.cmd";
|
|
41811
42225
|
}
|
|
@@ -42076,34 +42490,34 @@ function safeRunId(value) {
|
|
|
42076
42490
|
return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
|
|
42077
42491
|
}
|
|
42078
42492
|
function resolveWorkspaceRoot(input) {
|
|
42079
|
-
if (input.workspaceRoot) return (0,
|
|
42080
|
-
const batchStem = (0,
|
|
42081
|
-
const repoStem = (0,
|
|
42082
|
-
return (0,
|
|
42493
|
+
if (input.workspaceRoot) return (0, import_path18.resolve)(input.workspaceRoot);
|
|
42494
|
+
const batchStem = (0, import_path18.basename)((0, import_path18.resolve)(input.batchPath)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
|
|
42495
|
+
const repoStem = (0, import_path18.basename)((0, import_path18.resolve)(input.privateRepoRoot)).replace(/[^a-zA-Z0-9_.-]+/g, "-");
|
|
42496
|
+
return (0, import_path18.resolve)((0, import_os3.tmpdir)(), "foh-external-agent-workspaces", repoStem, batchStem);
|
|
42083
42497
|
}
|
|
42084
42498
|
function findNearestGitRoot(startPath) {
|
|
42085
|
-
let current = (0,
|
|
42499
|
+
let current = (0, import_path18.resolve)(startPath);
|
|
42086
42500
|
while (true) {
|
|
42087
|
-
if ((0,
|
|
42088
|
-
const parent = (0,
|
|
42501
|
+
if ((0, import_fs19.existsSync)((0, import_path18.join)(current, ".git"))) return current;
|
|
42502
|
+
const parent = (0, import_path18.dirname)(current);
|
|
42089
42503
|
if (parent === current) return null;
|
|
42090
42504
|
current = parent;
|
|
42091
42505
|
}
|
|
42092
42506
|
}
|
|
42093
42507
|
function resolvePrivateRepoRoot(input) {
|
|
42094
42508
|
if (input.explicitPrivateRepoRoot) {
|
|
42095
|
-
return { root: (0,
|
|
42509
|
+
return { root: (0, import_path18.resolve)(input.explicitPrivateRepoRoot), explicit: true };
|
|
42096
42510
|
}
|
|
42097
|
-
const cwd = (0,
|
|
42511
|
+
const cwd = (0, import_path18.resolve)(input.cwd || process.cwd());
|
|
42098
42512
|
const gitRoot = findNearestGitRoot(cwd);
|
|
42099
42513
|
if (gitRoot) return { root: gitRoot, explicit: false };
|
|
42100
42514
|
return {
|
|
42101
|
-
root: (0,
|
|
42515
|
+
root: (0, import_path18.join)(cwd, ".foh-no-private-repo-root-sentinel"),
|
|
42102
42516
|
explicit: false
|
|
42103
42517
|
};
|
|
42104
42518
|
}
|
|
42105
42519
|
function promptVersionFromPath(promptPath) {
|
|
42106
|
-
const raw = (0,
|
|
42520
|
+
const raw = (0, import_fs19.readFileSync)(promptPath, "utf8");
|
|
42107
42521
|
if (raw.includes("arbitrary-agency setup context") || raw.includes("Mission: prove whether an arbitrary estate agency") || raw.includes("ops reporting agency-setup-preview") || raw.includes("ops reporting agency-setup-workflow")) return "arbitrary-agency-setup-release.v1";
|
|
42108
42522
|
if (raw.includes("Do not assume access to the private source repository")) return "blank-setup.v1";
|
|
42109
42523
|
return "unknown";
|
|
@@ -42113,7 +42527,7 @@ function createExternalAgentExecutorPlan(options) {
|
|
|
42113
42527
|
if (runner !== "codex" && runner !== "gemini") {
|
|
42114
42528
|
throw new ExternalAgentExecutorError("unsupported_external_agent_runner", `Unsupported runner: ${runner}`);
|
|
42115
42529
|
}
|
|
42116
|
-
const batchPath = (0,
|
|
42530
|
+
const batchPath = (0, import_path18.resolve)(options.batchPath);
|
|
42117
42531
|
const batch = readBatch(batchPath);
|
|
42118
42532
|
const runnerProbe = validateRunner(options, runner);
|
|
42119
42533
|
const codexSandboxBackend = normalizeCodexSandboxBackend(options.codexSandboxBackend);
|
|
@@ -42132,17 +42546,17 @@ function createExternalAgentExecutorPlan(options) {
|
|
|
42132
42546
|
`Workspace root must be outside the private repository. workspace=${workspaceRoot} repo=${privateRepoRoot}`
|
|
42133
42547
|
);
|
|
42134
42548
|
}
|
|
42135
|
-
(0,
|
|
42136
|
-
const batchDir = (0,
|
|
42549
|
+
(0, import_fs19.mkdirSync)(workspaceRoot, { recursive: true });
|
|
42550
|
+
const batchDir = (0, import_path18.resolve)(String(batch.batch_dir || (0, import_path18.resolve)(batchPath, "..")));
|
|
42137
42551
|
const timeoutMinutes = Number.isFinite(options.timeoutMinutes) && Number(options.timeoutMinutes) > 0 ? Number(options.timeoutMinutes) : 30;
|
|
42138
42552
|
const runs = batch.runs.map((run) => {
|
|
42139
42553
|
const runId = safeRunId(requireString(run.run_id, "runs[].run_id"));
|
|
42140
|
-
const runDir = (0,
|
|
42141
|
-
const promptPath = (0,
|
|
42142
|
-
const workspaceDir = (0,
|
|
42143
|
-
(0,
|
|
42144
|
-
(0,
|
|
42145
|
-
(0,
|
|
42554
|
+
const runDir = (0, import_path18.resolve)(requireString(run.run_dir, `runs[${runId}].run_dir`));
|
|
42555
|
+
const promptPath = (0, import_path18.resolve)(requireString(run.prompt_path, `runs[${runId}].prompt_path`));
|
|
42556
|
+
const workspaceDir = (0, import_path18.join)(workspaceRoot, runId);
|
|
42557
|
+
(0, import_fs19.mkdirSync)(workspaceDir, { recursive: true });
|
|
42558
|
+
(0, import_fs19.writeFileSync)(
|
|
42559
|
+
(0, import_path18.join)(workspaceDir, "README.md"),
|
|
42146
42560
|
[
|
|
42147
42561
|
"# FOH External-Agent Workspace",
|
|
42148
42562
|
"",
|
|
@@ -42153,18 +42567,19 @@ function createExternalAgentExecutorPlan(options) {
|
|
|
42153
42567
|
].join("\n"),
|
|
42154
42568
|
"utf8"
|
|
42155
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);
|
|
42156
42571
|
const env = buildCodexExecutorEnv({
|
|
42157
42572
|
sourceEnv: options.env,
|
|
42158
42573
|
runDir,
|
|
42159
|
-
promptVersion:
|
|
42574
|
+
promptVersion: explicitPromptVersion
|
|
42160
42575
|
});
|
|
42161
42576
|
const promptVersion = String(env[EXTERNAL_AGENT_PROMPT_VERSION_ENV] || "unknown");
|
|
42162
42577
|
const outputStem = runner === "gemini" ? "gemini" : "codex";
|
|
42163
|
-
const jsonlPath = (0,
|
|
42164
|
-
const lastMessagePath = (0,
|
|
42165
|
-
const stderrPath = (0,
|
|
42166
|
-
const runPath = (0,
|
|
42167
|
-
const artifactSafetyPath = (0,
|
|
42578
|
+
const jsonlPath = (0, import_path18.join)(runDir, `${outputStem}-exec.jsonl`);
|
|
42579
|
+
const lastMessagePath = (0, import_path18.join)(runDir, `${outputStem}-last-message.md`);
|
|
42580
|
+
const stderrPath = (0, import_path18.join)(runDir, `${outputStem}-stderr.txt`);
|
|
42581
|
+
const runPath = (0, import_path18.join)(runDir, "run.json");
|
|
42582
|
+
const artifactSafetyPath = (0, import_path18.join)(runDir, "artifact-safety.json");
|
|
42168
42583
|
const args = runner === "gemini" ? [
|
|
42169
42584
|
...runnerProbe.globalArgs,
|
|
42170
42585
|
...runnerProbe.execArgs
|
|
@@ -42255,9 +42670,9 @@ function createExternalAgentExecutorPlan(options) {
|
|
|
42255
42670
|
};
|
|
42256
42671
|
}
|
|
42257
42672
|
function writeExternalAgentExecutorPlan(plan) {
|
|
42258
|
-
const path2 = (0,
|
|
42259
|
-
(0,
|
|
42260
|
-
(0,
|
|
42673
|
+
const path2 = (0, import_path18.join)(plan.batch_dir, "executor-plan.json");
|
|
42674
|
+
(0, import_fs19.mkdirSync)(plan.batch_dir, { recursive: true });
|
|
42675
|
+
(0, import_fs19.writeFileSync)(path2, `${JSON.stringify(plan, null, 2)}
|
|
42261
42676
|
`, "utf8");
|
|
42262
42677
|
return path2;
|
|
42263
42678
|
}
|
|
@@ -42272,7 +42687,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42272
42687
|
if (authPreflight && !authPreflight.ok) {
|
|
42273
42688
|
const endedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
42274
42689
|
const blockedResults = plan.runs.map((run) => {
|
|
42275
|
-
(0,
|
|
42690
|
+
(0, import_fs19.mkdirSync)(run.run_dir, { recursive: true });
|
|
42276
42691
|
const runArtifact = buildExecutedExternalAgentRunArtifact({
|
|
42277
42692
|
run,
|
|
42278
42693
|
startedAt,
|
|
@@ -42283,7 +42698,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42283
42698
|
timedOut: false,
|
|
42284
42699
|
durationMs: 0
|
|
42285
42700
|
});
|
|
42286
|
-
(0,
|
|
42701
|
+
(0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
|
|
42287
42702
|
`, "utf8");
|
|
42288
42703
|
return {
|
|
42289
42704
|
run_id: run.run_id,
|
|
@@ -42310,8 +42725,8 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42310
42725
|
}
|
|
42311
42726
|
for (const run of plan.runs) {
|
|
42312
42727
|
const runStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
42313
|
-
const commandCaptureDir = (0,
|
|
42314
|
-
(0,
|
|
42728
|
+
const commandCaptureDir = (0, import_path18.join)(run.workspace_dir, ".foh-capture");
|
|
42729
|
+
(0, import_fs19.mkdirSync)(commandCaptureDir, { recursive: true });
|
|
42315
42730
|
const env = buildCodexExecutorEnv({
|
|
42316
42731
|
sourceEnv: options.env,
|
|
42317
42732
|
runDir: commandCaptureDir,
|
|
@@ -42322,7 +42737,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42322
42737
|
args: run.args,
|
|
42323
42738
|
cwd: run.workspace_dir,
|
|
42324
42739
|
env,
|
|
42325
|
-
prompt: (0,
|
|
42740
|
+
prompt: (0, import_fs19.readFileSync)(run.prompt_path, "utf8"),
|
|
42326
42741
|
stdoutPath: run.outputs.jsonl,
|
|
42327
42742
|
stderrPath: run.outputs.stderr,
|
|
42328
42743
|
timeoutMs: plan.timeout_minutes * 60 * 1e3
|
|
@@ -42335,7 +42750,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42335
42750
|
privateRepoRoot,
|
|
42336
42751
|
writeRedacted: true
|
|
42337
42752
|
});
|
|
42338
|
-
(0,
|
|
42753
|
+
(0, import_fs19.writeFileSync)(run.outputs.artifact_safety, `${JSON.stringify(artifactSafety, null, 2)}
|
|
42339
42754
|
`, "utf8");
|
|
42340
42755
|
const runEndedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
42341
42756
|
const classification = classifyExternalAgentRun({
|
|
@@ -42354,7 +42769,7 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42354
42769
|
timedOut: spawned.timedOut,
|
|
42355
42770
|
durationMs: spawned.durationMs
|
|
42356
42771
|
});
|
|
42357
|
-
(0,
|
|
42772
|
+
(0, import_fs19.writeFileSync)(run.outputs.run, `${JSON.stringify(runArtifact, null, 2)}
|
|
42358
42773
|
`, "utf8");
|
|
42359
42774
|
results.push({
|
|
42360
42775
|
run_id: run.run_id,
|
|
@@ -42384,11 +42799,546 @@ async function executeExternalAgentExecutorPlan(plan, options = {}) {
|
|
|
42384
42799
|
};
|
|
42385
42800
|
}
|
|
42386
42801
|
|
|
42802
|
+
// src/lib/external-agent-run-summary.ts
|
|
42803
|
+
var import_fs20 = require("fs");
|
|
42804
|
+
var import_path19 = require("path");
|
|
42805
|
+
var REQUIRED_RUN_FIELDS = [
|
|
42806
|
+
"schema_version",
|
|
42807
|
+
"run_id",
|
|
42808
|
+
"status",
|
|
42809
|
+
"model_provider",
|
|
42810
|
+
"model_name",
|
|
42811
|
+
"prompt_version",
|
|
42812
|
+
"started_at",
|
|
42813
|
+
"manual_intervention_count",
|
|
42814
|
+
"environment",
|
|
42815
|
+
"public_entrypoints",
|
|
42816
|
+
"commands_run",
|
|
42817
|
+
"docs_pages_used",
|
|
42818
|
+
"artifacts"
|
|
42819
|
+
];
|
|
42820
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["pass", "hold", "fail"]);
|
|
42821
|
+
var DOC_URL_RE = /https:\/\/frontofhouse\.okii\.uk\/[^\s"'`)<>,;\\\]}]*/g;
|
|
42822
|
+
function quoteShellArg(value) {
|
|
42823
|
+
const text = String(value);
|
|
42824
|
+
if (/^[A-Za-z0-9_./:=@-]+$/.test(text)) return text;
|
|
42825
|
+
return `"${text.replace(/(["$`])/g, "\\$1")}"`;
|
|
42826
|
+
}
|
|
42827
|
+
function externalAgentSummaryCommand(root) {
|
|
42828
|
+
const summaryPath = (0, import_path19.join)(root, "latest-summary.json");
|
|
42829
|
+
const reportPath = (0, import_path19.join)(root, "summary.report.json");
|
|
42830
|
+
return [
|
|
42831
|
+
"foh",
|
|
42832
|
+
"eval",
|
|
42833
|
+
"external-agent",
|
|
42834
|
+
"summary",
|
|
42835
|
+
"--root",
|
|
42836
|
+
quoteShellArg(root),
|
|
42837
|
+
"--out",
|
|
42838
|
+
quoteShellArg(summaryPath),
|
|
42839
|
+
"--report",
|
|
42840
|
+
quoteShellArg(reportPath),
|
|
42841
|
+
"--json"
|
|
42842
|
+
].join(" ");
|
|
42843
|
+
}
|
|
42844
|
+
function readJson(filePath) {
|
|
42845
|
+
return JSON.parse((0, import_fs20.readFileSync)(filePath, "utf8").replace(/^\uFEFF/, ""));
|
|
42846
|
+
}
|
|
42847
|
+
function readNdjson(filePath) {
|
|
42848
|
+
if (!(0, import_fs20.existsSync)(filePath)) return [];
|
|
42849
|
+
return (0, import_fs20.readFileSync)(filePath, "utf8").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
42850
|
+
try {
|
|
42851
|
+
const parsed = JSON.parse(line);
|
|
42852
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
42853
|
+
} catch {
|
|
42854
|
+
return null;
|
|
42855
|
+
}
|
|
42856
|
+
}).filter((record2) => Boolean(record2));
|
|
42857
|
+
}
|
|
42858
|
+
function asObject(value) {
|
|
42859
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
42860
|
+
}
|
|
42861
|
+
function toArray2(value) {
|
|
42862
|
+
return Array.isArray(value) ? value : [];
|
|
42863
|
+
}
|
|
42864
|
+
function increment(map3, key, amount = 1) {
|
|
42865
|
+
const normalized = String(key || "unknown");
|
|
42866
|
+
map3.set(normalized, (map3.get(normalized) || 0) + amount);
|
|
42867
|
+
}
|
|
42868
|
+
function ranked(map3) {
|
|
42869
|
+
return Array.from(map3.entries()).map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
|
|
42870
|
+
}
|
|
42871
|
+
function collectDocUrls(text) {
|
|
42872
|
+
return Array.from(new Set((String(text || "").match(DOC_URL_RE) || []).map((url2) => url2.replace(/[.?!:]+$/g, "")).filter((url2) => url2.startsWith("https://frontofhouse.okii.uk/")))).sort();
|
|
42873
|
+
}
|
|
42874
|
+
function findRunCandidates(root) {
|
|
42875
|
+
if (!(0, import_fs20.existsSync)(root)) return [];
|
|
42876
|
+
const candidates = [];
|
|
42877
|
+
const seenRunDirs = /* @__PURE__ */ new Set();
|
|
42878
|
+
const captureDirs = [];
|
|
42879
|
+
const stack = [root];
|
|
42880
|
+
while (stack.length > 0) {
|
|
42881
|
+
const current = stack.pop();
|
|
42882
|
+
if (!current) continue;
|
|
42883
|
+
for (const entry of (0, import_fs20.readdirSync)(current, { withFileTypes: true })) {
|
|
42884
|
+
const absolute = (0, import_path19.join)(current, entry.name);
|
|
42885
|
+
if (entry.isDirectory()) {
|
|
42886
|
+
stack.push(absolute);
|
|
42887
|
+
} else if (entry.isFile() && entry.name === "run.json") {
|
|
42888
|
+
candidates.push({ path: absolute, synthetic: false });
|
|
42889
|
+
seenRunDirs.add((0, import_path19.dirname)(absolute));
|
|
42890
|
+
} else if (entry.isFile() && entry.name === "commands.ndjson") {
|
|
42891
|
+
captureDirs.push(current);
|
|
42892
|
+
}
|
|
42893
|
+
}
|
|
42894
|
+
}
|
|
42895
|
+
for (const captureDir of captureDirs) {
|
|
42896
|
+
if (seenRunDirs.has(captureDir)) continue;
|
|
42897
|
+
candidates.push({ path: (0, import_path19.join)(captureDir, "run.json"), synthetic: true });
|
|
42898
|
+
}
|
|
42899
|
+
return candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
42900
|
+
}
|
|
42901
|
+
function validateExternalAgentRun(value) {
|
|
42902
|
+
const findings = [];
|
|
42903
|
+
const run = asObject(value);
|
|
42904
|
+
if (!run) return [{ id: "run_not_object", detail: "run artifact must be an object" }];
|
|
42905
|
+
for (const field of REQUIRED_RUN_FIELDS) {
|
|
42906
|
+
if (!(field in run)) findings.push({ id: "required_field_missing", field });
|
|
42907
|
+
}
|
|
42908
|
+
if (run.schema_version !== "external_agent_run.v1") {
|
|
42909
|
+
findings.push({ id: "schema_version_invalid", expected: "external_agent_run.v1", actual: run.schema_version ?? null });
|
|
42910
|
+
}
|
|
42911
|
+
if (!VALID_STATUSES.has(String(run.status || ""))) {
|
|
42912
|
+
findings.push({ id: "status_invalid", expected: Array.from(VALID_STATUSES), actual: run.status ?? null });
|
|
42913
|
+
}
|
|
42914
|
+
if ((run.status === "hold" || run.status === "fail") && !String(run.failure_reason_code || "").trim()) {
|
|
42915
|
+
findings.push({ id: "failure_reason_code_missing" });
|
|
42916
|
+
}
|
|
42917
|
+
if (!Number.isInteger(run.manual_intervention_count) || Number(run.manual_intervention_count) < 0) {
|
|
42918
|
+
findings.push({ id: "manual_intervention_count_invalid" });
|
|
42919
|
+
}
|
|
42920
|
+
if (!Array.isArray(run.commands_run)) findings.push({ id: "commands_run_invalid" });
|
|
42921
|
+
if (!Array.isArray(run.docs_pages_used)) findings.push({ id: "docs_pages_used_invalid" });
|
|
42922
|
+
if (!asObject(run.environment)) findings.push({ id: "environment_invalid" });
|
|
42923
|
+
if (!asObject(run.artifacts)) findings.push({ id: "artifacts_invalid" });
|
|
42924
|
+
if (toArray2(run.public_entrypoints).length === 0) findings.push({ id: "public_entrypoints_missing" });
|
|
42925
|
+
return findings;
|
|
42926
|
+
}
|
|
42927
|
+
function runSortTime(run) {
|
|
42928
|
+
const raw = String(run.ended_at || run.started_at || "");
|
|
42929
|
+
const time3 = Date.parse(raw);
|
|
42930
|
+
return Number.isFinite(time3) ? time3 : 0;
|
|
42931
|
+
}
|
|
42932
|
+
function latestCommandTime(commands) {
|
|
42933
|
+
const times = commands.map((command) => String(command.completed_at || command.started_at || command.recorded_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => b.time - a.time);
|
|
42934
|
+
return times[0]?.raw ?? null;
|
|
42935
|
+
}
|
|
42936
|
+
function firstCommandTime(commands) {
|
|
42937
|
+
const times = commands.map((command) => String(command.started_at || command.recorded_at || command.completed_at || "")).map((raw) => ({ raw, time: Date.parse(raw) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => a.time - b.time);
|
|
42938
|
+
return times[0]?.raw ?? null;
|
|
42939
|
+
}
|
|
42940
|
+
function commandReasonCodes(commands) {
|
|
42941
|
+
const codes = /* @__PURE__ */ new Set();
|
|
42942
|
+
for (const command of commands) {
|
|
42943
|
+
if (command.reason_code) codes.add(String(command.reason_code));
|
|
42944
|
+
for (const reasonCode of toArray2(command.check_reason_codes)) {
|
|
42945
|
+
if (reasonCode) codes.add(String(reasonCode));
|
|
42946
|
+
}
|
|
42947
|
+
}
|
|
42948
|
+
return Array.from(codes);
|
|
42949
|
+
}
|
|
42950
|
+
function syntheticStatusFromCommands(commands) {
|
|
42951
|
+
const commandReasons = commandReasonCodes(commands);
|
|
42952
|
+
const failed = commands.find((command) => {
|
|
42953
|
+
const status = String(command.status || "").toLowerCase();
|
|
42954
|
+
return status === "fail" || typeof command.exit_code === "number" && command.exit_code !== 0 && status !== "hold";
|
|
42955
|
+
});
|
|
42956
|
+
if (failed) {
|
|
42957
|
+
return {
|
|
42958
|
+
status: "fail",
|
|
42959
|
+
reasonCode: String(failed.reason_code || commandReasons[0] || "external_agent_command_failed")
|
|
42960
|
+
};
|
|
42961
|
+
}
|
|
42962
|
+
const held = commands.find((command) => String(command.status || "").toLowerCase() === "hold");
|
|
42963
|
+
if (held) {
|
|
42964
|
+
return {
|
|
42965
|
+
status: "hold",
|
|
42966
|
+
reasonCode: String(held.reason_code || commandReasons[0] || "external_agent_command_held")
|
|
42967
|
+
};
|
|
42968
|
+
}
|
|
42969
|
+
if (commands.length === 0) {
|
|
42970
|
+
return { status: "hold", reasonCode: "external_agent_capture_empty" };
|
|
42971
|
+
}
|
|
42972
|
+
return { status: "pass", reasonCode: null };
|
|
42973
|
+
}
|
|
42974
|
+
function synthesizeRunFromCapture(runPath) {
|
|
42975
|
+
const runDir = (0, import_path19.dirname)(runPath);
|
|
42976
|
+
const commands = collapseCommandRecords(readNdjson((0, import_path19.join)(runDir, "commands.ndjson")));
|
|
42977
|
+
const metadata = asObject((0, import_fs20.existsSync)((0, import_path19.join)(runDir, "external-agent-metadata.json")) ? readJson((0, import_path19.join)(runDir, "external-agent-metadata.json")) : {});
|
|
42978
|
+
const blockerCodes = toArray2(metadata?.blocker_reason_codes).map(String).filter(Boolean);
|
|
42979
|
+
const commandClassification = syntheticStatusFromCommands(commands);
|
|
42980
|
+
const status = commandClassification.status === "fail" ? "fail" : blockerCodes.length > 0 ? "hold" : commandClassification.status;
|
|
42981
|
+
const reasonCode = commandClassification.status === "fail" ? commandClassification.reasonCode : blockerCodes[0] || commandClassification.reasonCode;
|
|
42982
|
+
const firstCommand = commands[0] || {};
|
|
42983
|
+
const startedAt = firstCommandTime(commands) || (/* @__PURE__ */ new Date(0)).toISOString();
|
|
42984
|
+
const endedAt = latestCommandTime(commands) || startedAt;
|
|
42985
|
+
const docs = toArray2(metadata?.docs_pages_used).map(String).filter(Boolean);
|
|
42986
|
+
const runId = (0, import_path19.dirname)(runPath).split(/[\\/]/).filter(Boolean).slice(-3).join("-") || "capture-only-run";
|
|
42987
|
+
return {
|
|
42988
|
+
schema_version: "external_agent_run.v1",
|
|
42989
|
+
run_id: runId,
|
|
42990
|
+
status,
|
|
42991
|
+
failure_reason_code: status === "pass" ? null : reasonCode || "external_agent_capture_unfinalized",
|
|
42992
|
+
model_provider: "unknown",
|
|
42993
|
+
model_name: "unknown",
|
|
42994
|
+
prompt_version: String(firstCommand.prompt_version || "unknown"),
|
|
42995
|
+
started_at: startedAt,
|
|
42996
|
+
ended_at: endedAt,
|
|
42997
|
+
manual_intervention_count: 0,
|
|
42998
|
+
environment: {
|
|
42999
|
+
foh_cli_version: firstCommand.cli_version || null,
|
|
43000
|
+
capture_only: true
|
|
43001
|
+
},
|
|
43002
|
+
public_entrypoints: [
|
|
43003
|
+
"https://frontofhouse.okii.uk",
|
|
43004
|
+
"npx --yes @f-o-h/cli@latest"
|
|
43005
|
+
],
|
|
43006
|
+
commands_run: commands.map((command) => String(command.command || "")).filter(Boolean),
|
|
43007
|
+
docs_pages_used: docs,
|
|
43008
|
+
artifacts: {
|
|
43009
|
+
command_log: "commands.ndjson",
|
|
43010
|
+
agent_metadata: (0, import_fs20.existsSync)((0, import_path19.join)(runDir, "external-agent-metadata.json")) ? "external-agent-metadata.json" : null,
|
|
43011
|
+
capture_only: true
|
|
43012
|
+
},
|
|
43013
|
+
summary: "Synthetic run record created from external-agent capture artifacts because run.json was missing."
|
|
43014
|
+
};
|
|
43015
|
+
}
|
|
43016
|
+
function cohortIdForRunPath(root, runPath) {
|
|
43017
|
+
const normalized = (0, import_path19.relative)(root, (0, import_path19.dirname)(runPath)).replaceAll("\\", "/");
|
|
43018
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
43019
|
+
if (parts.length === 0) return ".";
|
|
43020
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(parts[0]) && parts[1]) return `${parts[0]}/${parts[1]}`;
|
|
43021
|
+
return parts[0];
|
|
43022
|
+
}
|
|
43023
|
+
function readRunRecords(root, cwd) {
|
|
43024
|
+
const records = [];
|
|
43025
|
+
const invalid_runs = [];
|
|
43026
|
+
for (const candidate of findRunCandidates(root)) {
|
|
43027
|
+
const file2 = candidate.path;
|
|
43028
|
+
try {
|
|
43029
|
+
const parsed = candidate.synthetic ? synthesizeRunFromCapture(file2) : readJson(file2);
|
|
43030
|
+
const findings = validateExternalAgentRun(parsed);
|
|
43031
|
+
if (findings.length > 0) {
|
|
43032
|
+
invalid_runs.push({ path: (0, import_path19.relative)(cwd, file2).replaceAll("\\", "/"), findings });
|
|
43033
|
+
continue;
|
|
43034
|
+
}
|
|
43035
|
+
const run = parsed;
|
|
43036
|
+
records.push({
|
|
43037
|
+
path: file2,
|
|
43038
|
+
run,
|
|
43039
|
+
cohort_id: cohortIdForRunPath(root, file2),
|
|
43040
|
+
sort_time: runSortTime(run)
|
|
43041
|
+
});
|
|
43042
|
+
} catch (error2) {
|
|
43043
|
+
invalid_runs.push({
|
|
43044
|
+
path: (0, import_path19.relative)(cwd, file2).replaceAll("\\", "/"),
|
|
43045
|
+
findings: [{ id: "json_parse_failed", detail: error2 instanceof Error ? error2.message : String(error2) }]
|
|
43046
|
+
});
|
|
43047
|
+
}
|
|
43048
|
+
}
|
|
43049
|
+
return { records, invalid_runs };
|
|
43050
|
+
}
|
|
43051
|
+
function latestCohortId(records) {
|
|
43052
|
+
return records.slice().sort((a, b) => b.sort_time - a.sort_time || b.path.localeCompare(a.path))[0]?.cohort_id ?? null;
|
|
43053
|
+
}
|
|
43054
|
+
function ownerSubsystemFor(reasonCode) {
|
|
43055
|
+
const reason = String(reasonCode || "").toLowerCase();
|
|
43056
|
+
if (reason.includes("simulation") || reason.includes("certification") || reason.includes("scenario")) return "dojo_certification";
|
|
43057
|
+
if (reason.includes("contact_phone") || reason.includes("voice_contact") || reason.includes("provider_capacity") || reason.includes("byon")) return "voice_contact";
|
|
43058
|
+
if (reason.includes("exec_policy") || reason.includes("policy_blocked") || reason.includes("sandbox") || reason.includes("runner") || reason.includes("codex")) return "infra_runner";
|
|
43059
|
+
if (reason.includes("api") || reason.includes("http_4") || reason.includes("http_5") || reason.includes("404") || reason.includes("500") || reason.includes("roundtrip")) return "api_contract";
|
|
43060
|
+
if (reason.includes("cli") || reason.includes("command") || reason.includes("flag")) return "cli";
|
|
43061
|
+
if (reason.includes("docs") || reason.includes("unclear") || reason.includes("not_found")) return "docs";
|
|
43062
|
+
if (reason.includes("auth") || reason.includes("org") || reason.includes("config")) return "infra_runner";
|
|
43063
|
+
if (reason.includes("runtime") || reason.includes("widget") || reason.includes("proof")) return "runtime";
|
|
43064
|
+
return "product_ux";
|
|
43065
|
+
}
|
|
43066
|
+
function recommendedFixFor(reasonCode) {
|
|
43067
|
+
const owner = ownerSubsystemFor(reasonCode);
|
|
43068
|
+
if (owner === "api_contract") return "fix_api";
|
|
43069
|
+
if (owner === "cli") return "fix_cli";
|
|
43070
|
+
if (owner === "docs") return "fix_docs";
|
|
43071
|
+
if (owner === "runtime") return "fix_runtime";
|
|
43072
|
+
if (owner === "dojo_certification") return "add_test";
|
|
43073
|
+
return "fix_config";
|
|
43074
|
+
}
|
|
43075
|
+
function collapseCommandRecords(records) {
|
|
43076
|
+
const order = [];
|
|
43077
|
+
const byId = /* @__PURE__ */ new Map();
|
|
43078
|
+
for (const record2 of records) {
|
|
43079
|
+
const id = String(record2.command_id || `${record2.recorded_at || ""}:${record2.command || ""}`);
|
|
43080
|
+
if (!byId.has(id)) order.push(id);
|
|
43081
|
+
const previous = byId.get(id);
|
|
43082
|
+
byId.set(id, record2.phase === "completed" ? record2 : previous || record2);
|
|
43083
|
+
}
|
|
43084
|
+
return order.map((id) => byId.get(id)).filter((record2) => Boolean(record2));
|
|
43085
|
+
}
|
|
43086
|
+
function readCommandOutputJson(runDir, command) {
|
|
43087
|
+
const artifact = typeof command.output_artifact === "string" && command.output_artifact.trim() ? command.output_artifact.trim() : "";
|
|
43088
|
+
if (!artifact || artifact.includes("..") || artifact.includes("/") || artifact.includes("\\")) return null;
|
|
43089
|
+
const artifactPath = (0, import_path19.join)(runDir, artifact);
|
|
43090
|
+
if (!(0, import_fs20.existsSync)(artifactPath)) return null;
|
|
43091
|
+
try {
|
|
43092
|
+
const text = (0, import_fs20.readFileSync)(artifactPath, "utf8");
|
|
43093
|
+
const firstObject = text.indexOf("{");
|
|
43094
|
+
const lastObject = text.lastIndexOf("}");
|
|
43095
|
+
if (firstObject < 0 || lastObject <= firstObject) return null;
|
|
43096
|
+
return JSON.parse(text.slice(firstObject, lastObject + 1));
|
|
43097
|
+
} catch {
|
|
43098
|
+
return null;
|
|
43099
|
+
}
|
|
43100
|
+
}
|
|
43101
|
+
function commandTimingBreakdown(command, output) {
|
|
43102
|
+
const schemaVersion = String(output?.schema_version || "");
|
|
43103
|
+
if (schemaVersion === "foh_cli_proof_report.v1") {
|
|
43104
|
+
const timing = asObject(output?.timing) || {};
|
|
43105
|
+
const cacheSources = /* @__PURE__ */ new Map();
|
|
43106
|
+
let cacheWaitMs = 0;
|
|
43107
|
+
let cacheHitCount = 0;
|
|
43108
|
+
let cacheMissCount = 0;
|
|
43109
|
+
for (const check2 of toArray2(output?.checks)) {
|
|
43110
|
+
const detail = asObject(check2)?.detail;
|
|
43111
|
+
const proofCache = asObject(asObject(detail)?.proof_cache);
|
|
43112
|
+
if (!proofCache) continue;
|
|
43113
|
+
if (proofCache["hit"] === true) cacheHitCount += 1;
|
|
43114
|
+
if (proofCache["miss"] === true) cacheMissCount += 1;
|
|
43115
|
+
cacheWaitMs += Number(proofCache["waited_ms"] || 0);
|
|
43116
|
+
increment(cacheSources, proofCache["source"] || "unknown");
|
|
43117
|
+
}
|
|
43118
|
+
return {
|
|
43119
|
+
kind: "proof",
|
|
43120
|
+
command: command.command || "",
|
|
43121
|
+
total_ms: Number(timing.total_ms || command.duration_ms || 0),
|
|
43122
|
+
total_check_duration_ms: Number(timing.total_check_duration_ms || 0),
|
|
43123
|
+
slow_checks: toArray2(timing.slow_checks).slice(0, 5),
|
|
43124
|
+
cache_wait_ms: cacheWaitMs,
|
|
43125
|
+
cache_hit_count: cacheHitCount,
|
|
43126
|
+
cache_miss_count: cacheMissCount,
|
|
43127
|
+
cache_sources: ranked(cacheSources)
|
|
43128
|
+
};
|
|
43129
|
+
}
|
|
43130
|
+
if (schemaVersion === "foh_certification_run.v1") {
|
|
43131
|
+
const timing = asObject(output?.timing) || {};
|
|
43132
|
+
const certificate = asObject(output?.certificate) || {};
|
|
43133
|
+
return {
|
|
43134
|
+
kind: "certification",
|
|
43135
|
+
command: command.command || "",
|
|
43136
|
+
total_ms: Number(timing.total_ms || command.duration_ms || 0),
|
|
43137
|
+
api_ms: Number(timing.api_ms || 0),
|
|
43138
|
+
status: output?.status || null,
|
|
43139
|
+
reason_code: output?.reason_code || null,
|
|
43140
|
+
scenario_summary: certificate.scenario_summary || null
|
|
43141
|
+
};
|
|
43142
|
+
}
|
|
43143
|
+
return null;
|
|
43144
|
+
}
|
|
43145
|
+
function analyzeRunArtifacts(runPath, run, cwd) {
|
|
43146
|
+
const runDir = (0, import_path19.dirname)(runPath);
|
|
43147
|
+
const commands = collapseCommandRecords(readNdjson((0, import_path19.join)(runDir, "commands.ndjson")));
|
|
43148
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
43149
|
+
const slowSteps = [];
|
|
43150
|
+
const timingBreakdowns = [];
|
|
43151
|
+
let completed = 0;
|
|
43152
|
+
let withDuration = 0;
|
|
43153
|
+
let totalDuration = 0;
|
|
43154
|
+
for (const command of commands) {
|
|
43155
|
+
const output = readCommandOutputJson(runDir, command);
|
|
43156
|
+
const breakdown = commandTimingBreakdown(command, output);
|
|
43157
|
+
if (breakdown) timingBreakdowns.push({
|
|
43158
|
+
run_id: run.run_id,
|
|
43159
|
+
run_path: (0, import_path19.relative)(cwd, runPath).replaceAll("\\", "/"),
|
|
43160
|
+
...breakdown
|
|
43161
|
+
});
|
|
43162
|
+
if (command.phase === "completed" || command.completed_at) completed += 1;
|
|
43163
|
+
if (typeof command.duration_ms === "number") {
|
|
43164
|
+
withDuration += 1;
|
|
43165
|
+
totalDuration += command.duration_ms;
|
|
43166
|
+
slowSteps.push({
|
|
43167
|
+
run_id: run.run_id,
|
|
43168
|
+
run_path: (0, import_path19.relative)(cwd, runPath).replaceAll("\\", "/"),
|
|
43169
|
+
command: command.command || "",
|
|
43170
|
+
duration_ms: command.duration_ms,
|
|
43171
|
+
status: command.status || null,
|
|
43172
|
+
reason_code: command.reason_code || null,
|
|
43173
|
+
check_reason_codes: Array.isArray(command.check_reason_codes) ? command.check_reason_codes : []
|
|
43174
|
+
});
|
|
43175
|
+
}
|
|
43176
|
+
if (command.reason_code) increment(reasonCounts, command.reason_code);
|
|
43177
|
+
for (const reasonCode of toArray2(command.check_reason_codes)) {
|
|
43178
|
+
if (reasonCode) increment(reasonCounts, reasonCode);
|
|
43179
|
+
}
|
|
43180
|
+
}
|
|
43181
|
+
const codexEvents = readNdjson((0, import_path19.join)(runDir, "codex-exec.jsonl"));
|
|
43182
|
+
const codexDocs = /* @__PURE__ */ new Set();
|
|
43183
|
+
let codexCommandExecutions = 0;
|
|
43184
|
+
let codexFailedExitCodes = 0;
|
|
43185
|
+
for (const event of codexEvents) {
|
|
43186
|
+
const item = asObject(event.item) || event;
|
|
43187
|
+
if (item.type === "command_execution" && item.status === "completed") {
|
|
43188
|
+
codexCommandExecutions += 1;
|
|
43189
|
+
if (typeof item.exit_code === "number" && item.exit_code !== 0) codexFailedExitCodes += 1;
|
|
43190
|
+
}
|
|
43191
|
+
for (const url2 of collectDocUrls(JSON.stringify(event))) codexDocs.add(url2);
|
|
43192
|
+
}
|
|
43193
|
+
const docs = /* @__PURE__ */ new Set([
|
|
43194
|
+
...toArray2(run.docs_pages_used).map(String),
|
|
43195
|
+
...Array.from(codexDocs)
|
|
43196
|
+
]);
|
|
43197
|
+
return {
|
|
43198
|
+
command_log_present: (0, import_fs20.existsSync)((0, import_path19.join)(runDir, "commands.ndjson")),
|
|
43199
|
+
command_count: commands.length,
|
|
43200
|
+
completed_command_count: completed,
|
|
43201
|
+
missing_completion_count: Math.max(0, commands.length - completed),
|
|
43202
|
+
commands_with_duration_count: withDuration,
|
|
43203
|
+
total_command_duration_ms: totalDuration,
|
|
43204
|
+
command_reason_codes: ranked(reasonCounts),
|
|
43205
|
+
slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms) - Number(a.duration_ms)).slice(0, 10),
|
|
43206
|
+
timing_breakdowns: timingBreakdowns,
|
|
43207
|
+
docs_pages_observed: Array.from(docs).sort(),
|
|
43208
|
+
codex_command_execution_completed_count: codexCommandExecutions,
|
|
43209
|
+
codex_failed_exit_code_count: codexFailedExitCodes
|
|
43210
|
+
};
|
|
43211
|
+
}
|
|
43212
|
+
function summarizeExternalAgentRuns(options) {
|
|
43213
|
+
const cwd = (0, import_path19.resolve)(options.cwd || process.cwd());
|
|
43214
|
+
const root = (0, import_path19.resolve)(cwd, options.root);
|
|
43215
|
+
const loaded = readRunRecords(root, cwd);
|
|
43216
|
+
const selectedCohortId = options.cohortId || (options.currentBaselineOnly ? latestCohortId(loaded.records) : null);
|
|
43217
|
+
const records = selectedCohortId ? loaded.records.filter((record2) => record2.cohort_id === selectedCohortId) : loaded.records;
|
|
43218
|
+
const statusCounts = /* @__PURE__ */ new Map();
|
|
43219
|
+
const modelCounts = /* @__PURE__ */ new Map();
|
|
43220
|
+
const failureCounts = /* @__PURE__ */ new Map();
|
|
43221
|
+
const commandReasonCounts = /* @__PURE__ */ new Map();
|
|
43222
|
+
const docsCounts = /* @__PURE__ */ new Map();
|
|
43223
|
+
const slowSteps = [];
|
|
43224
|
+
const timingBreakdowns = [];
|
|
43225
|
+
let manualInterventions = 0;
|
|
43226
|
+
let commandCount = 0;
|
|
43227
|
+
let completedCommandCount = 0;
|
|
43228
|
+
let missingCompletionCount = 0;
|
|
43229
|
+
let commandsWithDurationCount = 0;
|
|
43230
|
+
let totalCommandDurationMs = 0;
|
|
43231
|
+
let commandLogRunCount = 0;
|
|
43232
|
+
let codexCommandExecutions = 0;
|
|
43233
|
+
let codexFailedExitCodes = 0;
|
|
43234
|
+
for (const record2 of records) {
|
|
43235
|
+
const run = record2.run;
|
|
43236
|
+
increment(statusCounts, run.status);
|
|
43237
|
+
increment(modelCounts, `${run.model_provider}/${run.model_name}`);
|
|
43238
|
+
manualInterventions += Number(run.manual_intervention_count || 0);
|
|
43239
|
+
if (run.status !== "pass") increment(failureCounts, run.failure_reason_code || "unknown");
|
|
43240
|
+
const artifactSummary = analyzeRunArtifacts(record2.path, run, cwd);
|
|
43241
|
+
if (artifactSummary.command_log_present) commandLogRunCount += 1;
|
|
43242
|
+
commandCount += Number(artifactSummary.command_count || 0);
|
|
43243
|
+
completedCommandCount += Number(artifactSummary.completed_command_count || 0);
|
|
43244
|
+
missingCompletionCount += Number(artifactSummary.missing_completion_count || 0);
|
|
43245
|
+
commandsWithDurationCount += Number(artifactSummary.commands_with_duration_count || 0);
|
|
43246
|
+
totalCommandDurationMs += Number(artifactSummary.total_command_duration_ms || 0);
|
|
43247
|
+
codexCommandExecutions += Number(artifactSummary.codex_command_execution_completed_count || 0);
|
|
43248
|
+
codexFailedExitCodes += Number(artifactSummary.codex_failed_exit_code_count || 0);
|
|
43249
|
+
for (const row of toArray2(artifactSummary.slow_steps)) slowSteps.push(row);
|
|
43250
|
+
for (const row of toArray2(artifactSummary.timing_breakdowns)) timingBreakdowns.push(row);
|
|
43251
|
+
for (const row of toArray2(artifactSummary.command_reason_codes)) {
|
|
43252
|
+
const entry = asObject(row);
|
|
43253
|
+
if (entry) increment(commandReasonCounts, entry.key, Number(entry.count || 1));
|
|
43254
|
+
}
|
|
43255
|
+
for (const page of toArray2(artifactSummary.docs_pages_observed)) increment(docsCounts, page);
|
|
43256
|
+
}
|
|
43257
|
+
const topFailures = ranked(failureCounts);
|
|
43258
|
+
const commandReasonCodes2 = ranked(commandReasonCounts);
|
|
43259
|
+
const recommendedFixes = topFailures.map((failure) => ({
|
|
43260
|
+
reason_code: failure.key,
|
|
43261
|
+
count: failure.count,
|
|
43262
|
+
recommended_fix: recommendedFixFor(failure.key),
|
|
43263
|
+
owner_subsystem: ownerSubsystemFor(failure.key)
|
|
43264
|
+
}));
|
|
43265
|
+
const nextRecommendedFix = recommendedFixes[0] || null;
|
|
43266
|
+
return {
|
|
43267
|
+
schema_version: "external_agent_run_summary.v1",
|
|
43268
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43269
|
+
root: (0, import_path19.relative)(cwd, root).replaceAll("\\", "/") || ".",
|
|
43270
|
+
cohort_id: selectedCohortId,
|
|
43271
|
+
current_baseline_only: Boolean(selectedCohortId),
|
|
43272
|
+
run_count: records.length,
|
|
43273
|
+
invalid_run_count: selectedCohortId ? 0 : loaded.invalid_runs.length,
|
|
43274
|
+
status_counts: Object.fromEntries(statusCounts),
|
|
43275
|
+
model_counts: ranked(modelCounts),
|
|
43276
|
+
manual_intervention_count: manualInterventions,
|
|
43277
|
+
top_failure_reason_codes: topFailures,
|
|
43278
|
+
docs_pages_observed: ranked(docsCounts),
|
|
43279
|
+
command_telemetry: {
|
|
43280
|
+
run_count_with_command_log: commandLogRunCount,
|
|
43281
|
+
command_count: commandCount,
|
|
43282
|
+
completed_command_count: completedCommandCount,
|
|
43283
|
+
missing_completion_count: missingCompletionCount,
|
|
43284
|
+
commands_with_duration_count: commandsWithDurationCount,
|
|
43285
|
+
total_command_duration_ms: totalCommandDurationMs,
|
|
43286
|
+
command_reason_codes: commandReasonCodes2,
|
|
43287
|
+
slow_steps: slowSteps.sort((a, b) => Number(b.duration_ms || 0) - Number(a.duration_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20),
|
|
43288
|
+
timing_breakdowns: timingBreakdowns.sort((a, b) => Number(b.total_ms || 0) - Number(a.total_ms || 0) || String(a.command || "").localeCompare(String(b.command || ""))).slice(0, 20)
|
|
43289
|
+
},
|
|
43290
|
+
codex_telemetry: {
|
|
43291
|
+
command_execution_completed_count: codexCommandExecutions,
|
|
43292
|
+
failed_exit_code_count: codexFailedExitCodes
|
|
43293
|
+
},
|
|
43294
|
+
recommended_fixes: recommendedFixes,
|
|
43295
|
+
next_recommended_fix: nextRecommendedFix,
|
|
43296
|
+
fix_selection_policy: {
|
|
43297
|
+
mode: "coherent_failure_cluster_first",
|
|
43298
|
+
rule: "Fix the highest-impact owner subsystem locally with focused proof, then rerun the same prompt once externally.",
|
|
43299
|
+
run_failure_weight: 3,
|
|
43300
|
+
command_reason_weight: 1
|
|
43301
|
+
},
|
|
43302
|
+
next_commands: nextRecommendedFix ? [`foh bug improve --from external-agent-run --file <run_dir>/run.json --json`] : [],
|
|
43303
|
+
invalid_runs: selectedCohortId ? [] : loaded.invalid_runs,
|
|
43304
|
+
run_paths: records.map((record2) => (0, import_path19.relative)(cwd, record2.path).replaceAll("\\", "/")).sort()
|
|
43305
|
+
};
|
|
43306
|
+
}
|
|
43307
|
+
function runExternalAgentRunSummary(options) {
|
|
43308
|
+
const summary = summarizeExternalAgentRuns(options);
|
|
43309
|
+
const invalidRuns = toArray2(summary.invalid_runs);
|
|
43310
|
+
const status = invalidRuns.length > 0 ? "failed" : "passed";
|
|
43311
|
+
const report = {
|
|
43312
|
+
report_schema_version: "script_report.v1",
|
|
43313
|
+
script: "foh eval external-agent summary",
|
|
43314
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43315
|
+
status,
|
|
43316
|
+
errors: invalidRuns.map((entry) => {
|
|
43317
|
+
const object3 = asObject(entry);
|
|
43318
|
+
return `${object3?.path || "unknown"}: ${JSON.stringify(object3?.findings || [])}`;
|
|
43319
|
+
}),
|
|
43320
|
+
warnings: Number(summary.run_count || 0) === 0 ? ["no external-agent run artifacts found"] : [],
|
|
43321
|
+
report: summary
|
|
43322
|
+
};
|
|
43323
|
+
if (options.out) {
|
|
43324
|
+
(0, import_fs20.mkdirSync)((0, import_path19.dirname)((0, import_path19.resolve)(options.cwd || process.cwd(), options.out)), { recursive: true });
|
|
43325
|
+
(0, import_fs20.writeFileSync)((0, import_path19.resolve)(options.cwd || process.cwd(), options.out), `${JSON.stringify(summary, null, 2)}
|
|
43326
|
+
`, "utf8");
|
|
43327
|
+
}
|
|
43328
|
+
if (options.report) {
|
|
43329
|
+
(0, import_fs20.mkdirSync)((0, import_path19.dirname)((0, import_path19.resolve)(options.cwd || process.cwd(), options.report)), { recursive: true });
|
|
43330
|
+
(0, import_fs20.writeFileSync)((0, import_path19.resolve)(options.cwd || process.cwd(), options.report), `${JSON.stringify(report, null, 2)}
|
|
43331
|
+
`, "utf8");
|
|
43332
|
+
}
|
|
43333
|
+
return { summary, report };
|
|
43334
|
+
}
|
|
43335
|
+
|
|
42387
43336
|
// src/commands/eval.ts
|
|
42388
43337
|
var DEFAULT_PROMPT_VERSION = "blank-setup.v1";
|
|
42389
43338
|
var DEFAULT_BATCH_MODELS = "openai/codex,anthropic/claude,cursor/agent";
|
|
42390
43339
|
var PROMPTS = {
|
|
42391
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.",
|
|
42392
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.",
|
|
42393
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.",
|
|
42394
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.",
|
|
@@ -43066,7 +44016,12 @@ function installSoftExitTrap() {
|
|
|
43066
44016
|
// src/lib/mission-help.ts
|
|
43067
44017
|
var CLI_MISSION_EXAMPLES = [
|
|
43068
44018
|
{ mission: "Start", command: "foh start", description: "guided setup and next action selector" },
|
|
43069
|
-
{ mission: "Setup", command:
|
|
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" },
|
|
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" },
|
|
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" },
|
|
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" },
|
|
44024
|
+
{ mission: "Setup Legacy", command: "foh setup --phone-mode observe --json", description: "create or update agent from an explicit template" },
|
|
43070
44025
|
{ mission: "Prove", command: "foh prove --agent <agent_id> --mission widget --json", description: "produce a machine-readable proof report" },
|
|
43071
44026
|
{ mission: "Publish", command: "foh publish --agent <agent_id> --json", description: "publish when proof and release evidence pass" },
|
|
43072
44027
|
{ mission: "Debug", command: "foh debug --out test-results/foh-cli-diag.latest.json --json", description: "collect auth/org/API diagnostics" }
|
|
@@ -43170,6 +44125,7 @@ registerCertify(program2);
|
|
|
43170
44125
|
registerDiag(program2);
|
|
43171
44126
|
registerBug(program2);
|
|
43172
44127
|
registerProve(program2);
|
|
44128
|
+
registerObjective(program2);
|
|
43173
44129
|
registerInteractive(program2);
|
|
43174
44130
|
registerAgentPublishCommand(program2, { publicAlias: true });
|
|
43175
44131
|
registerEval(program2);
|