pullfrog 0.1.29 → 0.1.31

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.
Files changed (60) hide show
  1. package/dist/agents/claudePretoolGate.d.ts +5 -1
  2. package/dist/agents/opencodePlugin.d.ts +2 -2
  3. package/dist/agents/postRun.d.ts +8 -31
  4. package/dist/agents/shared.d.ts +6 -0
  5. package/dist/agents/subagentToolGates.d.ts +22 -40
  6. package/dist/cli.mjs +63989 -59666
  7. package/dist/external.d.ts +45 -2
  8. package/dist/index.js +62509 -58186
  9. package/dist/internal/index.d.ts +2 -2
  10. package/dist/internal.js +99 -33
  11. package/dist/mcp/checkSuite.d.ts +3 -1
  12. package/dist/mcp/checkout.d.ts +4 -2
  13. package/dist/mcp/comment.d.ts +12 -4
  14. package/dist/mcp/commitInfo.d.ts +3 -1
  15. package/dist/mcp/dependencies.d.ts +6 -2
  16. package/dist/mcp/git.d.ts +32 -6
  17. package/dist/mcp/issue.d.ts +26 -1
  18. package/dist/mcp/issueComments.d.ts +3 -1
  19. package/dist/mcp/issueEvents.d.ts +3 -1
  20. package/dist/mcp/issueInfo.d.ts +3 -1
  21. package/dist/mcp/labels.d.ts +16 -1
  22. package/dist/mcp/output.d.ts +6 -2
  23. package/dist/mcp/pr.d.ts +19 -2
  24. package/dist/mcp/prInfo.d.ts +3 -1
  25. package/dist/mcp/resolveRepoCtx.d.ts +34 -0
  26. package/dist/mcp/review.d.ts +9 -3
  27. package/dist/mcp/reviewComments.d.ts +9 -3
  28. package/dist/mcp/selectMode.d.ts +3 -1
  29. package/dist/mcp/server.d.ts +7 -1
  30. package/dist/mcp/shared.d.ts +18 -1
  31. package/dist/mcp/shell.d.ts +6 -2
  32. package/dist/mcp/upload.d.ts +3 -1
  33. package/dist/mcp/xrepo.d.ts +15 -0
  34. package/dist/models.d.ts +58 -5
  35. package/dist/modes.d.ts +1 -1
  36. package/dist/toolState.d.ts +66 -16
  37. package/dist/utils/apiCommit.d.ts +40 -0
  38. package/dist/utils/apiKeys.d.ts +19 -0
  39. package/dist/utils/buildPullfrogFooter.d.ts +27 -0
  40. package/dist/utils/claudeSubscription.d.ts +30 -0
  41. package/dist/utils/errorReport.d.ts +2 -2
  42. package/dist/utils/github.d.ts +28 -2
  43. package/dist/utils/instructions.d.ts +11 -0
  44. package/dist/utils/isTransientNetworkError.d.ts +10 -0
  45. package/dist/utils/learnings.d.ts +14 -0
  46. package/dist/utils/modelAccess.d.ts +64 -0
  47. package/dist/utils/openCodeModels.d.ts +2 -2
  48. package/dist/utils/payload.d.ts +16 -0
  49. package/dist/utils/proxy.d.ts +3 -5
  50. package/dist/utils/runContext.d.ts +4 -0
  51. package/dist/utils/runContextData.d.ts +10 -0
  52. package/dist/utils/runErrorRenderer.d.ts +2 -2
  53. package/dist/utils/setup.d.ts +18 -3
  54. package/dist/utils/token.d.ts +22 -3
  55. package/dist/yes/index.d.ts +32 -0
  56. package/dist/yes/standard-schema.d.ts +117 -0
  57. package/dist/yes.js +1244 -0
  58. package/package.json +19 -10
  59. package/dist/utils/byokFallback.d.ts +0 -44
  60. package/dist/utils/retry.d.ts +0 -13
@@ -0,0 +1,34 @@
1
+ import { type RepoAccess } from "../toolState.ts";
2
+ import { type OctokitWithPlugins } from "../utils/github.ts";
3
+ import type { ToolContext } from "./server.ts";
4
+ /**
5
+ * per-repo execution context for a single tool call. resolved from the
6
+ * optional `repo` param (a bare repo name — owner-implicit, same account as
7
+ * the primary). carries the correct token tier + working directory so a tool
8
+ * can talk to GitHub and run git against the right checkout.
9
+ */
10
+ export interface RepoCtx {
11
+ owner: string;
12
+ name: string;
13
+ /** working tree: primary = process.cwd(); secondary = ctx.tmpdir/xrepo/<name> */
14
+ dir: string;
15
+ access: RepoAccess;
16
+ /** octokit scoped to the repo's tier (write tokens for primary/write, read token for read) */
17
+ octokit: OctokitWithPlugins;
18
+ /** git token scoped to the repo's tier */
19
+ gitToken: string;
20
+ /** re-mint this tier's git token on an auth-class push failure (push retries) */
21
+ refreshGitToken: ((stale: string) => Promise<string>) | undefined;
22
+ }
23
+ /**
24
+ * resolve the execution context for a `repo`-scoped tool call.
25
+ *
26
+ * - omitted / primary name → the primary ctx (unchanged single-repo path).
27
+ * - a `write`-tier secondary → the same write-scoped tokens as the primary
28
+ * (mcpToken + gitToken are minted over the write set ∪ primary).
29
+ * - a `read`-tier secondary → the contents:read token (clone-for-reference).
30
+ *
31
+ * throws when `repo` isn't a registered checkout — secondaries must be cloned
32
+ * via `checkout_repo` first, which is what populates `toolState.repos`.
33
+ */
34
+ export declare function resolveRepoCtx(ctx: ToolContext, repo?: string | undefined): RepoCtx;
@@ -1,5 +1,5 @@
1
1
  import type { RestEndpointMethodTypes } from "@octokit/rest";
2
- import type { CommentableLines } from "../toolState.ts";
2
+ import { type CommentableLines } from "../toolState.ts";
3
3
  import type { ToolContext } from "./server.ts";
4
4
  export type { CommentableLines };
5
5
  /**
@@ -69,7 +69,7 @@ export type DuplicateReviewDecision = {
69
69
  * adds noise to the PR.
70
70
  *
71
71
  * legitimate follow-up reviews after new commits ARE allowed: the
72
- * new-commits-mid-review path advances toolState.checkoutSha past the
72
+ * new-commits-mid-review path advances the primary repo state's checkoutSha past the
73
73
  * previously reviewed sha, and a subsequent checkout_pr advances it again.
74
74
  * any call where checkoutSha has moved past the prior reviewedSha is a real
75
75
  * follow-up and goes through. anything else — same sha, or no checkoutSha
@@ -103,6 +103,7 @@ export declare function duplicateReviewDecision(params: {
103
103
  */
104
104
  export declare function reviewSkipDecision(params: {
105
105
  approved: boolean;
106
+ requestChanges?: boolean;
106
107
  body: string | null | undefined;
107
108
  hasComments: boolean;
108
109
  prApproveEnabled: boolean;
@@ -112,6 +113,7 @@ export declare const CreatePullRequestReview: import("arktype/internal/variants/
112
113
  pull_number: number;
113
114
  body?: string;
114
115
  approved?: boolean;
116
+ request_changes?: boolean;
115
117
  commit_id?: string;
116
118
  comments?: {
117
119
  path: string;
@@ -126,6 +128,7 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
126
128
  pull_number: number;
127
129
  body?: string;
128
130
  approved?: boolean;
131
+ request_changes?: boolean;
129
132
  commit_id?: string;
130
133
  comments?: {
131
134
  path: string;
@@ -139,6 +142,7 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
139
142
  pull_number: number;
140
143
  body?: string;
141
144
  approved?: boolean;
145
+ request_changes?: boolean;
142
146
  commit_id?: string;
143
147
  comments?: {
144
148
  path: string;
@@ -148,7 +152,9 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
148
152
  suggestion?: string;
149
153
  start_line?: number;
150
154
  }[];
151
- }>>;
155
+ }>> & {
156
+ mutates?: boolean;
157
+ };
152
158
  /**
153
159
  * clear a pending review draft stranded on the PR by a prior hard-killed run
154
160
  * (workflow timeout, OOM) so the next createReview can succeed.
@@ -147,7 +147,9 @@ export declare function GetReviewCommentsTool(ctx: ToolContext): import("fastmcp
147
147
  }, {
148
148
  pull_number: number;
149
149
  review_id: number;
150
- }>>;
150
+ }>> & {
151
+ mutates?: boolean;
152
+ };
151
153
  export declare const ListPullRequestReviews: import("arktype/internal/variants/object.ts").ObjectType<{
152
154
  pull_number: number;
153
155
  }, {}>;
@@ -155,7 +157,9 @@ export declare function ListPullRequestReviewsTool(ctx: ToolContext): import("fa
155
157
  pull_number: number;
156
158
  }, {
157
159
  pull_number: number;
158
- }>>;
160
+ }>> & {
161
+ mutates?: boolean;
162
+ };
159
163
  export declare const ResolveReviewThread: import("arktype/internal/variants/object.ts").ObjectType<{
160
164
  thread_id: string;
161
165
  }, {}>;
@@ -163,5 +167,7 @@ export declare function ResolveReviewThreadTool(ctx: ToolContext): import("fastm
163
167
  thread_id: string;
164
168
  }, {
165
169
  thread_id: string;
166
- }>>;
170
+ }>> & {
171
+ mutates?: boolean;
172
+ };
167
173
  export {};
@@ -15,4 +15,6 @@ export declare function SelectModeTool(ctx: ToolContext): import("fastmcp").Tool
15
15
  }, {
16
16
  mode: string;
17
17
  issue_number?: number;
18
- }>>;
18
+ }>> & {
19
+ mutates?: boolean;
20
+ };
@@ -1,11 +1,12 @@
1
1
  import "./arkConfig.ts";
2
- import { type AgentId } from "../external.ts";
2
+ import { type AgentId, type XrepoConfig } from "../external.ts";
3
3
  import type { Mode } from "../modes.ts";
4
4
  import type { ToolState } from "../toolState.ts";
5
5
  import type { OctokitWithPlugins } from "../utils/github.ts";
6
6
  import type { ResolvedPayload } from "../utils/payload.ts";
7
7
  import type { AccountPlan } from "../utils/runContext.ts";
8
8
  import type { RunContextData } from "../utils/runContextData.ts";
9
+ import { type PullfrogTool } from "./shared.ts";
9
10
  export interface ToolContext {
10
11
  agentId: AgentId;
11
12
  repo: RunContextData["repo"];
@@ -13,11 +14,15 @@ export interface ToolContext {
13
14
  octokit: OctokitWithPlugins;
14
15
  githubInstallationToken: string;
15
16
  gitToken: string;
17
+ refreshGitToken: ((stale: string) => Promise<string>) | undefined;
18
+ readToken: string | undefined;
19
+ xrepo: XrepoConfig | undefined;
16
20
  apiToken: string;
17
21
  modes: Mode[];
18
22
  postCheckoutScript: string | null;
19
23
  prepushScript: string | null;
20
24
  prApproveEnabled: boolean;
25
+ signedCommits: boolean;
21
26
  modeInstructions: Record<string, string>;
22
27
  toolState: ToolState;
23
28
  runId: number | undefined;
@@ -29,6 +34,7 @@ export interface ToolContext {
29
34
  resolvedModel: string | undefined;
30
35
  }
31
36
  type JsonSchema = Record<string, unknown>;
37
+ export declare function buildOrchestratorTools(ctx: ToolContext, outputSchema?: JsonSchema): PullfrogTool[];
32
38
  type McpHttpServerOptions = {
33
39
  outputSchema?: JsonSchema | undefined;
34
40
  };
@@ -1,7 +1,24 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  import type { FastMCP, Tool } from "fastmcp";
3
3
  import type { ToolContext } from "./server.ts";
4
- export declare const tool: <const params>(toolDef: Tool<any, StandardSchemaV1<params>>) => Tool<any, StandardSchemaV1<params>>;
4
+ /** extract the HTTP status from an unknown thrown value (octokit RequestError etc.), or undefined. */
5
+ export declare function getHttpStatus(err: unknown): number | undefined;
6
+ /**
7
+ * a Pullfrog MCP tool definition. `mutates` marks a named state-changing tool
8
+ * that must be reserved for the orchestrator and denied to subagents — it is
9
+ * the single source of truth the subagent deny list derives from (see
10
+ * action/agents/subagentToolGates.ts). general-purpose execution tools (`git`,
11
+ * `shell`) are deliberately left unmarked: they can mutate but are allowed for
12
+ * subagents and gated by command-arg validation instead.
13
+ */
14
+ export type PullfrogTool = Tool<any, any> & {
15
+ mutates?: boolean;
16
+ };
17
+ export declare const tool: <const params>(toolDef: Tool<any, StandardSchemaV1<params>> & {
18
+ mutates?: boolean;
19
+ }) => Tool<any, StandardSchemaV1<params>> & {
20
+ mutates?: boolean;
21
+ };
5
22
  export interface ToolResult {
6
23
  content: {
7
24
  type: "text";
@@ -26,7 +26,9 @@ export declare function ShellTool(ctx: ToolContext): import("fastmcp").Tool<any,
26
26
  timeout?: number;
27
27
  working_directory?: string;
28
28
  background?: boolean;
29
- }>>;
29
+ }>> & {
30
+ mutates?: boolean;
31
+ };
30
32
  export declare const KillBackgroundParams: import("arktype/internal/variants/object.ts").ObjectType<{
31
33
  handle: string;
32
34
  }, {}>;
@@ -34,4 +36,6 @@ export declare function KillBackgroundTool(ctx: ToolContext): import("fastmcp").
34
36
  handle: string;
35
37
  }, {
36
38
  handle: string;
37
- }>>;
39
+ }>> & {
40
+ mutates?: boolean;
41
+ };
@@ -3,4 +3,6 @@ export declare function UploadFileTool(ctx: ToolContext): import("fastmcp").Tool
3
3
  path: string;
4
4
  }, {
5
5
  path: string;
6
- }>>;
6
+ }>> & {
7
+ mutates?: boolean;
8
+ };
@@ -0,0 +1,15 @@
1
+ import type { ToolContext } from "./server.ts";
2
+ export declare const ListRepos: import("arktype/internal/variants/object.ts").ObjectType<object, {}>;
3
+ export declare function ListReposTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<object, object>> & {
4
+ mutates?: boolean;
5
+ };
6
+ export declare const CheckoutRepo: import("arktype/internal/variants/object.ts").ObjectType<{
7
+ repo: string;
8
+ }, {}>;
9
+ export declare function CheckoutRepoTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
10
+ repo: string;
11
+ }, {
12
+ repo: string;
13
+ }>> & {
14
+ mutates?: boolean;
15
+ };
package/dist/models.d.ts CHANGED
@@ -36,7 +36,11 @@ export interface ModelAlias {
36
36
  preferred: boolean;
37
37
  /** whether this alias is free and requires no API key */
38
38
  isFree: boolean;
39
- /** slug of a replacement model presence implies this model is deprecated */
39
+ /** slug of a replacement model to resolve through. presence means this alias
40
+ * never runs as-is — resolution redirects to the replacement and it's hidden
41
+ * from pickers. covers permanent deprecation AND riding out a temporarily
42
+ * unavailable model: point it at a cheaper tier (downgrade) or a working
43
+ * sibling/higher tier (upgrade), then clear it when the model returns. */
40
44
  fallback: string | undefined;
41
45
  /** dynamic-resolution discriminant — see ModelRouting docs */
42
46
  routing: ModelRouting | undefined;
@@ -57,7 +61,9 @@ interface ModelDef {
57
61
  preferred?: boolean;
58
62
  envVars?: readonly string[];
59
63
  isFree?: boolean;
60
- /** slug of a replacement model presence implies this model is deprecated */
64
+ /** slug of a replacement model to resolve through permanent deprecation or
65
+ * temporary unavailability (downgrade/upgrade until the model returns). see
66
+ * ModelAlias.fallback. */
61
67
  fallback?: string;
62
68
  /** dynamic-resolution discriminant — see ModelRouting docs */
63
69
  routing?: ModelRouting;
@@ -104,9 +110,53 @@ export declare function getModelEnvVars(slug: string): string[];
104
110
  * see `provider.managedCredentials` and wiki/codex-auth.md. */
105
111
  export declare function getModelManagedCredentials(slug: string): string[];
106
112
  export declare const modelAliases: ModelAlias[];
107
- export declare const DEFAULT_PROXY_MODEL: string;
108
- /** short label for the model auto-select picks today (console hint copy). */
109
- export declare function getAutoSelectHintModel(): string;
113
+ /**
114
+ * `repo.model` sentinels for the two managed auto tiers. stored verbatim in
115
+ * the DB (they pass the `provider/model` shape check) and resolved to a
116
+ * concrete alias by `resolveDisplayAlias` below, so every downstream consumer
117
+ * (CLI resolve, OpenRouter resolve, footer label) handles them transparently.
118
+ *
119
+ * `efficient` mirrors the OSS/default subsidy model (Kimi K2 — fast + cheap);
120
+ * `intelligent` is the frontier pick (Claude Opus). a `null` model means the
121
+ * tier hasn't been pinned: callers default by card status via `defaultAutoTier`.
122
+ */
123
+ export declare const AUTO_EFFICIENT = "auto/efficient";
124
+ export declare const AUTO_INTELLIGENT = "auto/intelligent";
125
+ export type AutoTier = typeof AUTO_EFFICIENT | typeof AUTO_INTELLIGENT;
126
+ export declare function isAutoTier(slug: string | null | undefined): slug is AutoTier;
127
+ /**
128
+ * the tier to default to when the user hasn't pinned one. card on file →
129
+ * intelligent (they've signalled willingness to pay for frontier reviews);
130
+ * otherwise → efficient (safe, cheap). server (`run-context`) and the console
131
+ * picker both call this so the displayed default and the runtime default agree.
132
+ */
133
+ export declare function defaultAutoTier(hasCard: boolean): AutoTier;
134
+ /**
135
+ * the auto tier that actually runs for an account, enforcing the card gate:
136
+ * the intelligent tier (Opus) requires a card on file, so a no-card account is
137
+ * always clamped to efficient (Kimi) — whether intelligent was the card-based
138
+ * default or an explicit pick made while a card was once present. keeps a trial
139
+ * balance from being torched on a premium model before the user has a card.
140
+ * `model` may be null, an `auto/*` sentinel, or a concrete pick being
141
+ * clamped — callers route concrete picks here only when `!hasCard` (carded
142
+ * concrete picks resolve directly), so a concrete `model` always lands on the
143
+ * efficient tier via the no-card early return.
144
+ */
145
+ export declare function resolveAutoTier(params: {
146
+ model: string | null;
147
+ hasCard: boolean;
148
+ }): AutoTier;
149
+ /**
150
+ * Router-resolvable — the set a card on file unlocks. custom (non-Auto) picks
151
+ * are card-gated wholesale on the Router: the console locks the Custom tab
152
+ * without a card and the server clamps any stored pick to the efficient tier.
153
+ * a pick with no openRouterResolve (a model OpenRouter doesn't serve yet)
154
+ * lands on the efficient default with or without a card, so "add a card to
155
+ * run this model" messaging would be false for it — hence the predicate.
156
+ * resolveOpenRouterModel walks display aliases, so auto sentinels and
157
+ * deprecated slugs are judged by the model that actually runs.
158
+ */
159
+ export declare function isCardGatedModel(slug: string): boolean;
110
160
  /** resolve a model slug to its concrete models.dev specifier (e.g. "anthropic/claude-opus-4-6") */
111
161
  export declare function resolveModelSlug(slug: string): string | undefined;
112
162
  /**
@@ -132,6 +182,9 @@ export declare function resolveCliModel(slug: string): string | undefined;
132
182
  * (e.g. free opencode models).
133
183
  */
134
184
  export declare function resolveOpenRouterModel(slug: string): string | undefined;
185
+ export declare const DEFAULT_PROXY_MODEL: string;
186
+ /** short label for the model auto-select picks today (console hint copy). */
187
+ export declare function getAutoSelectHintModel(): string;
135
188
  /** env var that supplies the Bedrock model ID for the `bedrock/byok` slug. */
136
189
  export declare const BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
137
190
  /** env var that supplies the Vertex AI model ID for the `vertex/byok` slug. */
package/dist/modes.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface Mode {
5
5
  prompt?: string | undefined;
6
6
  }
7
7
  export declare const PR_SUMMARY_FORMAT = "### Default format\n\nThe body has at most three parts in this exact order:\n\n1. **Reviewed changes preamble** \u2014 one bolded inline lead-in describing what was reviewed in this run, a bullet list of the substantive changes, and an HTML comment carrying review metadata for downstream agents.\n2. **Cross-cutting issue sections** (zero or more) \u2014 one `### ` heading per concern, with a human-readable problem write-up and a collapsed `<details>Technical details</details>` block underneath.\n3. **`### \u2139\uFE0F Nitpicks`** at the very bottom (only if there are nits worth surfacing in the body) \u2014 a flat bullet list, no technical-details block.\n\nInline-vs-body split: concerns that anchor to a specific line go inline (use the `comments` parameter). Body `### ` sections are reserved for concerns that **have no line to anchor to** \u2014 typically because the concern is about *absence* (something the diff should have done but didn't), *sequencing* (rollout / deletion / migration order), *design decisions only the human can make*, or *scope questions the diff implicitly raises but doesn't address*. A concern that anchors to a line but has broad implications still goes inline (use the technical-details block there to capture the implications \u2014 see Inline technical details below). If you found no non-anchorable concerns, the body has zero `### ` issue sections \u2014 just the preamble + metadata.\n\n## 1. Reviewed changes preamble\n\nOpen with a single bolded inline lead-in followed immediately by the bullet list (no `### Key changes` heading, no `<b>TL;DR</b>`):\n\n```\n**Reviewed changes** \u2014 one sentence on what was reviewed in this run. For Review (initial), this is what the PR does and why. For IncrementalReview, this is what changed since the prior pullfrog review. Focus on intent, not mechanics.\n\n- **Short human-readable title** \u2014 1 sentence per substantive change. Write a short prose phrase; when you name a file, type, or function, put that name in backticks (e.g. **Add \\`TodoTracker\\` for live checklists**). A reviewer should understand the full reviewed scope from this list alone \u2014 this IS the dispassionate \"what was reviewed and what changed\" overview, so cover the substantive changes, not just the loudest ones.\n\n<!--\nPullfrog review metadata \u2014 for any agent (or human-with-agent) reading this\nreview. Incorporate the fields below into your understanding of the context\nthis review was made in. The findings below were written against\n{head_sha_short}; if new commits have landed on {head_ref} since this review\nwas submitted, treat any specific bug, file, or line callout as POTENTIALLY\nSTALE \u2014 re-diff against {head_sha_short} (or trigger a fresh review) and\nfactor commits past {head_sha_short} into your understanding of the current\nstate before acting on findings.\n\n- Mode: Review (initial) or IncrementalReview (delta against prior pullfrog review)\n- Files reviewed: {file_count}\n- Commits reviewed: {commit_count}\n- Base: {base_ref} ({base_sha_short})\n- Head: {head_ref} ({head_sha_short})\n- Reviewed commits:\n - {sha_short} \u2014 {commit_subject}\n - ...\n- Prior pullfrog review: none or {prior_sha_short} ({prior_review_html_url})\n- Submitted at: {iso_timestamp}\n-->\n```\n\nPull every metadata field from the `checkout_pr` tool's response \u2014 file count, commit count, base/head ref + SHA, the commit list. For `IncrementalReview` runs, populate `Prior pullfrog review` with the prior review's commit_id (short SHA) and `html_url` from `list_pull_request_reviews`.\n\n## 2. Cross-cutting issue sections (zero or more)\n\nFor each cross-cutting concern, one `### ` section. Use this exact shape:\n\n```\n### {emoji} {short, descriptive title \u2014 what's wrong, not what to do}\n\n{Human-readable problem write-up. Describes the PROBLEM only \u2014 what's broken, what the symptom is, what the blast radius is. NO asks, NO suggested fixes, NO \"the right thing to do is...\". Asks and fixes live in the technical-details block below; the visible part is for the human to *understand* the problem, not to implement it.}\n\n<details><summary>Technical details</summary>\n\n\\`\\`\\`\\`markdown\n# {title repeated}\n\n## Affected sites\n- {file path:line} \u2014 {what's wrong there}\n- ...\n\n## Required outcome\n- {what the fix needs to achieve, not how to achieve it}\n- ...\n\n## Suggested approach (optional)\n{When the fix shape is non-obvious, sketch one or more reasonable directions. Skip when the outcome alone makes the fix obvious.}\n\n## Open questions for the human (optional)\n- {Any decision an implementing agent shouldn't make unilaterally \u2014 pricing thresholds, breaking-change policy, naming, scope of follow-up.}\n\\`\\`\\`\\`\n\n</details>\n```\n\nConcrete example of the visible part of a non-anchored section (technical-details block unchanged from the template above):\n\n```\n### \u2139\uFE0F Legacy `opencode.ts` has no documented deletion plan\n\nThe v2 harness lands alongside the v1 file and imports one helper from it. Worth a follow-up issue or a TODO so the next maintainer doesn't have to re-derive the cleanup plan.\n```\n\nThe example's value is its *shape*: a finding about absence (no deletion plan), not a line-anchored bug. Body sections live or die on whether the concern genuinely doesn't fit on a line.\n\n**Heading severity emoji** \u2014 every `### ` heading carries one:\n\n- \uD83D\uDEA8 critical \u2014 blocks merge (data loss, security, broken core flow)\n- \u26A0\uFE0F important \u2014 must address before merging (regression, missing validation, incorrect behavior)\n- \u2139\uFE0F informational \u2014 surfaced for awareness; mergeable as-is\n\n**Visible problem write-up rules:**\n\n- **No asks, no suggested fixes** in the visible part. The visible portion describes the problem; the technical-details block describes the fix shape and any open questions. The exception: a fix so self-evident that NOT stating it would be weird (e.g. \"the typo is missing an 'r'\") \u2014 in that case, fold it into the problem statement and skip the suggested-approach block in technical details too.\n- **Never two successive plain paragraphs.** Every transition between block-level elements must alternate prose with structure: paragraph \u2192 bullet list \u2192 paragraph; paragraph \u2192 code fence \u2192 bullet list; paragraph \u2192 table \u2192 paragraph. Two consecutive paragraphs in a row create a wall of text that's impossible to digest. If you catch yourself writing one, find a way to split it: pull a list out of it, drop a 2-3 line code fence between them, or merge them into a single tighter paragraph.\n- **Per-paragraph budget:** ~3 sentences max. Past that, you're explaining where you should be structuring.\n- **Identifier discipline still applies** in the visible part. Lead with behavior in plain English; name an identifier only when it's the subject of the concern or a public surface a reader would recognize. The technical-details block is where dense identifier references belong.\n\n**Technical-details block rules:**\n\n- Wrapped in a 4-backtick markdown fence (`\\`\\`\\`\\`markdown ... \\`\\`\\`\\``) so it's visually distinct, one-click copyable, and can contain its own 3-backtick code fences without escape gymnastics. The contents are agent-readable \u2014 a fix-agent will pull the body down and use this block as the brief.\n- File paths and `file:line` refs are encouraged (and necessary) \u2014 the next agent uses these to navigate. Identifier density is fine here.\n- Slightly more verbose than the absolute minimum is OK when it materially helps the next agent: a small code snippet showing the symptom, a short table of mismatched key/column pairs, a one-paragraph \"why CI doesn't catch it\" note. Skip massive regression-test scaffolding or full route rewrites \u2014 the implementing agent writes those.\n- Use the four standard sections (`Affected sites`, `Required outcome`, optional `Suggested approach`, optional `Open questions for the human`). Skip the optional sections when they wouldn't add anything.\n\n## Inline technical details\n\nInline comments are short (~2-3 sentences) by default. When an inline finding has broader implications worth recording for a fix-agent \u2014 e.g. a localized bug whose proper fix requires touching several files, or where the right fix depends on a design decision the human needs to make \u2014 append a collapsed `<details><summary>Technical details</summary>` block to the inline comment's body. Same shape as the body-section technical-details block (4-backtick fenced markdown, `## Affected sites` / `## Required outcome` / optional `## Suggested approach` / optional `## Open questions for the human`).\n\nGitHub renders the same markdown parser in inline comments as in the review body, so the collapsed-details affordance works the same way. The visible part of the inline comment stays scannable; the depth is one click away for any agent that needs it.\n\n## 3. `### \u2139\uFE0F Nitpicks` (optional, last section)\n\nOnly when there are nits that for some reason can't be inlined. Filepaths in nit text are fine \u2014 these are simple enough that a human or agent reads once and acts. No technical-details block.\n\n```\n### \u2139\uFE0F Nitpicks\n\n- {nit, with file path inline if useful, \u2264 ~200 chars}\n- ...\n```\n\n## Inline comment shape\n\nInline comments use the same severity framing as body `### ` sections, scaled down for line-anchored use:\n\n- **Lead with a 1-2 sentence problem statement.** The reader is looking at the line in question, so don't restate what the line says \u2014 describe what's wrong with it. Optionally prefix the visible line with a severity emoji (\uD83D\uDEA8 / \u26A0\uFE0F / \u2139\uFE0F) when severity isn't obvious from context.\n- **Optional `<details><summary>Technical details</summary>...</details>` collapsible** for findings whose technical context (longer file:line references, related-code snippets, suggested approach, regression-risk notes) would overwhelm the human-readable lead-in. Same agent-readable purpose, same 4-backtick fence shape, and same 4-section structure as the body's technical-details block \u2014 see *Inline technical details* above. Encouraged whenever the depth helps a downstream fix-agent; don't force one when the inline lead-in already says everything.\n- **Visible portion \u2264 2-3 sentences.** If you find yourself writing more, that's the cue to split the depth into the `Technical details` collapsible.\n\n## Body-wide rules\n\n- **Inline-vs-body discipline (repeated for emphasis):** anything that anchors to a specific line goes inline (with a `<details>Technical details</details>` block when the implications are broad). The body is for non-anchorable concerns only \u2014 absence, sequencing, design decisions, scope questions, architectural risk.\n- **No `### Issues found` heading** above the issue sections \u2014 each `### ` heading IS the issue.\n- **Severity emoji on every `### ` heading** (\uD83D\uDEA8 / \u26A0\uFE0F / \u2139\uFE0F). No emoji on the preamble lead-in or anywhere else.\n- **GitHub block-level rendering**: GitHub's markdown parser requires a blank line between ALL block-level elements (HTML tags like `<br/>`, `<sub>`, `<details>`, `<b>` and markdown syntax like headings, lists, blockquotes, code fences, paragraphs). Without a blank line, GitHub treats following content as a continuation of the HTML block and renders markdown syntax as literal text. ALWAYS separate block-level elements with a blank line.\n- **Backtick-wrap** every variable, identifier, or file name when you mention one (in either visible or technical-details portions).\n- **Don't repeat diff content**, don't include raw `+123 / -45` stats, don't include a changelog section, don't use horizontal rules (`---`).\n- **Pull file/commit counts from `checkout_pr` metadata** \u2014 never count manually.\n- **Legacy headings REMOVED.** Do not use `### Key changes`, `### Issues found`, `<b>TL;DR</b>`, or `<sub><b>Summary</b>`. The new structure subsumes them.";
8
- export declare function computeModes(agentId: AgentId): Mode[];
8
+ export declare function computeModes(agentId: AgentId, signedCommits?: boolean): Mode[];
9
9
  export declare const modes: Mode[];
10
10
  /**
11
11
  * modes that legitimately never modify the working tree. used by the post-run
@@ -32,6 +32,40 @@ export type CommentableLines = {
32
32
  RIGHT: Set<number>;
33
33
  LEFT: Set<number>;
34
34
  };
35
+ /** access tier of a checked-out repo, drives token routing + push gating. */
36
+ export type RepoAccess = "primary" | "write" | "read";
37
+ /**
38
+ * per-repo working-tree + PR state. the primary repo is keyed like any other
39
+ * in `toolState.repos`; secondaries are added by `checkout_repo`. this is the
40
+ * single source of truth for all repo-scoped git/PR state — there are no
41
+ * parallel inline fields on `ToolState`. read via `primaryRepoState(toolState)`
42
+ * for primary-only call sites, or `requireRepoState(toolState, owner, name)`
43
+ * when a `repo` param routes to a specific checkout.
44
+ */
45
+ export interface RepoToolState {
46
+ owner: string;
47
+ name: string;
48
+ /** working directory: primary = `process.cwd()`; secondary = `ctx.tmpdir/xrepo/<name>` */
49
+ dir: string;
50
+ access: RepoAccess;
51
+ defaultBranch?: string;
52
+ pushUrl?: string;
53
+ pushDest?: StoredPushDest;
54
+ initialHead?: {
55
+ kind: "branch";
56
+ name: string;
57
+ } | {
58
+ kind: "detached";
59
+ sha: string;
60
+ };
61
+ issueNumber?: number;
62
+ checkoutSha?: string;
63
+ commentableLinesByFile?: Map<string, CommentableLines>;
64
+ commentableLinesPullNumber?: number;
65
+ commentableLinesCheckoutSha?: string | undefined;
66
+ beforeSha?: string;
67
+ diffCoverage?: DiffCoverageState | undefined;
68
+ }
35
69
  /**
36
70
  * mutable per-run record of facts that occurred during execution. shared
37
71
  * between the action process and the MCP server (one process — toolState is
@@ -55,21 +89,8 @@ export type CommentableLines = {
55
89
  * MCP tool already records.
56
90
  */
57
91
  export interface ToolState {
58
- pushUrl?: string;
59
- pushDest?: StoredPushDest;
60
- initialHead?: {
61
- kind: "branch";
62
- name: string;
63
- } | {
64
- kind: "detached";
65
- sha: string;
66
- };
67
- issueNumber?: number;
68
- checkoutSha?: string;
69
- commentableLinesByFile?: Map<string, CommentableLines>;
70
- commentableLinesPullNumber?: number;
71
- commentableLinesCheckoutSha?: string | undefined;
72
- beforeSha?: string;
92
+ repos: Map<string, RepoToolState>;
93
+ primaryRepoKey: string;
73
94
  selectedMode?: string;
74
95
  prepushFailureCount: number;
75
96
  backgroundProcesses: Map<string, BackgroundProcess>;
@@ -94,6 +115,7 @@ export interface ToolState {
94
115
  lastProgressBody?: string;
95
116
  wasUpdated?: boolean;
96
117
  finalSummaryWritten?: boolean;
118
+ answerCommentPosted?: boolean;
97
119
  existingPlanCommentId?: number;
98
120
  previousPlanBody?: string;
99
121
  summaryFilePath?: string;
@@ -102,15 +124,23 @@ export interface ToolState {
102
124
  learningsFilePath?: string;
103
125
  learningsSeed?: string;
104
126
  learningsPersistAttempted?: boolean;
127
+ xrepoLearningsFilePath?: string;
128
+ xrepoLearningsSeed?: string;
129
+ xrepoLearningsPersistAttempted?: boolean;
105
130
  output?: string | undefined;
106
131
  usageEntries: AgentUsage[];
107
132
  model?: string | undefined;
108
133
  modelFallback?: {
109
134
  from: string;
110
135
  } | undefined;
136
+ unselectedProxyDefault?: boolean | undefined;
137
+ modelClamped?: {
138
+ from: string;
139
+ reason: "card" | "noRouterPath";
140
+ } | undefined;
141
+ shaPinned?: boolean | undefined;
111
142
  oss?: boolean | undefined;
112
143
  todoTracker?: TodoTracker | undefined;
113
- diffCoverage?: DiffCoverageState | undefined;
114
144
  agentDiagnostic?: AgentDiagnostic | undefined;
115
145
  }
116
146
  interface InitToolStateParams {
@@ -118,6 +148,26 @@ interface InitToolStateParams {
118
148
  id: string;
119
149
  type: ProgressCommentType;
120
150
  } | undefined;
151
+ owner: string;
152
+ name: string;
153
+ dir: string;
121
154
  }
122
155
  export declare function initToolState(params: InitToolStateParams): ToolState;
156
+ /** stable "owner/name" key for the `repos` map. lowercased — GitHub repo names are case-insensitive. */
157
+ export declare function repoKey(owner: string, name: string): string;
158
+ /** the primary (triggering) repo's state. throws if init hasn't run. */
159
+ export declare function primaryRepoState(toolState: ToolState): RepoToolState;
160
+ /**
161
+ * state for a specific checked-out repo. throws when the repo isn't a
162
+ * registered checkout — keeps the push/PR allow-set honest (the only way a
163
+ * secondary becomes registered is via `checkout_repo`).
164
+ */
165
+ export declare function requireRepoState(toolState: ToolState, owner: string, name: string): RepoToolState;
166
+ /** get-or-create a repo's state (used by checkout_repo when registering a secondary). */
167
+ export declare function ensureRepoState(toolState: ToolState, init: {
168
+ owner: string;
169
+ name: string;
170
+ dir: string;
171
+ access: RepoAccess;
172
+ }): RepoToolState;
123
173
  export {};
@@ -0,0 +1,40 @@
1
+ /** one working-tree change to include in an API commit. */
2
+ export type ChangedFile = {
3
+ path: string;
4
+ deleted: boolean;
5
+ };
6
+ /**
7
+ * all working-tree changes vs HEAD: tracked changes (committed-to-index,
8
+ * staged, and unstaged all collapse into `git diff HEAD`) plus untracked
9
+ * files from `git status`. respects .gitignore. throws on unresolved
10
+ * conflicts — the caller's guidance is to resolve and `git add` first.
11
+ */
12
+ export declare function detectWorkingTreeChanges(): ChangedFile[];
13
+ /**
14
+ * refuse content the API path cannot faithfully commit: git-lfs files (the
15
+ * pointer would be committed without the lfs object upload that the git
16
+ * pre-push hook performs) and directories (nested repositories / submodule
17
+ * pointers). other clean filters are fine — blob content goes through
18
+ * `git hash-object`, which applies them exactly like a local commit.
19
+ */
20
+ export declare function assertApiCommittable(files: ChangedFile[]): Promise<void>;
21
+ /**
22
+ * create one GitHub-signed commit on `remoteBranch` containing `files` read
23
+ * from the working tree, parented on `parents` (first parent supplies the
24
+ * base tree; a second parent — MERGE_HEAD — makes it a true merge commit so
25
+ * base-branch integration doesn't pollute the PR diff). empty `files` with
26
+ * two parents concludes a merge that resolved to the first parent's tree.
27
+ * creates the remote branch at the new commit when it doesn't exist yet.
28
+ */
29
+ export declare function createSignedCommit(params: {
30
+ token: string;
31
+ owner: string;
32
+ repo: string;
33
+ remoteBranch: string;
34
+ message: string;
35
+ parents: string[];
36
+ files: ChangedFile[];
37
+ }): Promise<{
38
+ sha: string;
39
+ createdBranch: boolean;
40
+ }>;
@@ -27,8 +27,27 @@ export declare function validateAgentApiKey(params: {
27
27
  * {"type":"error","error":{"type":"authentication_error", ...
28
28
  * "Invalid bearer token"}}`) emitted by the Claude CLI for revoked /
29
29
  * mistyped / rotated `ANTHROPIC_API_KEY`. see #782.
30
+ * - expired credentials (#931): Bedrock 403 `Failed to authenticate. API
31
+ * Error: 403 {"Message":"*** has expired"}` (short-lived bearer tokens),
32
+ * OpenAI OAuth "Your authentication token has expired", and Codex
33
+ * "Token refresh failed: 401". the Bedrock pattern is anchored to the
34
+ * Claude CLI emission ("Failed to authenticate. API Error:") so generic
35
+ * auth chatter in agent stderr can't misclassify a hang as a key error.
36
+ * - DeepSeek invalid key (#960): `Authentication Fails, Your api key:
37
+ * ****XXXX is invalid`. anchored to "Your api key: ... is invalid" so it
38
+ * can't collide with DeepSeek's already-handled `Insufficient balance`
39
+ * billing shape (which routes to formatProviderBillingExhausted).
30
40
  */
31
41
  export declare function isApiKeyAuthError(text: string): boolean;
42
+ /**
43
+ * Expired OAuth-connection credential shapes (#931) — the fix is to
44
+ * re-authenticate the provider connection (`pullfrog auth <provider>`), not
45
+ * to rotate a repo-secret API key, so `formatApiKeyErrorSummary` renders
46
+ * distinct copy for these. Patterns are deliberately narrow:
47
+ * "authentication token has expired" (not bare "token has expired") so a
48
+ * GitHub installation-token expiry can't be misread as an LLM key problem.
49
+ */
50
+ export declare function isOAuthCredentialExpiredError(text: string): boolean;
32
51
  /**
33
52
  * Friendly Markdown summary for both the missing-key and invalid-key cases.
34
53
  * Used in the catch / result-failure paths in `main.ts` to overwrite the raw
@@ -24,6 +24,33 @@ export interface BuildPullfrogFooterParams {
24
24
  * so the substitution is visible in PR comments + reviews.
25
25
  */
26
26
  fallbackFrom?: string | undefined;
27
+ /**
28
+ * When a Router account had a model (or the intelligent tier) selected that
29
+ * the server clamped to the efficient default — custom picks are card-gated
30
+ * wholesale. `from` is the configured slug (e.g. "anthropic/claude-opus");
31
+ * `reason` names the binding constraint — "card" (no card on file) renders
32
+ * `Using <Kimi K2> (<Claude Opus> needs a card on file)`, "noRouterPath"
33
+ * (no openRouterResolve yet and no stored provider key) renders a
34
+ * provider-key nudge — so the downgrade is visible rather than silently
35
+ * presenting Kimi as the pick.
36
+ */
37
+ clamped?: {
38
+ from: string;
39
+ reason: "card" | "noRouterPath";
40
+ } | undefined;
41
+ /**
42
+ * true when the run used the default proxy model only because no model was
43
+ * selected (Router billing + "auto"). the footer appends a note nudging the
44
+ * user to pick a model — the cost-optimized default is a weaker reviewer
45
+ * than a frontier model.
46
+ */
47
+ unselectedProxyDefault?: boolean | undefined;
48
+ /**
49
+ * true when the action is pinned to a full commit SHA — the footer leads
50
+ * with a maintenance nudge to switch to the moving `@v0` tag (a SHA pin
51
+ * freezes the post-run cleanup step, which silently fails the workflow).
52
+ */
53
+ shaPinned?: boolean | undefined;
27
54
  /**
28
55
  * true when the run's model costs are covered by the Pullfrog for OSS
29
56
  * program — the footer renders `Using <model> (free via Pullfrog for OSS)`
@@ -0,0 +1,30 @@
1
+ export type SubscriptionPreflight = {
2
+ usable: true;
3
+ } | {
4
+ usable: false;
5
+ reason: string;
6
+ };
7
+ /**
8
+ * preflight a Claude subscription OAuth token (`CLAUDE_CODE_OAUTH_TOKEN`)
9
+ * with a 1-token Messages call, so the agent can fall back to
10
+ * `ANTHROPIC_API_KEY` when the subscription is exhausted or revoked instead
11
+ * of failing the whole run at its first model call. rides the same de-facto
12
+ * OAuth surface Claude Code itself uses: Bearer auth + the
13
+ * `claude-code-20250219,oauth-2025-04-20` betas + the identity system prompt.
14
+ *
15
+ * probes the run's own model when known — subscription limits can be
16
+ * per-model ("You've hit your Opus limit"), so a cheaper stand-in could pass
17
+ * preflight and still leave the run dead on arrival.
18
+ *
19
+ * fail-open by design: only 401 (revoked/expired token) and 429
20
+ * (session/weekly/per-model limit) mark the token unusable. network errors,
21
+ * 5xx, and request-shape drift (400) all keep today's subscription-first
22
+ * behavior, so the preflight can never fail a run that would have worked —
23
+ * the worst wrong answer is a run that bills the API key instead of the
24
+ * subscription.
25
+ */
26
+ export declare function preflightClaudeSubscription(params: {
27
+ token: string;
28
+ /** bare Anthropic model id the run will use (e.g. "claude-fable-5") */
29
+ model: string | undefined;
30
+ }): Promise<SubscriptionPreflight>;
@@ -1,4 +1,4 @@
1
- import type { ToolState } from "../toolState.ts";
1
+ import { type ToolState } from "../toolState.ts";
2
2
  interface ReportErrorParams {
3
3
  toolState: ToolState;
4
4
  error: string;
@@ -6,7 +6,7 @@ interface ReportErrorParams {
6
6
  /**
7
7
  * When the run has no pre-existing progress comment to update (silent
8
8
  * IncrementalReview / pull_request_synchronize, mode-less polls), create
9
- * a fresh issue comment on `toolState.issueNumber` instead of returning
9
+ * a fresh issue comment on the primary repo state's `issueNumber` instead of returning
10
10
  * silently. Used for terminal errors (BillingError, TransientError) where
11
11
  * the GH job summary is the only other surface and most users never open
12
12
  * it. see #775.