@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.
@@ -105,6 +105,7 @@ function withQuote(schema, required) {
105
105
  export class ApplyBridge {
106
106
  browser;
107
107
  host;
108
+ /** Read by the runtime for the #975 diagnostic: 0 calls is the finding, not an internal detail. */
108
109
  actions = 0;
109
110
  pages = 0;
110
111
  lastSnapshot = "";
@@ -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 { outcome: "blocked", summary, blockReason: "incomplete", questions: [`The CLI stopped before marking the form ready for review${tail}. Review the form yourself, or retry.`] };
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.83",
3
+ "version": "0.4.84",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",