pullfrog 0.0.201 → 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.
@@ -5,6 +5,9 @@ export type FormatFilesResult = {
5
5
  content: string;
6
6
  toc: string;
7
7
  };
8
+ export type FetchAndFormatPrDiffResult = FormatFilesResult & {
9
+ files: PullFile[];
10
+ };
8
11
  /**
9
12
  * formats PR files with explicit line numbers for each code line.
10
13
  * preserves all original diff info (file headers, hunk headers) and adds:
@@ -19,6 +22,7 @@ export type CheckoutPrResult = {
19
22
  success: true;
20
23
  number: number;
21
24
  title: string;
25
+ body: string | null;
22
26
  base: string;
23
27
  localBranch: string;
24
28
  remoteBranch: string;
@@ -29,13 +33,21 @@ export type CheckoutPrResult = {
29
33
  diffPath: string;
30
34
  incrementalDiffPath?: string | undefined;
31
35
  toc: string;
36
+ commitCount: number;
37
+ commitLog: string;
38
+ /** true when commitLog was capped because the PR has more commits than we render */
39
+ commitLogTruncated: boolean;
40
+ /** true when commit metadata could not be computed (e.g. base ref unreachable after shallow fetch). commitCount/commitLog are zero/empty in that case, not "no commits". */
41
+ commitLogUnavailable: boolean;
42
+ /** non-fatal warning from the post-checkout lifecycle hook, if any */
43
+ hookWarning?: string | undefined;
32
44
  instructions: string;
33
45
  };
34
46
  /**
35
47
  * fetches PR files from GitHub and formats them with line numbers and TOC.
36
48
  * this is the core diff formatting logic, extracted for testability.
37
49
  */
38
- export declare function fetchAndFormatPrDiff(ctx: ToolContext, pullNumber: number): Promise<FormatFilesResult>;
50
+ export declare function fetchAndFormatPrDiff(ctx: ToolContext, pullNumber: number): Promise<FetchAndFormatPrDiffResult>;
39
51
  import type { GitContext } from "../utils/setup.ts";
40
52
  export type PrData = {
41
53
  number: number;
@@ -54,7 +66,9 @@ type CheckoutPrBranchParams = GitContext & {
54
66
  * Assumes origin remote is already configured with authentication.
55
67
  * Updates toolState.issueNumber, toolState.checkoutSha, and toolState.pushUrl (for fork PRs).
56
68
  */
57
- export declare function checkoutPrBranch(pr: PrData, params: CheckoutPrBranchParams): Promise<void>;
69
+ export declare function checkoutPrBranch(pr: PrData, params: CheckoutPrBranchParams): Promise<{
70
+ hookWarning?: string | undefined;
71
+ }>;
58
72
  export declare function CheckoutPrTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
59
73
  pull_number: number;
60
74
  }, {
@@ -5,6 +5,7 @@ import type { ToolContext } from "./server.ts";
5
5
  * and hasn't been updated with progress or error messages.
6
6
  */
7
7
  export declare const LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
8
+ export declare function isLeapingIntoActionCommentBody(body: string): boolean;
8
9
  export declare function addFooter(ctx: ToolContext, body: string): string;
9
10
  export declare const Comment: import("arktype/internal/variants/object.ts").ObjectType<{
10
11
  issueNumber: number;
@@ -0,0 +1,17 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { Tool } from "fastmcp";
3
+ import type { ToolContext } from "./server.ts";
4
+ /**
5
+ * Recursively transform a JSON schema to gemini's stricter subset.
6
+ * See module header for the exact transforms applied.
7
+ */
8
+ export declare function sanitizeForGemini(schema: unknown): unknown;
9
+ export declare function wrapSchemaForGemini(schema: StandardSchemaV1<any>): StandardSchemaV1<any>;
10
+ export declare function sanitizeToolForGemini<T extends Tool<any, any>>(tool: T): T;
11
+ /**
12
+ * true when the effective upstream model is served by google's generative
13
+ * language API — directly (`google/*`), via opencode (`opencode/gemini-*`),
14
+ * or via openrouter (`openrouter/google/gemini-*`). slug-substring match
15
+ * works because every gemini route's model id contains "gemini".
16
+ */
17
+ export declare function isGeminiRouted(ctx: ToolContext): boolean;
package/dist/mcp/git.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { ToolContext } from "./server.ts";
2
+ export declare function rejectIfLeadingDash(value: string, kind: string): void;
3
+ export declare function rejectSpecialRef(value: string, kind: string): void;
4
+ export declare function validateTagName(tag: string): void;
2
5
  export declare const PushBranch: import("arktype/internal/variants/object.ts").ObjectType<{
3
6
  force: import("arktype/internal/attributes.ts").Default<boolean, false>;
4
7
  branchName?: string;
@@ -10,11 +13,14 @@ export declare function PushBranchTool(ctx: ToolContext): import("fastmcp").Tool
10
13
  branchName?: string;
11
14
  force?: boolean;
12
15
  }>>;
16
+ export declare const AUTH_REQUIRED_REDIRECT: Record<string, string>;
17
+ export declare const NOSHELL_BLOCKED_SUBCOMMANDS: Record<string, string>;
18
+ export declare const NOSHELL_BLOCKED_ARGS: string[];
13
19
  export declare function GitTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
14
- subcommand: string;
20
+ command: string;
15
21
  args?: string[];
16
22
  }, {
17
- subcommand: string;
23
+ command: string;
18
24
  args?: string[];
19
25
  }>>;
20
26
  export declare function GitFetchTool(ctx: ToolContext): import("fastmcp").Tool<any, import("@standard-schema/spec").StandardSchemaV1<{
@@ -1,4 +1,102 @@
1
+ import type { RestEndpointMethodTypes } from "@octokit/rest";
1
2
  import type { ToolContext } from "./server.ts";
3
+ export type CommentableLines = {
4
+ RIGHT: Set<number>;
5
+ LEFT: Set<number>;
6
+ };
7
+ /**
8
+ * parse a PR file's patch to determine which line numbers on each side are
9
+ * valid anchors for inline comments. GitHub only accepts comments on lines
10
+ * inside a diff hunk: added/context lines on RIGHT, removed/context lines
11
+ * on LEFT.
12
+ */
13
+ export declare function commentableLinesForFile(patch: string | undefined): CommentableLines;
14
+ export declare function buildCommentableMap(ctx: ToolContext, pullNumber: number): Promise<Map<string, CommentableLines>>;
15
+ export type ReviewCommentInput = NonNullable<RestEndpointMethodTypes["pulls"]["createReview"]["parameters"]["comments"]>[number];
16
+ export interface DroppedComment {
17
+ path: string;
18
+ line: number;
19
+ startLine?: number | undefined;
20
+ side: "LEFT" | "RIGHT";
21
+ reason: string;
22
+ }
23
+ export declare function validateInlineComments(comments: ReviewCommentInput[], map: Map<string, CommentableLines>): {
24
+ valid: ReviewCommentInput[];
25
+ dropped: DroppedComment[];
26
+ };
27
+ export declare const MAX_DROPPED_COMMENT_LINES = 50;
28
+ /**
29
+ * reason a create_pull_request_review call should be skipped without hitting
30
+ * GitHub. returned by reviewSkipDecision; null means submit normally.
31
+ */
32
+ export type ReviewSkipDecision = {
33
+ kind: "no-issues";
34
+ reason: string;
35
+ } | {
36
+ kind: "empty-downgraded-approve";
37
+ reason: string;
38
+ };
39
+ /**
40
+ * decision returned by duplicateReviewDecision when a session has already
41
+ * submitted a review and the current call would be a duplicate.
42
+ */
43
+ export type DuplicateReviewDecision = {
44
+ kind: "already-submitted";
45
+ reviewId: number;
46
+ reason: string;
47
+ };
48
+ /**
49
+ * decide whether a second create_pull_request_review call in the same session
50
+ * is a duplicate of an earlier submission.
51
+ *
52
+ * the agent is instructed to call create_pull_request_review exactly once per
53
+ * Review-mode session (see action/modes.ts), but in practice it sometimes
54
+ * submits twice — once with substantive feedback, then again with the
55
+ * canonical "Reviewed — no issues found." body when the prompt's branch
56
+ * logic re-classifies non-blocking observations. the second submission is
57
+ * always redundant: the first review is the record, and the duplicate just
58
+ * adds noise to the PR.
59
+ *
60
+ * legitimate follow-up reviews after new commits ARE allowed: the
61
+ * new-commits-mid-review path advances toolState.checkoutSha past the
62
+ * previously reviewed sha, and a subsequent checkout_pr advances it again.
63
+ * any call where checkoutSha has moved past the prior reviewedSha is a real
64
+ * follow-up and goes through. anything else — same sha, or no checkoutSha
65
+ * to compare against — is a duplicate.
66
+ */
67
+ export declare function duplicateReviewDecision(params: {
68
+ existing: {
69
+ id: number;
70
+ reviewedSha: string | undefined;
71
+ } | undefined;
72
+ currentCheckoutSha: string | undefined;
73
+ }): DuplicateReviewDecision | null;
74
+ /**
75
+ * decide whether to skip a review submission before any network call.
76
+ *
77
+ * GitHub rejects `event: "COMMENT"` reviews with no body and no inline comments
78
+ * with HTTP 422 "Unprocessable Entity". two paths produce that shape:
79
+ *
80
+ * 1. `!approved` + empty body/comments: agent's "no issues found" result.
81
+ * skipping preserves the agent's intent (nothing to post is a fine
82
+ * outcome for a review run) without a spurious 422.
83
+ * 2. `approved` + `!prApproveEnabled` + empty body/comments: the runtime
84
+ * downgrades APPROVE to COMMENT when prApproveEnabled is off, and the
85
+ * resulting empty-COMMENT is exactly the shape GitHub 422s. skipping
86
+ * here surfaces the cause (downgrade + nothing to say) instead of an
87
+ * opaque 422 the agent can't recover from.
88
+ *
89
+ * legitimate bare approvals (`approved` + `prApproveEnabled`, no body/comments)
90
+ * are never skipped — GitHub accepts empty APPROVE reviews and the approval
91
+ * stamp itself is the review's content.
92
+ */
93
+ export declare function reviewSkipDecision(params: {
94
+ approved: boolean;
95
+ body: string | null | undefined;
96
+ hasComments: boolean;
97
+ prApproveEnabled: boolean;
98
+ }): ReviewSkipDecision | null;
99
+ export declare function formatDroppedCommentsNote(dropped: DroppedComment[]): string;
2
100
  export declare const CreatePullRequestReview: import("arktype/internal/variants/object.ts").ObjectType<{
3
101
  pull_number: number;
4
102
  body?: string;
@@ -40,6 +138,47 @@ export declare function CreatePullRequestReviewTool(ctx: ToolContext): import("f
40
138
  start_line?: number;
41
139
  }[];
42
140
  }>>;
141
+ /**
142
+ * clear a pending review draft stranded on the PR by a prior hard-killed run
143
+ * (workflow timeout, OOM) so the next createReview can succeed.
144
+ *
145
+ * GitHub enforces one-pending-review-per-user-per-PR. if the previous process
146
+ * died between createReview(PENDING) and submitReview, the draft remains and
147
+ * the next run's createReview 422s with "already has a pending review".
148
+ * listReviews only exposes PENDING reviews to their author, so filtering on
149
+ * state === "PENDING" is already scoped to the authed token's own draft.
150
+ *
151
+ * if `originalErr` is not a pending-review 422, or no leftover is found, this
152
+ * function rethrows `originalErr` so the caller surfaces the original failure.
153
+ * delete failures with 404 (draft already gone) or 422 (draft submitted by a
154
+ * concurrent caller) are swallowed — the caller's retry will succeed in both
155
+ * cases. any other delete error is rethrown unchanged.
156
+ *
157
+ * known limitation: if two runs on the SAME PR share the authed token and
158
+ * overlap in time, the loser's createReview 422s on the winner's still-active
159
+ * draft. recovery would then delete the winner's active draft and the
160
+ * winner's submitReview would 404. this is not distinguishable from a
161
+ * genuinely-stranded draft via the review object alone (PENDING reviews
162
+ * expose no created_at timestamp, and both reviews are authored by the same
163
+ * bot user). rely on workflow-level concurrency controls (e.g. a concurrency
164
+ * key keyed to the PR number) to prevent overlap.
165
+ */
166
+ export declare function clearStrandedPendingReview(ctx: ToolContext, params: {
167
+ owner: string;
168
+ repo: string;
169
+ pull_number: number;
170
+ originalErr: unknown;
171
+ }): Promise<void>;
172
+ /**
173
+ * single-step createReview (event != PENDING) with stranded-draft recovery.
174
+ * the body path goes through createAndSubmitWithFooter which already recovers
175
+ * from a stranded PENDING draft at its own createReview call. the no-body path
176
+ * used to call createReview directly with no recovery — so a PR whose previous
177
+ * body-path run crashed between createReview(PENDING) and submitReview would
178
+ * permanently 422 any subsequent no-body review (approve-with-no-feedback or
179
+ * comments-only) until a body-path run happened to clear the draft.
180
+ */
181
+ export declare function createReviewWithStrandedRecovery(ctx: ToolContext, params: RestEndpointMethodTypes["pulls"]["createReview"]["parameters"]): Promise<Awaited<ReturnType<typeof ctx.octokit.rest.pulls.createReview>>>;
43
182
  /**
44
183
  * report the review node ID so the WorkflowRun is marked as "review submitted".
45
184
  * exported for use in main.ts post-agent cleanup.
@@ -3,10 +3,12 @@ import type { AgentUsage } from "../agents/index.ts";
3
3
  import { type AgentId } from "../external.ts";
4
4
  import type { Mode } from "../modes.ts";
5
5
  import type { PrepResult } from "../prep/index.ts";
6
+ import type { DiffCoverageState } from "../utils/diffCoverage.ts";
6
7
  import type { OctokitWithPlugins } from "../utils/github.ts";
7
8
  import type { ResolvedPayload } from "../utils/payload.ts";
8
9
  import type { RunContextData } from "../utils/runContextData.ts";
9
10
  import type { TodoTracker } from "../utils/todoTracking.ts";
11
+ import type { CommentableLines } from "./review.ts";
10
12
  export type BackgroundProcess = {
11
13
  pid: number;
12
14
  outputPath: string;
@@ -29,6 +31,9 @@ export interface ToolState {
29
31
  pushDest?: StoredPushDest;
30
32
  issueNumber?: number;
31
33
  checkoutSha?: string;
34
+ commentableLinesByFile?: Map<string, CommentableLines>;
35
+ commentableLinesPullNumber?: number;
36
+ commentableLinesCheckoutSha?: string | undefined;
32
37
  beforeSha?: string;
33
38
  selectedMode?: string;
34
39
  backgroundProcesses: Map<string, BackgroundProcess>;
@@ -55,6 +60,7 @@ export interface ToolState {
55
60
  usageEntries: AgentUsage[];
56
61
  model?: string | undefined;
57
62
  todoTracker?: TodoTracker | undefined;
63
+ diffCoverage?: DiffCoverageState | undefined;
58
64
  }
59
65
  interface InitToolStateParams {
60
66
  progressCommentId: string | undefined;
@@ -78,6 +84,7 @@ export interface ToolContext {
78
84
  jobId: string | undefined;
79
85
  mcpServerUrl: string;
80
86
  tmpdir: string;
87
+ resolvedModel: string | undefined;
81
88
  }
82
89
  type JsonSchema = Record<string, unknown>;
83
90
  type McpHttpServerOptions = {
@@ -85,6 +92,11 @@ type McpHttpServerOptions = {
85
92
  };
86
93
  /**
87
94
  * Start the MCP HTTP server.
95
+ *
96
+ * The returned disposer is idempotent — safe to call multiple times.
97
+ * Callers (e.g. the inner activity-timeout handler in main.ts) may need to
98
+ * stop the server before the `await using` block exits; a subsequent
99
+ * automatic dispose is then a no-op.
88
100
  */
89
101
  export declare function startMcpHttpServer(ctx: ToolContext, options?: McpHttpServerOptions): Promise<{
90
102
  url: string;
@@ -18,4 +18,4 @@ export declare const handleToolError: (error: unknown) => ToolResult;
18
18
  * @param toolName - optional tool name for error logging
19
19
  */
20
20
  export declare const execute: <T, R extends Record<string, any> | string>(fn: (params: T) => Promise<R>, toolName?: string) => (params: T) => Promise<ToolResult>;
21
- export declare const addTools: (_ctx: ToolContext, server: FastMCP<any>, tools: Tool<any, any>[]) => FastMCP<any>;
21
+ export declare const addTools: (ctx: ToolContext, server: FastMCP<any>, tools: Tool<any, any>[]) => FastMCP<any>;
package/dist/models.d.ts CHANGED
@@ -60,10 +60,27 @@ export declare function getModelEnvVars(slug: string): string[];
60
60
  export declare const modelAliases: ModelAlias[];
61
61
  /** resolve a model slug to its concrete models.dev specifier (e.g. "anthropic/claude-opus-4-6") */
62
62
  export declare function resolveModelSlug(slug: string): string | undefined;
63
+ /**
64
+ * walk the fallback chain to the terminal (non-deprecated) alias.
65
+ * returns undefined if the chain is broken, exhausted, or cyclic.
66
+ *
67
+ * use this in UI display sites (dropdown trigger labels, PR-comment footers,
68
+ * etc.) so a deprecated stored slug renders as the model the user actually
69
+ * runs against — not the historical name. selectable lists should still hide
70
+ * deprecated aliases by filtering on `!a.fallback`.
71
+ */
72
+ export declare function resolveDisplayAlias(slug: string): ModelAlias | undefined;
63
73
  /**
64
74
  * resolve a model slug to the CLI-ready model string, following the fallback
65
75
  * chain when a model is deprecated. returns the first non-deprecated resolve
66
76
  * target, or undefined if the chain is exhausted or broken.
67
77
  */
68
78
  export declare function resolveCliModel(slug: string): string | undefined;
79
+ /**
80
+ * resolve a model slug to the OpenRouter-ready model string, following the
81
+ * fallback chain when a model is deprecated. returns undefined if the chain
82
+ * is exhausted/broken or the terminal alias has no openrouter equivalent
83
+ * (e.g. free opencode models).
84
+ */
85
+ export declare function resolveOpenRouterModel(slug: string): string | undefined;
69
86
  export {};
package/dist/modes.d.ts CHANGED
@@ -4,6 +4,6 @@ export interface Mode {
4
4
  description: string;
5
5
  prompt?: string | undefined;
6
6
  }
7
- export declare const PR_SUMMARY_FORMAT = "### Default format\n\nFollow this structure exactly:\n\n<b>TL;DR</b> \u2014 1-3 sentences on what the PR does and why. Focus on intent, not mechanics.\nNOTE: use HTML bold <b>TL;DR</b>, NOT markdown bold **TL;DR**.\n\n### Key changes\n\n- **Short human-readable title** \u2014 1 sentence per change. Write a short prose phrase (title case or sentence case); 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 PR from this list alone.\n\n<sub><b>Summary</b> \uFF5C {file_count} files \uFF5C {commit_count} commits \uFF5C base: `{base}` \u2190 `{head}`</sub>\nNOTE: the metadata line goes AFTER the bullet list, not before it.\n\nThen for each key change, a ## section with a short descriptive title that reads like a documentation heading (e.g. ## Live todo checklist tracking).\n\n<br/>\n\n## Example readable section title\n\n> **Before:** [old behavior/state]<br/>**After:** [new behavior/state]\nIMPORTANT: Before and After MUST be on a SINGLE blockquote line with an inline <br/> between them. Two separate `>` lines creates a double line break.\n\n1-2 sentences of explanation. Break up text with tables, blockquotes, or lists \u2014 NEVER 3+ plain paragraphs in a row.\n\nIf a change warrants deeper explanation, use a blockquoted details/summary framed as a question:\n> <details><summary>How does X work?</summary>\n> Extended explanation here.\n> </details>\n\nEnd each section with a file links trail (3-4 key files max):\n[`file.ts`](https://github.com/{owner}/{repo}/pull/{number}/files#diff-{sha256hex_of_filepath}) \u00B7 ...\n\nSingle-feature PRs: skip the ## sections. Fold before/after and explanation into the header after key changes.\n\nCRITICAL \u2014 GitHub markdown rendering rule:\nGitHub's markdown parser requires a blank line between ALL block-level elements. This includes transitions between: HTML tags (<br/>, <sub>, <details>, <b>, etc.) and markdown syntax (headings, lists, blockquotes, paragraphs). Without a blank line, GitHub treats the 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\nRules:\n- `##` titles and key-change bullet lead-ins are plain-language summaries; backtick only actual code tokens (files, types, functions) where they appear in the title\n- ALL variable names, identifiers, and file names in body text must be in backticks\n- ALL file references MUST link to the PR Files Changed view. Compute anchors by running `echo -n 'path/to/file.ts' | sha256sum` via shell for each file. NEVER fabricate hex strings \u2014 run the actual command. If shell is unavailable, omit the #diff- anchor rather than guessing.\n- Add <br/> before each ## heading for visual spacing. Do NOT use horizontal rules (---)\n- Do NOT include raw diff stats like '+123 / -45' or line counts\n- Do NOT include code blocks or repeat diff contents\n- Do NOT include a changelog section \u2014 the key changes list serves this purpose\n- Focus on *intent*, not *what* \u2014 the diff already shows what changed\n- Get the file count and commit count from the checkout_pr metadata, not by counting manually";
7
+ export declare const PR_SUMMARY_FORMAT = "### Default format\n\nFollow this structure exactly:\n\n<b>TL;DR</b> \u2014 1-3 sentences on what the PR does and why. Focus on intent, not mechanics.\nNOTE: use HTML bold <b>TL;DR</b>, NOT markdown bold **TL;DR**.\n\n### Key changes\n\n- **Short human-readable title** \u2014 1 sentence per change. Write a short prose phrase (title case or sentence case); 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 PR from this list alone.\n\n<sub><b>Summary</b> \uFF5C {file_count} files \uFF5C {commit_count} commits \uFF5C base: `{base}` \u2190 `{head}`</sub>\nNOTE: the metadata line goes AFTER the bullet list, not before it.\n\nThen for each key change, a ## section with a short descriptive title that reads like a documentation heading (e.g. ## Live todo checklist tracking).\n\n<br/>\n\n## Example readable section title\n\n> **Before:** [old behavior/state]<br/>**After:** [new behavior/state]\nIMPORTANT: Before and After MUST be on a SINGLE blockquote line with an inline <br/> between them. Two separate `>` lines creates a double line break.\n\n1-2 sentences of explanation. Break up text with tables, blockquotes, or lists \u2014 NEVER 3+ plain paragraphs in a row.\n\nIf a change warrants deeper explanation, use a blockquoted details/summary framed as a question:\n> <details><summary>How does X work?</summary>\n> Extended explanation here.\n> </details>\n\nEnd each section with a file links trail (3-4 key files max):\n[`file.ts`](https://github.com/{owner}/{repo}/pull/{number}/files#diff-{sha256hex_of_filepath}) \u00B7 ...\n\nSingle-feature PRs: skip the ## sections. Fold before/after and explanation into the header after key changes.\n\nCRITICAL \u2014 GitHub markdown rendering rule:\nGitHub's markdown parser requires a blank line between ALL block-level elements. This includes transitions between: HTML tags (<br/>, <sub>, <details>, <b>, etc.) and markdown syntax (headings, lists, blockquotes, paragraphs). Without a blank line, GitHub treats the 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\nRules:\n- `##` titles and key-change bullet lead-ins are plain-language summaries; backtick only actual code tokens (files, types, functions) where they appear in the title\n- ALL variable names, identifiers, and file names in body text must be in backticks\n- ALL file references MUST link to the PR Files Changed view. Use the `diff-<hex>` anchor precomputed next to each filename in the `checkout_pr` TOC \u2014 do NOT run `sha256sum` or any other shell command to compute anchors. NEVER fabricate hex strings. If a file is not in the TOC, omit the `#diff-` anchor rather than guessing.\n- Add <br/> before each ## heading for visual spacing. Do NOT use horizontal rules (---)\n- Do NOT include raw diff stats like '+123 / -45' or line counts\n- Do NOT include code blocks or repeat diff contents\n- Do NOT include a changelog section \u2014 the key changes list serves this purpose\n- Focus on *intent*, not *what* \u2014 the diff already shows what changed\n- Get the file count and commit count from the checkout_pr metadata, not by counting manually";
8
8
  export declare function computeModes(agentId: AgentId): Mode[];
9
9
  export declare const modes: Mode[];
@@ -0,0 +1,188 @@
1
+ ---
2
+ name: git-archaeology
3
+ description: Investigate how code reached its current state — when a line, function, import, or whole file was changed or deleted, who removed it, and what it looked like before. Use when `git blame` came up empty, when content has been refactored away, or when you need the full evolution of a function across commits.
4
+ ---
5
+
6
+ # Git history archaeology
7
+
8
+ `git blame` only sees what's still in the working tree. For anything that was
9
+ deleted, moved, or refactored away, you need the commands below. Most agents
10
+ under-use them and end up scrolling through `git log -p` instead.
11
+
12
+ ## Output discipline (read first)
13
+
14
+ `git log -p` on a long-lived file can dump tens of thousands of lines and blow
15
+ the context window. Always:
16
+
17
+ 1. **Start narrow.** Use `--oneline` or `--stat` to get a list of candidate
18
+ commits.
19
+ 2. **Drill in.** Use `git show <sha> -- <path>` for the diff of one specific
20
+ commit.
21
+ 3. **Scope the search.** Add `--since="3 months ago"`, `-n 20`, or a path
22
+ restriction (`-- <path>`) so output stays manageable.
23
+ 4. **Avoid `git log -p` without a path filter** on any non-trivial repo.
24
+
25
+ ## Decision tree (by agent intent)
26
+
27
+ ### "When did this exact line, string, or import disappear?"
28
+
29
+ ```bash
30
+ git log -S'<exact-string>' --oneline -- <file>
31
+ ```
32
+
33
+ The pickaxe. Returns commits that **changed the count** of that string in the
34
+ file. The most recent hit is typically the removal commit. Add `-p` only after
35
+ you've narrowed to a few candidates.
36
+
37
+ Notes:
38
+ - `-S` is exact-string by default. Add `--pickaxe-regex` to make it a regex.
39
+ - The argument is "cuddled" with `-S` (`-S'foo bar'`), no space.
40
+ - `-S` will not detect pure in-file moves (count unchanged). Use `-G` for that.
41
+ - `--pickaxe-all` shows the entire changeset of matching commits, useful when
42
+ a commit changes both a definition and its call sites in other files.
43
+
44
+ ### "When did the diff stop matching this regex?"
45
+
46
+ ```bash
47
+ git log -G'<regex>' --oneline -- <file>
48
+ ```
49
+
50
+ Like `-S` but matches any added or removed hunk line against the regex. Use
51
+ `-G` when:
52
+ - You don't know the exact string but know a pattern.
53
+ - You want to catch in-file moves (`-S` won't).
54
+ - You want to find any diff that touched a pattern, even if the count was
55
+ preserved (e.g., a refactor that changed call sites without removing the
56
+ function).
57
+
58
+ ### "How did this function evolve over time?"
59
+
60
+ ```bash
61
+ git log -L :<function-name>:<file>
62
+ ```
63
+
64
+ Every commit that touched the function, with diffs scoped to just the function
65
+ body. Works for languages git understands (most mainstream ones).
66
+
67
+ ### "How did lines N–M evolve?"
68
+
69
+ ```bash
70
+ git log -L <N>,<M>:<file>
71
+ ```
72
+
73
+ ### "What's the full history of this file, including across renames?"
74
+
75
+ ```bash
76
+ git log --follow --oneline -- <file> # overview
77
+ git log --follow -p -- <file> # with diffs (use sparingly)
78
+ ```
79
+
80
+ `--follow` only works for a single file, not directories.
81
+
82
+ ### "Where was a now-deleted line last present?"
83
+
84
+ Two-step pattern when you have an exact deleted string:
85
+
86
+ ```bash
87
+ # 1. find a historical commit that contained the string
88
+ git log -S'<deleted-string>' --oneline --all -- <file>
89
+
90
+ # 2. reverse-blame from that commit to find the last commit it survived in
91
+ git blame --reverse <old-sha>..HEAD -- <file>
92
+ ```
93
+
94
+ The reverse blame tells you, for each line, the last commit it survived in
95
+ before being modified or deleted. Pinpoints the exact deletion commit.
96
+
97
+ ### "This file no longer exists — when was it deleted, and what was in it?"
98
+
99
+ ```bash
100
+ # find all commits that touched the path, even on other branches
101
+ git log --all --full-history --oneline -- <deleted-path>
102
+
103
+ # the most recent of those is usually the deletion. confirm:
104
+ git show <sha> --stat
105
+
106
+ # view the file's contents at any commit where it existed
107
+ git show <sha>^:<deleted-path>
108
+ ```
109
+
110
+ If you don't know the path, find it from filename alone:
111
+
112
+ ```bash
113
+ # list all delete events with paths
114
+ git log --all --diff-filter=D --summary | grep -i '<filename>'
115
+
116
+ # or glob across all branches
117
+ git log --all --oneline -- '**/<filename>.*'
118
+ ```
119
+
120
+ ### "Who deleted it, in one shot?"
121
+
122
+ ```bash
123
+ git rev-list -n 1 HEAD -- <deleted-path> # the deletion commit
124
+ git show $(git rev-list -n 1 HEAD -- <deleted-path>) -- <deleted-path>
125
+ ```
126
+
127
+ ### "Restore a deleted file (locally, no commit)"
128
+
129
+ ```bash
130
+ git restore --source=<deletion-sha>^ -- <deleted-path>
131
+ # or, on older git:
132
+ git checkout <deletion-sha>^ -- <deleted-path>
133
+ ```
134
+
135
+ The `^` is critical — at the deletion commit the file is already gone, so we
136
+ read from its parent.
137
+
138
+ ### "Search commit messages, not content"
139
+
140
+ ```bash
141
+ git log --all --grep='<text>' --oneline
142
+ git log --all --grep='<text>' -i --oneline # case-insensitive
143
+ ```
144
+
145
+ Orthogonal to `-S`/`-G`, which only see the diff.
146
+
147
+ ## Standard workflow for "why does this code look like this"
148
+
149
+ 1. `git log --follow --oneline -- <file>` — overview of commits touching it.
150
+ 2. If a recent commit looks suspicious: `git show <sha> -- <file>`.
151
+ 3. If you expected to find something and it's missing:
152
+ `git log -S'<expected-string>' --oneline -- <file>`.
153
+ 4. For a specific function's full lifecycle:
154
+ `git log -L :<fn>:<file>`.
155
+ 5. For the deletion point of a known string: pickaxe to find an old commit
156
+ that contained it, then `git blame --reverse <old-sha>..HEAD -- <file>`.
157
+
158
+ ## Useful flags reference
159
+
160
+ | Flag | Effect |
161
+ |------|--------|
162
+ | `--all` | Search all refs, not just the current branch. Use when investigating something that may have lived only on a feature branch. |
163
+ | `--full-history` | Keeps commits that history-simplification would otherwise drop. Needed for accurate history across merges. |
164
+ | `--follow` | Track a single file across renames. Single-file only. |
165
+ | `-M` / `-C` | Detect renames (`-M`) and copies (`-C`) when reading diffs. |
166
+ | `--diff-filter=D` | Restrict to commits that **deleted** something. `A`=added, `M`=modified, `R`=renamed. |
167
+ | `--source` | When combined with `--all`, annotate each commit with the ref it was reached from. |
168
+ | `--pickaxe-all` | With `-S`/`-G`, show all files in the matching commit, not just the matching file. |
169
+ | `--pickaxe-regex` | Treat the `-S` argument as a regex. |
170
+ | `--since` / `--until` | Time-bound the search. Cheap perf win on big repos. |
171
+ | `-n <count>` | Cap result count. |
172
+ | `--stat` | Per-commit file stats instead of full patches. Good first pass. |
173
+
174
+ ## Notes and pitfalls
175
+
176
+ - Always include `--` before paths to disambiguate from refs (e.g.
177
+ `git log -S'foo' -- src/auth.ts`).
178
+ - `-S` triggers on **count change**. A pure refactor that moves a line within
179
+ the same file will not match. Use `-G` for those.
180
+ - `-G` runs diff twice and greps; it's slower than `-S`. Scope with paths and
181
+ `--since` on big repos.
182
+ - Without `--all`, `git log -- <path>` shows nothing if the path never existed
183
+ on the current branch. When in doubt, add `--all`.
184
+ - `git log --full-history -- <path>` alone has had bugs in some git versions
185
+ for deleted files; pair with `--all` for reliability.
186
+ - For files that were renamed, `git log -- <new-path>` only shows post-rename
187
+ history. Use `--follow` (one file) or `git log --all -- <old-path>` when
188
+ hunting across rename events.
@@ -1,5 +1,7 @@
1
1
  export declare const DEFAULT_ACTIVITY_TIMEOUT_MS = 300000;
2
2
  export declare const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5000;
3
+ export declare const ACTIVITY_NOISE_PATTERNS: readonly RegExp[];
4
+ export declare function isActivityNoise(chunk: string | Uint8Array): boolean;
3
5
  type ActivityTimeoutContext = {
4
6
  timeoutMs: number;
5
7
  checkIntervalMs: number;
@@ -7,6 +9,8 @@ type ActivityTimeoutContext = {
7
9
  export type ActivityTimeout = {
8
10
  promise: Promise<never>;
9
11
  stop: () => void;
12
+ /** force the timeout to reject immediately with a custom reason */
13
+ forceReject: (reason: string) => void;
10
14
  };
11
15
  /**
12
16
  * mark activity to reset the no-output timeout.
@@ -3,7 +3,9 @@ import type { Agent } from "../agents/index.ts";
3
3
  * resolve the effective model for this run.
4
4
  *
5
5
  * priority:
6
- * 1. PULLFROG_MODEL env var (explicit specifier override)
6
+ * 1. PULLFROG_MODEL env var resolved through the alias registry first,
7
+ * so values like "anthropic/claude-opus" become "anthropic/claude-opus-4-7".
8
+ * raw specifiers (e.g. "anthropic/claude-opus-4-6") pass through unchanged.
7
9
  * 2. slug from repo config / payload → alias registry
8
10
  * 3. undefined — agent will auto-select
9
11
  */
@@ -0,0 +1,62 @@
1
+ export type DiffLineRange = {
2
+ startLine: number;
3
+ endLine: number;
4
+ };
5
+ export type DiffTocEntry = {
6
+ filename: string;
7
+ startLine: number;
8
+ endLine: number;
9
+ };
10
+ export type DiffCoverageFileBreakdown = {
11
+ filename: string;
12
+ startLine: number;
13
+ endLine: number;
14
+ totalLines: number;
15
+ coveredLines: number;
16
+ coveredRanges: DiffLineRange[];
17
+ unreadRanges: DiffLineRange[];
18
+ };
19
+ export type DiffCoverageBreakdown = {
20
+ totalLines: number;
21
+ coveredLines: number;
22
+ unreadLines: number;
23
+ coveragePercent: number;
24
+ coveredRanges: DiffLineRange[];
25
+ unreadRanges: DiffLineRange[];
26
+ files: DiffCoverageFileBreakdown[];
27
+ };
28
+ export type DiffCoverageState = {
29
+ diffPath: string;
30
+ totalLines: number;
31
+ tocEntries: DiffTocEntry[];
32
+ coveredRanges: DiffLineRange[];
33
+ coveragePreflightRan: boolean;
34
+ lastBreakdown?: string | undefined;
35
+ };
36
+ export declare function countLines(params: {
37
+ content: string;
38
+ }): number;
39
+ export declare function parseDiffTocEntries(params: {
40
+ toc: string;
41
+ }): DiffTocEntry[];
42
+ export declare function createDiffCoverageState(params: {
43
+ diffPath: string;
44
+ totalLines: number;
45
+ toc: string;
46
+ }): DiffCoverageState;
47
+ export declare function recordDiffReadFromToolUse(params: {
48
+ state: DiffCoverageState | undefined;
49
+ toolName: string;
50
+ input: unknown;
51
+ cwd: string;
52
+ }): boolean;
53
+ export declare function getDiffCoverageBreakdown(params: {
54
+ state: DiffCoverageState;
55
+ }): DiffCoverageBreakdown;
56
+ export declare function renderDiffCoverageBreakdown(params: {
57
+ diffPath: string;
58
+ breakdown: DiffCoverageBreakdown;
59
+ }): string;
60
+ export declare function countLinesInRanges(params: {
61
+ ranges: DiffLineRange[];
62
+ }): number;
@@ -2,8 +2,20 @@ export interface ExecuteLifecycleHookParams {
2
2
  event: string;
3
3
  script: string | null;
4
4
  }
5
+ export interface LifecycleHookResult {
6
+ /**
7
+ * human-readable warning when the hook failed. includes retry guidance:
8
+ * transient spawn/exit errors are worth retrying, timeouts and
9
+ * persistent failures are not. absent when the hook succeeded or was
10
+ * skipped.
11
+ */
12
+ warning?: string;
13
+ }
5
14
  /**
6
15
  * execute a lifecycle hook script if one is configured.
7
- * runs the script in a bash shell with a timeout.
16
+ *
17
+ * soft-fails: instead of throwing on hook errors, returns a warning string
18
+ * so callers can choose whether to surface it (mcp tools) or upgrade it to
19
+ * a fatal error (setup/prepush). timeouts are flagged as non-retryable.
8
20
  */
9
- export declare function executeLifecycleHook(params: ExecuteLifecycleHookParams): Promise<void>;
21
+ export declare function executeLifecycleHook(params: ExecuteLifecycleHookParams): Promise<LifecycleHookResult>;