pullfrog 0.1.61 → 0.1.63

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/dist/cli.mjs CHANGED
@@ -107293,6 +107293,9 @@ function isProviderNoRoutableEndpoints(text) {
107293
107293
  function isOpenRouterKeyLimitExceeded(text) {
107294
107294
  return /Key limit exceeded \(total limit\)/i.test(text) || /openrouter\.ai\/[^\s"')]*\/keys\//i.test(text);
107295
107295
  }
107296
+ function isProviderMissingCredential(text) {
107297
+ return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
107298
+ }
107296
107299
  function isTransientUpstreamError(text) {
107297
107300
  return /API Error:\s*5\d\d/i.test(text) || /\bOverloaded\b/.test(text) || /"error_type"\s*:\s*"(?:provider_unavailable|timeout)"/i.test(text) || /Streaming response failed:\s*\[5\d\d\]/i.test(text);
107298
107301
  }
@@ -107355,7 +107358,7 @@ var import_semver = __toESM(require_semver2(), 1);
107355
107358
  // package.json
107356
107359
  var package_default = {
107357
107360
  name: "pullfrog",
107358
- version: "0.1.61",
107361
+ version: "0.1.63",
107359
107362
  type: "module",
107360
107363
  bin: {
107361
107364
  pullfrog: "dist/cli.mjs",
@@ -108446,7 +108449,6 @@ function getUnsubmittedReview(toolState, expectsReviewOutput2 = toolState.hadPro
108446
108449
  }
108447
108450
  function expectsReviewOutput(ctx) {
108448
108451
  if (ctx.toolState.hadProgressComment) return true;
108449
- if (ctx.payload.progressComments) return false;
108450
108452
  return ctx.payload.event.silent !== true && ctx.payload.event.issue_number !== void 0;
108451
108453
  }
108452
108454
  function buildStopHookPrompt(failure2) {
@@ -108661,6 +108663,7 @@ async function runPostRunRetryLoop(params) {
108661
108663
  result = preResume;
108662
108664
  break;
108663
108665
  }
108666
+ if (onlySummaryStale) result = { ...result, output: preResume.output || result.output };
108664
108667
  gateResumeCount++;
108665
108668
  }
108666
108669
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
@@ -116005,24 +116008,23 @@ async function installOpencodeCli(params) {
116005
116008
  var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this \u2014 if the picker is locked, this run had no Pullfrog Router funding: add a card or top up your credit.";
116006
116009
  function autoSelectModel() {
116007
116010
  const authorized2 = getAuthorizedModels();
116008
- if (authorized2.size > 0) {
116009
- const servable = (a2) => {
116010
- if (!authorized2.has(a2.resolve)) return false;
116011
- const envVars = getModelEnvVars(a2.resolve);
116012
- return envVars.length === 0 || envVars.some((name) => process.env[name]);
116013
- };
116014
- const match3 = modelAliases.find((a2) => !a2.hidden && !a2.fallback && a2.preferred && servable(a2)) ?? modelAliases.find((a2) => !a2.hidden && !a2.fallback && servable(a2));
116015
- if (match3) {
116016
- log2.info(
116017
- `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
116018
- );
116019
- log2.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
116020
- return match3.resolve;
116021
- }
116011
+ const selectable = modelAliases.filter((a2) => !a2.hidden && !a2.fallback && !a2.routing);
116012
+ const servable = selectable.filter((a2) => {
116013
+ const envVars = getModelEnvVars(a2.resolve);
116014
+ return envVars.length === 0 || envVars.some((name) => process.env[name]);
116015
+ });
116016
+ const pick4 = (list) => list.find((a2) => a2.preferred) ?? list[0];
116017
+ const match3 = pick4(servable.filter((a2) => authorized2.has(a2.resolve))) ?? pick4(servable);
116018
+ if (match3) {
116022
116019
  log2.info(
116023
- `\xBB opencode has ${authorized2.size} models but none match curated aliases \u2014 letting OpenCode auto-select`
116020
+ `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
116024
116021
  );
116022
+ log2.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
116023
+ return match3.resolve;
116025
116024
  }
116025
+ log2.info(
116026
+ `\xBB opencode has ${authorized2.size} models but none match curated aliases and none hold a credential \u2014 letting OpenCode auto-select: ${[...authorized2].join(", ")}`
116027
+ );
116026
116028
  log2.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
116027
116029
  return void 0;
116028
116030
  }
@@ -116235,7 +116237,7 @@ function isModelOutput(ctx, part) {
116235
116237
  async function dispatchEvent(ctx, event) {
116236
116238
  if (event.type === "message.part.updated") {
116237
116239
  ctx.lastEventAt = performance7.now();
116238
- if (isModelOutput(ctx, event.properties.part)) ctx.sawModelOutput = true;
116240
+ if (isModelOutput(ctx, event.properties.part)) ctx.diagnostic.sawModelOutput = true;
116239
116241
  await onPartUpdated(ctx, event.properties.part);
116240
116242
  return;
116241
116243
  }
@@ -116381,7 +116383,8 @@ async function runPromptTurn(ctx, params) {
116381
116383
  const turn = ctx.currentTurn;
116382
116384
  const part = { type: "text", text: params.text };
116383
116385
  ctx.promptMessageID = void 0;
116384
- ctx.sawModelOutput = false;
116386
+ ctx.diagnostic.sawModelOutput = false;
116387
+ ctx.diagnostic.idleSec = void 0;
116385
116388
  let assistant;
116386
116389
  let returnedParts;
116387
116390
  let networkError = null;
@@ -116441,7 +116444,7 @@ async function runPromptTurn(ctx, params) {
116441
116444
  usage
116442
116445
  };
116443
116446
  }
116444
- if (!ctx.sawModelOutput && finalText.trim().length === 0 && !usage) {
116447
+ if (!ctx.diagnostic.sawModelOutput && finalText.trim().length === 0 && !usage) {
116445
116448
  return {
116446
116449
  success: false,
116447
116450
  output: finalText,
@@ -116566,19 +116569,18 @@ function startInnerActivityWatchdog(params) {
116566
116569
  const id = setInterval(() => {
116567
116570
  if (fired) return;
116568
116571
  const idleMs = performance7.now() - params.ctx.lastEventAt;
116569
- const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116572
+ const compiledMs = params.ctx.diagnostic.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
116570
116573
  const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
116571
116574
  compiledMs,
116572
- params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
116575
+ params.ctx.diagnostic.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
116573
116576
  );
116574
116577
  if (idleMs <= budgetMs) return;
116575
116578
  fired = true;
116576
116579
  everFired = true;
116577
116580
  const idleSec = Math.round(idleMs / 1e3);
116578
116581
  params.ctx.diagnostic.idleSec = idleSec;
116579
- params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
116580
116582
  log2.info(
116581
- params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
116583
+ params.ctx.diagnostic.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
116582
116584
  );
116583
116585
  params.abortTurn();
116584
116586
  try {
@@ -116722,7 +116724,6 @@ var opencode = agent({
116722
116724
  currentTurn: null,
116723
116725
  eventCount: 0,
116724
116726
  lastEventAt: performance7.now(),
116725
- sawModelOutput: false,
116726
116727
  promptMessageID: void 0,
116727
116728
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
116728
116729
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
@@ -116786,7 +116787,9 @@ var opencode = agent({
116786
116787
  );
116787
116788
  };
116788
116789
  const standDownIfRecovered = (turn) => {
116789
- if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
116790
+ if (!watchdog.firedThisTurn() || !turn.success) return turn;
116791
+ runnerCtx.diagnostic.idleSec = void 0;
116792
+ ctx.onTurnRecovered?.();
116790
116793
  return turn;
116791
116794
  };
116792
116795
  const result = await attempt(text);
@@ -188634,8 +188637,8 @@ function validatePushDestination(repoState, branch, cwd) {
188634
188637
  throw new Error(
188635
188638
  `Push blocked: destination does not match expected repository.
188636
188639
  Expected: ${pushUrl}
188637
- Actual: ${dest.url}
188638
- Git configuration may have been tampered with.`
188640
+ Actual: remote '${dest.remoteName}' -> ${dest.url}
188641
+ Git configuration may have been tampered with, or a url.*.insteadOf rewrite is in effect.`
188639
188642
  );
188640
188643
  }
188641
188644
  return dest;
@@ -190757,6 +190760,9 @@ async function configureRepoGit(params) {
190757
190760
  removeIncludeIfEntries(repoDir);
190758
190761
  const originUrl = `https://github.com/${params.owner}/${params.name}.git`;
190759
190762
  $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
190763
+ $("git", ["config", "--local", "--replace-all", "remote.origin.pushurl", originUrl], {
190764
+ cwd: repoDir
190765
+ });
190760
190766
  repoState.pushUrl = originUrl;
190761
190767
  $("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
190762
190768
  repoState.initialHead = captureInitialHead(repoDir);
@@ -191095,6 +191101,9 @@ async function checkoutPrBranch(pr, params) {
191095
191101
  $("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
191096
191102
  log2.debug(`\xBB updated remote '${remoteName}' for fork ${pr.headRepoFullName}`);
191097
191103
  }
191104
+ $("git", ["config", "--local", "--replace-all", `remote.${remoteName}.pushurl`, forkUrl], {
191105
+ log: false
191106
+ });
191098
191107
  $("git", ["config", `branch.${localBranch}.pushRemote`, remoteName], { log: false });
191099
191108
  $("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${pr.headRef}`], { log: false });
191100
191109
  log2.debug(`\xBB configured branch '${localBranch}' to push to '${remoteName}/${pr.headRef}'`);
@@ -196268,6 +196277,43 @@ function resolveCwd(cwd) {
196268
196277
  if (isAbsolute2(cwd)) return cwd;
196269
196278
  return workspace ? resolve2(workspace, cwd) : cwd;
196270
196279
  }
196280
+ var DISPATCH_PAYLOAD_KEY = '"~pullfrog"';
196281
+ function sliceBalancedObject(text, open) {
196282
+ let depth = 0;
196283
+ let inString = false;
196284
+ for (let i2 = open; i2 < text.length; i2++) {
196285
+ const char = text[i2];
196286
+ if (inString) {
196287
+ if (char === "\\") i2++;
196288
+ else if (char === '"') inString = false;
196289
+ continue;
196290
+ }
196291
+ if (char === '"') inString = true;
196292
+ else if (char === "{") depth++;
196293
+ else if (char === "}" && --depth === 0) return text.slice(open, i2 + 1);
196294
+ }
196295
+ return void 0;
196296
+ }
196297
+ function findEmbeddedDispatchPayload(text) {
196298
+ const limit = text.lastIndexOf(DISPATCH_PAYLOAD_KEY);
196299
+ if (limit === -1) return void 0;
196300
+ for (let open = text.indexOf("{"); open !== -1 && open < limit; open = text.indexOf("{", open + 1)) {
196301
+ const candidate = sliceBalancedObject(text, open);
196302
+ if (!candidate) continue;
196303
+ try {
196304
+ const parsed2 = JSON.parse(candidate);
196305
+ if (JsonPayload.allows(parsed2)) return parsed2;
196306
+ } catch {
196307
+ }
196308
+ }
196309
+ return void 0;
196310
+ }
196311
+ function assertDispatchPayloadIntact(promptInput) {
196312
+ if (!findEmbeddedDispatchPayload(promptInput)) return;
196313
+ throw new Error(
196314
+ "this workflow wraps `${{ inputs.prompt }}` in other text, so Pullfrog's dispatch payload can't be read and the run would lose its pull request context, its progress comment, and its standing instructions. in `.github/workflows/pullfrog.yml`, set `prompt:` to exactly `${{ inputs.prompt }}`, and move your own preamble into Standing instructions in the Pullfrog console."
196315
+ );
196316
+ }
196271
196317
  function resolvePromptInput() {
196272
196318
  const promptInput = core7.getInput("prompt");
196273
196319
  const promptFile = core7.getInput("prompt_file");
@@ -196284,9 +196330,9 @@ function resolvePromptInput() {
196284
196330
  try {
196285
196331
  parsed2 = JSON.parse(promptInput);
196286
196332
  } catch {
196287
- return promptInput;
196288
196333
  }
196289
196334
  if (!parsed2 || typeof parsed2 !== "object" || !("~pullfrog" in parsed2)) {
196335
+ assertDispatchPayloadIntact(promptInput);
196290
196336
  return promptInput;
196291
196337
  }
196292
196338
  const jsonPayload = JsonPayload.assert(parsed2);
@@ -196916,7 +196962,7 @@ async function handleAgentResult(ctx) {
196916
196962
  const mode = toolState.selectedMode;
196917
196963
  const isReviewMode = mode === "Review" || mode === "IncrementalReview";
196918
196964
  const payload = ctx.toolContext.payload;
196919
- const hasCommentTarget = toolState.hadProgressComment || !payload.progressComments && payload.event.issue_number !== void 0;
196965
+ const hasCommentTarget = toolState.hadProgressComment || payload.event.issue_number !== void 0;
196920
196966
  if (!isReviewMode && !toolState.wasUpdated && hasCommentTarget && !ctx.silent) {
196921
196967
  const tracker = toolState.todoTracker;
196922
196968
  if (tracker) {
@@ -197350,6 +197396,19 @@ ${input.errorMessage}
197350
197396
  \`\`\``
197351
197397
  ].join("\n");
197352
197398
  }
197399
+ function formatProviderMissingCredential(input) {
197400
+ return [
197401
+ "**No credential reached the model provider.**",
197402
+ "",
197403
+ "The run started without a key for the provider it ended up using, so the request was refused before any model work happened. Nothing was billed, and no key of yours was rejected \u2014 add a provider key, or pick a model you already have one for.",
197404
+ "",
197405
+ `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
197406
+ "",
197407
+ `\`\`\`
197408
+ ${input.errorMessage}
197409
+ \`\`\``
197410
+ ].join("\n");
197411
+ }
197353
197412
  function formatProviderModelNotFoundSummary(input) {
197354
197413
  return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
197355
197414
 
@@ -197395,6 +197454,16 @@ ${body}`, comment: body };
197395
197454
  });
197396
197455
  return { summary: `### \u274C Pullfrog failed
197397
197456
 
197457
+ ${body}`, comment: body };
197458
+ }
197459
+ if (isProviderMissingCredential(input.errorMessage)) {
197460
+ const body = formatProviderMissingCredential({
197461
+ owner: input.repo.owner,
197462
+ name: input.repo.name,
197463
+ errorMessage: input.errorMessage
197464
+ });
197465
+ return { summary: `### \u274C Pullfrog failed
197466
+
197398
197467
  ${body}`, comment: body };
197399
197468
  }
197400
197469
  const apiKeySource = hangBody ?? input.errorMessage;
@@ -197441,7 +197510,7 @@ ${body}`, comment: body };
197441
197510
  ${body}`, comment: body };
197442
197511
  }
197443
197512
  if (hangBody) {
197444
- const explained = input.agentDiagnostic?.lastProviderError !== void 0;
197513
+ const explained = input.agentDiagnostic?.lastProviderError !== void 0 || input.agentDiagnostic?.sawModelOutput === false;
197445
197514
  return {
197446
197515
  summary: `### \u274C Pullfrog failed
197447
197516
 
@@ -199234,7 +199303,7 @@ async function runCli4(input) {
199234
199303
  }
199235
199304
 
199236
199305
  // cli.ts
199237
- var VERSION10 = "0.1.61";
199306
+ var VERSION10 = "0.1.63";
199238
199307
  var bin = basename2(process.argv[1] || "");
199239
199308
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
199240
199309
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -105126,6 +105126,9 @@ function isProviderNoRoutableEndpoints(text) {
105126
105126
  function isOpenRouterKeyLimitExceeded(text) {
105127
105127
  return /Key limit exceeded \(total limit\)/i.test(text) || /openrouter\.ai\/[^\s"')]*\/keys\//i.test(text);
105128
105128
  }
105129
+ function isProviderMissingCredential(text) {
105130
+ return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
105131
+ }
105129
105132
  function isTransientUpstreamError(text) {
105130
105133
  return /API Error:\s*5\d\d/i.test(text) || /\bOverloaded\b/.test(text) || /"error_type"\s*:\s*"(?:provider_unavailable|timeout)"/i.test(text) || /Streaming response failed:\s*\[5\d\d\]/i.test(text);
105131
105134
  }
@@ -105188,7 +105191,7 @@ var import_semver = __toESM(require_semver2(), 1);
105188
105191
  // package.json
105189
105192
  var package_default = {
105190
105193
  name: "pullfrog",
105191
- version: "0.1.61",
105194
+ version: "0.1.63",
105192
105195
  type: "module",
105193
105196
  bin: {
105194
105197
  pullfrog: "dist/cli.mjs",
@@ -106279,7 +106282,6 @@ function getUnsubmittedReview(toolState, expectsReviewOutput2 = toolState.hadPro
106279
106282
  }
106280
106283
  function expectsReviewOutput(ctx) {
106281
106284
  if (ctx.toolState.hadProgressComment) return true;
106282
- if (ctx.payload.progressComments) return false;
106283
106285
  return ctx.payload.event.silent !== true && ctx.payload.event.issue_number !== void 0;
106284
106286
  }
106285
106287
  function buildStopHookPrompt(failure2) {
@@ -106494,6 +106496,7 @@ async function runPostRunRetryLoop(params) {
106494
106496
  result = preResume;
106495
106497
  break;
106496
106498
  }
106499
+ if (onlySummaryStale) result = { ...result, output: preResume.output || result.output };
106497
106500
  gateResumeCount++;
106498
106501
  }
106499
106502
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
@@ -113885,24 +113888,23 @@ async function installOpencodeCli(params) {
113885
113888
  var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this \u2014 if the picker is locked, this run had no Pullfrog Router funding: add a card or top up your credit.";
113886
113889
  function autoSelectModel() {
113887
113890
  const authorized2 = getAuthorizedModels();
113888
- if (authorized2.size > 0) {
113889
- const servable = (a) => {
113890
- if (!authorized2.has(a.resolve)) return false;
113891
- const envVars = getModelEnvVars(a.resolve);
113892
- return envVars.length === 0 || envVars.some((name) => process.env[name]);
113893
- };
113894
- const match3 = modelAliases.find((a) => !a.hidden && !a.fallback && a.preferred && servable(a)) ?? modelAliases.find((a) => !a.hidden && !a.fallback && servable(a));
113895
- if (match3) {
113896
- log.info(
113897
- `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
113898
- );
113899
- log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
113900
- return match3.resolve;
113901
- }
113891
+ const selectable = modelAliases.filter((a) => !a.hidden && !a.fallback && !a.routing);
113892
+ const servable = selectable.filter((a) => {
113893
+ const envVars = getModelEnvVars(a.resolve);
113894
+ return envVars.length === 0 || envVars.some((name) => process.env[name]);
113895
+ });
113896
+ const pick4 = (list) => list.find((a) => a.preferred) ?? list[0];
113897
+ const match3 = pick4(servable.filter((a) => authorized2.has(a.resolve))) ?? pick4(servable);
113898
+ if (match3) {
113902
113899
  log.info(
113903
- `\xBB opencode has ${authorized2.size} models but none match curated aliases \u2014 letting OpenCode auto-select`
113900
+ `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
113904
113901
  );
113902
+ log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
113903
+ return match3.resolve;
113905
113904
  }
113905
+ log.info(
113906
+ `\xBB opencode has ${authorized2.size} models but none match curated aliases and none hold a credential \u2014 letting OpenCode auto-select: ${[...authorized2].join(", ")}`
113907
+ );
113906
113908
  log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
113907
113909
  return void 0;
113908
113910
  }
@@ -114115,7 +114117,7 @@ function isModelOutput(ctx, part) {
114115
114117
  async function dispatchEvent(ctx, event) {
114116
114118
  if (event.type === "message.part.updated") {
114117
114119
  ctx.lastEventAt = performance7.now();
114118
- if (isModelOutput(ctx, event.properties.part)) ctx.sawModelOutput = true;
114120
+ if (isModelOutput(ctx, event.properties.part)) ctx.diagnostic.sawModelOutput = true;
114119
114121
  await onPartUpdated(ctx, event.properties.part);
114120
114122
  return;
114121
114123
  }
@@ -114261,7 +114263,8 @@ async function runPromptTurn(ctx, params) {
114261
114263
  const turn = ctx.currentTurn;
114262
114264
  const part = { type: "text", text: params.text };
114263
114265
  ctx.promptMessageID = void 0;
114264
- ctx.sawModelOutput = false;
114266
+ ctx.diagnostic.sawModelOutput = false;
114267
+ ctx.diagnostic.idleSec = void 0;
114265
114268
  let assistant;
114266
114269
  let returnedParts;
114267
114270
  let networkError = null;
@@ -114321,7 +114324,7 @@ async function runPromptTurn(ctx, params) {
114321
114324
  usage
114322
114325
  };
114323
114326
  }
114324
- if (!ctx.sawModelOutput && finalText.trim().length === 0 && !usage) {
114327
+ if (!ctx.diagnostic.sawModelOutput && finalText.trim().length === 0 && !usage) {
114325
114328
  return {
114326
114329
  success: false,
114327
114330
  output: finalText,
@@ -114446,19 +114449,18 @@ function startInnerActivityWatchdog(params) {
114446
114449
  const id = setInterval(() => {
114447
114450
  if (fired) return;
114448
114451
  const idleMs = performance7.now() - params.ctx.lastEventAt;
114449
- const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114452
+ const compiledMs = params.ctx.diagnostic.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114450
114453
  const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
114451
114454
  compiledMs,
114452
- params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
114455
+ params.ctx.diagnostic.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
114453
114456
  );
114454
114457
  if (idleMs <= budgetMs) return;
114455
114458
  fired = true;
114456
114459
  everFired = true;
114457
114460
  const idleSec = Math.round(idleMs / 1e3);
114458
114461
  params.ctx.diagnostic.idleSec = idleSec;
114459
- params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
114460
114462
  log.info(
114461
- params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
114463
+ params.ctx.diagnostic.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
114462
114464
  );
114463
114465
  params.abortTurn();
114464
114466
  try {
@@ -114602,7 +114604,6 @@ var opencode = agent({
114602
114604
  currentTurn: null,
114603
114605
  eventCount: 0,
114604
114606
  lastEventAt: performance7.now(),
114605
- sawModelOutput: false,
114606
114607
  promptMessageID: void 0,
114607
114608
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
114608
114609
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
@@ -114666,7 +114667,9 @@ var opencode = agent({
114666
114667
  );
114667
114668
  };
114668
114669
  const standDownIfRecovered = (turn) => {
114669
- if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
114670
+ if (!watchdog.firedThisTurn() || !turn.success) return turn;
114671
+ runnerCtx.diagnostic.idleSec = void 0;
114672
+ ctx.onTurnRecovered?.();
114670
114673
  return turn;
114671
114674
  };
114672
114675
  const result = await attempt(text);
@@ -186514,8 +186517,8 @@ function validatePushDestination(repoState, branch, cwd) {
186514
186517
  throw new Error(
186515
186518
  `Push blocked: destination does not match expected repository.
186516
186519
  Expected: ${pushUrl}
186517
- Actual: ${dest.url}
186518
- Git configuration may have been tampered with.`
186520
+ Actual: remote '${dest.remoteName}' -> ${dest.url}
186521
+ Git configuration may have been tampered with, or a url.*.insteadOf rewrite is in effect.`
186519
186522
  );
186520
186523
  }
186521
186524
  return dest;
@@ -188637,6 +188640,9 @@ async function configureRepoGit(params) {
188637
188640
  removeIncludeIfEntries(repoDir);
188638
188641
  const originUrl = `https://github.com/${params.owner}/${params.name}.git`;
188639
188642
  $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
188643
+ $("git", ["config", "--local", "--replace-all", "remote.origin.pushurl", originUrl], {
188644
+ cwd: repoDir
188645
+ });
188640
188646
  repoState.pushUrl = originUrl;
188641
188647
  $("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
188642
188648
  repoState.initialHead = captureInitialHead(repoDir);
@@ -188975,6 +188981,9 @@ async function checkoutPrBranch(pr, params) {
188975
188981
  $("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
188976
188982
  log.debug(`\xBB updated remote '${remoteName}' for fork ${pr.headRepoFullName}`);
188977
188983
  }
188984
+ $("git", ["config", "--local", "--replace-all", `remote.${remoteName}.pushurl`, forkUrl], {
188985
+ log: false
188986
+ });
188978
188987
  $("git", ["config", `branch.${localBranch}.pushRemote`, remoteName], { log: false });
188979
188988
  $("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${pr.headRef}`], { log: false });
188980
188989
  log.debug(`\xBB configured branch '${localBranch}' to push to '${remoteName}/${pr.headRef}'`);
@@ -194148,6 +194157,43 @@ function resolveCwd(cwd) {
194148
194157
  if (isAbsolute2(cwd)) return cwd;
194149
194158
  return workspace ? resolve2(workspace, cwd) : cwd;
194150
194159
  }
194160
+ var DISPATCH_PAYLOAD_KEY = '"~pullfrog"';
194161
+ function sliceBalancedObject(text, open) {
194162
+ let depth = 0;
194163
+ let inString = false;
194164
+ for (let i = open; i < text.length; i++) {
194165
+ const char = text[i];
194166
+ if (inString) {
194167
+ if (char === "\\") i++;
194168
+ else if (char === '"') inString = false;
194169
+ continue;
194170
+ }
194171
+ if (char === '"') inString = true;
194172
+ else if (char === "{") depth++;
194173
+ else if (char === "}" && --depth === 0) return text.slice(open, i + 1);
194174
+ }
194175
+ return void 0;
194176
+ }
194177
+ function findEmbeddedDispatchPayload(text) {
194178
+ const limit = text.lastIndexOf(DISPATCH_PAYLOAD_KEY);
194179
+ if (limit === -1) return void 0;
194180
+ for (let open = text.indexOf("{"); open !== -1 && open < limit; open = text.indexOf("{", open + 1)) {
194181
+ const candidate = sliceBalancedObject(text, open);
194182
+ if (!candidate) continue;
194183
+ try {
194184
+ const parsed2 = JSON.parse(candidate);
194185
+ if (JsonPayload.allows(parsed2)) return parsed2;
194186
+ } catch {
194187
+ }
194188
+ }
194189
+ return void 0;
194190
+ }
194191
+ function assertDispatchPayloadIntact(promptInput) {
194192
+ if (!findEmbeddedDispatchPayload(promptInput)) return;
194193
+ throw new Error(
194194
+ "this workflow wraps `${{ inputs.prompt }}` in other text, so Pullfrog's dispatch payload can't be read and the run would lose its pull request context, its progress comment, and its standing instructions. in `.github/workflows/pullfrog.yml`, set `prompt:` to exactly `${{ inputs.prompt }}`, and move your own preamble into Standing instructions in the Pullfrog console."
194195
+ );
194196
+ }
194151
194197
  function resolvePromptInput() {
194152
194198
  const promptInput = core7.getInput("prompt");
194153
194199
  const promptFile = core7.getInput("prompt_file");
@@ -194164,9 +194210,9 @@ function resolvePromptInput() {
194164
194210
  try {
194165
194211
  parsed2 = JSON.parse(promptInput);
194166
194212
  } catch {
194167
- return promptInput;
194168
194213
  }
194169
194214
  if (!parsed2 || typeof parsed2 !== "object" || !("~pullfrog" in parsed2)) {
194215
+ assertDispatchPayloadIntact(promptInput);
194170
194216
  return promptInput;
194171
194217
  }
194172
194218
  const jsonPayload = JsonPayload.assert(parsed2);
@@ -194796,7 +194842,7 @@ async function handleAgentResult(ctx) {
194796
194842
  const mode = toolState.selectedMode;
194797
194843
  const isReviewMode = mode === "Review" || mode === "IncrementalReview";
194798
194844
  const payload = ctx.toolContext.payload;
194799
- const hasCommentTarget = toolState.hadProgressComment || !payload.progressComments && payload.event.issue_number !== void 0;
194845
+ const hasCommentTarget = toolState.hadProgressComment || payload.event.issue_number !== void 0;
194800
194846
  if (!isReviewMode && !toolState.wasUpdated && hasCommentTarget && !ctx.silent) {
194801
194847
  const tracker = toolState.todoTracker;
194802
194848
  if (tracker) {
@@ -195230,6 +195276,19 @@ ${input.errorMessage}
195230
195276
  \`\`\``
195231
195277
  ].join("\n");
195232
195278
  }
195279
+ function formatProviderMissingCredential(input) {
195280
+ return [
195281
+ "**No credential reached the model provider.**",
195282
+ "",
195283
+ "The run started without a key for the provider it ended up using, so the request was refused before any model work happened. Nothing was billed, and no key of yours was rejected \u2014 add a provider key, or pick a model you already have one for.",
195284
+ "",
195285
+ `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
195286
+ "",
195287
+ `\`\`\`
195288
+ ${input.errorMessage}
195289
+ \`\`\``
195290
+ ].join("\n");
195291
+ }
195233
195292
  function formatProviderModelNotFoundSummary(input) {
195234
195293
  return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
195235
195294
 
@@ -195275,6 +195334,16 @@ ${body}`, comment: body };
195275
195334
  });
195276
195335
  return { summary: `### \u274C Pullfrog failed
195277
195336
 
195337
+ ${body}`, comment: body };
195338
+ }
195339
+ if (isProviderMissingCredential(input.errorMessage)) {
195340
+ const body = formatProviderMissingCredential({
195341
+ owner: input.repo.owner,
195342
+ name: input.repo.name,
195343
+ errorMessage: input.errorMessage
195344
+ });
195345
+ return { summary: `### \u274C Pullfrog failed
195346
+
195278
195347
  ${body}`, comment: body };
195279
195348
  }
195280
195349
  const apiKeySource = hangBody ?? input.errorMessage;
@@ -195321,7 +195390,7 @@ ${body}`, comment: body };
195321
195390
  ${body}`, comment: body };
195322
195391
  }
195323
195392
  if (hangBody) {
195324
- const explained = input.agentDiagnostic?.lastProviderError !== void 0;
195393
+ const explained = input.agentDiagnostic?.lastProviderError !== void 0 || input.agentDiagnostic?.sawModelOutput === false;
195325
195394
  return {
195326
195395
  summary: `### \u274C Pullfrog failed
195327
195396
 
@@ -50,6 +50,12 @@ export declare function isProviderNoRoutableEndpoints(text: string): boolean;
50
50
  * …/settings/credits`) carries no `/keys/` path and correctly stays out.
51
51
  */
52
52
  export declare function isOpenRouterKeyLimitExceeded(text: string): boolean;
53
+ /**
54
+ * the provider was reached with NO credential at all — the AI SDK's shared `loadApiKey`
55
+ * refusal, so it is provider-agnostic. NOT `isApiKeyAuthError`, which is a key we hold
56
+ * being rejected: "rotate your key" names a credential that does not exist here.
57
+ */
58
+ export declare function isProviderMissingCredential(text: string): boolean;
53
59
  /**
54
60
  * The upstream is having a moment: Anthropic's `API Error: 529 Overloaded`,
55
61
  * OpenRouter's `provider_unavailable` / `timeout`, OpenCode Zen's `Streaming
@@ -45,12 +45,12 @@
45
45
  * `"activity timeout"` or `"agent still pending"` AND none of the
46
46
  * above matched. The harness keeps structured diagnostic state on
47
47
  * `toolState.agentDiagnostic`; `formatAgentHangBody` renders that into
48
- * the job summary. The PR comment instead collapses to a one-line
49
- * `**Run failed.** [View the logs →]` the watchdog jargon, event
50
- * counts, and benign stderr tail are operator-grade detail that only
51
- * alarm the average user. The one exception is a hang masking billing
52
- * exhaustion (#778), where `formatAgentHangBody` emits an actionable
53
- * top-up CTA that the comment keeps verbatim.
48
+ * the job summary. The PR comment keeps that same body whenever the hang
49
+ * is EXPLAINED a classified provider error (#778, #1183) or a provider
50
+ * that never returned a first token and otherwise collapses to a
51
+ * one-line `**Run failed.** [View the logs →]`, because watchdog jargon,
52
+ * event counts and a benign stderr tail are operator-grade detail that
53
+ * only alarm the average user.
54
54
  *
55
55
  * 6. Default — the job summary gets a plain-English lead sentence plus the
56
56
  * raw error in a fenced code block under the `### ❌ Pullfrog failed`
@@ -59,7 +59,7 @@
59
59
  *
60
60
  * Net: the actionable classifications (billing, API-key, model-not-found,
61
61
  * no-provider-available, context-overflow, transient-upstream) render identical
62
- * bodies on both surfaces; the non-actionable ones (hang,
62
+ * bodies on both surfaces; the non-actionable ones (unexplained hang,
63
63
  * generic) keep the forensics in the Actions job summary and show a calm
64
64
  * one-liner in the PR comment, whose footer already carries Pullfrog
65
65
  * branding + rerun links.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",