Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 250fe7eaa1 | |||
| 4a8c432a48 |
@@ -143506,10 +143506,15 @@ var findInstallationId = async (jwt2, repoOwner, repoName) => {
|
||||
);
|
||||
};
|
||||
async function acquireTokenViaGitHubApp(opts) {
|
||||
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||
throw new Error(
|
||||
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||
);
|
||||
}
|
||||
const repoContext = parseRepoContext();
|
||||
const config3 = {
|
||||
appId: process.env.GITHUB_APP_ID,
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||
repoOwner: repoContext.owner,
|
||||
repoName: repoContext.name
|
||||
};
|
||||
@@ -144705,6 +144710,33 @@ async function checkoutPrBranch(pullNumber, params) {
|
||||
forkUrl: isFork ? `https://github.com/${headRepo.full_name}.git` : void 0
|
||||
};
|
||||
}
|
||||
function deepenForBeforeSha(params) {
|
||||
const isShallow = $("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||
if (!isShallow) return;
|
||||
const maxIterations = 10;
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
try {
|
||||
$("git", ["cat-file", "-t", params.beforeSha], { log: false });
|
||||
log.debug(`\xBB before_sha ${params.beforeSha.slice(0, 7)} is now reachable`);
|
||||
return;
|
||||
} catch {
|
||||
}
|
||||
log.debug(
|
||||
`\xBB deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})`
|
||||
);
|
||||
try {
|
||||
$git("fetch", ["--deepen=50", "--no-tags", "origin"], {
|
||||
token: params.gitToken
|
||||
});
|
||||
} catch {
|
||||
log.debug(`\xBB deepen for before_sha failed (force-push may have rewritten history)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.debug(
|
||||
`\xBB before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits`
|
||||
);
|
||||
}
|
||||
function CheckoutPrTool(ctx) {
|
||||
return tool({
|
||||
name: "checkout_pr",
|
||||
@@ -144720,6 +144752,13 @@ function CheckoutPrTool(ctx) {
|
||||
shell: ctx.payload.shell,
|
||||
postCheckoutScript: ctx.postCheckoutScript
|
||||
});
|
||||
const event = ctx.payload.event;
|
||||
if ("before_sha" in event && event.before_sha) {
|
||||
deepenForBeforeSha({
|
||||
gitToken: ctx.gitToken,
|
||||
beforeSha: event.before_sha
|
||||
});
|
||||
}
|
||||
const pr = await ctx.octokit.rest.pulls.get({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
@@ -144975,7 +145014,7 @@ function fixDoubleEscapedString(str) {
|
||||
}
|
||||
|
||||
// mcp/comment.ts
|
||||
async function updatePlanCommentId(ctx, planCommentNodeId) {
|
||||
async function updateCommentNodeId(ctx, field, nodeId) {
|
||||
if (ctx.runId === void 0 || !ctx.apiToken) return;
|
||||
try {
|
||||
await retry(
|
||||
@@ -144987,7 +145026,7 @@ async function updatePlanCommentId(ctx, planCommentNodeId) {
|
||||
authorization: `Bearer ${ctx.apiToken}`,
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ planCommentNodeId }),
|
||||
body: JSON.stringify({ [field]: nodeId }),
|
||||
signal: AbortSignal.timeout(1e4)
|
||||
});
|
||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||
@@ -144995,11 +145034,11 @@ async function updatePlanCommentId(ctx, planCommentNodeId) {
|
||||
{
|
||||
maxAttempts: 3,
|
||||
delayMs: 2e3,
|
||||
label: "updatePlanCommentId"
|
||||
label: `updateCommentNodeId(${field})`
|
||||
}
|
||||
);
|
||||
} catch (error49) {
|
||||
log.warning(`updatePlanCommentId exhausted retries: ${error49}`);
|
||||
log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error49}`);
|
||||
}
|
||||
}
|
||||
async function buildCommentFooter(params) {
|
||||
@@ -145038,14 +145077,14 @@ async function addFooter(ctx, body) {
|
||||
var Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type.enumerated("Plan", "Comment").describe(
|
||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
||||
type: type.enumerated("Plan", "Summary", "Comment").describe(
|
||||
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||
).optional()
|
||||
});
|
||||
function CreateCommentTool(ctx) {
|
||||
return tool({
|
||||
name: "create_issue_comment",
|
||||
description: "Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.",
|
||||
description: "Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.",
|
||||
parameters: Comment,
|
||||
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||
const bodyWithFooter = await addFooter(ctx, body);
|
||||
@@ -145056,7 +145095,10 @@ function CreateCommentTool(ctx) {
|
||||
body: bodyWithFooter
|
||||
});
|
||||
if (commentType === "Plan" && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||
}
|
||||
if (commentType === "Summary" && result.data.node_id) {
|
||||
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
@@ -145128,7 +145170,7 @@ async function reportProgress(ctx, params) {
|
||||
});
|
||||
ctx.toolState.wasUpdated = true;
|
||||
if (isPlanMode && result2.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result2.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id);
|
||||
}
|
||||
return {
|
||||
commentId: result2.data.id,
|
||||
@@ -145154,7 +145196,7 @@ async function reportProgress(ctx, params) {
|
||||
});
|
||||
ctx.toolState.wasUpdated = true;
|
||||
if (isPlanMode && result2.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result2.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result2.data.node_id);
|
||||
}
|
||||
return {
|
||||
commentId: result2.data.id,
|
||||
@@ -145195,7 +145237,7 @@ async function reportProgress(ctx, params) {
|
||||
body: bodyWithPlanLink
|
||||
});
|
||||
if (updateResult.data.node_id) {
|
||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id);
|
||||
}
|
||||
return {
|
||||
commentId: updateResult.data.id,
|
||||
@@ -146219,6 +146261,7 @@ var NOSHELL_BLOCKED_SUBCOMMANDS = {
|
||||
bisect: "Blocked: git bisect run can execute arbitrary shell commands."
|
||||
};
|
||||
var NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
||||
var COLLAPSE_THRESHOLD = 200;
|
||||
var subcommandPattern = regex("^[a-z][a-z0-9-]*$");
|
||||
var Git = type({
|
||||
subcommand: type(subcommandPattern).describe("Git subcommand (e.g., 'status', 'log', 'diff')"),
|
||||
@@ -146253,6 +146296,14 @@ function GitTool(ctx) {
|
||||
}
|
||||
}
|
||||
const output = $("git", [subcommand, ...args2], { log: false });
|
||||
const lineCount = output.split("\n").length;
|
||||
if (lineCount > COLLAPSE_THRESHOLD) {
|
||||
log.group(`git ${subcommand} output (${lineCount} lines)`, () => {
|
||||
log.info(output);
|
||||
});
|
||||
} else if (output) {
|
||||
log.info(output);
|
||||
}
|
||||
return { success: true, output };
|
||||
})
|
||||
});
|
||||
@@ -147410,7 +147461,7 @@ function ResolveReviewThreadTool(ctx) {
|
||||
// mcp/selectMode.ts
|
||||
var SelectModeParams = type({
|
||||
mode: type.string.describe(
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')"
|
||||
),
|
||||
"issue_number?": type("number").describe(
|
||||
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
||||
@@ -147577,7 +147628,37 @@ An existing plan comment was found for this issue. Update that comment with the
|
||||
3. Finalize:
|
||||
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`
|
||||
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
|
||||
Summarize: `### Checklist
|
||||
|
||||
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`.
|
||||
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||
- the diff file path
|
||||
- PR metadata (title, file count, commit count, base/head branches)
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort.`,
|
||||
SummaryUpdate: `### Checklist (updating existing summary)
|
||||
|
||||
An existing summary comment was found for this PR. Update it rather than creating a new one.
|
||||
|
||||
1. Use \`previousSummaryBody\` from this response as the current summary to revise.
|
||||
2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`.
|
||||
3. Delegate a subagent with:
|
||||
- the diff file path and PR metadata
|
||||
- the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch
|
||||
- format instructions from EVENT INSTRUCTIONS (if any)
|
||||
- instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\`
|
||||
4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body.
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort.`
|
||||
};
|
||||
var modeInstructionParent = {
|
||||
IncrementalReview: "Review",
|
||||
@@ -147609,6 +147690,21 @@ async function fetchExistingPlanComment(ctx, issueNumber) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function fetchExistingSummaryComment(ctx, prNumber) {
|
||||
if (!ctx.apiToken) return null;
|
||||
try {
|
||||
const response = await apiFetch({
|
||||
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
||||
method: "GET",
|
||||
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||
signal: AbortSignal.timeout(1e4)
|
||||
});
|
||||
const data = await response.json();
|
||||
return response.ok && "commentId" in data ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function SelectModeTool(ctx) {
|
||||
return tool({
|
||||
name: "select_mode",
|
||||
@@ -147651,6 +147747,23 @@ function SelectModeTool(ctx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedMode.name === "Summarize") {
|
||||
const prNumber = ctx.payload.event.issue_number;
|
||||
if (prNumber !== void 0) {
|
||||
const existing = await fetchExistingSummaryComment(ctx, prNumber);
|
||||
if (existing !== null) {
|
||||
ctx.toolState.existingSummaryCommentId = existing.commentId;
|
||||
return {
|
||||
...buildOrchestratorGuidance(selectedMode, {
|
||||
...guidanceOpts,
|
||||
overrideGuidance: modeGuidance.SummaryUpdate
|
||||
}),
|
||||
existingSummaryCommentId: existing.commentId,
|
||||
previousSummaryBody: existing.body
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
||||
})
|
||||
});
|
||||
@@ -148426,6 +148539,21 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in
|
||||
5. **PROGRESS** - ${reportProgressInstruction}
|
||||
|
||||
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`
|
||||
},
|
||||
{
|
||||
name: "Summarize",
|
||||
description: "Summarize a PR with a structured comment that is updated in place on subsequent pushes",
|
||||
prompt: `Follow these steps.
|
||||
|
||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath.
|
||||
|
||||
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections \u2014 do not read the entire file unless the PR is small.
|
||||
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||
|
||||
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||
|
||||
${permalinkTip}`
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -149609,7 +149737,7 @@ import { isAbsolute, resolve } from "node:path";
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.179",
|
||||
version: "0.0.180",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
@@ -25898,10 +25898,15 @@ var findInstallationId = async (jwt, repoOwner, repoName) => {
|
||||
);
|
||||
};
|
||||
async function acquireTokenViaGitHubApp(opts) {
|
||||
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||
throw new Error(
|
||||
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||
);
|
||||
}
|
||||
const repoContext = parseRepoContext();
|
||||
const config = {
|
||||
appId: process.env.GITHUB_APP_ID,
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||
repoOwner: repoContext.owner,
|
||||
repoName: repoContext.name
|
||||
};
|
||||
|
||||
@@ -334,6 +334,44 @@ export async function checkoutPrBranch(
|
||||
};
|
||||
}
|
||||
|
||||
type DeepenForBeforeShaParams = {
|
||||
gitToken: string;
|
||||
beforeSha: string;
|
||||
};
|
||||
|
||||
function deepenForBeforeSha(params: DeepenForBeforeShaParams): void {
|
||||
const isShallow =
|
||||
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||
if (!isShallow) return;
|
||||
|
||||
const maxIterations = 10;
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
try {
|
||||
$("git", ["cat-file", "-t", params.beforeSha], { log: false });
|
||||
log.debug(`» before_sha ${params.beforeSha.slice(0, 7)} is now reachable`);
|
||||
return;
|
||||
} catch {
|
||||
// not reachable yet, deepen
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`» deepening by 50 to reach before_sha ${params.beforeSha.slice(0, 7)} (attempt ${i + 1}/${maxIterations})`
|
||||
);
|
||||
try {
|
||||
$git("fetch", ["--deepen=50", "--no-tags", "origin"], {
|
||||
token: params.gitToken,
|
||||
});
|
||||
} catch {
|
||||
log.debug(`» deepen for before_sha failed (force-push may have rewritten history)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`» before_sha ${params.beforeSha.slice(0, 7)} not reachable after ${maxIterations * 50} commits`
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckoutPrTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "checkout_pr",
|
||||
@@ -352,6 +390,16 @@ export function CheckoutPrTool(ctx: ToolContext) {
|
||||
postCheckoutScript: ctx.postCheckoutScript,
|
||||
});
|
||||
|
||||
// for incremental review/rereview: deepen the clone to include before_sha
|
||||
// so `git diff before_sha...HEAD` works without the agent needing to fetch manually
|
||||
const event = ctx.payload.event;
|
||||
if ("before_sha" in event && event.before_sha) {
|
||||
deepenForBeforeSha({
|
||||
gitToken: ctx.gitToken,
|
||||
beforeSha: event.before_sha,
|
||||
});
|
||||
}
|
||||
|
||||
const pr = await ctx.octokit.rest.pulls.get({
|
||||
owner: ctx.repo.owner,
|
||||
repo: ctx.repo.name,
|
||||
|
||||
+21
-12
@@ -9,8 +9,14 @@ import { retry } from "../utils/retry.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
/** PATCH workflow-run with plan comment node_id so plan revisions can update that comment in place. */
|
||||
async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string): Promise<void> {
|
||||
type CommentNodeIdField = "planCommentNodeId" | "summaryCommentNodeId";
|
||||
|
||||
/** PATCH workflow-run with a comment node_id so future runs can update that comment in place. */
|
||||
export async function updateCommentNodeId(
|
||||
ctx: ToolContext,
|
||||
field: CommentNodeIdField,
|
||||
nodeId: string
|
||||
): Promise<void> {
|
||||
if (ctx.runId === undefined || !ctx.apiToken) return;
|
||||
try {
|
||||
await retry(
|
||||
@@ -22,7 +28,7 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string):
|
||||
authorization: `Bearer ${ctx.apiToken}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ planCommentNodeId }),
|
||||
body: JSON.stringify({ [field]: nodeId }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||
@@ -30,11 +36,11 @@ async function updatePlanCommentId(ctx: ToolContext, planCommentNodeId: string):
|
||||
{
|
||||
maxAttempts: 3,
|
||||
delayMs: 2000,
|
||||
label: "updatePlanCommentId",
|
||||
label: `updateCommentNodeId(${field})`,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
log.warning(`updatePlanCommentId exhausted retries: ${error}`);
|
||||
log.warning(`updateCommentNodeId(${field}) exhausted retries: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +113,9 @@ export const Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type
|
||||
.enumerated("Plan", "Comment")
|
||||
.enumerated("Plan", "Summary", "Comment")
|
||||
.describe(
|
||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
||||
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
@@ -118,7 +124,7 @@ export function CreateCommentTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "create_issue_comment",
|
||||
description:
|
||||
"Create a comment on a GitHub issue. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' only when creating a standalone plan comment to record as this run's plan.",
|
||||
"Create a comment on a GitHub issue or PR. For progress/plan updates on the current run use report_progress instead. Use type: 'Plan' for plan comments, type: 'Summary' for PR summary comments.",
|
||||
parameters: Comment,
|
||||
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||
const bodyWithFooter = await addFooter(ctx, body);
|
||||
@@ -131,7 +137,10 @@ export function CreateCommentTool(ctx: ToolContext) {
|
||||
});
|
||||
|
||||
if (commentType === "Plan" && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||
}
|
||||
if (commentType === "Summary" && result.data.node_id) {
|
||||
await updateCommentNodeId(ctx, "summaryCommentNodeId", result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -241,7 +250,7 @@ export async function reportProgress(
|
||||
ctx.toolState.wasUpdated = true;
|
||||
|
||||
if (isPlanMode && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -278,7 +287,7 @@ export async function reportProgress(
|
||||
ctx.toolState.wasUpdated = true;
|
||||
|
||||
if (isPlanMode && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -336,7 +345,7 @@ export async function reportProgress(
|
||||
});
|
||||
|
||||
if (updateResult.data.node_id) {
|
||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||
await updateCommentNodeId(ctx, "planCommentNodeId", updateResult.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+11
@@ -218,6 +218,8 @@ const NOSHELL_BLOCKED_SUBCOMMANDS: Record<string, string> = {
|
||||
// (avoids false positives like --exclude matching --exec)
|
||||
const NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];
|
||||
|
||||
const COLLAPSE_THRESHOLD = 200;
|
||||
|
||||
// SECURITY: subcommand must match [a-z][a-z0-9-]* to reject flags passed as the subcommand.
|
||||
// this blocks injection of global git options like -c, -C, --exec-path, --config-env, etc.
|
||||
//
|
||||
@@ -269,6 +271,15 @@ export function GitTool(ctx: ToolContext) {
|
||||
}
|
||||
|
||||
const output = $("git", [subcommand, ...args], { log: false });
|
||||
const lineCount = output.split("\n").length;
|
||||
if (lineCount > COLLAPSE_THRESHOLD) {
|
||||
log.group(`git ${subcommand} output (${lineCount} lines)`, () => {
|
||||
log.info(output);
|
||||
});
|
||||
} else if (output) {
|
||||
log.info(output);
|
||||
}
|
||||
|
||||
return { success: true, output };
|
||||
}),
|
||||
});
|
||||
|
||||
+73
-1
@@ -7,7 +7,7 @@ import { execute, tool } from "./shared.ts";
|
||||
|
||||
export const SelectModeParams = type({
|
||||
mode: type.string.describe(
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts')"
|
||||
"the name of the mode to select (e.g., 'Build', 'Plan', 'Review', 'IncrementalReview', 'Fix', 'AddressReviews', 'Task', 'ResolveConflicts', 'Summarize')"
|
||||
),
|
||||
"issue_number?": type("number").describe(
|
||||
"optional issue number; when provided with Plan mode, used to look up an existing plan comment for this issue (edit vs create)"
|
||||
@@ -185,6 +185,38 @@ An existing plan comment was found for this issue. Update that comment with the
|
||||
- call \`${ghPullfrogMcpName}/report_progress\` with results
|
||||
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
|
||||
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
|
||||
|
||||
Summarize: `### Checklist
|
||||
|
||||
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`.
|
||||
2. Delegate a subagent to analyze the diff and produce a structured summary. Include in its prompt:
|
||||
- the diff file path
|
||||
- PR metadata (title, file count, commit count, base/head branches)
|
||||
- format instructions from EVENT INSTRUCTIONS (if any); otherwise use default format: TL;DR, key changes list, per-change sections with before/after framing
|
||||
- instruct it to use the TOC to selectively read relevant diff sections, not the entire file
|
||||
- instruct it to return the full summary markdown via \`${ghPullfrogMcpName}/set_output\`
|
||||
3. After the subagent completes, call \`${ghPullfrogMcpName}/create_issue_comment\` with \`type: "Summary"\` and the summary body.
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort.`,
|
||||
|
||||
SummaryUpdate: `### Checklist (updating existing summary)
|
||||
|
||||
An existing summary comment was found for this PR. Update it rather than creating a new one.
|
||||
|
||||
1. Use \`previousSummaryBody\` from this response as the current summary to revise.
|
||||
2. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` — this returns PR metadata and a \`diffPath\`.
|
||||
3. Delegate a subagent with:
|
||||
- the diff file path and PR metadata
|
||||
- the existing summary body (\`previousSummaryBody\`) so it can update rather than rewrite from scratch
|
||||
- format instructions from EVENT INSTRUCTIONS (if any)
|
||||
- instruct it to produce an updated summary reflecting the current state of the PR and return via \`${ghPullfrogMcpName}/set_output\`
|
||||
4. After the subagent completes, call \`${ghPullfrogMcpName}/edit_issue_comment\` with \`commentId: existingSummaryCommentId\` (from this response) and the updated summary body.
|
||||
|
||||
### Effort
|
||||
|
||||
Use mini or auto effort.`,
|
||||
};
|
||||
|
||||
type OrchestratorGuidance = {
|
||||
@@ -218,6 +250,9 @@ function buildOrchestratorGuidance(mode: Mode, opts: BuildGuidanceOpts = {}): Or
|
||||
// matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment
|
||||
export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||
|
||||
// matches the API response for /repo/[owner]/[repo]/pr/[prNumber]/summary-comment
|
||||
export type SummaryCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||
|
||||
async function fetchExistingPlanComment(
|
||||
ctx: ToolContext,
|
||||
issueNumber: number
|
||||
@@ -237,6 +272,25 @@ async function fetchExistingPlanComment(
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExistingSummaryComment(
|
||||
ctx: ToolContext,
|
||||
prNumber: number
|
||||
): Promise<Extract<SummaryCommentResponsePayload, { commentId: number }> | null> {
|
||||
if (!ctx.apiToken) return null;
|
||||
try {
|
||||
const response = await apiFetch({
|
||||
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/pr/${prNumber}/summary-comment`,
|
||||
method: "GET",
|
||||
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
const data = (await response.json()) as SummaryCommentResponsePayload;
|
||||
return response.ok && "commentId" in data ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function SelectModeTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "select_mode",
|
||||
@@ -287,6 +341,24 @@ export function SelectModeTool(ctx: ToolContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedMode.name === "Summarize") {
|
||||
const prNumber = ctx.payload.event.issue_number;
|
||||
if (prNumber !== undefined) {
|
||||
const existing = await fetchExistingSummaryComment(ctx, prNumber);
|
||||
if (existing !== null) {
|
||||
ctx.toolState.existingSummaryCommentId = existing.commentId;
|
||||
return {
|
||||
...buildOrchestratorGuidance(selectedMode, {
|
||||
...guidanceOpts,
|
||||
overrideGuidance: modeGuidance.SummaryUpdate,
|
||||
}),
|
||||
existingSummaryCommentId: existing.commentId,
|
||||
previousSummaryBody: existing.body,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buildOrchestratorGuidance(selectedMode, guidanceOpts);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface ToolState {
|
||||
// set by select_mode when Plan + issue_number and plan-comment API returns existing plan (for report_progress target_plan_comment)
|
||||
existingPlanCommentId?: number;
|
||||
previousPlanBody?: string;
|
||||
// set by select_mode when Summarize mode and summary-comment API returns existing summary
|
||||
existingSummaryCommentId?: number;
|
||||
output?: string;
|
||||
usageEntries: AgentUsage[];
|
||||
}
|
||||
|
||||
@@ -312,6 +312,22 @@ Your job is to fix issues THIS PR introduced, not to fix all CI failures. If in
|
||||
|
||||
Do NOT overwrite a good comment with links/details with a generic message like "I have completed the task." If your previous report_progress call already contains all the necessary information and links, skip the final call entirely.`,
|
||||
},
|
||||
{
|
||||
name: "Summarize",
|
||||
description:
|
||||
"Summarize a PR with a structured comment that is updated in place on subsequent pushes",
|
||||
prompt: `Follow these steps.
|
||||
|
||||
1. **CHECKOUT** - Call ${ghPullfrogMcpName}/checkout_pr with the PR number to get PR metadata and diffPath.
|
||||
|
||||
2. **ANALYZE** - Read the diff file. Use the TOC to selectively read relevant sections — do not read the entire file unless the PR is small.
|
||||
|
||||
3. **SUMMARIZE** - Write a structured summary following the format from EVENT INSTRUCTIONS. If no format instructions are provided, produce a concise summary with a TL;DR, key changes list, and per-change sections with before/after framing.
|
||||
|
||||
4. **POST** - Call ${ghPullfrogMcpName}/create_issue_comment with type: 'Summary' and the summary body.
|
||||
|
||||
${permalinkTip}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.179",
|
||||
"version": "0.0.180",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -41256,8 +41256,8 @@ var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
||||
var Comment = type({
|
||||
issueNumber: type.number.describe("the issue number to comment on"),
|
||||
body: type.string.describe("the comment body content"),
|
||||
type: type.enumerated("Plan", "Comment").describe(
|
||||
"Plan: record this comment as the plan for this run (use report_progress for progress/plan updates on the current run). Comment: regular comment (default)."
|
||||
type: type.enumerated("Plan", "Summary", "Comment").describe(
|
||||
"Plan: record as the plan for this run. Summary: record as the PR summary comment (one per PR, updated in place). Comment: regular comment (default)."
|
||||
).optional()
|
||||
});
|
||||
var EditComment = type({
|
||||
@@ -41284,7 +41284,7 @@ var core3 = __toESM(require_core(), 1);
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.179",
|
||||
version: "0.0.180",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
@@ -216,10 +216,6 @@ export async function runAgentStreaming(options: RunStreamingOptions): Promise<A
|
||||
GITHUB_OUTPUT: githubOutputFile,
|
||||
};
|
||||
|
||||
// clear CI runner's GITHUB_TOKEN so ensureGitHubToken() mints a
|
||||
// properly scoped token for the target GITHUB_REPOSITORY via OIDC
|
||||
delete subEnv.GITHUB_TOKEN;
|
||||
|
||||
const child = spawn("node", ["play.ts", "--raw", JSON.stringify(fixture)], {
|
||||
cwd: actionDir,
|
||||
env: subEnv as Record<string, string>,
|
||||
|
||||
+28
-11
@@ -277,11 +277,17 @@ const findInstallationId = async (
|
||||
|
||||
// for local development only
|
||||
async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<string> {
|
||||
if (!process.env.GITHUB_APP_ID || !process.env.GITHUB_PRIVATE_KEY) {
|
||||
throw new Error(
|
||||
"cannot acquire token via GitHub App: GITHUB_APP_ID and GITHUB_PRIVATE_KEY must be set"
|
||||
);
|
||||
}
|
||||
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
const config: GitHubAppConfig = {
|
||||
appId: process.env.GITHUB_APP_ID!,
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY?.replace(/\\n/g, "\n")!,
|
||||
appId: process.env.GITHUB_APP_ID,
|
||||
privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
||||
repoOwner: repoContext.owner,
|
||||
repoName: repoContext.name,
|
||||
};
|
||||
@@ -292,20 +298,31 @@ async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<str
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a GitHub token is available in the environment.
|
||||
* ensure a GitHub token is available in the environment.
|
||||
*
|
||||
* If neither `GITHUB_TOKEN` nor `GH_TOKEN` is set, attempts to acquire an
|
||||
* installation token using `GITHUB_APP_ID` / `GITHUB_PRIVATE_KEY`.
|
||||
* when OIDC is available (CI), always mints a fresh token scoped to
|
||||
* GITHUB_REPOSITORY — overriding any inherited GITHUB_TOKEN that may
|
||||
* be scoped to the wrong repo.
|
||||
*
|
||||
* **Not intended for production use** — this is a convenience for local
|
||||
* development and test harnesses where tokens aren't pre-provisioned.
|
||||
* otherwise falls back to GitHub App credentials for local development.
|
||||
*
|
||||
* only called from play.ts (test/dev path) — the live action calls
|
||||
* main() directly and never calls this.
|
||||
*/
|
||||
export async function ensureGitHubToken(): Promise<void> {
|
||||
// when OIDC is available, always mint a fresh token scoped to
|
||||
// GITHUB_REPOSITORY. the inherited GITHUB_TOKEN may be scoped to a
|
||||
// different repo (e.g., runner token for pullfrog/app when tests
|
||||
// target pullfrog/test-repo).
|
||||
if (isOIDCAvailable()) {
|
||||
const token = await acquireNewToken();
|
||||
process.env.GITHUB_TOKEN = token;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
|
||||
if (isOIDCAvailable() || (process.env.GITHUB_APP_ID && process.env.GITHUB_PRIVATE_KEY)) {
|
||||
const token = await acquireNewToken();
|
||||
process.env.GITHUB_TOKEN = token;
|
||||
}
|
||||
const token = await acquireNewToken();
|
||||
process.env.GITHUB_TOKEN = token;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user