@proagentstore/cli 0.4.83 → 0.4.84
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.
|
@@ -56,7 +56,10 @@ export const LOCAL_APPLY_BLOCK_REASONS = [
|
|
|
56
56
|
"source_unavailable",
|
|
57
57
|
"artifact_changed",
|
|
58
58
|
"job_unavailable",
|
|
59
|
+
"bridge_unused",
|
|
59
60
|
];
|
|
61
|
+
export const LOCAL_APPLY_DIAGNOSTIC_CAUSES = ["bridge_unused", "engine_exited_nonzero", "timed_out", "no_engine_output"];
|
|
62
|
+
export const LOCAL_APPLY_SIGNALS = ["approval_policy_blocked", "bridge_tools_missing", "auth_prompt", "no_output", "engine_refused_task"];
|
|
60
63
|
export const LOCAL_APPLY_UNAVAILABLE_REASONS = ["expired", "unavailable"];
|
|
61
64
|
export const LOCAL_APPLY_EVENT_TYPES = [
|
|
62
65
|
"engine.auth_checked",
|
|
@@ -162,6 +165,24 @@ export function parseLocalApplyResult(raw) {
|
|
|
162
165
|
out.submitted = { url, at, gateId };
|
|
163
166
|
out.submitAttempted = true;
|
|
164
167
|
}
|
|
168
|
+
// #975 — accepted ONLY as the closed vocabulary it declares. A runner that sends prose here gets
|
|
169
|
+
// it dropped rather than stored: the whole point of a structured diagnostic is that no new
|
|
170
|
+
// free-text channel opens between the machine and the owner's records.
|
|
171
|
+
const d = o.diagnostic && typeof o.diagnostic === "object" && !Array.isArray(o.diagnostic) ? o.diagnostic : null;
|
|
172
|
+
const cause = d ? oneOf(LOCAL_APPLY_DIAGNOSTIC_CAUSES, d.cause) : null;
|
|
173
|
+
if (d && cause) {
|
|
174
|
+
const count = (v) => (typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1_000_000 ? v : 0);
|
|
175
|
+
out.diagnostic = {
|
|
176
|
+
cause,
|
|
177
|
+
bridgeCalls: count(d.bridgeCalls),
|
|
178
|
+
engineExit: typeof d.engineExit === "number" && Number.isInteger(d.engineExit) && d.engineExit >= -1 && d.engineExit <= 255 ? d.engineExit : 0,
|
|
179
|
+
activeMs: count(d.activeMs),
|
|
180
|
+
pages: count(d.pages),
|
|
181
|
+
filled: count(d.filled),
|
|
182
|
+
// Deduped from a closed 5-value vocabulary, so it is bounded by the enum itself — no cap needed.
|
|
183
|
+
signals: [...new Set((Array.isArray(d.signals) ? d.signals : []).filter((x) => LOCAL_APPLY_SIGNALS.includes(x)))],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
165
186
|
if (outcome === "blocked") {
|
|
166
187
|
out.blockReason = oneOf(LOCAL_APPLY_BLOCK_REASONS, o.blockReason) ?? "incomplete";
|
|
167
188
|
out.questions = (Array.isArray(o.questions) ? o.questions : [])
|
|
@@ -147,6 +147,23 @@ export function verifyArtifact(workspace, home, a) {
|
|
|
147
147
|
return { error: `The approved ${label} at ${a.path} could not be read.` };
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* What to tell the owner when the CLI put nothing on the page (#975) — built from the diagnostic's
|
|
152
|
+
* own ids, so the advice follows the observation rather than guessing at it.
|
|
153
|
+
*/
|
|
154
|
+
function bridgeUnusedHelp(engine, d) {
|
|
155
|
+
if (d.signals.includes("approval_policy_blocked"))
|
|
156
|
+
return `The ${engine} CLI would not use the browser tools because its own approval policy refused them. Allow the PAGS bridge toolset for this session, then retry.`;
|
|
157
|
+
if (d.signals.includes("bridge_tools_missing"))
|
|
158
|
+
return `The ${engine} CLI never saw the browser tools. Update the CLI (npm i -g @proagentstore/cli), run \`pags up\` again, then retry.`;
|
|
159
|
+
if (d.signals.includes("auth_prompt"))
|
|
160
|
+
return `The ${engine} CLI stopped on a sign-in prompt before opening the page. Sign it in on this machine, then retry.`;
|
|
161
|
+
if (d.signals.includes("no_output"))
|
|
162
|
+
return `The ${engine} CLI produced no output at all before exiting. Check that it runs on this machine, then retry.`;
|
|
163
|
+
if (d.signals.includes("engine_refused_task"))
|
|
164
|
+
return `The ${engine} CLI declined the task and never opened the page. Its own closing message is on this run's summary.`;
|
|
165
|
+
return `The ${engine} CLI ran for ${Math.round(d.activeMs / 1000)}s and exited (code ${d.engineExit}) without opening the application page — no navigation, no checkpoint, no fill. Nothing was submitted. Retry, or run the CLI by hand on this machine to see why it stops.`;
|
|
166
|
+
}
|
|
150
167
|
export class LocalApplyRuntime {
|
|
151
168
|
deps;
|
|
152
169
|
runs = new Map();
|
|
@@ -304,6 +321,42 @@ export class LocalApplyRuntime {
|
|
|
304
321
|
}, 5_000);
|
|
305
322
|
run.timer.unref?.();
|
|
306
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* What the runner OBSERVED about its own CLI (#975), as ids this platform defines.
|
|
326
|
+
*
|
|
327
|
+
* Matched against the engine's structured output on the machine; only the matching signal's ID
|
|
328
|
+
* crosses the contract, never the line that matched it. That is what makes a diagnostic safe to
|
|
329
|
+
* persist: a CLI prints the owner's résumé prose and their typed answers while it works, and no
|
|
330
|
+
* redaction can reliably tell those from the rest of its chatter.
|
|
331
|
+
*/
|
|
332
|
+
signalsOf(run) {
|
|
333
|
+
const text = run.output.join("\n");
|
|
334
|
+
const out = [];
|
|
335
|
+
if (!text.trim())
|
|
336
|
+
out.push("no_output");
|
|
337
|
+
// #952's live failure, invisible from the cloud and fixable by configuration.
|
|
338
|
+
if (/approval policy is\s*[`'"]?never|requires? approval|permission denied for tool|not allowed to use/i.test(text))
|
|
339
|
+
out.push("approval_policy_blocked");
|
|
340
|
+
if (/browser_navigate.{0,40}(not found|unknown tool|unavailable)|no such tool|tool .{0,40}is not available/i.test(text))
|
|
341
|
+
out.push("bridge_tools_missing");
|
|
342
|
+
if (missingLogin(run.envelope.engine, text))
|
|
343
|
+
out.push("auth_prompt");
|
|
344
|
+
if (/\b(i (cannot|can't|won't)|unable to) (help|assist|complete|do that|proceed)/i.test(text))
|
|
345
|
+
out.push("engine_refused_task");
|
|
346
|
+
return [...new Set(out)];
|
|
347
|
+
}
|
|
348
|
+
/** The bounded, closed-vocabulary diagnostic for a run that put nothing on the page (#975). */
|
|
349
|
+
diagnosticOf(run, code, cause) {
|
|
350
|
+
return {
|
|
351
|
+
cause,
|
|
352
|
+
bridgeCalls: run.bridge?.actions ?? 0,
|
|
353
|
+
engineExit: Number.isInteger(code) && code >= -1 && code <= 255 ? code : 0,
|
|
354
|
+
activeMs: this.activeMs(run),
|
|
355
|
+
pages: run.bridge?.pages ?? 0,
|
|
356
|
+
filled: run.bridge?.filled ?? 0,
|
|
357
|
+
signals: this.signalsOf(run),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
307
360
|
/** What the run amounts to when the CLI exits — decided by what the bridge did, not what the CLI says. */
|
|
308
361
|
outcomeOf(run, code) {
|
|
309
362
|
const e = run.envelope;
|
|
@@ -323,10 +376,26 @@ export class LocalApplyRuntime {
|
|
|
323
376
|
return { outcome: "blocked", summary, blockReason: "engine_not_signed_in", questions: [signInHelp(e.engine)], engineAuth: "missing_login" };
|
|
324
377
|
if (b?.blocked)
|
|
325
378
|
return { outcome: "blocked", summary, blockReason: b.blocked.reason, questions: b.blocked.questions };
|
|
326
|
-
if (run.timedOut)
|
|
327
|
-
return { outcome: "blocked", summary, blockReason: "incomplete", questions: [`Stopped at the ${e.limits.maxMinutes}-minute limit before the form was ready for review.`] };
|
|
379
|
+
if (run.timedOut) {
|
|
380
|
+
return { outcome: "blocked", summary, blockReason: "incomplete", questions: [`Stopped at the ${e.limits.maxMinutes}-minute limit before the form was ready for review.`], diagnostic: this.diagnosticOf(run, code, "timed_out") };
|
|
381
|
+
}
|
|
382
|
+
// #975: a CLI that never called the bridge did NOTHING on the page, which is a different
|
|
383
|
+
// failure from stopping partway — different cause, different fix — and saying `incomplete`
|
|
384
|
+
// for both left the owner with a 51-second run they could not explain. The diagnostic says
|
|
385
|
+
// how long it ran, how many bridge calls it made (0) and what the runner saw it print.
|
|
386
|
+
const calls = b?.actions ?? 0;
|
|
387
|
+
if (calls === 0) {
|
|
388
|
+
const diagnostic = this.diagnosticOf(run, code, !run.output.join("").trim() ? "no_engine_output" : code !== 0 ? "engine_exited_nonzero" : "bridge_unused");
|
|
389
|
+
return { outcome: "blocked", summary, blockReason: "bridge_unused", questions: [bridgeUnusedHelp(e.engine, diagnostic)], diagnostic };
|
|
390
|
+
}
|
|
328
391
|
const tail = code !== 0 ? ` (the ${e.engine} CLI exited with code ${code})` : "";
|
|
329
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
outcome: "blocked",
|
|
394
|
+
summary,
|
|
395
|
+
blockReason: "incomplete",
|
|
396
|
+
questions: [`The CLI stopped before marking the form ready for review${tail}. Review the form yourself, or retry.`],
|
|
397
|
+
diagnostic: this.diagnosticOf(run, code, code !== 0 ? "engine_exited_nonzero" : "bridge_unused"),
|
|
398
|
+
};
|
|
330
399
|
}
|
|
331
400
|
activeMs(run) {
|
|
332
401
|
return run.activeMs + (run.state === "running" ? this.now() - run.activeSince : 0);
|
|
@@ -524,6 +593,9 @@ export class LocalApplyRuntime {
|
|
|
524
593
|
...(r.outcome === "submitted" && r.submitted ? { submitted: r.submitted } : {}),
|
|
525
594
|
...(r.outcome === "blocked" ? { blockReason: r.blockReason ?? "incomplete", questions: (r.questions ?? []).map((q) => redactText(q, run.secrets)), ...(r.blockReason === "job_unavailable" && r.unavailable ? { unavailable: r.unavailable } : {}) } : {}),
|
|
526
595
|
...(r.outcome === "failed" ? { error: redactText((r.error ?? "The run failed without a reason.").slice(0, 1000), run.secrets) } : {}),
|
|
596
|
+
// #975 — carried through as the closed vocabulary it is. Nothing here is redacted because
|
|
597
|
+
// nothing here is text: counts, an exit code, and signal ids this platform defines.
|
|
598
|
+
...(r.diagnostic ? { diagnostic: r.diagnostic } : {}),
|
|
527
599
|
};
|
|
528
600
|
if (run.child && run.child.exitCode === null)
|
|
529
601
|
this.kill(run);
|