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/index.js CHANGED
@@ -62663,7 +62663,7 @@ var require_snapshot_recorder = __commonJS({
62663
62663
  "node_modules/.pnpm/undici@7.22.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
62664
62664
  "use strict";
62665
62665
  var { writeFile: writeFile2, readFile, mkdir } = __require("node:fs/promises");
62666
- var { dirname: dirname3, resolve: resolve3 } = __require("node:path");
62666
+ var { dirname: dirname4, resolve: resolve3 } = __require("node:path");
62667
62667
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
62668
62668
  var { InvalidArgumentError, UndiciError } = require_errors4();
62669
62669
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -62894,7 +62894,7 @@ var require_snapshot_recorder = __commonJS({
62894
62894
  throw new InvalidArgumentError("Snapshot path is required");
62895
62895
  }
62896
62896
  const resolvedPath = resolve3(path3);
62897
- await mkdir(dirname3(resolvedPath), { recursive: true });
62897
+ await mkdir(dirname4(resolvedPath), { recursive: true });
62898
62898
  const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
62899
62899
  hash: hash2,
62900
62900
  snapshot: snapshot2
@@ -97475,14 +97475,14 @@ var require_turndown_cjs = __commonJS({
97475
97475
  } else if (node2.nodeType === 1) {
97476
97476
  replacement = replacementForNode.call(self2, node2);
97477
97477
  }
97478
- return join15(output, replacement);
97478
+ return join16(output, replacement);
97479
97479
  }, "");
97480
97480
  }
97481
97481
  function postProcess(output) {
97482
97482
  var self2 = this;
97483
97483
  this.rules.forEach(function(rule) {
97484
97484
  if (typeof rule.append === "function") {
97485
- output = join15(output, rule.append(self2.options));
97485
+ output = join16(output, rule.append(self2.options));
97486
97486
  }
97487
97487
  });
97488
97488
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -97494,7 +97494,7 @@ var require_turndown_cjs = __commonJS({
97494
97494
  if (whitespace.leading || whitespace.trailing) content = content.trim();
97495
97495
  return whitespace.leading + rule.replacement(content, node2, this.options) + whitespace.trailing;
97496
97496
  }
97497
- function join15(output, replacement) {
97497
+ function join16(output, replacement) {
97498
97498
  var s1 = trimTrailingNewlines(output);
97499
97499
  var s2 = trimLeadingNewlines(replacement);
97500
97500
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -98925,8 +98925,8 @@ var require_fast_content_type_parse = __commonJS({
98925
98925
 
98926
98926
  // main.ts
98927
98927
  var core6 = __toESM(require_core(), 1);
98928
- import { existsSync as existsSync6, readdirSync } from "node:fs";
98929
- import { join as join14 } from "node:path";
98928
+ import { existsSync as existsSync7, readdirSync } from "node:fs";
98929
+ import { join as join15 } from "node:path";
98930
98930
 
98931
98931
  // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
98932
98932
  var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -107401,7 +107401,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
107401
107401
  // agents/shared.ts
107402
107402
  import { execFileSync } from "node:child_process";
107403
107403
  var MAX_STDERR_LINES = 20;
107404
- var MAX_COMMIT_RETRIES = 3;
107404
+ var MAX_POST_RUN_RETRIES = 3;
107405
107405
  function getGitStatus() {
107406
107406
  try {
107407
107407
  return execFileSync("git", ["status", "--porcelain"], {
@@ -107412,7 +107412,7 @@ function getGitStatus() {
107412
107412
  return "";
107413
107413
  }
107414
107414
  }
107415
- function buildCommitPrompt(_agentId, status) {
107415
+ function buildCommitPrompt(status) {
107416
107416
  return [
107417
107417
  `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.`,
107418
107418
  "",
@@ -107421,6 +107421,9 @@ function buildCommitPrompt(_agentId, status) {
107421
107421
  "```"
107422
107422
  ].join("\n");
107423
107423
  }
107424
+ function hasPostRunIssues(issues) {
107425
+ return issues.stopHook !== void 0 || issues.dirtyTree !== void 0;
107426
+ }
107424
107427
  var agent = (input) => {
107425
107428
  return {
107426
107429
  ...input,
@@ -107732,7 +107735,7 @@ var providers = {
107732
107735
  "claude-opus": {
107733
107736
  displayName: "Claude Opus",
107734
107737
  resolve: "anthropic/claude-opus-4-7",
107735
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
107738
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
107736
107739
  preferred: true
107737
107740
  },
107738
107741
  "claude-sonnet": {
@@ -107751,16 +107754,38 @@ var providers = {
107751
107754
  displayName: "OpenAI",
107752
107755
  envVars: ["OPENAI_API_KEY"],
107753
107756
  models: {
107757
+ gpt: {
107758
+ displayName: "GPT",
107759
+ resolve: "openai/gpt-5.5",
107760
+ openRouterResolve: "openrouter/openai/gpt-5.5",
107761
+ preferred: true
107762
+ },
107763
+ "gpt-pro": {
107764
+ displayName: "GPT Pro",
107765
+ resolve: "openai/gpt-5.5-pro",
107766
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
107767
+ },
107768
+ "gpt-mini": {
107769
+ displayName: "GPT Mini",
107770
+ resolve: "openai/gpt-5.4-mini",
107771
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
107772
+ },
107773
+ // legacy aliases — openai unified the codex line into the main GPT family
107774
+ // and is shutting down every "-codex" snapshot on 2026-07-23. transparently
107775
+ // upgrade existing users via the fallback chain. UI display sites resolve
107776
+ // to the terminal alias's label (so dropdown trigger + PR footers show
107777
+ // "GPT" / "GPT Mini", not the historical name).
107754
107778
  "gpt-codex": {
107755
107779
  displayName: "GPT Codex",
107756
107780
  resolve: "openai/gpt-5.3-codex",
107757
107781
  openRouterResolve: "openrouter/openai/gpt-5.3-codex",
107758
- preferred: true
107782
+ fallback: "openai/gpt"
107759
107783
  },
107760
107784
  "gpt-codex-mini": {
107761
107785
  displayName: "GPT Codex Mini",
107762
107786
  resolve: "openai/gpt-5.1-codex-mini",
107763
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
107787
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
107788
+ fallback: "openai/gpt-mini"
107764
107789
  },
107765
107790
  o3: {
107766
107791
  displayName: "O3",
@@ -107811,16 +107836,30 @@ var providers = {
107811
107836
  displayName: "DeepSeek",
107812
107837
  envVars: ["DEEPSEEK_API_KEY"],
107813
107838
  models: {
107839
+ "deepseek-pro": {
107840
+ displayName: "DeepSeek Pro",
107841
+ resolve: "deepseek/deepseek-v4-pro",
107842
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro",
107843
+ preferred: true
107844
+ },
107845
+ "deepseek-flash": {
107846
+ displayName: "DeepSeek Flash",
107847
+ resolve: "deepseek/deepseek-v4-flash",
107848
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
107849
+ },
107850
+ // legacy aliases — deepseek retires these on 2026-07-24; transparently
107851
+ // upgrade existing users to the v4 family via the fallback chain.
107814
107852
  "deepseek-reasoner": {
107815
107853
  displayName: "DeepSeek Reasoner",
107816
107854
  resolve: "deepseek/deepseek-reasoner",
107817
107855
  openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
107818
- preferred: true
107856
+ fallback: "deepseek/deepseek-pro"
107819
107857
  },
107820
107858
  "deepseek-chat": {
107821
107859
  displayName: "DeepSeek Chat",
107822
107860
  resolve: "deepseek/deepseek-chat",
107823
- openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
107861
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
107862
+ fallback: "deepseek/deepseek-flash"
107824
107863
  }
107825
107864
  }
107826
107865
  }),
@@ -107830,8 +107869,8 @@ var providers = {
107830
107869
  models: {
107831
107870
  "kimi-k2": {
107832
107871
  displayName: "Kimi K2",
107833
- resolve: "moonshotai/kimi-k2.5",
107834
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5",
107872
+ resolve: "moonshotai/kimi-k2.6",
107873
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6",
107835
107874
  preferred: true
107836
107875
  }
107837
107876
  }
@@ -107850,7 +107889,7 @@ var providers = {
107850
107889
  "claude-opus": {
107851
107890
  displayName: "Claude Opus",
107852
107891
  resolve: "opencode/claude-opus-4-7",
107853
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6"
107892
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7"
107854
107893
  },
107855
107894
  "claude-sonnet": {
107856
107895
  displayName: "Claude Sonnet",
@@ -107862,15 +107901,33 @@ var providers = {
107862
107901
  resolve: "opencode/claude-haiku-4-5",
107863
107902
  openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
107864
107903
  },
107904
+ gpt: {
107905
+ displayName: "GPT",
107906
+ resolve: "opencode/gpt-5.5",
107907
+ openRouterResolve: "openrouter/openai/gpt-5.5"
107908
+ },
107909
+ "gpt-pro": {
107910
+ displayName: "GPT Pro",
107911
+ resolve: "opencode/gpt-5.5-pro",
107912
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
107913
+ },
107914
+ "gpt-mini": {
107915
+ displayName: "GPT Mini",
107916
+ resolve: "opencode/gpt-5.4-mini",
107917
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
107918
+ },
107919
+ // legacy aliases — see openai provider above for context.
107865
107920
  "gpt-codex": {
107866
107921
  displayName: "GPT Codex",
107867
107922
  resolve: "opencode/gpt-5.3-codex",
107868
- openRouterResolve: "openrouter/openai/gpt-5.3-codex"
107923
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
107924
+ fallback: "opencode/gpt"
107869
107925
  },
107870
107926
  "gpt-codex-mini": {
107871
107927
  displayName: "GPT Codex Mini",
107872
107928
  resolve: "opencode/gpt-5.1-codex-mini",
107873
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
107929
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
107930
+ fallback: "opencode/gpt-mini"
107874
107931
  },
107875
107932
  "gemini-pro": {
107876
107933
  displayName: "Gemini Pro",
@@ -107884,8 +107941,8 @@ var providers = {
107884
107941
  },
107885
107942
  "kimi-k2": {
107886
107943
  displayName: "Kimi K2",
107887
- resolve: "opencode/kimi-k2.5",
107888
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
107944
+ resolve: "opencode/kimi-k2.6",
107945
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
107889
107946
  },
107890
107947
  "gpt-5-nano": {
107891
107948
  displayName: "GPT Nano",
@@ -107905,12 +107962,6 @@ var providers = {
107905
107962
  resolve: "opencode/minimax-m2.5-free",
107906
107963
  envVars: [],
107907
107964
  isFree: true
107908
- },
107909
- "nemotron-3-super-free": {
107910
- displayName: "Nemotron 3 Super",
107911
- resolve: "opencode/nemotron-3-super-free",
107912
- envVars: [],
107913
- isFree: true
107914
107965
  }
107915
107966
  }
107916
107967
  }),
@@ -107920,8 +107971,8 @@ var providers = {
107920
107971
  models: {
107921
107972
  "claude-opus": {
107922
107973
  displayName: "Claude Opus",
107923
- resolve: "openrouter/anthropic/claude-opus-4.6",
107924
- openRouterResolve: "openrouter/anthropic/claude-opus-4.6",
107974
+ resolve: "openrouter/anthropic/claude-opus-4.7",
107975
+ openRouterResolve: "openrouter/anthropic/claude-opus-4.7",
107925
107976
  preferred: true
107926
107977
  },
107927
107978
  "claude-sonnet": {
@@ -107934,15 +107985,33 @@ var providers = {
107934
107985
  resolve: "openrouter/anthropic/claude-haiku-4.5",
107935
107986
  openRouterResolve: "openrouter/anthropic/claude-haiku-4.5"
107936
107987
  },
107988
+ gpt: {
107989
+ displayName: "GPT",
107990
+ resolve: "openrouter/openai/gpt-5.5",
107991
+ openRouterResolve: "openrouter/openai/gpt-5.5"
107992
+ },
107993
+ "gpt-pro": {
107994
+ displayName: "GPT Pro",
107995
+ resolve: "openrouter/openai/gpt-5.5-pro",
107996
+ openRouterResolve: "openrouter/openai/gpt-5.5-pro"
107997
+ },
107998
+ "gpt-mini": {
107999
+ displayName: "GPT Mini",
108000
+ resolve: "openrouter/openai/gpt-5.4-mini",
108001
+ openRouterResolve: "openrouter/openai/gpt-5.4-mini"
108002
+ },
108003
+ // legacy aliases — see openai provider for context.
107937
108004
  "gpt-codex": {
107938
108005
  displayName: "GPT Codex",
107939
108006
  resolve: "openrouter/openai/gpt-5.3-codex",
107940
- openRouterResolve: "openrouter/openai/gpt-5.3-codex"
108007
+ openRouterResolve: "openrouter/openai/gpt-5.3-codex",
108008
+ fallback: "openrouter/gpt"
107941
108009
  },
107942
108010
  "gpt-codex-mini": {
107943
108011
  displayName: "GPT Codex Mini",
107944
108012
  resolve: "openrouter/openai/gpt-5.1-codex-mini",
107945
- openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini"
108013
+ openRouterResolve: "openrouter/openai/gpt-5.1-codex-mini",
108014
+ fallback: "openrouter/gpt-mini"
107946
108015
  },
107947
108016
  "o4-mini": {
107948
108017
  displayName: "O4 Mini",
@@ -107964,31 +108033,44 @@ var providers = {
107964
108033
  resolve: "openrouter/x-ai/grok-4",
107965
108034
  openRouterResolve: "openrouter/x-ai/grok-4"
107966
108035
  },
108036
+ "deepseek-pro": {
108037
+ displayName: "DeepSeek Pro",
108038
+ resolve: "openrouter/deepseek/deepseek-v4-pro",
108039
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-pro"
108040
+ },
108041
+ "deepseek-flash": {
108042
+ displayName: "DeepSeek Flash",
108043
+ resolve: "openrouter/deepseek/deepseek-v4-flash",
108044
+ openRouterResolve: "openrouter/deepseek/deepseek-v4-flash"
108045
+ },
108046
+ // legacy alias — deepseek retires this on 2026-07-24; transparently
108047
+ // upgrade existing users to the v4 family via the fallback chain.
107967
108048
  "deepseek-chat": {
107968
108049
  displayName: "DeepSeek Chat",
107969
108050
  resolve: "openrouter/deepseek/deepseek-v3.2",
107970
- openRouterResolve: "openrouter/deepseek/deepseek-v3.2"
108051
+ openRouterResolve: "openrouter/deepseek/deepseek-v3.2",
108052
+ fallback: "openrouter/deepseek-flash"
107971
108053
  },
107972
108054
  "kimi-k2": {
107973
108055
  displayName: "Kimi K2",
107974
- resolve: "openrouter/moonshotai/kimi-k2.5",
107975
- openRouterResolve: "openrouter/moonshotai/kimi-k2.5"
108056
+ resolve: "openrouter/moonshotai/kimi-k2.6",
108057
+ openRouterResolve: "openrouter/moonshotai/kimi-k2.6"
107976
108058
  }
107977
108059
  }
107978
108060
  })
107979
108061
  };
107980
- function parseModel(slug) {
107981
- const slashIdx = slug.indexOf("/");
108062
+ function parseModel(slug2) {
108063
+ const slashIdx = slug2.indexOf("/");
107982
108064
  if (slashIdx === -1) {
107983
- throw new Error(`invalid model slug "${slug}" \u2014 expected "provider/model"`);
108065
+ throw new Error(`invalid model slug "${slug2}" \u2014 expected "provider/model"`);
107984
108066
  }
107985
- return { provider: slug.slice(0, slashIdx), model: slug.slice(slashIdx + 1) };
108067
+ return { provider: slug2.slice(0, slashIdx), model: slug2.slice(slashIdx + 1) };
107986
108068
  }
107987
- function getModelProvider(slug) {
107988
- return parseModel(slug).provider;
108069
+ function getModelProvider(slug2) {
108070
+ return parseModel(slug2).provider;
107989
108071
  }
107990
- function getModelEnvVars(slug) {
107991
- const parsed2 = parseModel(slug);
108072
+ function getModelEnvVars(slug2) {
108073
+ const parsed2 = parseModel(slug2);
107992
108074
  const providerConfig = providers[parsed2.provider];
107993
108075
  if (!providerConfig) {
107994
108076
  return [];
@@ -108012,26 +108094,29 @@ var modelAliases = Object.entries(providers).flatMap(
108012
108094
  }))
108013
108095
  );
108014
108096
  var MAX_FALLBACK_DEPTH = 10;
108015
- function resolveCliModel(slug) {
108016
- let current = slug;
108097
+ function resolveDisplayAlias(slug2) {
108098
+ let current = slug2;
108017
108099
  const visited = /* @__PURE__ */ new Set();
108018
108100
  for (let i = 0; i < MAX_FALLBACK_DEPTH; i++) {
108019
108101
  if (visited.has(current)) return void 0;
108020
108102
  visited.add(current);
108021
108103
  const alias = modelAliases.find((a) => a.slug === current);
108022
108104
  if (!alias) return void 0;
108023
- if (!alias.fallback) return alias.resolve;
108105
+ if (!alias.fallback) return alias;
108024
108106
  current = alias.fallback;
108025
108107
  }
108026
108108
  return void 0;
108027
108109
  }
108110
+ function resolveCliModel(slug2) {
108111
+ return resolveDisplayAlias(slug2)?.resolve;
108112
+ }
108028
108113
 
108029
108114
  // utils/buildPullfrogFooter.ts
108030
108115
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
108031
108116
  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>`;
108032
- function formatModelLabel(slug) {
108033
- const alias = modelAliases.find((a) => a.slug === slug);
108034
- if (!alias) return `\`${slug}\``;
108117
+ function formatModelLabel(slug2) {
108118
+ const alias = resolveDisplayAlias(slug2);
108119
+ if (!alias) return `\`${slug2}\``;
108035
108120
  return alias.isFree ? `\`${alias.displayName}\` (free)` : `\`${alias.displayName}\``;
108036
108121
  }
108037
108122
  function buildPullfrogFooter(params) {
@@ -142105,7 +142190,7 @@ var import_semver = __toESM(require_semver2(), 1);
142105
142190
  // package.json
142106
142191
  var package_default = {
142107
142192
  name: "pullfrog",
142108
- version: "0.0.202",
142193
+ version: "0.0.203",
142109
142194
  type: "module",
142110
142195
  bin: {
142111
142196
  pullfrog: "dist/cli.mjs",
@@ -143246,6 +143331,18 @@ function validateInlineComments(comments, map2) {
143246
143331
  return { valid, dropped };
143247
143332
  }
143248
143333
  var MAX_DROPPED_COMMENT_LINES = 50;
143334
+ function duplicateReviewDecision(params) {
143335
+ const existing = params.existing;
143336
+ if (!existing) return null;
143337
+ if (params.currentCheckoutSha && existing.reviewedSha && params.currentCheckoutSha !== existing.reviewedSha) {
143338
+ return null;
143339
+ }
143340
+ return {
143341
+ kind: "already-submitted",
143342
+ reviewId: existing.id,
143343
+ reason: `review ${existing.id} was already submitted in this session; ignoring duplicate call (call \`checkout_pr\` again first if new commits were pushed)`
143344
+ };
143345
+ }
143249
143346
  function reviewSkipDecision(params) {
143250
143347
  if (params.body || params.hasComments) return null;
143251
143348
  if (!params.approved) {
@@ -143315,6 +143412,19 @@ function CreatePullRequestReviewTool(ctx) {
143315
143412
  execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
143316
143413
  if (body) body = fixDoubleEscapedString(body);
143317
143414
  ctx.toolState.issueNumber = pull_number;
143415
+ const dup = duplicateReviewDecision({
143416
+ existing: ctx.toolState.review,
143417
+ currentCheckoutSha: ctx.toolState.checkoutSha
143418
+ });
143419
+ if (dup) {
143420
+ log.info(`skipping duplicate review submission: ${dup.reason}`);
143421
+ return {
143422
+ success: true,
143423
+ skipped: true,
143424
+ reason: dup.reason,
143425
+ reviewId: dup.reviewId
143426
+ };
143427
+ }
143318
143428
  const skip = reviewSkipDecision({
143319
143429
  approved: approved ?? false,
143320
143430
  body,
@@ -143432,6 +143542,9 @@ function CreatePullRequestReviewTool(ctx) {
143432
143542
  nodeId: reviewNodeId,
143433
143543
  reviewedSha: actuallyReviewedSha
143434
143544
  };
143545
+ await deleteProgressComment(ctx).catch((err) => {
143546
+ log.debug(`progress comment cleanup after review failed: ${err}`);
143547
+ });
143435
143548
  if (ctx.toolState.checkoutSha && latestHeadSha && latestHeadSha !== ctx.toolState.checkoutSha) {
143436
143549
  const fromSha = ctx.toolState.checkoutSha;
143437
143550
  const toSha = latestHeadSha;
@@ -145175,6 +145288,18 @@ function ResolveReviewThreadTool(ctx) {
145175
145288
  });
145176
145289
  }
145177
145290
 
145291
+ // agents/reviewer.ts
145292
+ var REVIEWER_AGENT_NAME = "reviewfrog";
145293
+ 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.
145294
+
145295
+ HARD CONSTRAINTS (non-negotiable, regardless of orchestrator instructions):
145296
+ - 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).
145297
+ - 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.
145298
+ - Do NOT spawn further subagents. You are a leaf reviewer; recursive dispatch pre-aggregates findings through an intermediate model and defeats the design.
145299
+ - 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.
145300
+
145301
+ 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.`;
145302
+
145178
145303
  // modes.ts
145179
145304
  var PR_SUMMARY_FORMAT = `### Default format
145180
145305
 
@@ -145246,9 +145371,36 @@ function computeModes(agentId) {
145246
145371
  - 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.
145247
145372
  - run relevant tests/lints before committing
145248
145373
 
145249
- 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:
145250
- - verify only intended changes are present, no debug artifacts or commented-out code remain, and no unrelated files were modified
145251
- - commit locally via shell (\`git add . && git commit -m "..."\`)
145374
+ 4. **self-review**: judgment call \u2014 does YOUR diff warrant a fresh-eyes pass?
145375
+
145376
+ Skip self-review (commit directly) when the diff is **genuinely trivial**:
145377
+ - doc typos, comment-only edits, whitespace/format-only, import reordering
145378
+ - 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)
145379
+ - low-risk dep patch bump from a trusted source
145380
+
145381
+ Run self-review when the diff has **any behavioral surface, however small**:
145382
+ - 1-line changes to SQL operators / comparison logic / regexes / redirects / HTTP methods / response codes
145383
+ - any change to money / tax / currency / billing / fee / refund / payout calculations or constants
145384
+ - any change to auth / permissions / roles / sessions / tokens / signature verification
145385
+ - any change to feature-flag defaults, retry counts, timeouts, rate limits, batch sizes
145386
+ - new endpoints, new code paths, new error branches \u2014 even small ones
145387
+ - mixed diffs (whitespace + a single semantic line) \u2014 the semantic line still triggers self-review
145388
+ - anything you're uncertain about
145389
+
145390
+ 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.
145391
+
145392
+ 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.
145393
+
145394
+ 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.
145395
+
145396
+ Delegation + research discipline (distilled from \`/anneal\` canonical \u2014 these are codified learnings from many review rounds, not theoretical best practices):
145397
+ - Do NOT summarize what you implemented \u2014 that biases the subagent toward validating the shape of your solution rather than questioning it.
145398
+ - Do NOT curate a reading list of files. Let the subagent discover scope from the diff and codebase.
145399
+ - Do NOT pre-shape output with a severity / category schema. That leaks your hypotheses; severity is your call during evaluation.
145400
+ - 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.
145401
+ - 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.
145402
+
145403
+ 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 "..."\`).
145252
145404
 
145253
145405
  5. **finalize**:
145254
145406
  - confirm a clean working tree, then push via \`${t("push_branch")}\` (see *SYSTEM* Git rules if this fails \u2014 prepush errors are usually the repo's tests/lint, not infra timeouts)
@@ -145272,11 +145424,12 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
145272
145424
 
145273
145425
  3. For each comment:
145274
145426
  - understand the feedback
145275
- - make the code change using your native tools
145276
- - record what was done
145427
+ - 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.
145428
+ - if the request stands, make the code change using your native tools; otherwise reply explaining why
145429
+ - record what was done (or why nothing was done)
145277
145430
 
145278
145431
  4. Quality check:
145279
- - 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
145432
+ - 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
145280
145433
  - commit locally via shell (\`git add . && git commit -m "..."\`)
145281
145434
 
145282
145435
  5. Finalize:
@@ -145287,28 +145440,93 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
145287
145440
 
145288
145441
  ${learningsStep(t, 6)}`
145289
145442
  },
145443
+ // Review and IncrementalReview use the multi-lens orchestrator pattern
145444
+ // (canonical source: .claude/commands/anneal.md). The orchestrator does
145445
+ // triage → parallel read-only subagent fan-out → aggregate → draft comments
145446
+ // → submit. For someone else's PR, parallel lenses (correctness, security,
145447
+ // research-validated claims, user-journey, etc.) provide breadth across
145448
+ // angles that a single subagent can't carry coherently. Build mode keeps
145449
+ // a single fresh-eyes subagent (different problem shape — orchestrator
145450
+ // wrote the code and bias-mitigation comes from delegating to one
145451
+ // subagent that doesn't share the implementation context).
145452
+ // Deliberate omission vs canonical /anneal: severity categorization in the
145453
+ // final message (the review body has its own CAUTION/IMPORTANT framing
145454
+ // instead of a severity table).
145290
145455
  {
145291
145456
  name: "Review",
145292
145457
  description: "Review code, PRs, or implementations; provide feedback or suggestions; identify issues; or check code quality, style, and correctness",
145293
145458
  prompt: `### Checklist
145294
145459
 
145295
- 1. Checkout the PR via \`${t("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.
145296
-
145297
- 2. For each area of change:
145298
- - read the diff and trace data flow, check boundaries, and verify assumptions
145299
- - plan your investigation: identify the highest-risk areas (tricky state transitions, boundary crossings, assumption chains) and prioritize depth over breadth
145300
- - use \`${t("get_pull_request")}\` and other read-only GitHub tools for additional context
145301
- - 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
145302
- - report impact-analysis findings in the summary body, ordered by severity (runtime breakage > incorrect docs > stale comments)
145303
- - draft inline comments with NEW line numbers from the diff \u2014 every comment must be actionable (2-3 sentences max)
145304
- - use GitHub permalink format for code references
145305
- - 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.
145460
+ 1. **checkout**: call \`${t("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.
145461
+
145462
+ 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 \`${t("get_pull_request")}\` and other read-only GitHub tools for additional context if needed.
145463
+
145464
+ 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.
145465
+
145466
+ "Genuinely trivial" (skip):
145467
+ - single-word doc typo, whitespace/format-only, comment-only across any number of files
145468
+ - lockfile or generated-code regeneration (size of diff is irrelevant \u2014 read the *shape*)
145469
+ - mechanical rename whose only effect is import-path updates
145470
+ - low-risk dep patch bump
145471
+
145472
+ "Looks trivial but isn't" (do **NOT** skip \u2014 small diff, big blast radius):
145473
+ - any 1-line change to SQL / regex / auth / billing / permission / signature-verification code
145474
+ - flipping a feature-flag default, default config value, or retry/timeout constant
145475
+ - changing a money/tax/currency/fee constant by any amount
145476
+ - changing an HTTP method, redirect URL, response code, or status enum
145477
+ - tightening or loosening a comparison operator (\`<\` \u2194 \`<=\`, \`==\` \u2194 \`!=\`)
145478
+ - renaming a public API surface (still trivial in shape, but needs an impact lens)
145479
+ - adding a new direct dependency (supply-chain surface)
145480
+ - any "typo fix" in user-facing copy that changes meaning ("approved" \u2192 "denied")
145481
+ - mixed diffs where a semantic 1-liner is buried in whitespace/formatting changes
145482
+
145483
+ 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.
145484
+
145485
+ 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:
145486
+
145487
+ - **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)
145488
+ - **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)
145489
+ - **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)
145490
+ - **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.
145491
+
145492
+ lenses come in two flavors, and you can mix them:
145493
+ - **themed lenses** \u2014 a perspective applied across the whole diff (correctness, security, user-journey, performance, etc.).
145494
+ - **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.
145495
+
145496
+ starter menu (combine, omit, or invent your own):
145497
+ - **correctness & invariants** \u2014 bugs, races, error handling, edge cases, state-machine boundaries
145498
+ - **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
145499
+ - **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.
145500
+ - **security** \u2014 new endpoints, authZ, input validation, secrets handling, replay/CSRF/injection, cross-tenant isolation
145501
+ - **user-journey** \u2014 UX-touching flows: walk through happy path and failure modes as a user
145502
+ - **operational readiness** \u2014 observability, alerting, migrations (forward + rollback), feature flags, on-call burden
145503
+ - **integration & cross-cutting** \u2014 API contracts between modules, backward-compat of public surfaces, multi-service ordering
145504
+ - **test integrity** \u2014 meaningful coverage for the changed behavior; deterministic; no shared-state pollution
145505
+ - **performance** \u2014 N+1 queries, hot-path allocation, latency budgets, index coverage
145506
+ - **holistic** \u2014 does the PR make sense as a whole? symmetric flows (delete for every create, rollback for every migration)?
145507
+ - **subsystem lenses** (invent as the PR demands) \u2014 auth, billing, payments, schema migration, webhooks, secrets, RBAC, multi-tenant isolation, cron/scheduling, etc.
145508
+
145509
+ 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:
145510
+ - the diff path / target \u2014 reading the diff and the codebase is its job
145511
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145512
+ - **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\`.
145513
+ - 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.
145514
+ - 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."
145515
+ - 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.
145516
+
145517
+ delegation discipline:
145518
+ - 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)
145519
+ - do NOT summarize the PR for them (biases toward a validation frame)
145520
+ - do NOT hand them a curated reading list (let them discover scope)
145521
+ - do NOT pre-shape their output with a finding schema
145522
+ - do NOT mention the other lenses (independence is the point \u2014 overlapping findings are a strong signal)
145523
+
145524
+ 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.
145525
+
145526
+ 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.
145527
+
145528
+ 5. **submit**: ALWAYS submit exactly one review via \`${t("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.
145306
145529
 
145307
- 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.
145308
-
145309
- 4. Submit \u2014 ALWAYS submit exactly one review via \`${t("create_pull_request_review")}\`.
145310
- Do NOT call \`report_progress\` \u2014 the review is the final record and the progress
145311
- comment will be cleaned up automatically.
145312
145530
  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.
145313
145531
 
145314
145532
  - **critical issues** (blocks merge \u2014 bugs, security, data loss):
@@ -145322,29 +145540,56 @@ ${learningsStep(t, 6)}`
145322
145540
  - **no actionable issues**:
145323
145541
  \`approved: true\`, body: "Reviewed \u2014 no issues found."`
145324
145542
  },
145543
+ // IncrementalReview shares Review's multi-lens orchestrator pattern but
145544
+ // scopes the target to the incremental diff and adds prior-review-feedback
145545
+ // tracking. The "issues must be NEW since the last Pullfrog review" filter
145546
+ // lives at aggregation time (step 5), NOT in the subagent prompt — pushing
145547
+ // the filter into subagents matches the canonical anneal anti-pattern of
145548
+ // "list known pre-existing failures — don't flag these" and suppresses
145549
+ // signal on regressions the new commits amplified. The body-format rules
145550
+ // (Reviewed changes / Prior review feedback) are unchanged from the prior
145551
+ // version. Same severity-table omission as Review.
145325
145552
  {
145326
145553
  name: "IncrementalReview",
145327
145554
  description: "Re-review a PR after new commits are pushed; focus on new changes since the last review",
145328
145555
  prompt: `### Checklist
145329
145556
 
145330
- 1. Checkout the PR via \`${t("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.
145557
+ 1. **checkout**: call \`${t("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.
145558
+
145559
+ 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.
145560
+
145561
+ 3. **prior feedback**: fetch previous reviews via \`${t("list_pull_request_reviews")}\`. for the most recent Pullfrog review, call \`${t("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.
145562
+
145563
+ 4. **triage & fan out**: orient on the *incremental* changes \u2014 domain, seams, external contracts, user-facing surfaces.
145564
+
145565
+ 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).
145566
+
145567
+ "Genuinely trivial" (skip): formatting/comment tweaks, import reordering, lockfile regen, mechanical rename of import paths, whitespace-only.
145568
+ "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.
145569
+ When unsure, treat as non-trivial.
145570
+
145571
+ 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.
145331
145572
 
145332
- 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.
145573
+ 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:
145574
+ - 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
145575
+ - **only one lens** \u2014 never a multi-section "review for X, Y, and Z" prompt
145576
+ - **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\`.
145577
+ - 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.
145578
+ - 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."
145579
+ - ask the subagent to report findings with file paths and NEW line numbers from the full PR diff so you can anchor inline comments.
145333
145580
 
145334
- 3. Fetch previous reviews via \`${t("list_pull_request_reviews")}\`. For the most recent Pullfrog review, call \`${t("get_review_comments")}\` with the review ID to retrieve specific prior line-level feedback.
145581
+ delegation discipline:
145582
+ - do NOT lens-review the diff yourself in parallel with the subagents
145583
+ - do NOT summarize the changes for them (biases toward validation frame)
145584
+ - do NOT hand them a curated reading list (let them discover scope)
145585
+ - do NOT pre-shape their output with a finding schema
145586
+ - do NOT mention the other lenses (independence is the point)
145335
145587
 
145336
- 4. For each area of the new changes:
145337
- - review the incremental diff while using the full diff for context
145338
- - check whether prior review feedback was addressed by the new commits
145339
- - trace data flow, check boundaries, verify assumptions, consider lifecycle, spot performance issues
145340
- - 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
145341
- - never repeat prior feedback. only comment on genuinely new issues introduced by the new commits.
145342
- - draft inline comments with NEW line numbers from the full PR diff \u2014 every comment must be actionable (2-3 sentences max)
145343
- - 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.
145588
+ 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 \`${t("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.
145344
145589
 
145345
- 5. Self-critique: drop any comments that are praise, style preferences, speculative, about pre-existing code, or not actionable.
145590
+ then check: which prior review comments were addressed by the new commits? track the addressed ones for step 6b.
145346
145591
 
145347
- 6. **Summarize**: build two distinct sections for the review body:
145592
+ 6. **build the review body** \u2014 two distinct sections:
145348
145593
  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.
145349
145594
  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.
145350
145595
  - no headings, no tables, no prose paragraphs in either section \u2014 just bullets
@@ -146122,8 +146367,8 @@ async function startMcpHttpServer(ctx, options) {
146122
146367
 
146123
146368
  // agents/claude.ts
146124
146369
  import { execFileSync as execFileSync3 } from "node:child_process";
146125
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "node:fs";
146126
- import { join as join9 } from "node:path";
146370
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
146371
+ import { join as join10 } from "node:path";
146127
146372
  import { performance as performance6 } from "node:perf_hooks";
146128
146373
 
146129
146374
  // utils/install.ts
@@ -146232,8 +146477,35 @@ function detectProviderError(text) {
146232
146477
 
146233
146478
  // utils/skills.ts
146234
146479
  import { spawnSync as spawnSync5 } from "node:child_process";
146480
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync6 } from "node:fs";
146235
146481
  import { tmpdir } from "node:os";
146482
+ import { dirname as dirname2, join as join9 } from "node:path";
146483
+ import { fileURLToPath } from "node:url";
146236
146484
  var skillsVersion = getDevDependencyVersion("skills");
146485
+ var BUNDLED_SKILL_NAMES = ["git-archaeology"];
146486
+ function resolveSkillPath(name) {
146487
+ const here = dirname2(fileURLToPath(import.meta.url));
146488
+ const candidates = [
146489
+ join9(here, "..", "skills", name, "SKILL.md"),
146490
+ join9(here, "skills", name, "SKILL.md")
146491
+ ];
146492
+ for (const candidate of candidates) {
146493
+ if (existsSync6(candidate)) return candidate;
146494
+ }
146495
+ throw new Error(`bundled skill not found: ${name} (looked in ${candidates.join(", ")})`);
146496
+ }
146497
+ var SKILL_TARGET_DIRS = [".opencode/skills", ".claude/skills", ".agents/skills"];
146498
+ function installBundledSkills(params) {
146499
+ for (const name of BUNDLED_SKILL_NAMES) {
146500
+ const content = readFileSync4(resolveSkillPath(name), "utf8");
146501
+ for (const targetDir of SKILL_TARGET_DIRS) {
146502
+ const skillDir = join9(params.home, targetDir, name);
146503
+ mkdirSync3(skillDir, { recursive: true });
146504
+ writeFileSync6(join9(skillDir, "SKILL.md"), content);
146505
+ }
146506
+ }
146507
+ log.info(`installed bundled skills: ${BUNDLED_SKILL_NAMES.join(", ")}`);
146508
+ }
146237
146509
  function addSkill(params) {
146238
146510
  const result = spawnSync5(
146239
146511
  "npx",
@@ -146307,6 +146579,213 @@ var ThinkingTimer = class {
146307
146579
  }
146308
146580
  };
146309
146581
 
146582
+ // agents/postRun.ts
146583
+ var MAX_HOOK_OUTPUT_CHARS = 4096;
146584
+ function truncateHookOutput(raw2) {
146585
+ if (raw2.length <= MAX_HOOK_OUTPUT_CHARS) return raw2;
146586
+ return `...(truncated, showing last ${MAX_HOOK_OUTPUT_CHARS} chars)
146587
+ ${raw2.slice(-MAX_HOOK_OUTPUT_CHARS)}`;
146588
+ }
146589
+ async function executeStopHook(script) {
146590
+ log.info("\xBB executing stop hook...");
146591
+ try {
146592
+ const result = await spawn({
146593
+ cmd: "bash",
146594
+ args: ["-c", script],
146595
+ env: process.env,
146596
+ timeout: LIFECYCLE_HOOK_TIMEOUT_MS,
146597
+ activityTimeout: 0,
146598
+ onStdout: (chunk) => process.stdout.write(chunk),
146599
+ onStderr: (chunk) => process.stderr.write(chunk)
146600
+ });
146601
+ if (result.exitCode === 0) {
146602
+ log.info("\xBB stop hook passed");
146603
+ return null;
146604
+ }
146605
+ const combined = [result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n");
146606
+ const output = truncateHookOutput(combined);
146607
+ log.info(`\xBB stop hook failed with exit code ${result.exitCode}`);
146608
+ return { exitCode: result.exitCode, output };
146609
+ } catch (err) {
146610
+ const isTimeout = err instanceof SpawnTimeoutError && (err.code === SPAWN_TIMEOUT_CODE || err.code === SPAWN_ACTIVITY_TIMEOUT_CODE);
146611
+ const msg = err instanceof Error ? err.message : String(err);
146612
+ log.warning(
146613
+ `stop hook ${isTimeout ? "timed out" : "failed to spawn"}: ${msg} \u2014 skipping retry`
146614
+ );
146615
+ return null;
146616
+ }
146617
+ }
146618
+ function buildStopHookPrompt(failure) {
146619
+ return [
146620
+ `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.`,
146621
+ "",
146622
+ "```",
146623
+ failure.output || "(no output)",
146624
+ "```"
146625
+ ].join("\n");
146626
+ }
146627
+ async function collectPostRunIssues(params) {
146628
+ const issues = {};
146629
+ if (params.stopScript) {
146630
+ const failure = await executeStopHook(params.stopScript);
146631
+ if (failure) issues.stopHook = failure;
146632
+ }
146633
+ const status = getGitStatus();
146634
+ if (status) issues.dirtyTree = status;
146635
+ return issues;
146636
+ }
146637
+ function buildPostRunPrompt(issues) {
146638
+ const parts = [];
146639
+ if (issues.stopHook) parts.push(buildStopHookPrompt(issues.stopHook));
146640
+ if (issues.dirtyTree) parts.push(buildCommitPrompt(issues.dirtyTree));
146641
+ return parts.join("\n\n---\n\n");
146642
+ }
146643
+ function buildLearningsReflectionPrompt(agentId) {
146644
+ const t = (name) => formatMcpToolRef(agentId, name);
146645
+ return [
146646
+ `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?`,
146647
+ "",
146648
+ `if so, call \`${t("update_learnings")}\` to persist it.`,
146649
+ "",
146650
+ `rules:`,
146651
+ `- only call \`${t("update_learnings")}\` when the finding is high-confidence and broadly useful. skip if unsure, speculative, or one-off.`,
146652
+ `- pass the FULL merged list: existing learnings from the original prompt + your new discoveries. one fact per bullet, lines starting with \`- \`.`,
146653
+ `- deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`,
146654
+ `- if you already called \`${t("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.`
146655
+ ].join("\n");
146656
+ }
146657
+ async function runPostRunRetryLoop(params) {
146658
+ let result = params.initialResult;
146659
+ let aggregatedUsage = params.initialUsage;
146660
+ let finalIssues = {};
146661
+ let gateResumeCount = 0;
146662
+ let pendingReflection = params.reflectionPrompt;
146663
+ while (gateResumeCount < MAX_POST_RUN_RETRIES) {
146664
+ if (!result.success) break;
146665
+ const issues = await collectPostRunIssues({ stopScript: params.stopScript });
146666
+ finalIssues = issues;
146667
+ if (!hasPostRunIssues(issues)) {
146668
+ if (!pendingReflection) break;
146669
+ if (params.canResume && !params.canResume(result)) break;
146670
+ log.info("\xBB post-run reflection: nudging agent to update learnings if relevant");
146671
+ const preReflection = result;
146672
+ const reflectionResult = await params.resume({
146673
+ prompt: pendingReflection,
146674
+ previousResult: result
146675
+ });
146676
+ aggregatedUsage = mergeAgentUsage(aggregatedUsage, reflectionResult.usage);
146677
+ pendingReflection = void 0;
146678
+ if (!reflectionResult.success) {
146679
+ log.warning(
146680
+ `\xBB reflection turn failed (${reflectionResult.error ?? "unknown error"}), preserving prior successful result`
146681
+ );
146682
+ result = preReflection;
146683
+ break;
146684
+ }
146685
+ result = {
146686
+ ...reflectionResult,
146687
+ output: preReflection.output || reflectionResult.output
146688
+ };
146689
+ continue;
146690
+ }
146691
+ if (params.canResume && !params.canResume(result)) {
146692
+ log.info("\xBB post-run retry skipped: cannot resume agent session");
146693
+ break;
146694
+ }
146695
+ log.info(`\xBB post-run retry (attempt ${gateResumeCount + 1}/${MAX_POST_RUN_RETRIES})`);
146696
+ const prompt = buildPostRunPrompt(issues);
146697
+ result = await params.resume({ prompt, previousResult: result });
146698
+ aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
146699
+ gateResumeCount++;
146700
+ }
146701
+ if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
146702
+ finalIssues = await collectPostRunIssues({ stopScript: params.stopScript });
146703
+ }
146704
+ if (result.success && finalIssues.stopHook) {
146705
+ const retryNote = gateResumeCount > 0 ? ` after ${gateResumeCount} retry ${gateResumeCount === 1 ? "attempt" : "attempts"}` : "";
146706
+ return {
146707
+ ...result,
146708
+ success: false,
146709
+ error: `stop hook failed${retryNote} (exit code ${finalIssues.stopHook.exitCode}): ${finalIssues.stopHook.output || "(no output)"}`,
146710
+ usage: aggregatedUsage
146711
+ };
146712
+ }
146713
+ return { ...result, usage: aggregatedUsage };
146714
+ }
146715
+
146716
+ // agents/sessionLabeler.ts
146717
+ var ORCHESTRATOR_LABEL = "orchestrator";
146718
+ var LENS_PROMPT_PATTERN = /^\s*(?:lens|Lens|LENS)\s*[:=]\s*([A-Za-z][\w &/.-]{0,60})/m;
146719
+ function slug(value2) {
146720
+ return value2.trim().toLowerCase().replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
146721
+ }
146722
+ function deriveLabelFromTaskInput(input) {
146723
+ if (typeof input.prompt === "string") {
146724
+ const match3 = input.prompt.match(LENS_PROMPT_PATTERN);
146725
+ if (match3?.[1]) {
146726
+ const slugged = slug(match3[1]);
146727
+ if (slugged) return `lens:${slugged}`;
146728
+ }
146729
+ }
146730
+ if (input.description) {
146731
+ const slugged = slug(input.description);
146732
+ if (slugged) return `lens:${slugged}`;
146733
+ }
146734
+ if (input.subagent_type) {
146735
+ return input.subagent_type;
146736
+ }
146737
+ return "subagent";
146738
+ }
146739
+ var SessionLabeler = class {
146740
+ labels = /* @__PURE__ */ new Map();
146741
+ pendingLabels = [];
146742
+ fallbackCounter = 0;
146743
+ recordTaskDispatch(input) {
146744
+ const label = deriveLabelFromTaskInput(input);
146745
+ this.pendingLabels.push(label);
146746
+ return label;
146747
+ }
146748
+ /**
146749
+ * Return a label for the given sessionID. Binds on first call.
146750
+ * Pass undefined/empty for events that lack a session id — the caller
146751
+ * gets ORCHESTRATOR_LABEL so the line is still attributable.
146752
+ */
146753
+ labelFor(sessionID) {
146754
+ if (!sessionID) return ORCHESTRATOR_LABEL;
146755
+ const existing = this.labels.get(sessionID);
146756
+ if (existing) return existing;
146757
+ let label;
146758
+ if (this.labels.size === 0) {
146759
+ label = ORCHESTRATOR_LABEL;
146760
+ } else if (this.pendingLabels.length > 0) {
146761
+ label = this.pendingLabels.shift();
146762
+ } else {
146763
+ this.fallbackCounter += 1;
146764
+ label = `subagent#${this.fallbackCounter}`;
146765
+ }
146766
+ this.labels.set(sessionID, label);
146767
+ return label;
146768
+ }
146769
+ /** number of distinct sessions seen so far (for diagnostics) */
146770
+ size() {
146771
+ return this.labels.size;
146772
+ }
146773
+ /** all (sessionID, label) pairs, oldest first */
146774
+ entries() {
146775
+ return Array.from(this.labels.entries());
146776
+ }
146777
+ /** how many pending labels are queued waiting to bind to a new session */
146778
+ pendingDispatchCount() {
146779
+ return this.pendingLabels.length;
146780
+ }
146781
+ };
146782
+ function formatWithLabel(label, message) {
146783
+ const MAGENTA2 = "\x1B[35m";
146784
+ const RESET2 = "\x1B[0m";
146785
+ const colored = `${MAGENTA2}[${label}]${RESET2} `;
146786
+ return message.split("\n").map((line) => `${colored}${line}`).join("\n");
146787
+ }
146788
+
146310
146789
  // agents/claude.ts
146311
146790
  async function installClaudeCli() {
146312
146791
  return await installFromNpmTarball({
@@ -146317,10 +146796,10 @@ async function installClaudeCli() {
146317
146796
  });
146318
146797
  }
146319
146798
  function writeMcpConfig(ctx) {
146320
- const configDir = join9(ctx.tmpdir, ".claude");
146321
- mkdirSync3(configDir, { recursive: true });
146322
- const configPath = join9(configDir, "mcp.json");
146323
- writeFileSync6(
146799
+ const configDir = join10(ctx.tmpdir, ".claude");
146800
+ mkdirSync4(configDir, { recursive: true });
146801
+ const configPath = join10(configDir, "mcp.json");
146802
+ writeFileSync7(
146324
146803
  configPath,
146325
146804
  JSON.stringify({
146326
146805
  mcpServers: {
@@ -146330,6 +146809,15 @@ function writeMcpConfig(ctx) {
146330
146809
  );
146331
146810
  return configPath;
146332
146811
  }
146812
+ function buildAgentsJson() {
146813
+ const agents2 = {
146814
+ [REVIEWER_AGENT_NAME]: {
146815
+ 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.",
146816
+ prompt: REVIEWER_SYSTEM_PROMPT
146817
+ }
146818
+ };
146819
+ return JSON.stringify(agents2);
146820
+ }
146333
146821
  function stripProviderPrefix(specifier) {
146334
146822
  const slashIndex = specifier.indexOf("/");
146335
146823
  return slashIndex > 0 ? specifier.slice(slashIndex + 1) : specifier;
@@ -146380,6 +146868,13 @@ async function runClaude(params) {
146380
146868
  }
146381
146869
  thinkingTimer.markToolCall();
146382
146870
  log.toolCall({ toolName, input: block.input || {} });
146871
+ if (toolName === "Task" && block.input && typeof block.input === "object") {
146872
+ const taskInput = block.input;
146873
+ const label = deriveLabelFromTaskInput(taskInput);
146874
+ log.info(
146875
+ `\xBB dispatching subagent: ${label}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
146876
+ );
146877
+ }
146383
146878
  if (toolName.includes("report_progress") && params.todoTracker) {
146384
146879
  log.debug("\xBB report_progress detected, disabling todo tracking");
146385
146880
  params.todoTracker.cancel();
@@ -146645,9 +147140,9 @@ var claude = agent({
146645
147140
  const model = specifier ? stripProviderPrefix(specifier) : void 0;
146646
147141
  const homeEnv = {
146647
147142
  HOME: ctx.tmpdir,
146648
- XDG_CONFIG_HOME: join9(ctx.tmpdir, ".config")
147143
+ XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
146649
147144
  };
146650
- mkdirSync3(join9(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
147145
+ mkdirSync4(join10(homeEnv.XDG_CONFIG_HOME, "claude"), { recursive: true });
146651
147146
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
146652
147147
  addSkill({
146653
147148
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -146655,6 +147150,7 @@ var claude = agent({
146655
147150
  env: homeEnv,
146656
147151
  agent: "claude"
146657
147152
  });
147153
+ installBundledSkills({ home: homeEnv.HOME });
146658
147154
  const mcpConfigPath = writeMcpConfig(ctx);
146659
147155
  const effort = resolveEffort(model);
146660
147156
  installManagedSettings();
@@ -146669,7 +147165,9 @@ var claude = agent({
146669
147165
  "--effort",
146670
147166
  effort,
146671
147167
  "--disallowedTools",
146672
- "Bash,Agent(Bash)"
147168
+ "Bash,Agent(Bash)",
147169
+ "--agents",
147170
+ buildAgentsJson()
146673
147171
  ];
146674
147172
  if (model) {
146675
147173
  baseArgs.push("--model", model);
@@ -146690,37 +147188,32 @@ var claude = agent({
146690
147188
  onActivityTimeout: ctx.onActivityTimeout,
146691
147189
  onToolUse: ctx.onToolUse
146692
147190
  };
146693
- let result = await runClaude({
147191
+ const result = await runClaude({
146694
147192
  ...runParams,
146695
147193
  args: [...baseArgs, "-p", ctx.instructions.full]
146696
147194
  });
146697
- let aggregatedUsage = result.usage;
146698
- for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
146699
- if (!result.success || !result.sessionId) break;
146700
- const status = getGitStatus();
146701
- if (!status) break;
146702
- log.info(`\xBB dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):
146703
- ${status}`);
146704
- result = await runClaude({
146705
- ...runParams,
146706
- args: [
146707
- ...baseArgs,
146708
- "-p",
146709
- buildCommitPrompt("claude", status),
146710
- "--resume",
146711
- result.sessionId
146712
- ]
146713
- });
146714
- aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
146715
- }
146716
- return { ...result, usage: aggregatedUsage };
147195
+ return runPostRunRetryLoop({
147196
+ initialResult: result,
147197
+ initialUsage: result.usage,
147198
+ stopScript: ctx.stopScript,
147199
+ reflectionPrompt: buildLearningsReflectionPrompt("claude"),
147200
+ canResume: (r) => Boolean(r.sessionId),
147201
+ resume: async (c) => {
147202
+ const sessionId = c.previousResult.sessionId;
147203
+ if (!sessionId) throw new Error("unreachable: canResume gated on sessionId");
147204
+ return runClaude({
147205
+ ...runParams,
147206
+ args: [...baseArgs, "-p", c.prompt, "--resume", sessionId]
147207
+ });
147208
+ }
147209
+ });
146717
147210
  }
146718
147211
  });
146719
147212
 
146720
147213
  // agents/opencode.ts
146721
147214
  import { execFileSync as execFileSync4 } from "node:child_process";
146722
- import { mkdirSync as mkdirSync4 } from "node:fs";
146723
- import { join as join10 } from "node:path";
147215
+ import { mkdirSync as mkdirSync5 } from "node:fs";
147216
+ import { join as join11 } from "node:path";
146724
147217
  import { performance as performance7 } from "node:perf_hooks";
146725
147218
  async function installOpencodeCli() {
146726
147219
  return await installFromNpmTarball({
@@ -146742,7 +147235,8 @@ function buildSecurityConfig(ctx, model) {
146742
147235
  },
146743
147236
  mcp: {
146744
147237
  [pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl }
146745
- }
147238
+ },
147239
+ agent: buildReviewerAgentConfig()
146746
147240
  };
146747
147241
  if (model) {
146748
147242
  config3.model = model;
@@ -146753,6 +147247,15 @@ function buildSecurityConfig(ctx, model) {
146753
147247
  }
146754
147248
  return JSON.stringify(config3);
146755
147249
  }
147250
+ function buildReviewerAgentConfig() {
147251
+ return {
147252
+ [REVIEWER_AGENT_NAME]: {
147253
+ 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.",
147254
+ mode: "subagent",
147255
+ prompt: REVIEWER_SYSTEM_PROMPT
147256
+ }
147257
+ };
147258
+ }
146756
147259
  function getOpenCodeModels(cliPath) {
146757
147260
  try {
146758
147261
  const output = execFileSync4(cliPath, ["models"], {
@@ -146801,6 +147304,29 @@ async function runOpenCode(params) {
146801
147304
  let currentStepId = null;
146802
147305
  let currentStepType = null;
146803
147306
  let stepHistory = [];
147307
+ const labeler = new SessionLabeler();
147308
+ function eventLabel(event) {
147309
+ const sid = event.sessionID ?? event.session_id;
147310
+ return labeler.labelFor(typeof sid === "string" ? sid : null);
147311
+ }
147312
+ function withLabel(label, message) {
147313
+ return label === ORCHESTRATOR_LABEL ? message : formatWithLabel(label, message);
147314
+ }
147315
+ const taskDispatchByCallID = /* @__PURE__ */ new Map();
147316
+ const pendingTaskDispatches = [];
147317
+ const knownNonTaskCallIDs = /* @__PURE__ */ new Set();
147318
+ function emitSubagentFinished(dispatch, status, output2, matchKind) {
147319
+ const subagentDuration = performance7.now() - dispatch.startedAt;
147320
+ const outputStr = typeof output2 === "string" ? output2 : "";
147321
+ const outputPreview = outputStr.length > 120 ? `${outputStr.slice(0, 120)}\u2026` : outputStr;
147322
+ const matchSuffix = matchKind === "fifo" ? " [fifo-matched]" : "";
147323
+ log.info(
147324
+ `\xBB subagent finished: ${dispatch.label} (${(subagentDuration / 1e3).toFixed(1)}s, status=${status})${matchSuffix}` + (outputPreview ? ` \u2014 ${outputPreview.replace(/\n/g, " ")}` : "")
147325
+ );
147326
+ taskDispatchByCallID.delete(dispatch.toolUseCallID);
147327
+ const idx = pendingTaskDispatches.indexOf(dispatch);
147328
+ if (idx >= 0) pendingTaskDispatches.splice(idx, 1);
147329
+ }
146804
147330
  function buildUsage() {
146805
147331
  const totalInput = accumulatedTokens.input + accumulatedTokens.cacheRead + accumulatedTokens.cacheWrite;
146806
147332
  return totalInput > 0 || accumulatedTokens.output > 0 ? {
@@ -146814,39 +147340,63 @@ async function runOpenCode(params) {
146814
147340
  }
146815
147341
  const handlers2 = {
146816
147342
  init: (event) => {
147343
+ const label = labeler.labelFor(event.session_id ?? null);
146817
147344
  log.debug(
146818
- `\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
147345
+ withLabel(
147346
+ label,
147347
+ `\xBB ${params.label} init: session_id=${event.session_id || "unknown"}, model=${event.model || "unknown"}`
147348
+ )
146819
147349
  );
146820
- log.debug(`\xBB ${params.label} init event (full): ${JSON.stringify(event)}`);
146821
- finalOutput = "";
146822
- accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
146823
- accumulatedCostUsd = 0;
146824
- tokensLogged = false;
147350
+ log.debug(withLabel(label, `\xBB ${params.label} init event (full): ${JSON.stringify(event)}`));
147351
+ if (label === ORCHESTRATOR_LABEL) {
147352
+ finalOutput = "";
147353
+ accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
147354
+ accumulatedCostUsd = 0;
147355
+ tokensLogged = false;
147356
+ } else {
147357
+ log.info(`\xBB ${params.label} subagent init: ${label} (session ${event.session_id || "?"})`);
147358
+ }
146825
147359
  },
146826
147360
  message: (event) => {
147361
+ const label = eventLabel(event);
146827
147362
  if (event.role === "assistant" && event.content?.trim()) {
146828
147363
  const message = event.content.trim();
146829
147364
  if (event.delta) {
146830
147365
  log.debug(
146831
- `\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
147366
+ withLabel(
147367
+ label,
147368
+ `\xBB ${params.label} thinking: ${message.substring(0, 300)}${message.length > 300 ? "..." : ""}`
147369
+ )
146832
147370
  );
146833
147371
  } else {
146834
147372
  log.debug(
146835
- `\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
147373
+ withLabel(
147374
+ label,
147375
+ `\xBB ${params.label} message (${event.role}): ${message.substring(0, 100)}${message.length > 100 ? "..." : ""}`
147376
+ )
146836
147377
  );
146837
- finalOutput = message;
147378
+ if (label === ORCHESTRATOR_LABEL) {
147379
+ finalOutput = message;
147380
+ }
146838
147381
  }
146839
147382
  } else if (event.role === "user") {
146840
147383
  log.debug(
146841
- `\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
147384
+ withLabel(
147385
+ label,
147386
+ `\xBB ${params.label} message (${event.role}): ${event.content?.substring(0, 100) || ""}${event.content && event.content.length > 100 ? "..." : ""}`
147387
+ )
146842
147388
  );
146843
147389
  }
146844
147390
  },
146845
147391
  text: (event) => {
146846
147392
  if (event.part?.text?.trim()) {
146847
147393
  const message = event.part.text.trim();
146848
- log.box(message, { title: params.label });
146849
- finalOutput = message;
147394
+ const label = eventLabel(event);
147395
+ const boxTitle = label === ORCHESTRATOR_LABEL ? params.label : `${params.label} [${label}]`;
147396
+ log.box(message, { title: boxTitle });
147397
+ if (label === ORCHESTRATOR_LABEL) {
147398
+ finalOutput = message;
147399
+ }
146850
147400
  }
146851
147401
  },
146852
147402
  step_start: (event) => {
@@ -146882,6 +147432,23 @@ async function runOpenCode(params) {
146882
147432
  );
146883
147433
  return;
146884
147434
  }
147435
+ if (toolName === "task") {
147436
+ const taskInput = event.part?.state?.input ?? {};
147437
+ const dispatchedLabel = labeler.recordTaskDispatch(taskInput);
147438
+ const dispatch = {
147439
+ label: dispatchedLabel,
147440
+ startedAt: performance7.now(),
147441
+ toolUseCallID: toolId
147442
+ };
147443
+ taskDispatchByCallID.set(toolId, dispatch);
147444
+ pendingTaskDispatches.push(dispatch);
147445
+ log.info(
147446
+ `\xBB dispatching subagent: ${dispatchedLabel}` + (taskInput.subagent_type ? ` (subagent_type=${taskInput.subagent_type})` : "")
147447
+ );
147448
+ } else {
147449
+ knownNonTaskCallIDs.add(toolId);
147450
+ }
147451
+ const label = eventLabel(event);
146885
147452
  if (stepHistory.length > 0) {
146886
147453
  stepHistory[stepHistory.length - 1].toolCalls.push(toolName);
146887
147454
  }
@@ -146892,9 +147459,11 @@ async function runOpenCode(params) {
146892
147459
  });
146893
147460
  }
146894
147461
  thinkingTimer.markToolCall();
146895
- log.toolCall({ toolName, input: event.part?.state?.input || {} });
147462
+ const inputFormatted = formatJsonValue(event.part?.state?.input || {});
147463
+ const toolCallLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
147464
+ log.info(withLabel(label, toolCallLine));
146896
147465
  if (event.part?.state?.status === "completed" && event.part.state.output) {
146897
- log.debug(` output: ${event.part.state.output}`);
147466
+ log.debug(withLabel(label, ` output: ${event.part.state.output}`));
146898
147467
  }
146899
147468
  if (toolName.includes("report_progress") && params.todoTracker) {
146900
147469
  log.debug("\xBB report_progress detected, disabling todo tracking");
@@ -146908,7 +147477,20 @@ async function runOpenCode(params) {
146908
147477
  const toolId = event.part?.callID || event.tool_id;
146909
147478
  const status = event.part?.state?.status || event.status || "unknown";
146910
147479
  const output2 = event.part?.state?.output || event.output;
147480
+ const label = eventLabel(event);
146911
147481
  thinkingTimer.markToolResult();
147482
+ if (taskDispatchByCallID.size > 0 || pendingTaskDispatches.length > 0) {
147483
+ if (toolId && taskDispatchByCallID.has(toolId)) {
147484
+ const dispatch = taskDispatchByCallID.get(toolId);
147485
+ if (dispatch) emitSubagentFinished(dispatch, status, output2, "exact");
147486
+ } else {
147487
+ const callIDIsKnownNonTask = toolId ? knownNonTaskCallIDs.has(toolId) : false;
147488
+ if (!callIDIsKnownNonTask && pendingTaskDispatches.length > 0) {
147489
+ const dispatch = pendingTaskDispatches[0];
147490
+ emitSubagentFinished(dispatch, status, output2, "fifo");
147491
+ }
147492
+ }
147493
+ }
146912
147494
  if (toolId) {
146913
147495
  const toolStartTime = toolCallTimings.get(toolId);
146914
147496
  if (toolStartTime) {
@@ -146916,24 +147498,35 @@ async function runOpenCode(params) {
146916
147498
  toolCallTimings.delete(toolId);
146917
147499
  const stepContext = currentStepId ? ` (step=${currentStepType || "unknown"})` : "";
146918
147500
  log.debug(
146919
- `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
147501
+ withLabel(
147502
+ label,
147503
+ `\xBB ${params.label} tool_result${stepContext}: id=${toolId}, status=${status}, duration=${Math.round(toolDuration)}ms`
147504
+ )
146920
147505
  );
146921
147506
  if (output2) {
146922
- log.debug(` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`);
147507
+ log.debug(
147508
+ withLabel(
147509
+ label,
147510
+ ` output: ${typeof output2 === "string" ? output2 : JSON.stringify(output2)}`
147511
+ )
147512
+ );
146923
147513
  }
146924
147514
  if (toolDuration > 5e3) {
146925
147515
  log.info(
146926
- `\xBB tool call took ${(toolDuration / 1e3).toFixed(1)}s - may indicate network latency`
147516
+ withLabel(
147517
+ label,
147518
+ `\xBB tool call took ${(toolDuration / 1e3).toFixed(1)}s - may indicate network latency`
147519
+ )
146927
147520
  );
146928
147521
  }
146929
147522
  }
146930
147523
  }
146931
147524
  if (status === "error") {
146932
147525
  const errorMsg = typeof output2 === "string" ? output2 : JSON.stringify(output2);
146933
- log.info(`\xBB tool call failed: ${errorMsg}`);
147526
+ log.info(withLabel(label, `\xBB tool call failed: ${errorMsg}`));
146934
147527
  } else if (output2) {
146935
147528
  const outputStr = typeof output2 === "string" ? output2 : JSON.stringify(output2);
146936
- log.debug(`tool output: ${outputStr}`);
147529
+ log.debug(withLabel(label, `tool output: ${outputStr}`));
146937
147530
  }
146938
147531
  },
146939
147532
  result: async (event) => {
@@ -147030,6 +147623,16 @@ async function runOpenCode(params) {
147030
147623
  } else {
147031
147624
  params.todoTracker?.cancel();
147032
147625
  }
147626
+ if (pendingTaskDispatches.length > 0) {
147627
+ for (const dispatch of [...pendingTaskDispatches]) {
147628
+ const elapsed = performance7.now() - dispatch.startedAt;
147629
+ log.info(
147630
+ `\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`
147631
+ );
147632
+ }
147633
+ pendingTaskDispatches.length = 0;
147634
+ taskDispatchByCallID.clear();
147635
+ }
147033
147636
  const duration4 = performance7.now() - startTime;
147034
147637
  log.info(
147035
147638
  `\xBB ${params.label} completed in ${Math.round(duration4)}ms with exit code ${result.exitCode}`
@@ -147097,9 +147700,9 @@ var opencode = agent({
147097
147700
  const model = ctx.payload.proxyModel ?? ctx.resolvedModel ?? autoSelectModel(cliPath);
147098
147701
  const homeEnv = {
147099
147702
  HOME: ctx.tmpdir,
147100
- XDG_CONFIG_HOME: join10(ctx.tmpdir, ".config")
147703
+ XDG_CONFIG_HOME: join11(ctx.tmpdir, ".config")
147101
147704
  };
147102
- mkdirSync4(join10(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
147705
+ mkdirSync5(join11(homeEnv.XDG_CONFIG_HOME, "opencode"), { recursive: true });
147103
147706
  const agentBrowserVersion = getDevDependencyVersion("agent-browser");
147104
147707
  addSkill({
147105
147708
  ref: `vercel-labs/agent-browser@v${agentBrowserVersion}`,
@@ -147107,6 +147710,7 @@ var opencode = agent({
147107
147710
  env: homeEnv,
147108
147711
  agent: "opencode"
147109
147712
  });
147713
+ installBundledSkills({ home: homeEnv.HOME });
147110
147714
  const baseArgs = ["run", "--format", "json", "--print-logs"];
147111
147715
  const permissionOverride = JSON.stringify({
147112
147716
  external_directory: { "*": "deny", "/tmp/*": "allow" }
@@ -147130,24 +147734,20 @@ var opencode = agent({
147130
147734
  onActivityTimeout: ctx.onActivityTimeout,
147131
147735
  onToolUse: ctx.onToolUse
147132
147736
  };
147133
- let result = await runOpenCode({
147737
+ const result = await runOpenCode({
147134
147738
  ...runParams,
147135
147739
  args: [...baseArgs, ctx.instructions.full]
147136
147740
  });
147137
- let aggregatedUsage = result.usage;
147138
- for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
147139
- if (!result.success) break;
147140
- const status = getGitStatus();
147141
- if (!status) break;
147142
- log.info(`\xBB dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):
147143
- ${status}`);
147144
- result = await runOpenCode({
147741
+ return runPostRunRetryLoop({
147742
+ initialResult: result,
147743
+ initialUsage: result.usage,
147744
+ stopScript: ctx.stopScript,
147745
+ reflectionPrompt: buildLearningsReflectionPrompt("opencode"),
147746
+ resume: async (c) => runOpenCode({
147145
147747
  ...runParams,
147146
- args: [...baseArgs, "--continue", buildCommitPrompt("opencode", status)]
147147
- });
147148
- aggregatedUsage = mergeAgentUsage(aggregatedUsage, result.usage);
147149
- }
147150
- return { ...result, usage: aggregatedUsage };
147748
+ args: [...baseArgs, "--continue", c.prompt]
147749
+ })
147750
+ });
147151
147751
  }
147152
147752
  });
147153
147753
 
@@ -147335,7 +147935,7 @@ async function fetchBodyHtml(ctx) {
147335
147935
  var core2 = __toESM(require_core(), 1);
147336
147936
  import { createSign } from "node:crypto";
147337
147937
  import { rename, writeFile } from "node:fs/promises";
147338
- import { dirname as dirname2, join as join11 } from "node:path";
147938
+ import { dirname as dirname3, join as join12 } from "node:path";
147339
147939
 
147340
147940
  // node_modules/.pnpm/@octokit+plugin-throttling@11.0.3_@octokit+core@7.0.5/node_modules/@octokit/plugin-throttling/dist-bundle/index.js
147341
147941
  var import_light = __toESM(require_light(), 1);
@@ -151167,7 +151767,7 @@ function getGitHubUsageSummary() {
151167
151767
  }
151168
151768
  async function writeGitHubUsageSummaryToFile(path3) {
151169
151769
  const summary2 = getGitHubUsageSummary();
151170
- const tmpPath = join11(dirname2(path3), `.usage-summary-${process.pid}.tmp`);
151770
+ const tmpPath = join12(dirname3(path3), `.usage-summary-${process.pid}.tmp`);
151171
151771
  await writeFile(tmpPath, JSON.stringify(summary2));
151172
151772
  await rename(tmpPath, path3);
151173
151773
  }
@@ -151356,9 +151956,9 @@ ${ctx.error}` : ctx.error;
151356
151956
 
151357
151957
  // utils/gitAuthServer.ts
151358
151958
  import { randomUUID as randomUUID3 } from "node:crypto";
151359
- import { writeFileSync as writeFileSync7 } from "node:fs";
151959
+ import { writeFileSync as writeFileSync8 } from "node:fs";
151360
151960
  import { createServer as createServer2 } from "node:http";
151361
- import { join as join12 } from "node:path";
151961
+ import { join as join13 } from "node:path";
151362
151962
  var CODE_TTL_MS = 5 * 60 * 1e3;
151363
151963
  var TAMPER_WINDOW_MS = 6e4;
151364
151964
  function revokeGitHubToken(token) {
@@ -151430,7 +152030,7 @@ async function startGitAuthServer(tmpdir3) {
151430
152030
  function writeAskpassScript(code) {
151431
152031
  const scriptId = randomUUID3();
151432
152032
  const scriptName = `askpass-${scriptId}.js`;
151433
- const scriptPath = join12(tmpdir3, scriptName);
152033
+ const scriptPath = join13(tmpdir3, scriptName);
151434
152034
  const content = [
151435
152035
  `#!/usr/bin/env node`,
151436
152036
  `var a=process.argv[2]||"";`,
@@ -151445,7 +152045,7 @@ async function startGitAuthServer(tmpdir3) {
151445
152045
  `try{require("fs").unlinkSync("${scriptPath.replace(/\\/g, "\\\\")}")}catch(e){}`,
151446
152046
  `})}).on("error",function(){process.exit(1)})}`
151447
152047
  ].join("\n");
151448
- writeFileSync7(scriptPath, content, { mode: 448 });
152048
+ writeFileSync8(scriptPath, content, { mode: 448 });
151449
152049
  return scriptPath;
151450
152050
  }
151451
152051
  async function close() {
@@ -152015,7 +152615,9 @@ async function handleAgentResult(ctx) {
152015
152615
  output: ctx.result.output
152016
152616
  };
152017
152617
  }
152018
- if (!ctx.toolState.wasUpdated && ctx.toolState.hadProgressComment && !ctx.silent) {
152618
+ const mode = ctx.toolState.selectedMode;
152619
+ const isReviewMode = mode === "Review" || mode === "IncrementalReview";
152620
+ if (!isReviewMode && !ctx.toolState.wasUpdated && ctx.toolState.hadProgressComment && !ctx.silent) {
152019
152621
  const error49 = ctx.result.error || "agent completed without reporting progress";
152020
152622
  try {
152021
152623
  await reportErrorToComment({
@@ -152048,6 +152650,7 @@ var defaultSettings = {
152048
152650
  setupScript: null,
152049
152651
  postCheckoutScript: null,
152050
152652
  prepushScript: null,
152653
+ stopScript: null,
152051
152654
  push: "restricted",
152052
152655
  shell: "restricted",
152053
152656
  prApproveEnabled: false,
@@ -152092,7 +152695,8 @@ async function fetchRunContext(params) {
152092
152695
  modes: data.settings?.modes ?? [],
152093
152696
  setupScript: data.settings?.setupScript ?? null,
152094
152697
  postCheckoutScript: data.settings?.postCheckoutScript ?? null,
152095
- prepushScript: data.settings?.prepushScript ?? null
152698
+ prepushScript: data.settings?.prepushScript ?? null,
152699
+ stopScript: data.settings?.stopScript ?? null
152096
152700
  },
152097
152701
  apiToken: data.apiToken,
152098
152702
  oss: data.oss ?? false,
@@ -152136,9 +152740,9 @@ async function resolveRunContextData(params) {
152136
152740
  import { execFileSync as execFileSync5, execSync as execSync3 } from "node:child_process";
152137
152741
  import { mkdtempSync } from "node:fs";
152138
152742
  import { tmpdir as tmpdir2 } from "node:os";
152139
- import { join as join13 } from "node:path";
152743
+ import { join as join14 } from "node:path";
152140
152744
  function createTempDirectory() {
152141
- const sharedTempDir = mkdtempSync(join13(tmpdir2(), "pullfrog-"));
152745
+ const sharedTempDir = mkdtempSync(join14(tmpdir2(), "pullfrog-"));
152142
152746
  process.env.PULLFROG_TEMP_DIR = sharedTempDir;
152143
152747
  log.info(`\xBB created temp dir at ${sharedTempDir}`);
152144
152748
  return sharedTempDir;
@@ -152665,8 +153269,8 @@ ${instructions.user}` : null,
152665
153269
  log.info(instructions.full);
152666
153270
  });
152667
153271
  if (agentId === "opencode") {
152668
- const pluginDir = join14(process.cwd(), ".opencode", "plugin");
152669
- const hasPlugins = existsSync6(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
153272
+ const pluginDir = join15(process.cwd(), ".opencode", "plugin");
153273
+ const hasPlugins = existsSync7(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
152670
153274
  if (hasPlugins && toolState.dependencyInstallation?.promise) {
152671
153275
  log.info(
152672
153276
  "\xBB .opencode/plugin/ detected \u2014 awaiting dependency installation before agent start"
@@ -152720,6 +153324,7 @@ ${instructions.user}` : null,
152720
153324
  tmpdir: tmpdir3,
152721
153325
  instructions,
152722
153326
  todoTracker,
153327
+ stopScript: runContext.repoSettings.stopScript,
152723
153328
  onActivityTimeout: onInnerActivityTimeout,
152724
153329
  onToolUse: (event) => {
152725
153330
  const wasTracked = recordDiffReadFromToolUse({
@@ -152774,11 +153379,6 @@ ${instructions.user}` : null,
152774
153379
  log.debug(`post-review cleanup failed: ${error49}`);
152775
153380
  });
152776
153381
  }
152777
- if (toolContext && toolState.review && toolState.progressCommentId) {
152778
- await deleteProgressComment(toolContext).catch((error49) => {
152779
- log.debug(`review progress comment cleanup failed: ${error49}`);
152780
- });
152781
- }
152782
153382
  const trackerWasLastWriter = todoTracker?.hasPublished && !toolState.finalSummaryWritten;
152783
153383
  if (toolContext && toolState.progressCommentId && (!toolState.wasUpdated || trackerWasLastWriter)) {
152784
153384
  await deleteProgressComment(toolContext).catch((error49) => {