pullfrog 0.0.202 → 0.0.203

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
@@ -62880,7 +62880,7 @@ var require_snapshot_recorder = __commonJS({
62880
62880
  "node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
62881
62881
  "use strict";
62882
62882
  var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
62883
- var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
62883
+ var { dirname: dirname5, resolve: resolve3 } = __require("node:path");
62884
62884
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
62885
62885
  var { InvalidArgumentError, UndiciError } = require_errors4();
62886
62886
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -63111,7 +63111,7 @@ var require_snapshot_recorder = __commonJS({
63111
63111
  throw new InvalidArgumentError("Snapshot path is required");
63112
63112
  }
63113
63113
  const resolvedPath = resolve3(path3);
63114
- await mkdir(dirname4(resolvedPath), { recursive: true });
63114
+ await mkdir(dirname5(resolvedPath), { recursive: true });
63115
63115
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
63116
63116
  hash: hash2,
63117
63117
  snapshot: snapshot2
@@ -97692,14 +97692,14 @@ var require_turndown_cjs = __commonJS({
97692
97692
  } else if (node2.nodeType === 1) {
97693
97693
  replacement = replacementForNode.call(self2, node2);
97694
97694
  }
97695
- return join15(output, replacement);
97695
+ return join16(output, replacement);
97696
97696
  }, "");
97697
97697
  }
97698
97698
  function postProcess(output) {
97699
97699
  var self2 = this;
97700
97700
  this.rules.forEach(function(rule) {
97701
97701
  if (typeof rule.append === "function") {
97702
- output = join15(output, rule.append(self2.options));
97702
+ output = join16(output, rule.append(self2.options));
97703
97703
  }
97704
97704
  });
97705
97705
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -97711,7 +97711,7 @@ var require_turndown_cjs = __commonJS({
97711
97711
  if (whitespace.leading || whitespace.trailing) content = content.trim();
97712
97712
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
97713
97713
  }
97714
- function join15(output, replacement) {
97714
+ function join16(output, replacement) {
97715
97715
  var s1 = trimTrailingNewlines(output);
97716
97716
  var s2 = trimLeadingNewlines(replacement);
97717
97717
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -99204,12 +99204,12 @@ import { basename as basename2 } from "node:path";
99204
99204
  // commands/gha.ts
99205
99205
  var core7 = __toESM(require_core(), 1);
99206
99206
  var import_arg = __toESM(require_arg(), 1);
99207
- import { dirname as dirname3 } from "node:path";
99207
+ import { dirname as dirname4 } from "node:path";
99208
99208
 
99209
99209
  // main.ts
99210
99210
  var core6 = __toESM(require_core(), 1);
99211
- import { existsSync as existsSync6, readdirSync } from "node:fs";
99212
- import { join as join14 } from "node:path";
99211
+ import { existsSync as existsSync7, readdirSync } from "node:fs";
99212
+ import { join as join15 } from "node:path";
99213
99213
 
99214
99214
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
99215
99215
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -107684,7 +107684,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
107684
107684
  // agents/shared.ts
107685
107685
  import { execFileSync } from "node:child_process";
107686
107686
  var MAX_STDERR_LINES = 20;
107687
- var MAX_COMMIT_RETRIES = 3;
107687
+ var MAX_POST_RUN_RETRIES = 3;
107688
107688
  function getGitStatus() {
107689
107689
  try {
107690
107690
  return execFileSync("git", ["status", "--porcelain"], {
@@ -107695,7 +107695,7 @@ function getGitStatus() {
107695
107695
  return "";
107696
107696
  }
107697
107697
  }
107698
- function buildCommitPrompt(_agentId, status) {
107698
+ function buildCommitPrompt(status) {
107699
107699
  return [
107700
107700
  `UNCOMMITTED CHANGES \u2014 the working tree is dirty. push all changes to a pull request (new or existing). \`git status\` must be clean before you finish.`,
107701
107701
  "",
@@ -107704,6 +107704,9 @@ function buildCommitPrompt(_agentId, status) {
107704
107704
  "```"
107705
107705
  ].join("\n");
107706
107706
  }
107707
+ function hasPostRunIssues(issues) {
107708
+ return issues.stopHook !== void 0 || issues.dirtyTree !== void 0;
107709
+ }
107707
107710
  var agent = (input) => {
107708
107711
  return {
107709
107712
  ...input,
@@ -108015,7 +108018,7 @@ var providers = {
108015
108018
  "claude-opus": {
108016
108019
  displayName: "Claude Opus",
108017
108020
  resolve: "anthropic/claude-opus-4-7",
108018
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
108021
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
108019
108022
  preferred: true
108020
108023
  },
108021
108024
  "claude-sonnet": {
@@ -108034,16 +108037,38 @@ var providers = {
108034
108037
  displayName: "OpenAI",
108035
108038
  envVars: ["OPENAI_API_KEY"],
108036
108039
  models: {
108040
+ gpt: {
108041
+ displayName: "GPT",
108042
+ resolve: "openai/gpt-5.5",
108043
+ openRouterResolve: "openrouter/openai/gpt-5.5",
108044
+ preferred: true
108045
+ },
108046
+ "gpt-pro": {
108047
+ displayName: "GPT Pro",
108048
+ resolve: "openai/gpt-5.5-pro",
108049
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108050
+ },
108051
+ "gpt-mini": {
108052
+ displayName: "GPT Mini",
108053
+ resolve: "openai/gpt-5.4-mini",
108054
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
108055
+ },
108056
+ // legacy aliases — openai unified the codex line into the main GPT family
108057
+ // and is shutting down every "-codex" snapshot on 2026-07-23. transparently
108058
+ // upgrade existing users via the fallback chain. UI display sites resolve
108059
+ // to the terminal alias's label (so dropdown trigger + PR footers show
108060
+ // "GPT" / "GPT Mini", not the historical name).
108037
108061
  "gpt-codex": {
108038
108062
  displayName: "GPT Codex",
108039
108063
  resolve: "openai/gpt-5.3-codex",
108040
108064
  openRouterResolve: "openrouter/openai/gpt-5.3-codex",
108041
- preferred: true
108065
+ fallback: "openai/gpt"
108042
108066
  },
108043
108067
  "gpt-codex-mini": {
108044
108068
  displayName: "GPT Codex Mini",
108045
108069
  resolve: "openai/gpt-5.1-codex-mini",
108046
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
108070
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
108071
+ fallback: "openai/gpt-mini"
108047
108072
  },
108048
108073
  o3: {
108049
108074
  displayName: "O3",
@@ -108094,16 +108119,30 @@ var providers = {
108094
108119
  displayName: "DeepSeek",
108095
108120
  envVars: ["DEEPSEEK_API_KEY"],
108096
108121
  models: {
108122
+ "deepseek-pro": {
108123
+ displayName: "DeepSeek Pro",
108124
+ resolve: "deepseek/deepseek-v4-pro",
108125
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
108126
+ preferred: true
108127
+ },
108128
+ "deepseek-flash": {
108129
+ displayName: "DeepSeek Flash",
108130
+ resolve: "deepseek/deepseek-v4-flash",
108131
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
108132
+ },
108133
+ // legacy aliases — deepseek retires these on 2026-07-24; transparently
108134
+ // upgrade existing users to the v4 family via the fallback chain.
108097
108135
  "deepseek-reasoner": {
108098
108136
  displayName: "DeepSeek Reasoner",
108099
108137
  resolve: "deepseek/deepseek-reasoner",
108100
108138
  openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
108101
- preferred: true
108139
+ fallback: "deepseek/deepseek-pro"
108102
108140
  },
108103
108141
  "deepseek-chat": {
108104
108142
  displayName: "DeepSeek Chat",
108105
108143
  resolve: "deepseek/deepseek-chat",
108106
- openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
108144
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
108145
+ fallback: "deepseek/deepseek-flash"
108107
108146
  }
108108
108147
  }
108109
108148
  }),
@@ -108113,8 +108152,8 @@ var providers = {
108113
108152
  models: {
108114
108153
  "kimi-k2": {
108115
108154
  displayName: "Kimi K2",
108116
- resolve: "moonshotai/kimi-k2.5",
108117
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
108155
+ resolve: "moonshotai/kimi-k2.6",
108156
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
108118
108157
  preferred: true
108119
108158
  }
108120
108159
  }
@@ -108133,7 +108172,7 @@ var providers = {
108133
108172
  "claude-opus": {
108134
108173
  displayName: "Claude Opus",
108135
108174
  resolve: "opencode/claude-opus-4-7",
108136
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6"
108175
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7"
108137
108176
  },
108138
108177
  "claude-sonnet": {
108139
108178
  displayName: "Claude Sonnet",
@@ -108145,15 +108184,33 @@ var providers = {
108145
108184
  resolve: "opencode/claude-haiku-4-5",
108146
108185
  openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
108147
108186
  },
108187
+ gpt: {
108188
+ displayName: "GPT",
108189
+ resolve: "opencode/gpt-5.5",
108190
+ openRouterResolve: "openrouter/openai/gpt-5.5"
108191
+ },
108192
+ "gpt-pro": {
108193
+ displayName: "GPT Pro",
108194
+ resolve: "opencode/gpt-5.5-pro",
108195
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108196
+ },
108197
+ "gpt-mini": {
108198
+ displayName: "GPT Mini",
108199
+ resolve: "opencode/gpt-5.4-mini",
108200
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
108201
+ },
108202
+ // legacy aliases — see openai provider above for context.
108148
108203
  "gpt-codex": {
108149
108204
  displayName: "GPT Codex",
108150
108205
  resolve: "opencode/gpt-5.3-codex",
108151
- openRouterResolve: "openrouter/openai/gpt-5.3-codex"
108206
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
108207
+ fallback: "opencode/gpt"
108152
108208
  },
108153
108209
  "gpt-codex-mini": {
108154
108210
  displayName: "GPT Codex Mini",
108155
108211
  resolve: "opencode/gpt-5.1-codex-mini",
108156
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
108212
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
108213
+ fallback: "opencode/gpt-mini"
108157
108214
  },
108158
108215
  "gemini-pro": {
108159
108216
  displayName: "Gemini Pro",
@@ -108167,8 +108224,8 @@ var providers = {
108167
108224
  },
108168
108225
  "kimi-k2": {
108169
108226
  displayName: "Kimi K2",
108170
- resolve: "opencode/kimi-k2.5",
108171
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
108227
+ resolve: "opencode/kimi-k2.6",
108228
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
108172
108229
  },
108173
108230
  "gpt-5-nano": {
108174
108231
  displayName: "GPT Nano",
@@ -108188,12 +108245,6 @@ var providers = {
108188
108245
  resolve: "opencode/minimax-m2.5-free",
108189
108246
  envVars: [],
108190
108247
  isFree: true
108191
- },
108192
- "nemotron-3-super-free": {
108193
- displayName: "Nemotron 3 Super",
108194
- resolve: "opencode/nemotron-3-super-free",
108195
- envVars: [],
108196
- isFree: true
108197
108248
  }
108198
108249
  }
108199
108250
  }),
@@ -108203,8 +108254,8 @@ var providers = {
108203
108254
  models: {
108204
108255
  "claude-opus": {
108205
108256
  displayName: "Claude Opus",
108206
- resolve: "openrouter/anthropic/claude-opus-4.6",
108207
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
108257
+ resolve: "openrouter/anthropic/claude-opus-4.7",
108258
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
108208
108259
  preferred: true
108209
108260
  },
108210
108261
  "claude-sonnet": {
@@ -108217,15 +108268,33 @@ var providers = {
108217
108268
  resolve: "openrouter/anthropic/claude-haiku-4.5",
108218
108269
  openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
108219
108270
  },
108271
+ gpt: {
108272
+ displayName: "GPT",
108273
+ resolve: "openrouter/openai/gpt-5.5",
108274
+ openRouterResolve: "openrouter/openai/gpt-5.5"
108275
+ },
108276
+ "gpt-pro": {
108277
+ displayName: "GPT Pro",
108278
+ resolve: "openrouter/openai/gpt-5.5-pro",
108279
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
108280
+ },
108281
+ "gpt-mini": {
108282
+ displayName: "GPT Mini",
108283
+ resolve: "openrouter/openai/gpt-5.4-mini",
108284
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
108285
+ },
108286
+ // legacy aliases — see openai provider for context.
108220
108287
  "gpt-codex": {
108221
108288
  displayName: "GPT Codex",
108222
108289
  resolve: "openrouter/openai/gpt-5.3-codex",
108223
- openRouterResolve: "openrouter/openai/gpt-5.3-codex"
108290
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
108291
+ fallback: "openrouter/gpt"
108224
108292
  },
108225
108293
  "gpt-codex-mini": {
108226
108294
  displayName: "GPT Codex Mini",
108227
108295
  resolve: "openrouter/openai/gpt-5.1-codex-mini",
108228
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
108296
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
108297
+ fallback: "openrouter/gpt-mini"
108229
108298
  },
108230
108299
  "o4-mini": {
108231
108300
  displayName: "O4 Mini",
@@ -108247,31 +108316,44 @@ var providers = {
108247
108316
  resolve: "openrouter/x-ai/grok-4",
108248
108317
  openRouterResolve: "openrouter/x-ai/grok-4"
108249
108318
  },
108319
+ "deepseek-pro": {
108320
+ displayName: "DeepSeek Pro",
108321
+ resolve: "openrouter/deepseek/deepseek-v4-pro",
108322
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
108323
+ },
108324
+ "deepseek-flash": {
108325
+ displayName: "DeepSeek Flash",
108326
+ resolve: "openrouter/deepseek/deepseek-v4-flash",
108327
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
108328
+ },
108329
+ // legacy alias — deepseek retires this on 2026-07-24; transparently
108330
+ // upgrade existing users to the v4 family via the fallback chain.
108250
108331
  "deepseek-chat": {
108251
108332
  displayName: "DeepSeek Chat",
108252
108333
  resolve: "openrouter/deepseek/deepseek-v3.2",
108253
- openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
108334
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
108335
+ fallback: "openrouter/deepseek-flash"
108254
108336
  },
108255
108337
  "kimi-k2": {
108256
108338
  displayName: "Kimi K2",
108257
- resolve: "openrouter/moonshotai/kimi-k2.5",
108258
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
108339
+ resolve: "openrouter/moonshotai/kimi-k2.6",
108340
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
108259
108341
  }
108260
108342
  }
108261
108343
  })
108262
108344
  };
108263
- function parseModel(slug) {
108264
- const slashIdx = slug.indexOf("/");
108345
+ function parseModel(slug2) {
108346
+ const slashIdx = slug2.indexOf("/");
108265
108347
  if (slashIdx === -1) {
108266
- throw new Error(`invalid model slug "${slug}" \u2014 expected "provider/model"`);
108348
+ throw new Error(`invalid model slug "${slug2}" \u2014 expected "provider/model"`);
108267
108349
  }
108268
- return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
108350
+ return { provider: slug2.slice(0, slashIdx), model: slug2.slice(slashIdx + 1) };
108269
108351
  }
108270
- function getModelProvider(slug) {
108271
- return parseModel(slug).provider;
108352
+ function getModelProvider(slug2) {
108353
+ return parseModel(slug2).provider;
108272
108354
  }
108273
- function getModelEnvVars(slug) {
108274
- const parsed2 = parseModel(slug);
108355
+ function getModelEnvVars(slug2) {
108356
+ const parsed2 = parseModel(slug2);
108275
108357
  const providerConfig = providers[parsed2.provider];
108276
108358
  if (!providerConfig) {
108277
108359
  return [];
@@ -108295,26 +108377,29 @@ var modelAliases = Object.entries(providers).flatMap(
108295
108377
  }))
108296
108378
  );
108297
108379
  var MAX_FALLBACK_DEPTH = 10;
108298
- function resolveCliModel(slug) {
108299
- let current = slug;
108380
+ function resolveDisplayAlias(slug2) {
108381
+ let current = slug2;
108300
108382
  const visited = /* @__PURE__ */ new Set();
108301
108383
  for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) {
108302
108384
  if (visited.has(current)) return void 0;
108303
108385
  visited.add(current);
108304
108386
  const alias = modelAliases.find((a) => a.slug === current);
108305
108387
  if (!alias) return void 0;
108306
- if (!alias.fallback) return alias.resolve;
108388
+ if (!alias.fallback) return alias;
108307
108389
  current = alias.fallback;
108308
108390
  }
108309
108391
  return void 0;
108310
108392
  }
108393
+ function resolveCliModel(slug2) {
108394
+ return resolveDisplayAlias(slug2)?.resolve;
108395
+ }
108311
108396
 
108312
108397
  // utils/buildPullfrogFooter.ts
108313
108398
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
108314
108399
  var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
108315
- function formatModelLabel(slug) {
108316
- const alias = modelAliases.find((a) => a.slug === slug);
108317
- if (!alias) return `\`${slug}\``;
108400
+ function formatModelLabel(slug2) {
108401
+ const alias = resolveDisplayAlias(slug2);
108402
+ if (!alias) return `\`${slug2}\``;
108318
108403
  return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
108319
108404
  }
108320
108405
  function buildPullfrogFooter(params) {
@@ -142394,7 +142479,7 @@ var import_semver = __toESM(require_semver2(), 1);
142394
142479
  // package.json
142395
142480
  var package_default = {
142396
142481
  name: "pullfrog",
142397
- version: "0.0.202",
142482
+ version: "0.0.203",
142398
142483
  type: "module",
142399
142484
  bin: {
142400
142485
  pullfrog: "dist/cli.mjs",
@@ -143535,6 +143620,18 @@ function validateInlineComments(comments, map2) {
143535
143620
  return { valid, dropped };
143536
143621
  }
143537
143622
  var MAX_DROPPED_COMMENT_LINES = 50;
143623
+ function duplicateReviewDecision(params) {
143624
+ const existing = params.existing;
143625
+ if (!existing) return null;
143626
+ if (params.currentCheckoutSha && existing.reviewedSha && params.currentCheckoutSha !== existing.reviewedSha) {
143627
+ return null;
143628
+ }
143629
+ return {
143630
+ kind: "already-submitted",
143631
+ reviewId: existing.id,
143632
+ reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`
143633
+ };
143634
+ }
143538
143635
  function reviewSkipDecision(params) {
143539
143636
  if (params.body || params.hasComments) return null;
143540
143637
  if (!params.approved) {
@@ -143604,6 +143701,19 @@ function CreatePullRequestReviewTool(ctx) {
143604
143701
  execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
143605
143702
  if (body) body = fixDoubleEscapedString(body);
143606
143703
  ctx.toolState.issueNumber = pull_number;
143704
+ const dup = duplicateReviewDecision({
143705
+ existing: ctx.toolState.review,
143706
+ currentCheckoutSha: ctx.toolState.checkoutSha
143707
+ });
143708
+ if (dup) {
143709
+ log.info(`skipping duplicate review submission: ${dup.reason}`);
143710
+ return {
143711
+ success: true,
143712
+ skipped: true,
143713
+ reason: dup.reason,
143714
+ reviewId: dup.reviewId
143715
+ };
143716
+ }
143607
143717
  const skip = reviewSkipDecision({
143608
143718
  approved: approved ?? false,
143609
143719
  body,
@@ -143721,6 +143831,9 @@ function CreatePullRequestReviewTool(ctx) {
143721
143831
  nodeId: reviewNodeId,
143722
143832
  reviewedSha: actuallyReviewedSha
143723
143833
  };
143834
+ await deleteProgressComment(ctx).catch((err) => {
143835
+ log.debug(`progress comment cleanup after review failed: ${err}`);
143836
+ });
143724
143837
  if (ctx.toolState.checkoutSha && latestHeadSha && latestHeadSha !== ctx.toolState.checkoutSha) {
143725
143838
  const fromSha = ctx.toolState.checkoutSha;
143726
143839
  const toSha = latestHeadSha;
@@ -145464,6 +145577,18 @@ function ResolveReviewThreadTool(ctx) {
145464
145577
  });
145465
145578
  }
145466
145579
 
145580
+ // agents/reviewer.ts
145581
+ var REVIEWER_AGENT_NAME = "reviewfrog";
145582
+ var REVIEWER_SYSTEM_PROMPT = `You are a read-only review subagent. Your role is to find flaws in code or artifacts provided by the orchestrator and report findings \u2014 never to modify state.
145583
+
145584
+ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
145585
+ - Read-only tools only. Do NOT write or edit files. Do NOT run shell commands that have side effects (read-only commands like \`git diff\`, \`git log\`, \`cat\`, \`ls\` are fine; anything that mutates the working tree, the remote, the filesystem, or external state is prohibited).
145586
+ - Do NOT call any state-changing MCP tool. State-changing means: posts a comment, pushes a branch, creates/updates a PR or issue, changes labels, resolves review threads, persists learnings, sets workflow output, installs dependencies, uploads files, kills processes, etc. Read-only MCP queries (\`get_*\`, \`list_*\`, log inspection, diff retrieval) are fine.
145587
+ - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
145588
+ - Test for any tool call before invoking it: would this still be a no-op if reverted? If not, do not call it. Apply this test to tools added after this prompt was written \u2014 the rule is the invariant, not the enumeration.
145589
+
145590
+ Report findings clearly with file:line references and quoted evidence where possible. Flag uncertainty explicitly \u2014 if you cannot verify a claim, say so rather than guess.`;
145591
+
145467
145592
  // modes.ts
145468
145593
  var PR_SUMMARY_FORMAT = `### Default format
145469
145594
 
@@ -145535,9 +145660,36 @@ function computeModes(agentId) {
145535
145660
  - plan your approach before writing code: identify which files need to change, key design decisions, and edge cases. for non-trivial changes, consider whether there's a more elegant approach.
145536
145661
  - run relevant tests/lints before committing
145537
145662
 
145538
- 4. **self-review**: delegate a read-only subagent to review your diff. the subagent must ONLY read files, grep, and search \u2014 no MCP tools, no writes, no shell commands, no side effects. provide it with the output of \`git diff\` and instruct it to look for bugs, logic errors, missing edge cases, and unintended changes. review its findings, address any valid points, and discard nitpicks or false positives. then:
145539
- - verify only intended changes are present, no debug artifacts or commented-out code remain, and no unrelated files were modified
145540
- - commit locally via shell (\`git add . && git commit -m "..."\`)
145663
+ 4. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
145664
+
145665
+ Skip self-review (commit directly) when the diff is **genuinely trivial**:
145666
+ - doc typos, comment-only edits, whitespace/format-only, import reordering
145667
+ - lockfile or generated-code regeneration, mechanical rename whose only effect is import-path updates (size of diff is irrelevant \u2014 read the *shape*, not the line count)
145668
+ - low-risk dep patch bump from a trusted source
145669
+
145670
+ Run self-review when the diff has **any behavioral surface, however small**:
145671
+ - 1-line changes to SQL operators / comparison logic / regexes / redirects / HTTP methods / response codes
145672
+ - any change to money / tax / currency / billing / fee / refund / payout calculations or constants
145673
+ - any change to auth / permissions / roles / sessions / tokens / signature verification
145674
+ - any change to feature-flag defaults, retry counts, timeouts, rate limits, batch sizes
145675
+ - new endpoints, new code paths, new error branches \u2014 even small ones
145676
+ - mixed diffs (whitespace + a single semantic line) \u2014 the semantic line still triggers self-review
145677
+ - anything you're uncertain about
145678
+
145679
+ Tie-breaker: when in doubt, run self-review. One false-positive subagent dispatch costs cents; one false-negative shipped bug costs much more. There's no value in dispatching for a typo, but there's also no excuse for skipping on a 1-line change to a billing path.
145680
+
145681
+ Otherwise delegate the \`${REVIEWER_AGENT_NAME}\` subagent to review your diff with fresh eyes against YOUR TASK. The subagent's baked-in system prompt enforces a non-mutative + non-recursive contract: read-only file/search/web tools and read-only MCP queries only; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch. Enforcement is prose-only \u2014 restate the constraint in your dispatch instructions and do not relax it.
145682
+
145683
+ Provide the subagent with YOUR TASK, the output of \`git diff\`, and a tight summary (not raw output) of any lint/typecheck/test failures you fixed during build \u2014 what broke, root cause, the fix \u2014 so it can check that fixes addressed root causes rather than suppressed symptoms; say "no build-phase failures" if the build path was clean. Instruct it to flag bugs, logic errors, missing edge cases, gaps between request and diff, and unintended changes.
145684
+
145685
+ Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
145686
+ - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
145687
+ - Do NOT curate a reading list of files. Let the subagent discover scope from the diff and codebase.
145688
+ - Do NOT pre-shape output with a severity / category schema. That leaks your hypotheses; severity is your call during evaluation.
145689
+ - Do NOT defect-hunt the diff yourself in parallel with the subagent. Your role is dispatch + evaluation; doing the review yourself reintroduces the implementation bias the subagent is meant to mitigate.
145690
+ - For diffs that rely on third-party API contracts, SDK semantics, framework directives, or DB engine specifics, instruct the subagent to verify load-bearing claims via web search and quote source URLs rather than trust training data \u2014 this is the single most common review-quality failure mode.
145691
+
145692
+ Review the findings, address valid points, and discard nitpicks or false positives. The reviewer is fallible \u2014 it biases toward *recommending additions* (defensive checks for impossible cases, extra logging, new abstractions used once, comments restating code, tests asserting tautologies, "just-in-case" guards). For each finding, ask: would applying it leave the code more sound, correct, AND elegant? Two-out-of-three is not enough \u2014 a fix that improves correctness while degrading elegance still degrades the codebase. Reject bloat-shaped findings without applying them, and after applying the rest re-read your diff and be discerning about what *you just changed*: if any fix turned out to be bloat in context, revert it. The goal is code that is sound and correct *while remaining elegant*; the smallest diff that fixes the real defect almost always wins. Then verify only intended changes are present, no debug artifacts or commented-out code remain, no unrelated files were modified. Commit locally via shell (\`git add . && git commit -m "..."\`).
145541
145693
 
145542
145694
  5. **finalize**:
145543
145695
  - confirm a clean working tree, then push via \`${t2("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
@@ -145561,11 +145713,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
145561
145713
 
145562
145714
  3. For each comment:
145563
145715
  - understand the feedback
145564
- - make the code change using your native tools
145565
- - record what was done
145716
+ - evaluate whether applying it would leave the code more **sound, correct, AND elegant**. reviewers are fallible and bias toward *recommending additions* (defensive checks for impossible cases, extra abstractions, comments restating obvious code, tests asserting tautologies, "just-in-case" guards). if a request would add bloat \u2014 ceremony without commensurate correctness benefit \u2014 push back in your reply rather than mechanically applying it. two-out-of-three is not enough; improving correctness while degrading elegance still degrades the code.
145717
+ - if the request stands, make the code change using your native tools; otherwise reply explaining why
145718
+ - record what was done (or why nothing was done)
145566
145719
 
145567
145720
  4. Quality check:
145568
- - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, and the changes are clean enough that a senior engineer would approve without hesitation
145721
+ - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
145569
145722
  - commit locally via shell (\`git add . && git commit -m "..."\`)
145570
145723
 
145571
145724
  5. Finalize:
@@ -145576,28 +145729,93 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
145576
145729
 
145577
145730
  ${learningsStep(t2, 6)}`
145578
145731
  },
145732
+ // Review and IncrementalReview use the multi-lens orchestrator pattern
145733
+ // (canonical source: .claude/commands/anneal.md). The orchestrator does
145734
+ // triage → parallel read-only subagent fan-out → aggregate → draft comments
145735
+ // → submit. For someone else's PR, parallel lenses (correctness, security,
145736
+ // research-validated claims, user-journey, etc.) provide breadth across
145737
+ // angles that a single subagent can't carry coherently. Build mode keeps
145738
+ // a single fresh-eyes subagent (different problem shape — orchestrator
145739
+ // wrote the code and bias-mitigation comes from delegating to one
145740
+ // subagent that doesn't share the implementation context).
145741
+ // Deliberate omission vs canonical /anneal: severity categorization in the
145742
+ // final message (the review body has its own CAUTION/IMPORTANT framing
145743
+ // instead of a severity table).
145579
145744
  {
145580
145745
  name: "Review",
145581
145746
  description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
145582
145747
  prompt: `### Checklist
145583
145748
 
145584
- 1. Checkout the PR via \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC first and treat its file line ranges as your coverage checklist.
145585
-
145586
- 2. For each area of change:
145587
- - read the diff and trace data flow, check boundaries, and verify assumptions
145588
- - plan your investigation: identify the highest-risk areas (tricky state transitions, boundary crossings, assumption chains) and prioritize depth over breadth
145589
- - use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context
145590
- - if the PR removes features, deletes exports, renames identifiers, or changes architectural patterns, run a dedicated impact analysis: list what changed, then use grep across code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, and UI to find stale references
145591
- - report impact-analysis findings in the summary body, ordered by severity (runtime breakage > incorrect docs > stale comments)
145592
- - draft inline comments with NEW line numbers from the diff \u2014 every comment must be actionable (2-3 sentences max)
145593
- - use GitHub permalink format for code references
145594
- - for large or cross-cutting PRs that touch disparate subsystems, consider delegating read-only subagents to investigate areas in parallel. subagents must ONLY read files, grep, and search \u2014 no MCP tools, no writes, no shell commands, no side effects. collect their findings and use them to draft comments.
145749
+ 1. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata and a \`diffPath\`. read the diff TOC end-to-end and treat its file line ranges as your coverage checklist.
145750
+
145751
+ 2. **triage**: orient yourself on the PR \u2014 identify *what kind of thing this is* (domain it touches, seams it crosses, external contracts it depends on, user-facing surfaces it changes). orientation only \u2014 defer specific defect-hunting to the subagents; pre-reviewing biases the lenses you pick. use \`${t2("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
145752
+
145753
+ if the PR is **genuinely trivial**, skip steps 3\u20134 entirely and submit \`Reviewed \u2014 no issues found.\` per step 5. there's no value in dispatching even one lens for a typo.
145754
+
145755
+ "Genuinely trivial" (skip):
145756
+ - single-word doc typo, whitespace/format-only, comment-only across any number of files
145757
+ - lockfile or generated-code regeneration (size of diff is irrelevant \u2014 read the *shape*)
145758
+ - mechanical rename whose only effect is import-path updates
145759
+ - low-risk dep patch bump
145760
+
145761
+ "Looks trivial but isn't" (do **NOT** skip \u2014 small diff, big blast radius):
145762
+ - any 1-line change to SQL / regex / auth / billing / permission / signature-verification code
145763
+ - flipping a feature-flag default, default config value, or retry/timeout constant
145764
+ - changing a money/tax/currency/fee constant by any amount
145765
+ - changing an HTTP method, redirect URL, response code, or status enum
145766
+ - tightening or loosening a comparison operator (\`<\` \u2194 \`<=\`, \`==\` \u2194 \`!=\`)
145767
+ - renaming a public API surface (still trivial in shape, but needs an impact lens)
145768
+ - adding a new direct dependency (supply-chain surface)
145769
+ - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
145770
+ - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
145771
+
145772
+ When unsure, treat as non-trivial. The cost of one extra subagent is cents; the cost of a missed billing/auth/data bug is much more.
145773
+
145774
+ otherwise pick lenses by where the PR concentrates risk \u2014 **there's no fixed count**. lens count is judgment, not a formula. concrete shapes to anchor against:
145775
+
145776
+ - **1 lens** \u2014 pure refactor / mechanical rename across many files (impact); new test file with no source change (test-integrity); small isolated bug fix (correctness); doc-only PR with non-trivial technical content (research-validated or holistic)
145777
+ - **2\u20133 lenses (most PRs land here)** \u2014 new CRUD endpoint (correctness + security + test-integrity); new UI flow (user-journey + correctness); a single bug fix in a non-critical subsystem (correctness + test-integrity); design doc covering one domain (research-validated + correctness or holistic)
145778
+ - **4\u20135 lenses (high-stakes subsystem touches)** \u2014 any billing/payments change (billing-subsystem + correctness + security + operational-readiness); new auth flow (auth-subsystem + correctness + security + test-integrity); schema migration (schema-migration-subsystem + correctness + operational-readiness + impact); cross-subsystem PR that touches billing AND auth AND schema (one subsystem lens per domain + correctness)
145779
+ - **6+ lenses** \u2014 almost always a smell; you're either covering overlapping ground or this PR should have been split. push back via the review body rather than expanding lens count.
145780
+
145781
+ lenses come in two flavors, and you can mix them:
145782
+ - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
145783
+ - **subsystem lenses** \u2014 a domain-scoped frame for high-stakes subsystems the PR touches (e.g. "the auth lens", "the billing lens", "the schema-migration lens"). a subsystem lens is "review the PR specifically for what could go wrong in this subsystem" and naturally combines theme + scope. **for high-stakes domains, lead with the subsystem lens rather than the generic themed equivalent** \u2014 "billing-subsystem" outperforms "correctness on billing code" because the framing primes the subagent to remember domain-specific failure modes (double-charges, refund races, currency rounding, dispute flows) the generic lens misses.
145784
+
145785
+ starter menu (combine, omit, or invent your own):
145786
+ - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
145787
+ - **impact** \u2014 when the PR removes features, deletes exports, renames identifiers, or changes architectural patterns: stale references in code, tests, docs (\`docs/\`, \`wiki/\`), comments, configs, UI
145788
+ - **research-validated assumptions** \u2014 third-party API contracts, SDK semantics, framework directives, version-gated behavior. the subagent must verify load-bearing claims via web search and quote source URLs.
145789
+ - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
145790
+ - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
145791
+ - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
145792
+ - **integration & cross-cutting** \u2014 API contracts between modules, backward-compat of public surfaces, multi-service ordering
145793
+ - **test integrity** \u2014 meaningful coverage for the changed behavior; deterministic; no shared-state pollution
145794
+ - **performance** \u2014 N+1 queries, hot-path allocation, latency budgets, index coverage
145795
+ - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
145796
+ - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
145797
+
145798
+ 3. **fan out**: dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). when picking 2+ lenses, dispatch them in a **single assistant turn with multiple parallel subagent calls**; issuing one and awaiting reply before the next collapses the fan-out into a serial review. if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 3 entirely on a single subagent failure. each subagent gets:
145799
+ - the diff path / target \u2014 reading the diff and the codebase is its job
145800
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145801
+ - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
145802
+ - the read-only contract restated in your dispatch instructions so the rule is present twice (the subagent's system prompt also enforces it). The test: would this call still be a no-op if reverted? If not (PR comments, branch pushes, issue updates, set_output, label changes, dependency installs, etc.), don't make it.
145803
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search rather than trust training data, and to quote source URLs in its reasoning. action runs are non-interactive \u2014 there's no human in the loop to catch "I'm pretty sure Stripe does X."
145804
+ - ask the subagent to report findings with file paths and NEW line numbers from the diff so you can anchor inline comments without re-reading the entire diff.
145805
+
145806
+ delegation discipline:
145807
+ - do NOT lens-review the diff yourself in parallel with the subagents (your job is dispatch + comment-drafting; doing the lens work yourself reintroduces the bias the fan-out avoids)
145808
+ - do NOT summarize the PR for them (biases toward a validation frame)
145809
+ - do NOT hand them a curated reading list (let them discover scope)
145810
+ - do NOT pre-shape their output with a finding schema
145811
+ - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
145812
+
145813
+ 4. **aggregate & draft**: merge findings; de-dup overlaps (two lenses catching the same issue = higher-confidence signal); trace each finding yourself before accepting it. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the PR (heuristic: if the finding's root cause lives in lines this PR added or modified, it's in scope; otherwise drop unless the PR plausibly introduced or amplified the regression), and anything not actionable. also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or worse, degrades elegance to nominally improve correctness) makes the codebase worse, not better.
145814
+
145815
+ for surviving findings, draft inline comments with NEW line numbers from the diff. every comment must be actionable, 2-3 sentences max. use GitHub permalink format for code references. for impact-analysis findings (stale references after rename/remove), report them in the review body ordered by severity (runtime breakage > incorrect docs > stale comments) rather than as inline comments unless they're anchored to a specific line.
145816
+
145817
+ 5. **submit**: ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`. Do NOT call \`report_progress\` \u2014 the review is the final record and the progress comment will be cleaned up automatically.
145595
145818
 
145596
- 3. Self-critique: review all drafted comments and drop any that are praise, style preferences, speculative/unverified claims, about pre-existing code unrelated to the PR, or not actionable.
145597
-
145598
- 4. Submit \u2014 ALWAYS submit exactly one review via \`${t2("create_pull_request_review")}\`.
145599
- Do NOT call \`report_progress\` \u2014 the review is the final record and the progress
145600
- comment will be cleaned up automatically.
145601
145819
  note: the first create_pull_request_review submission may error with a one-time diff-coverage nudge listing unread TOC regions. retry the same call to proceed \u2014 optionally after reading the listed ranges. the pre-flight will not block again this session.
145602
145820
 
145603
145821
  - **critical issues** (blocks merge \u2014 bugs, security, data loss):
@@ -145611,29 +145829,56 @@ ${learningsStep(t2, 6)}`
145611
145829
  - **no actionable issues**:
145612
145830
  \`approved: true\`, body: "Reviewed \u2014 no issues found."`
145613
145831
  },
145832
+ // IncrementalReview shares Review's multi-lens orchestrator pattern but
145833
+ // scopes the target to the incremental diff and adds prior-review-feedback
145834
+ // tracking. The "issues must be NEW since the last Pullfrog review" filter
145835
+ // lives at aggregation time (step 5), NOT in the subagent prompt — pushing
145836
+ // the filter into subagents matches the canonical anneal anti-pattern of
145837
+ // "list known pre-existing failures — don't flag these" and suppresses
145838
+ // signal on regressions the new commits amplified. The body-format rules
145839
+ // (Reviewed changes / Prior review feedback) are unchanged from the prior
145840
+ // version. Same severity-table omission as Review.
145614
145841
  {
145615
145842
  name: "IncrementalReview",
145616
145843
  description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
145617
145844
  prompt: `### Checklist
145618
145845
 
145619
- 1. Checkout the PR via \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
145846
+ 1. **checkout**: call \`${t2("checkout_pr")}\` \u2014 this returns PR metadata, \`diffPath\` (full diff), and \`incrementalDiffPath\` (changes since last reviewed version, if available). read the diff TOC first and use its line ranges as your coverage checklist.
145847
+
145848
+ 2. **incremental scope**: if \`incrementalDiffPath\` is present, read it to see what changed since the last review. this is a range-diff that isolates the net changes, filtering out base branch noise. if not present, fall back to reviewing the full PR diff and determine what changed since Pullfrog's most recent review.
145849
+
145850
+ 3. **prior feedback**: fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback. you'll need this in step 6 to track which prior comments were addressed.
145851
+
145852
+ 4. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
145853
+
145854
+ if the incremental changes are **genuinely trivial**, skip the fan-out entirely and jump to step 7's non-substantive path (do NOT submit a review).
145855
+
145856
+ "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
145857
+ "Looks trivial but isn't" (do NOT skip \u2014 same anti-patterns as Review mode): 1-line changes to SQL/regex/auth/billing/permissions/signature-verification code; flipping feature-flag defaults or retry/timeout constants; money/tax/HTTP-method/redirect changes; tightening or loosening a comparison operator; mixed diffs with a semantic line buried in formatting.
145858
+ When unsure, treat as non-trivial.
145859
+
145860
+ otherwise pick lenses by where the new commits concentrate risk \u2014 **there's no fixed count**, same calibration as Review mode (1 lens for pure refactor / isolated fix; 2\u20133 for typical features; 4\u20135 for high-stakes subsystem touches; 6+ is a smell). lens framing follows Review mode: themed lenses (correctness & invariants, impact when new commits remove/rename/deprecate things, research-validated assumptions, security, user-journey, operational readiness, integration & cross-cutting, test integrity, performance, holistic) and subsystem lenses (auth, billing, schema migration, etc.) \u2014 for high-stakes domains lead with the subsystem lens rather than the generic themed equivalent.
145620
145861
 
145621
- 2. If \`incrementalDiffPath\` is present, read it to see what changed since the last review. This is a range-diff that isolates the net changes, filtering out base branch noise. If not present, fall back to reviewing the full PR diff.
145862
+ dispatch one \`${REVIEWER_AGENT_NAME}\` subagent per lens \u2014 its baked-in system prompt enforces the non-mutative + non-recursive contract (read-only file/search/web tools and read-only MCP queries; no writes, shell side effects, state-changing MCP calls, or nested subagent dispatch). dispatch them in a **single assistant turn with multiple parallel subagent calls** (serial dispatch collapses the fan-out). if a subagent errors out, times out, or returns nothing usable, retry once with the same lens; if it still fails, proceed with partial coverage and note the missing lens in the review body \u2014 do not skip step 4 entirely on a single subagent failure. each subagent gets:
145863
+ - the diff scope (incremental diff path if available, full diff otherwise). do NOT tell them to skip pre-existing issues \u2014 that suppresses regressions the new commits amplified; the "issues must be NEW" filter lives at aggregation time (step 5), not in the subagent prompt
145864
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145865
+ - **a Task \`description\` set to the lens name** (e.g. \`"security"\`, \`"correctness"\`, \`"billing-subsystem"\`) \u2014 the harness reads this field to label the subagent's log lines so parallel runs can be told apart in CI output. without it, every subagent shows up as \`subagent#N\`.
145866
+ - the read-only contract restated in your dispatch instructions so the rule is present twice (the subagent's system prompt also enforces it). The test: would this call still be a no-op if reverted? If not (PR comments, branch pushes, issue updates, set_output, label changes, dependency installs, etc.), don't make it.
145867
+ - if the lens touches external contracts, instruct the subagent to verify load-bearing claims via web search and quote source URLs. action runs are non-interactive \u2014 there's no human to catch "I'm pretty sure Stripe does X."
145868
+ - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
145622
145869
 
145623
- 3. Fetch previous reviews via \`${t2("list_pull_request_reviews")}\`. For the most recent Pullfrog review, call \`${t2("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback.
145870
+ delegation discipline:
145871
+ - do NOT lens-review the diff yourself in parallel with the subagents
145872
+ - do NOT summarize the changes for them (biases toward validation frame)
145873
+ - do NOT hand them a curated reading list (let them discover scope)
145874
+ - do NOT pre-shape their output with a finding schema
145875
+ - do NOT mention the other lenses (independence is the point)
145624
145876
 
145625
- 4. For each area of the new changes:
145626
- - review the incremental diff while using the full diff for context
145627
- - check whether prior review feedback was addressed by the new commits
145628
- - trace data flow, check boundaries, verify assumptions, consider lifecycle, spot performance issues
145629
- - if the new commits remove, rename, or deprecate anything, run impact analysis with grep across code/tests/docs/comments/configs to find stale references and include those findings in the summary body
145630
- - never repeat prior feedback. only comment on genuinely new issues introduced by the new commits.
145631
- - draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable (2-3 sentences max)
145632
- - for large or cross-cutting PRs, consider delegating read-only subagents for parallel investigation. subagents must ONLY read files, grep, and search \u2014 no MCP tools, no writes, no shell commands, no side effects. collect their findings and use them to draft comments.
145877
+ 5. **aggregate, draft, self-critique**: merge findings; de-dup overlaps; trace each finding yourself. drop praise, style preferences, speculative/unverified claims, findings about pre-existing code unrelated to the new commits, anything not actionable, and anything that re-states prior review feedback (heuristic: if the finding's root cause lives in lines the *new commits* added or modified, it's in scope; otherwise drop). also drop **bloat-shaped findings** \u2014 proposed fixes that would add defensive checks for cases that can't happen, abstractions used once, comments restating obvious code, tests asserting tautologies, or "just-in-case" guards. subagents are fallible and bias toward recommending changes; the bar for an actionable inline comment is sound + correct + elegant. recommending a change that improves only one of the three (or degrades elegance to nominally improve correctness) makes the codebase worse, not better. To compute "lines the new commits added or modified": if \`incrementalDiffPath\` from step 1 is present, use it directly. Otherwise, take the prior Pullfrog review's \`commit_id\` (returned alongside each entry from \`${t2("list_pull_request_reviews")}\` in step 3) and run \`git diff <prior-review-sha>..HEAD\` to isolate the lines added since that review. draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable, 2-3 sentences max.
145633
145878
 
145634
- 5. Self-critique: drop any comments that are praise, style preferences, speculative, about pre-existing code, or not actionable.
145879
+ then check: which prior review comments were addressed by the new commits? track the addressed ones for step 6b.
145635
145880
 
145636
- 6. **Summarize**: build two distinct sections for the review body:
145881
+ 6. **build the review body** \u2014 two distinct sections:
145637
145882
  a. **Reviewed changes**: summarize at the logical-change level, not per-file. each bullet starts with a past-tense verb (e.g. \`- Extracted shared CLI runtime into a single module\`, \`- Renamed package to pullfrog\`). avoid file paths unless they add clarity. if the changes can be described in one sentence, use one sentence \u2014 no bullets needed.
145638
145883
  b. **Prior review feedback** (only if any were addressed): list only the prior review comments that WERE addressed by the new commits (\`- [x] safeParse instead of parse \u2014 addressed\`). omit unaddressed comments. omit this entire section if nothing was addressed. a change can appear in both sections.
145639
145884
  - no headings, no tables, no prose paragraphs in either section \u2014 just bullets
@@ -146411,8 +146656,8 @@ async function startMcpHttpServer(ctx, options) {
146411
146656
 
146412
146657
  // agents/claude.ts
146413
146658
  import { execFileSync as execFileSync3 } from "node:child_process";
146414
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
146415
- import { join as join9 } from "node:path";
146659
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
146660
+ import { join as join10 } from "node:path";
146416
146661
  import { performance as performance6 } from "node:perf_hooks";
146417
146662
 
146418
146663
  // utils/install.ts
@@ -146521,8 +146766,35 @@ function detectProviderError(text) {
146521
146766
 
146522
146767
  // utils/skills.ts
146523
146768
  import { spawnSync as spawnSync5 } from "node:child_process";
146769
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "node:fs";
146524
146770
  import { tmpdir } from "node:os";
146771
+ import { dirname as dirname2, join as join9 } from "node:path";
146772
+ import { fileURLToPath } from "node:url";
146525
146773
  var skillsVersion = getDevDependencyVersion("skills");
146774
+ var BUNDLED_SKILL_NAMES = ["git-archaeology"];
146775
+ function resolveSkillPath(name) {
146776
+ const here = dirname2(fileURLToPath(import.meta.url));
146777
+ const candidates = [
146778
+ join9(here, "..", "skills", name, "SKILL.md"),
146779
+ join9(here, "skills", name, "SKILL.md")
146780
+ ];
146781
+ for (const candidate of candidates) {
146782
+ if (existsSync6(candidate)) return candidate;
146783
+ }
146784
+ throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
146785
+ }
146786
+ var SKILL_TARGET_DIRS = [".opencode/skills", ".claude/skills", ".agents/skills"];
146787
+ function installBundledSkills(params) {
146788
+ for (const name of BUNDLED_SKILL_NAMES) {
146789
+ const content = readFileSync4(resolveSkillPath(name), "utf8");
146790
+ for (const targetDir of SKILL_TARGET_DIRS) {
146791
+ const skillDir = join9(params.home, targetDir, name);
146792
+ mkdirSync3(skillDir, { recursive: true });
146793
+ writeFileSync6(join9(skillDir, "SKILL.md"), content);
146794
+ }
146795
+ }
146796
+ log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146797
+ }
146526
146798
  function addSkill(params) {
146527
146799
  const result = spawnSync5(
146528
146800
  "npx",
@@ -146596,6 +146868,213 @@ var ThinkingTimer = class {
146596
146868
  }
146597
146869
  };
146598
146870
 
146871
+ // agents/postRun.ts
146872
+ var MAX_HOOK_OUTPUT_CHARS = 4096;
146873
+ function truncateHookOutput(raw2) {
146874
+ if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
146875
+ return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
146876
+ ${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
146877
+ }
146878
+ async function executeStopHook(script) {
146879
+ log.info("\xBB executing stop hook...");
146880
+ try {
146881
+ const result = await spawn({
146882
+ cmd: "bash",
146883
+ args: ["-c", script],
146884
+ env: process.env,
146885
+ timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
146886
+ activityTimeout: 0,
146887
+ onStdout: (chunk) => process.stdout.write(chunk),
146888
+ onStderr: (chunk) => process.stderr.write(chunk)
146889
+ });
146890
+ if (result.exitCode === 0) {
146891
+ log.info("\xBB stop hook passed");
146892
+ return null;
146893
+ }
146894
+ const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
146895
+ const output = truncateHookOutput(combined);
146896
+ log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
146897
+ return { exitCode: result.exitCode, output };
146898
+ } catch (err) {
146899
+ const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
146900
+ const msg = err instanceof Error ? err.message : String(err);
146901
+ log.warning(
146902
+ `stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
146903
+ );
146904
+ return null;
146905
+ }
146906
+ }
146907
+ function buildStopHookPrompt(failure) {
146908
+ return [
146909
+ `STOP HOOK FAILED \u2014 the repo-configured stop hook exited with code ${failure.exitCode}. your work is not done until the hook exits cleanly. address the issue below and push any resulting changes to a pull request.`,
146910
+ "",
146911
+ "```",
146912
+ failure.output || "(no output)",
146913
+ "```"
146914
+ ].join("\n");
146915
+ }
146916
+ async function collectPostRunIssues(params) {
146917
+ const issues = {};
146918
+ if (params.stopScript) {
146919
+ const failure = await executeStopHook(params.stopScript);
146920
+ if (failure) issues.stopHook = failure;
146921
+ }
146922
+ const status = getGitStatus();
146923
+ if (status) issues.dirtyTree = status;
146924
+ return issues;
146925
+ }
146926
+ function buildPostRunPrompt(issues) {
146927
+ const parts = [];
146928
+ if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
146929
+ if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
146930
+ return parts.join("\n\n---\n\n");
146931
+ }
146932
+ function buildLearningsReflectionPrompt(agentId) {
146933
+ const t2 = (name) => formatMcpToolRef(agentId, name);
146934
+ return [
146935
+ `REFLECTION \u2014 before you finish, think back over this task: did you discover anything about this repo's setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs?`,
146936
+ "",
146937
+ `if so, call \`${t2("update_learnings")}\` to persist it.`,
146938
+ "",
146939
+ `rules:`,
146940
+ `- only call \`${t2("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
146941
+ `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
146942
+ `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
146943
+ `- if you already called \`${t2("update_learnings")}\` earlier in this run, or nothing new is worth capturing, just reply "done" and stop \u2014 do not edit the repo for this reflection.`
146944
+ ].join("\n");
146945
+ }
146946
+ async function runPostRunRetryLoop(params) {
146947
+ let result = params.initialResult;
146948
+ let aggregatedUsage = params.initialUsage;
146949
+ let finalIssues = {};
146950
+ let gateResumeCount = 0;
146951
+ let pendingReflection = params.reflectionPrompt;
146952
+ while (gateResumeCount < MAX_POST_RUN_RETRIES) {
146953
+ if (!result.success) break;
146954
+ const issues = await collectPostRunIssues({ stopScript: params.stopScript });
146955
+ finalIssues = issues;
146956
+ if (!hasPostRunIssues(issues)) {
146957
+ if (!pendingReflection) break;
146958
+ if (params.canResume && !params.canResume(result)) break;
146959
+ log.info("\xBB post-run reflection: nudging agent to update learnings if relevant");
146960
+ const preReflection = result;
146961
+ const reflectionResult = await params.resume({
146962
+ prompt: pendingReflection,
146963
+ previousResult: result
146964
+ });
146965
+ aggregatedUsage = mergeAgentUsage(aggregatedUsage, reflectionResult.usage);
146966
+ pendingReflection = void 0;
146967
+ if (!reflectionResult.success) {
146968
+ log.warning(
146969
+ `\xBB reflection turn failed (${reflectionResult.error ?? "unknown error"}), preserving prior successful result`
146970
+ );
146971
+ result = preReflection;
146972
+ break;
146973
+ }
146974
+ result = {
146975
+ ...reflectionResult,
146976
+ output: preReflection.output || reflectionResult.output
146977
+ };
146978
+ continue;
146979
+ }
146980
+ if (params.canResume && !params.canResume(result)) {
146981
+ log.info("\xBB post-run retry skipped: cannot resume agent session");
146982
+ break;
146983
+ }
146984
+ log.info(`\xBB post-run retry (attempt ${gateResumeCount + 1}/${MAX_POST_RUN_RETRIES})`);
146985
+ const prompt = buildPostRunPrompt(issues);
146986
+ result = await params.resume({ prompt, previousResult: result });
146987
+ aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
146988
+ gateResumeCount++;
146989
+ }
146990
+ if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
146991
+ finalIssues = await collectPostRunIssues({ stopScript: params.stopScript });
146992
+ }
146993
+ if (result.success && finalIssues.stopHook) {
146994
+ const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
146995
+ return {
146996
+ ...result,
146997
+ success: false,
146998
+ error: `stop hook failed${retryNote} (exit code ${finalIssues.stopHook.exitCode}): ${finalIssues.stopHook.output || "(no output)"}`,
146999
+ usage: aggregatedUsage
147000
+ };
147001
+ }
147002
+ return { ...result, usage: aggregatedUsage };
147003
+ }
147004
+
147005
+ // agents/sessionLabeler.ts
147006
+ var ORCHESTRATOR_LABEL = "orchestrator";
147007
+ var LENS_PROMPT_PATTERN = /^\s*(?:lens|Lens|LENS)\s*[:=]\s*([A-Za-z][\w &/.-]{0,60})/m;
147008
+ function slug(value2) {
147009
+ return value2.trim().toLowerCase().replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
147010
+ }
147011
+ function deriveLabelFromTaskInput(input) {
147012
+ if (typeof input.prompt === "string") {
147013
+ const match3 = input.prompt.match(LENS_PROMPT_PATTERN);
147014
+ if (match3?.[1]) {
147015
+ const slugged = slug(match3[1]);
147016
+ if (slugged) return `lens:${slugged}`;
147017
+ }
147018
+ }
147019
+ if (input.description) {
147020
+ const slugged = slug(input.description);
147021
+ if (slugged) return `lens:${slugged}`;
147022
+ }
147023
+ if (input.subagent_type) {
147024
+ return input.subagent_type;
147025
+ }
147026
+ return "subagent";
147027
+ }
147028
+ var SessionLabeler = class {
147029
+ labels = /* @__PURE__ */ new Map();
147030
+ pendingLabels = [];
147031
+ fallbackCounter = 0;
147032
+ recordTaskDispatch(input) {
147033
+ const label = deriveLabelFromTaskInput(input);
147034
+ this.pendingLabels.push(label);
147035
+ return label;
147036
+ }
147037
+ /**
147038
+ * Return a label for the given sessionID. Binds on first call.
147039
+ * Pass undefined/empty for events that lack a session id — the caller
147040
+ * gets ORCHESTRATOR_LABEL so the line is still attributable.
147041
+ */
147042
+ labelFor(sessionID) {
147043
+ if (!sessionID) return ORCHESTRATOR_LABEL;
147044
+ const existing = this.labels.get(sessionID);
147045
+ if (existing) return existing;
147046
+ let label;
147047
+ if (this.labels.size === 0) {
147048
+ label = ORCHESTRATOR_LABEL;
147049
+ } else if (this.pendingLabels.length > 0) {
147050
+ label = this.pendingLabels.shift();
147051
+ } else {
147052
+ this.fallbackCounter += 1;
147053
+ label = `subagent#${this.fallbackCounter}`;
147054
+ }
147055
+ this.labels.set(sessionID, label);
147056
+ return label;
147057
+ }
147058
+ /** number of distinct sessions seen so far (for diagnostics) */
147059
+ size() {
147060
+ return this.labels.size;
147061
+ }
147062
+ /** all (sessionID, label) pairs, oldest first */
147063
+ entries() {
147064
+ return Array.from(this.labels.entries());
147065
+ }
147066
+ /** how many pending labels are queued waiting to bind to a new session */
147067
+ pendingDispatchCount() {
147068
+ return this.pendingLabels.length;
147069
+ }
147070
+ };
147071
+ function formatWithLabel(label, message) {
147072
+ const MAGENTA2 = "\x1B[35m";
147073
+ const RESET2 = "\x1B[0m";
147074
+ const colored = `${MAGENTA2}[${label}]${RESET2} `;
147075
+ return message.split("\n").map((line) => `${colored}${line}`).join("\n");
147076
+ }
147077
+
146599
147078
  // agents/claude.ts
146600
147079
  async function installClaudeCli() {
146601
147080
  return await installFromNpmTarball({
@@ -146606,10 +147085,10 @@ async function installClaudeCli() {
146606
147085
  });
146607
147086
  }
146608
147087
  function writeMcpConfig(ctx) {
146609
- const configDir = join9(ctx.tmpdir, ".claude");
146610
- mkdirSync3(configDir, { recursive: true });
146611
- const configPath = join9(configDir, "mcp.json");
146612
- writeFileSync6(
147088
+ const configDir = join10(ctx.tmpdir, ".claude");
147089
+ mkdirSync4(configDir, { recursive: true });
147090
+ const configPath = join10(configDir, "mcp.json");
147091
+ writeFileSync7(
146613
147092
  configPath,
146614
147093
  JSON.stringify({
146615
147094
  mcpServers: {
@@ -146619,6 +147098,15 @@ function writeMcpConfig(ctx) {
146619
147098
  );
146620
147099
  return configPath;
146621
147100
  }
147101
+ function buildAgentsJson() {
147102
+ const agents2 = {
147103
+ [REVIEWER_AGENT_NAME]: {
147104
+ description: "Read-only review subagent for self-review and lens-based code review. Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
147105
+ prompt: REVIEWER_SYSTEM_PROMPT
147106
+ }
147107
+ };
147108
+ return JSON.stringify(agents2);
147109
+ }
146622
147110
  function stripProviderPrefix(specifier) {
146623
147111
  const slashIndex = specifier.indexOf("/");
146624
147112
  return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
@@ -146669,6 +147157,13 @@ async function runClaude(params) {
146669
147157
  }
146670
147158
  thinkingTimer.markToolCall();
146671
147159
  log.toolCall({ toolName, input: block.input || {} });
147160
+ if (toolName === "Task" && block.input && typeof block.input === "object") {
147161
+ const taskInput = block.input;
147162
+ const label = deriveLabelFromTaskInput(taskInput);
147163
+ log.info(
147164
+ `\xBB dispatching subagent: ${label}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147165
+ );
147166
+ }
146672
147167
  if (toolName.includes("report_progress") && params.todoTracker) {
146673
147168
  log.debug("\xBB report_progress detected, disabling todo tracking");
146674
147169
  params.todoTracker.cancel();
@@ -146934,9 +147429,9 @@ var claude = agent({
146934
147429
  const model = specifier ? stripProviderPrefix(specifier) : void 0;
146935
147430
  const homeEnv = {
146936
147431
  HOME: ctx.tmpdir,
146937
- XDG_CONFIG_HOME: join9(ctx.tmpdir, ".config")
147432
+ XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
146938
147433
  };
146939
- mkdirSync3(join9(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
147434
+ mkdirSync4(join10(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
146940
147435
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
146941
147436
  addSkill({
146942
147437
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -146944,6 +147439,7 @@ var claude = agent({
146944
147439
  env: homeEnv,
146945
147440
  agent: "claude"
146946
147441
  });
147442
+ installBundledSkills({ home: homeEnv.HOME });
146947
147443
  const mcpConfigPath = writeMcpConfig(ctx);
146948
147444
  const effort = resolveEffort(model);
146949
147445
  installManagedSettings();
@@ -146958,7 +147454,9 @@ var claude = agent({
146958
147454
  "--effort",
146959
147455
  effort,
146960
147456
  "--disallowedTools",
146961
- "Bash,Agent(Bash)"
147457
+ "Bash,Agent(Bash)",
147458
+ "--agents",
147459
+ buildAgentsJson()
146962
147460
  ];
146963
147461
  if (model) {
146964
147462
  baseArgs.push("--model", model);
@@ -146979,37 +147477,32 @@ var claude = agent({
146979
147477
  onActivityTimeout: ctx.onActivityTimeout,
146980
147478
  onToolUse: ctx.onToolUse
146981
147479
  };
146982
- let result = await runClaude({
147480
+ const result = await runClaude({
146983
147481
  ...runParams,
146984
147482
  args: [...baseArgs, "-p", ctx.instructions.full]
146985
147483
  });
146986
- let aggregatedUsage = result.usage;
146987
- for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
146988
- if (!result.success || !result.sessionId) break;
146989
- const status = getGitStatus();
146990
- if (!status) break;
146991
- log.info(`\xBB dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):
146992
- ${status}`);
146993
- result = await runClaude({
146994
- ...runParams,
146995
- args: [
146996
- ...baseArgs,
146997
- "-p",
146998
- buildCommitPrompt("claude", status),
146999
- "--resume",
147000
- result.sessionId
147001
- ]
147002
- });
147003
- aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
147004
- }
147005
- return { ...result, usage: aggregatedUsage };
147484
+ return runPostRunRetryLoop({
147485
+ initialResult: result,
147486
+ initialUsage: result.usage,
147487
+ stopScript: ctx.stopScript,
147488
+ reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147489
+ canResume: (r) => Boolean(r.sessionId),
147490
+ resume: async (c2) => {
147491
+ const sessionId = c2.previousResult.sessionId;
147492
+ if (!sessionId) throw new Error("unreachable: canResume gated on sessionId");
147493
+ return runClaude({
147494
+ ...runParams,
147495
+ args: [...baseArgs, "-p", c2.prompt, "--resume", sessionId]
147496
+ });
147497
+ }
147498
+ });
147006
147499
  }
147007
147500
  });
147008
147501
 
147009
147502
  // agents/opencode.ts
147010
147503
  import { execFileSync as execFileSync4 } from "node:child_process";
147011
- import { mkdirSync as mkdirSync4 } from "node:fs";
147012
- import { join as join10 } from "node:path";
147504
+ import { mkdirSync as mkdirSync5 } from "node:fs";
147505
+ import { join as join11 } from "node:path";
147013
147506
  import { performance as performance7 } from "node:perf_hooks";
147014
147507
  async function installOpencodeCli() {
147015
147508
  return await installFromNpmTarball({
@@ -147031,7 +147524,8 @@ function buildSecurityConfig(ctx, model) {
147031
147524
  },
147032
147525
  mcp: {
147033
147526
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
147034
- }
147527
+ },
147528
+ agent: buildReviewerAgentConfig()
147035
147529
  };
147036
147530
  if (model) {
147037
147531
  config3.model = model;
@@ -147042,6 +147536,15 @@ function buildSecurityConfig(ctx, model) {
147042
147536
  }
147043
147537
  return JSON.stringify(config3);
147044
147538
  }
147539
+ function buildReviewerAgentConfig() {
147540
+ return {
147541
+ [REVIEWER_AGENT_NAME]: {
147542
+ description: "Read-only review subagent for self-review and lens-based code review. Reads only \u2014 no writes, no state-changing shell or MCP calls, no nested subagent dispatch.",
147543
+ mode: "subagent",
147544
+ prompt: REVIEWER_SYSTEM_PROMPT
147545
+ }
147546
+ };
147547
+ }
147045
147548
  function getOpenCodeModels(cliPath) {
147046
147549
  try {
147047
147550
  const output = execFileSync4(cliPath, ["models"], {
@@ -147090,6 +147593,29 @@ async function runOpenCode(params) {
147090
147593
  let currentStepId = null;
147091
147594
  let currentStepType = null;
147092
147595
  let stepHistory = [];
147596
+ const labeler = new SessionLabeler();
147597
+ function eventLabel(event) {
147598
+ const sid = event.sessionID ?? event.session_id;
147599
+ return labeler.labelFor(typeof sid === "string" ? sid : null);
147600
+ }
147601
+ function withLabel(label, message) {
147602
+ return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
147603
+ }
147604
+ const taskDispatchByCallID = /* @__PURE__ */ new Map();
147605
+ const pendingTaskDispatches = [];
147606
+ const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
147607
+ function emitSubagentFinished(dispatch, status, output2, matchKind) {
147608
+ const subagentDuration = performance7.now() - dispatch.startedAt;
147609
+ const outputStr = typeof output2 === "string" ? output2 : "";
147610
+ const outputPreview = outputStr.length > 120 ? `${outputStr.slice(0, 120)}\u2026` : outputStr;
147611
+ const matchSuffix = matchKind === "fifo" ? " [fifo-matched]" : "";
147612
+ log.info(
147613
+ `\xBB subagent finished: ${dispatch.label} (${(subagentDuration / 1e3).toFixed(1)}s, status=${status})${matchSuffix}` + (outputPreview ? ` \u2014 ${outputPreview.replace(/\n/g, " ")}` : "")
147614
+ );
147615
+ taskDispatchByCallID.delete(dispatch.toolUseCallID);
147616
+ const idx = pendingTaskDispatches.indexOf(dispatch);
147617
+ if (idx >= 0) pendingTaskDispatches.splice(idx, 1);
147618
+ }
147093
147619
  function buildUsage() {
147094
147620
  const totalInput = accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
147095
147621
  return totalInput > 0 || accumulatedTokens.output > 0 ? {
@@ -147103,39 +147629,63 @@ async function runOpenCode(params) {
147103
147629
  }
147104
147630
  const handlers2 = {
147105
147631
  init: (event) => {
147632
+ const label = labeler.labelFor(event.session_id ?? null);
147106
147633
  log.debug(
147107
- `\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
147634
+ withLabel(
147635
+ label,
147636
+ `\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
147637
+ )
147108
147638
  );
147109
- log.debug(`\xBB ${params.label} init event (full): ${JSON.stringify(event)}`);
147110
- finalOutput = "";
147111
- accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
147112
- accumulatedCostUsd = 0;
147113
- tokensLogged = false;
147639
+ log.debug(withLabel(label, `\xBB ${params.label} init event (full): ${JSON.stringify(event)}`));
147640
+ if (label === ORCHESTRATOR_LABEL) {
147641
+ finalOutput = "";
147642
+ accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
147643
+ accumulatedCostUsd = 0;
147644
+ tokensLogged = false;
147645
+ } else {
147646
+ log.info(`\xBB ${params.label} subagent init: ${label} (session ${event.session_id || "?"})`);
147647
+ }
147114
147648
  },
147115
147649
  message: (event) => {
147650
+ const label = eventLabel(event);
147116
147651
  if (event.role === "assistant" && event.content?.trim()) {
147117
147652
  const message = event.content.trim();
147118
147653
  if (event.delta) {
147119
147654
  log.debug(
147120
- `\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
147655
+ withLabel(
147656
+ label,
147657
+ `\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
147658
+ )
147121
147659
  );
147122
147660
  } else {
147123
147661
  log.debug(
147124
- `\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
147662
+ withLabel(
147663
+ label,
147664
+ `\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
147665
+ )
147125
147666
  );
147126
- finalOutput = message;
147667
+ if (label === ORCHESTRATOR_LABEL) {
147668
+ finalOutput = message;
147669
+ }
147127
147670
  }
147128
147671
  } else if (event.role === "user") {
147129
147672
  log.debug(
147130
- `\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
147673
+ withLabel(
147674
+ label,
147675
+ `\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
147676
+ )
147131
147677
  );
147132
147678
  }
147133
147679
  },
147134
147680
  text: (event) => {
147135
147681
  if (event.part?.text?.trim()) {
147136
147682
  const message = event.part.text.trim();
147137
- log.box(message, { title: params.label });
147138
- finalOutput = message;
147683
+ const label = eventLabel(event);
147684
+ const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
147685
+ log.box(message, { title: boxTitle });
147686
+ if (label === ORCHESTRATOR_LABEL) {
147687
+ finalOutput = message;
147688
+ }
147139
147689
  }
147140
147690
  },
147141
147691
  step_start: (event) => {
@@ -147171,6 +147721,23 @@ async function runOpenCode(params) {
147171
147721
  );
147172
147722
  return;
147173
147723
  }
147724
+ if (toolName === "task") {
147725
+ const taskInput = event.part?.state?.input ?? {};
147726
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147727
+ const dispatch = {
147728
+ label: dispatchedLabel,
147729
+ startedAt: performance7.now(),
147730
+ toolUseCallID: toolId
147731
+ };
147732
+ taskDispatchByCallID.set(toolId, dispatch);
147733
+ pendingTaskDispatches.push(dispatch);
147734
+ log.info(
147735
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147736
+ );
147737
+ } else {
147738
+ knownNonTaskCallIDs.add(toolId);
147739
+ }
147740
+ const label = eventLabel(event);
147174
147741
  if (stepHistory.length > 0) {
147175
147742
  stepHistory[stepHistory.length - 1].toolCalls.push(toolName);
147176
147743
  }
@@ -147181,9 +147748,11 @@ async function runOpenCode(params) {
147181
147748
  });
147182
147749
  }
147183
147750
  thinkingTimer.markToolCall();
147184
- log.toolCall({ toolName, input: event.part?.state?.input || {} });
147751
+ const inputFormatted = formatJsonValue(event.part?.state?.input || {});
147752
+ const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
147753
+ log.info(withLabel(label, toolCallLine));
147185
147754
  if (event.part?.state?.status === "completed" && event.part.state.output) {
147186
- log.debug(` output: ${event.part.state.output}`);
147755
+ log.debug(withLabel(label, ` output: ${event.part.state.output}`));
147187
147756
  }
147188
147757
  if (toolName.includes("report_progress") && params.todoTracker) {
147189
147758
  log.debug("\xBB report_progress detected, disabling todo tracking");
@@ -147197,7 +147766,20 @@ async function runOpenCode(params) {
147197
147766
  const toolId = event.part?.callID || event.tool_id;
147198
147767
  const status = event.part?.state?.status || event.status || "unknown";
147199
147768
  const output2 = event.part?.state?.output || event.output;
147769
+ const label = eventLabel(event);
147200
147770
  thinkingTimer.markToolResult();
147771
+ if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
147772
+ if (toolId && taskDispatchByCallID.has(toolId)) {
147773
+ const dispatch = taskDispatchByCallID.get(toolId);
147774
+ if (dispatch) emitSubagentFinished(dispatch, status, output2, "exact");
147775
+ } else {
147776
+ const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
147777
+ if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
147778
+ const dispatch = pendingTaskDispatches[0];
147779
+ emitSubagentFinished(dispatch, status, output2, "fifo");
147780
+ }
147781
+ }
147782
+ }
147201
147783
  if (toolId) {
147202
147784
  const toolStartTime = toolCallTimings.get(toolId);
147203
147785
  if (toolStartTime) {
@@ -147205,24 +147787,35 @@ async function runOpenCode(params) {
147205
147787
  toolCallTimings.delete(toolId);
147206
147788
  const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
147207
147789
  log.debug(
147208
- `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
147790
+ withLabel(
147791
+ label,
147792
+ `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
147793
+ )
147209
147794
  );
147210
147795
  if (output2) {
147211
- log.debug(` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`);
147796
+ log.debug(
147797
+ withLabel(
147798
+ label,
147799
+ ` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
147800
+ )
147801
+ );
147212
147802
  }
147213
147803
  if (toolDuration > 5e3) {
147214
147804
  log.info(
147215
- `\xBB tool call took ${(toolDuration / 1e3).toFixed(1)}s - may indicate network latency`
147805
+ withLabel(
147806
+ label,
147807
+ `\xBB tool call took ${(toolDuration / 1e3).toFixed(1)}s - may indicate network latency`
147808
+ )
147216
147809
  );
147217
147810
  }
147218
147811
  }
147219
147812
  }
147220
147813
  if (status === "error") {
147221
147814
  const errorMsg = typeof output2 === "string" ? output2 : JSON.stringify(output2);
147222
- log.info(`\xBB tool call failed: ${errorMsg}`);
147815
+ log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
147223
147816
  } else if (output2) {
147224
147817
  const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
147225
- log.debug(`tool output: ${outputStr}`);
147818
+ log.debug(withLabel(label, `tool output: ${outputStr}`));
147226
147819
  }
147227
147820
  },
147228
147821
  result: async (event) => {
@@ -147319,6 +147912,16 @@ async function runOpenCode(params) {
147319
147912
  } else {
147320
147913
  params.todoTracker?.cancel();
147321
147914
  }
147915
+ if (pendingTaskDispatches.length > 0) {
147916
+ for (const dispatch of [...pendingTaskDispatches]) {
147917
+ const elapsed = performance7.now() - dispatch.startedAt;
147918
+ log.info(
147919
+ `\xBB subagent finished (inferred at run-end): ${dispatch.label} (\u2264${(elapsed / 1e3).toFixed(1)}s) \u2014 no matching tool_result observed; subagent reply likely arrived via assistant message`
147920
+ );
147921
+ }
147922
+ pendingTaskDispatches.length = 0;
147923
+ taskDispatchByCallID.clear();
147924
+ }
147322
147925
  const duration4 = performance7.now() - startTime;
147323
147926
  log.info(
147324
147927
  `\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
@@ -147386,9 +147989,9 @@ var opencode = agent({
147386
147989
  const model = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
147387
147990
  const homeEnv = {
147388
147991
  HOME: ctx.tmpdir,
147389
- XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
147992
+ XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
147390
147993
  };
147391
- mkdirSync4(join10(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
147994
+ mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
147392
147995
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
147393
147996
  addSkill({
147394
147997
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -147396,6 +147999,7 @@ var opencode = agent({
147396
147999
  env: homeEnv,
147397
148000
  agent: "opencode"
147398
148001
  });
148002
+ installBundledSkills({ home: homeEnv.HOME });
147399
148003
  const baseArgs = ["run", "--format", "json", "--print-logs"];
147400
148004
  const permissionOverride = JSON.stringify({
147401
148005
  external_directory: { "*": "deny", "/tmp/*": "allow" }
@@ -147419,24 +148023,20 @@ var opencode = agent({
147419
148023
  onActivityTimeout: ctx.onActivityTimeout,
147420
148024
  onToolUse: ctx.onToolUse
147421
148025
  };
147422
- let result = await runOpenCode({
148026
+ const result = await runOpenCode({
147423
148027
  ...runParams,
147424
148028
  args: [...baseArgs, ctx.instructions.full]
147425
148029
  });
147426
- let aggregatedUsage = result.usage;
147427
- for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
147428
- if (!result.success) break;
147429
- const status = getGitStatus();
147430
- if (!status) break;
147431
- log.info(`\xBB dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):
147432
- ${status}`);
147433
- result = await runOpenCode({
148030
+ return runPostRunRetryLoop({
148031
+ initialResult: result,
148032
+ initialUsage: result.usage,
148033
+ stopScript: ctx.stopScript,
148034
+ reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
148035
+ resume: async (c2) => runOpenCode({
147434
148036
  ...runParams,
147435
- args: [...baseArgs, "--continue", buildCommitPrompt("opencode", status)]
147436
- });
147437
- aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
147438
- }
147439
- return { ...result, usage: aggregatedUsage };
148037
+ args: [...baseArgs, "--continue", c2.prompt]
148038
+ })
148039
+ });
147440
148040
  }
147441
148041
  });
147442
148042
 
@@ -147624,7 +148224,7 @@ async function fetchBodyHtml(ctx) {
147624
148224
  var core2 = __toESM(require_core(), 1);
147625
148225
  import { createSign } from "node:crypto";
147626
148226
  import { rename, writeFile } from "node:fs/promises";
147627
- import { dirname as dirname2, join as join11 } from "node:path";
148227
+ import { dirname as dirname3, join as join12 } from "node:path";
147628
148228
 
147629
148229
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
147630
148230
  var import_light = __toESM(require_light(), 1);
@@ -151456,7 +152056,7 @@ function getGitHubUsageSummary() {
151456
152056
  }
151457
152057
  async function writeGitHubUsageSummaryToFile(path3) {
151458
152058
  const summary2 = getGitHubUsageSummary();
151459
- const tmpPath = join11(dirname2(path3), `.usage-summary-${process.pid}.tmp`);
152059
+ const tmpPath = join12(dirname3(path3), `.usage-summary-${process.pid}.tmp`);
151460
152060
  await writeFile(tmpPath, JSON.stringify(summary2));
151461
152061
  await rename(tmpPath, path3);
151462
152062
  }
@@ -151645,9 +152245,9 @@ ${ctx.error}` : ctx.error;
151645
152245
 
151646
152246
  // utils/gitAuthServer.ts
151647
152247
  import { randomUUID as randomUUID3 } from "node:crypto";
151648
- import { writeFileSync as writeFileSync7 } from "node:fs";
152248
+ import { writeFileSync as writeFileSync8 } from "node:fs";
151649
152249
  import { createServer as createServer2 } from "node:http";
151650
- import { join as join12 } from "node:path";
152250
+ import { join as join13 } from "node:path";
151651
152251
  var CODE_TTL_MS = 5 * 60 * 1e3;
151652
152252
  var TAMPER_WINDOW_MS = 6e4;
151653
152253
  function revokeGitHubToken(token) {
@@ -151719,7 +152319,7 @@ async function startGitAuthServer(tmpdir3) {
151719
152319
  function writeAskpassScript(code) {
151720
152320
  const scriptId = randomUUID3();
151721
152321
  const scriptName = `askpass-${scriptId}.js`;
151722
- const scriptPath = join12(tmpdir3, scriptName);
152322
+ const scriptPath = join13(tmpdir3, scriptName);
151723
152323
  const content = [
151724
152324
  `#!/usr/bin/env node`,
151725
152325
  `var a=process.argv[2]||"";`,
@@ -151734,7 +152334,7 @@ async function startGitAuthServer(tmpdir3) {
151734
152334
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
151735
152335
  `})}).on("error",function(){process.exit(1)})}`
151736
152336
  ].join("\n");
151737
- writeFileSync7(scriptPath, content, { mode: 448 });
152337
+ writeFileSync8(scriptPath, content, { mode: 448 });
151738
152338
  return scriptPath;
151739
152339
  }
151740
152340
  async function close() {
@@ -152304,7 +152904,9 @@ async function handleAgentResult(ctx) {
152304
152904
  output: ctx.result.output
152305
152905
  };
152306
152906
  }
152307
- if (!ctx.toolState.wasUpdated && ctx.toolState.hadProgressComment && !ctx.silent) {
152907
+ const mode = ctx.toolState.selectedMode;
152908
+ const isReviewMode = mode === "Review" || mode === "IncrementalReview";
152909
+ if (!isReviewMode && !ctx.toolState.wasUpdated && ctx.toolState.hadProgressComment && !ctx.silent) {
152308
152910
  const error49 = ctx.result.error || "agent completed without reporting progress";
152309
152911
  try {
152310
152912
  await reportErrorToComment({
@@ -152337,6 +152939,7 @@ var defaultSettings = {
152337
152939
  setupScript: null,
152338
152940
  postCheckoutScript: null,
152339
152941
  prepushScript: null,
152942
+ stopScript: null,
152340
152943
  push: "restricted",
152341
152944
  shell: "restricted",
152342
152945
  prApproveEnabled: false,
@@ -152381,7 +152984,8 @@ async function fetchRunContext(params) {
152381
152984
  modes: data.settings?.modes ?? [],
152382
152985
  setupScript: data.settings?.setupScript ?? null,
152383
152986
  postCheckoutScript: data.settings?.postCheckoutScript ?? null,
152384
- prepushScript: data.settings?.prepushScript ?? null
152987
+ prepushScript: data.settings?.prepushScript ?? null,
152988
+ stopScript: data.settings?.stopScript ?? null
152385
152989
  },
152386
152990
  apiToken: data.apiToken,
152387
152991
  oss: data.oss ?? false,
@@ -152425,9 +153029,9 @@ async function resolveRunContextData(params) {
152425
153029
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
152426
153030
  import { mkdtempSync } from "node:fs";
152427
153031
  import { tmpdir as tmpdir2 } from "node:os";
152428
- import { join as join13 } from "node:path";
153032
+ import { join as join14 } from "node:path";
152429
153033
  function createTempDirectory() {
152430
- const sharedTempDir = mkdtempSync(join13(tmpdir2(), "pullfrog-"));
153034
+ const sharedTempDir = mkdtempSync(join14(tmpdir2(), "pullfrog-"));
152431
153035
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
152432
153036
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
152433
153037
  return sharedTempDir;
@@ -152954,8 +153558,8 @@ ${instructions.user}` : null,
152954
153558
  log.info(instructions.full);
152955
153559
  });
152956
153560
  if (agentId === "opencode") {
152957
- const pluginDir = join14(process.cwd(), ".opencode", "plugin");
152958
- const hasPlugins = existsSync6(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
153561
+ const pluginDir = join15(process.cwd(), ".opencode", "plugin");
153562
+ const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
152959
153563
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
152960
153564
  log.info(
152961
153565
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
@@ -153009,6 +153613,7 @@ ${instructions.user}` : null,
153009
153613
  tmpdir: tmpdir3,
153010
153614
  instructions,
153011
153615
  todoTracker,
153616
+ stopScript: runContext.repoSettings.stopScript,
153012
153617
  onActivityTimeout: onInnerActivityTimeout,
153013
153618
  onToolUse: (event) => {
153014
153619
  const wasTracked = recordDiffReadFromToolUse({
@@ -153063,11 +153668,6 @@ ${instructions.user}` : null,
153063
153668
  log.debug(`post-review cleanup failed: ${error49}`);
153064
153669
  });
153065
153670
  }
153066
- if (toolContext && toolState.review && toolState.progressCommentId) {
153067
- await deleteProgressComment(toolContext).catch((error49) => {
153068
- log.debug(`review progress comment cleanup failed: ${error49}`);
153069
- });
153070
- }
153071
153671
  const trackerWasLastWriter = todoTracker?.hasPublished && !toolState.finalSummaryWritten;
153072
153672
  if (toolContext && toolState.progressCommentId && (!toolState.wasUpdated || trackerWasLastWriter)) {
153073
153673
  await deleteProgressComment(toolContext).catch((error49) => {
@@ -153275,7 +153875,7 @@ async function runPostCleanup() {
153275
153875
  }
153276
153876
 
153277
153877
  // commands/gha.ts
153278
- process.env.PATH = `${dirname3(process.execPath)}:${process.env.PATH}`;
153878
+ process.env.PATH = `${dirname4(process.execPath)}:${process.env.PATH}`;
153279
153879
  var STATE_TOKEN = "token";
153280
153880
  async function runMain() {
153281
153881
  try {
@@ -154346,7 +154946,7 @@ function link(text, url4) {
154346
154946
  }
154347
154947
  function buildProviders() {
154348
154948
  return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
154349
- const aliases = modelAliases.filter((a) => a.provider === key);
154949
+ const aliases = modelAliases.filter((a) => a.provider === key && !a.fallback);
154350
154950
  const recommended = aliases.find((a) => a.preferred);
154351
154951
  const sorted = [...aliases].sort((a, b) => {
154352
154952
  if (a.preferred && !b.preferred) return -1;
@@ -154366,8 +154966,8 @@ function buildProviders() {
154366
154966
  });
154367
154967
  }
154368
154968
  var CLI_PROVIDERS = buildProviders();
154369
- function resolveModelProvider(slug) {
154370
- const providerId = slug.split("/")[0];
154969
+ function resolveModelProvider(slug2) {
154970
+ const providerId = slug2.split("/")[0];
154371
154971
  return CLI_PROVIDERS.find((p2) => p2.id === providerId) ?? null;
154372
154972
  }
154373
154973
  var activeSpin = null;
@@ -154918,8 +155518,10 @@ async function main2() {
154918
155518
  const resolved = resolveModelProvider(secrets.model);
154919
155519
  if (!resolved) bail(`unknown model provider: ${secrets.model}`);
154920
155520
  provider2 = resolved;
155521
+ const displayAlias = resolveDisplayAlias(secrets.model);
155522
+ const label = displayAlias ? displayAlias.displayName : secrets.model;
154921
155523
  spin.start("");
154922
- spin.stop(`using model ${import_picocolors.default.cyan(secrets.model)}`);
155524
+ spin.stop(`using model ${import_picocolors.default.cyan(label)}`);
154923
155525
  } else {
154924
155526
  const providerId = await _t({
154925
155527
  message: "select your preferred model provider",
@@ -155057,7 +155659,7 @@ async function run2() {
155057
155659
  }
155058
155660
 
155059
155661
  // cli.ts
155060
- var VERSION10 = "0.0.202";
155662
+ var VERSION10 = "0.0.203";
155061
155663
  var bin = basename2(process.argv[1] || "");
155062
155664
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
155063
155665
  var rawArgs = process.argv.slice(2);