feat: Improving the plan revisions (#465)
* feat(plans): Suggested plan for plan revisions. * fix: add planCommentId to reduce GitHub API calls. * Revert "fix: add planCommentId to reduce GitHub API calls." This reverts commit ef9c24811fa291b12ac3601cc4cd3edb7c9a0fca. * Improving plan revision: the implementation draft. * fix schema composition order. * fix: reusing existing retry helper (action) for reportPlanCommentToRun. * mv: updatePlanCommentId. * fix: higher severity for logging error. * fix: add error handling when calling findExistingPlanCommentIdForIssue. * feat: improving the revisit plan request detection by adding PLAN_REVISION_VERBS. * Updating the plan with alternative non-determenistic solution. * add more verbs to PLAN_REVISION_VERBS. * fix: supply the previous plan in the event context as previousPlanBody, updating Plan mode instructions. * fix: adjusting the way PLAN_REVISION_VERBS are used in sentences. * fix: using GraphQL approach with NodeId to find commentId in findExistingPlanCommentIdForIssue. * fix: use double word boundaries (both sides). * fix condition in findExistingPlanCommentIdForIssue. * fix: rm unused args from findExistingPlanCommentIdForIssue. * bump the action version. * fix(plan): rm everything related to approach A. * fix(plan): No limit for progress comments. * feat(plan): the new plan. * Revert: changed to webhook (no longer involved). * feat: mv plan comment lookup into a new API endpoint. * Revert: changes to select_mode tool. * FEAT: The new implementation. * fix arktype issues. * fix plan diagram. * fix(selectMode): e2e type constraints for fetchExistingPlanComment. * Revert "fix(selectMode): e2e type constraints for fetchExistingPlanComment." This reverts commit 53f3b6650a9928e3080700faa9eead0052e94333. * fix(selectMode): type constraints (copy) for fetchExistingPlanComment. * feat: improving isHttpError helper and reusing it consistently instead of casting. * address review: remove unconditional retry, add plan comment warning, dedupe type, remove dead guard, fix GraphQL types * Fix: tightening the PlanEdit guidance. * fix(select_mode): Providing the agent with existingPlanCommentId as well. * fix(instructions): Adjusting the primary guidance to prefer Plan mode for issue-related ambiguous requests. * fix(wiki): updating the delegation docs according to the current instructions. * fix(instructions): rm implication to call for issue details. * fix(select_mode): tweak for PlanEdit. * fix(select_mode): more tweaks to PlanEdit. * fix(select_mode): tweaks for the order of instructions and context. * revert: to the state of 5c400efce1f1fec0a0855eeacad2bc3b721fd1bf. * fix(select_mode): Correcting the guideline. * rm the plan from the branch (impletemented). --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
6c9747585f
commit
9c99bcbbac
@@ -145074,6 +145074,33 @@ function stripExistingFooter(body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mcp/comment.ts
|
// mcp/comment.ts
|
||||||
|
async function updatePlanCommentId(ctx, planCommentNodeId) {
|
||||||
|
if (ctx.runId === void 0 || !ctx.apiToken) return;
|
||||||
|
try {
|
||||||
|
await retry(
|
||||||
|
async () => {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/workflow-run/${ctx.runId}`,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${ctx.apiToken}`,
|
||||||
|
"content-type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ planCommentNodeId }),
|
||||||
|
signal: AbortSignal.timeout(1e4)
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
delayMs: 2e3,
|
||||||
|
label: "updatePlanCommentId"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error49) {
|
||||||
|
log.warning(`updatePlanCommentId exhausted retries: ${error49}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
async function buildCommentFooter({
|
async function buildCommentFooter({
|
||||||
agent: agent2,
|
agent: agent2,
|
||||||
octokit,
|
octokit,
|
||||||
@@ -145117,14 +145144,17 @@ async function addFooter(ctx, body) {
|
|||||||
}
|
}
|
||||||
var Comment = type({
|
var Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content")
|
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)."
|
||||||
|
).optional()
|
||||||
});
|
});
|
||||||
function CreateCommentTool(ctx) {
|
function CreateCommentTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description: "Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress 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.",
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: execute(async ({ issueNumber, body }) => {
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||||
const bodyWithFooter = await addFooter(ctx, body);
|
const bodyWithFooter = await addFooter(ctx, body);
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
owner: ctx.repo.owner,
|
owner: ctx.repo.owner,
|
||||||
@@ -145132,6 +145162,9 @@ function CreateCommentTool(ctx) {
|
|||||||
issue_number: issueNumber,
|
issue_number: issueNumber,
|
||||||
body: bodyWithFooter
|
body: bodyWithFooter
|
||||||
});
|
});
|
||||||
|
if (commentType === "Plan" && result.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result.data.node_id);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
commentId: result.data.id,
|
commentId: result.data.id,
|
||||||
@@ -145169,16 +145202,50 @@ function EditCommentTool(ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
var ReportProgress = type({
|
var ReportProgress = type({
|
||||||
body: type.string.describe("the progress update content to share")
|
body: type.string.describe("the progress update content to share"),
|
||||||
|
"target_plan_comment?": type("boolean").describe(
|
||||||
|
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
async function reportProgress(ctx, { body }) {
|
async function reportProgress(ctx, params) {
|
||||||
|
const { body, target_plan_comment } = params;
|
||||||
ctx.toolState.lastProgressBody = body;
|
ctx.toolState.lastProgressBody = body;
|
||||||
if (ctx.payload.event.silent) {
|
if (ctx.payload.event.silent) {
|
||||||
return { body, action: "skipped" };
|
return { body, action: "skipped" };
|
||||||
}
|
}
|
||||||
const existingCommentId = ctx.toolState.progressCommentId;
|
|
||||||
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
||||||
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
||||||
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === void 0) {
|
||||||
|
log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
|
||||||
|
}
|
||||||
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== void 0) {
|
||||||
|
const commentId = ctx.toolState.existingPlanCommentId;
|
||||||
|
const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, commentId)] : void 0;
|
||||||
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
|
const footer = await buildCommentFooter({
|
||||||
|
agent: ctx.agent,
|
||||||
|
octokit: ctx.octokit,
|
||||||
|
customParts
|
||||||
|
});
|
||||||
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
|
const result2 = await ctx.octokit.rest.issues.updateComment({
|
||||||
|
owner: ctx.repo.owner,
|
||||||
|
repo: ctx.repo.name,
|
||||||
|
comment_id: commentId,
|
||||||
|
body: bodyWithFooter
|
||||||
|
});
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
if (isPlanMode && result2.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result2.data.node_id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
commentId: result2.data.id,
|
||||||
|
url: result2.data.html_url,
|
||||||
|
body: result2.data.body || "",
|
||||||
|
action: "updated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const existingCommentId = ctx.toolState.progressCommentId;
|
||||||
if (existingCommentId) {
|
if (existingCommentId) {
|
||||||
const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, existingCommentId)] : void 0;
|
const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, existingCommentId)] : void 0;
|
||||||
const bodyWithoutFooter = stripExistingFooter(body);
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
@@ -145195,6 +145262,9 @@ async function reportProgress(ctx, { body }) {
|
|||||||
body: bodyWithFooter
|
body: bodyWithFooter
|
||||||
});
|
});
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
|
if (isPlanMode && result2.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result2.data.node_id);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
commentId: result2.data.id,
|
commentId: result2.data.id,
|
||||||
url: result2.data.html_url,
|
url: result2.data.html_url,
|
||||||
@@ -145234,6 +145304,9 @@ async function reportProgress(ctx, { body }) {
|
|||||||
comment_id: result.data.id,
|
comment_id: result.data.id,
|
||||||
body: bodyWithPlanLink
|
body: bodyWithPlanLink
|
||||||
});
|
});
|
||||||
|
if (updateResult.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
commentId: updateResult.data.id,
|
commentId: updateResult.data.id,
|
||||||
url: updateResult.data.html_url,
|
url: updateResult.data.html_url,
|
||||||
@@ -145253,8 +145326,12 @@ function ReportProgressTool(ctx) {
|
|||||||
name: "report_progress",
|
name: "report_progress",
|
||||||
description: "Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
description: "Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
||||||
parameters: ReportProgress,
|
parameters: ReportProgress,
|
||||||
execute: execute(async ({ body }) => {
|
execute: execute(async (params) => {
|
||||||
const result = await reportProgress(ctx, { body });
|
const reportParams = { body: params.body };
|
||||||
|
if (params.target_plan_comment !== void 0) {
|
||||||
|
reportParams.target_plan_comment = params.target_plan_comment;
|
||||||
|
}
|
||||||
|
const result = await reportProgress(ctx, reportParams);
|
||||||
if (result.action === "skipped") {
|
if (result.action === "skipped") {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -147693,6 +147770,9 @@ function ResolveReviewThreadTool(ctx) {
|
|||||||
var SelectModeParams = type({
|
var SelectModeParams = type({
|
||||||
mode: type.string.describe(
|
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')"
|
||||||
|
),
|
||||||
|
"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)"
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
function resolveMode(modes2, modeName) {
|
function resolveMode(modes2, modeName) {
|
||||||
@@ -147845,6 +147925,21 @@ Use max effort for thorough reviews.`,
|
|||||||
|
|
||||||
### Effort
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`,
|
||||||
|
PlanEdit: `### Checklist (editing existing plan)
|
||||||
|
|
||||||
|
An existing plan comment was found for this issue. Update that comment with the revised plan \u2014 do not create a new plan comment.
|
||||||
|
|
||||||
|
1. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
|
||||||
|
2. When delegating, the subagent prompt must contain:
|
||||||
|
- the current plan (\`previousPlanBody\`) and the user's revision request
|
||||||
|
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||||
|
- instructions to produce a structured plan with clear milestones and to return the full plan via \`${ghPullfrogMcpName}/set_output\` as markdown (do not create plan files or save to disk)
|
||||||
|
3. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the full plan text and \`{ target_plan_comment: true }\` so the revised plan updates the existing plan comment (not the progress comment).
|
||||||
|
4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${ghPullfrogMcpName}/report_progress\` so it is not left as "Leaping...".
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
Use mini or auto effort.`,
|
Use mini or auto effort.`,
|
||||||
Fix: `### Checklist
|
Fix: `### Checklist
|
||||||
|
|
||||||
@@ -147884,14 +147979,29 @@ Use auto effort.`,
|
|||||||
- 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
|
||||||
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`
|
5. Use mini effort for simple research tasks, auto for typical tasks, max for complex multi-file changes.`
|
||||||
};
|
};
|
||||||
function buildOrchestratorGuidance(mode) {
|
function buildOrchestratorGuidance(mode, overrideGuidance) {
|
||||||
const guidance = modeGuidance[mode.name] ?? "";
|
const guidance = overrideGuidance ?? modeGuidance[mode.name] ?? "";
|
||||||
return {
|
return {
|
||||||
modeName: mode.name,
|
modeName: mode.name,
|
||||||
description: mode.description,
|
description: mode.description,
|
||||||
orchestratorGuidance: guidance
|
orchestratorGuidance: guidance
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async function fetchExistingPlanComment(ctx, issueNumber) {
|
||||||
|
if (!ctx.apiToken) return null;
|
||||||
|
try {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issue/${issueNumber}/plan-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) {
|
function SelectModeTool(ctx) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "select_mode",
|
name: "select_mode",
|
||||||
@@ -147903,11 +148013,12 @@ function SelectModeTool(ctx) {
|
|||||||
error: `mode already selected: "${ctx.toolState.selectedMode}". mode selection is final and cannot be changed. complete your current workflow within this mode.`
|
error: `mode already selected: "${ctx.toolState.selectedMode}". mode selection is final and cannot be changed. complete your current workflow within this mode.`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedMode = resolveMode(ctx.modes, params.mode);
|
const modeName = params.mode;
|
||||||
|
const selectedMode = resolveMode(ctx.modes, modeName);
|
||||||
if (!selectedMode) {
|
if (!selectedMode) {
|
||||||
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
||||||
return {
|
return {
|
||||||
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
|
error: `mode "${modeName}" not found. available modes: ${availableModes}`,
|
||||||
availableModes: ctx.modes.map((m) => ({
|
availableModes: ctx.modes.map((m) => ({
|
||||||
name: m.name,
|
name: m.name,
|
||||||
description: m.description
|
description: m.description
|
||||||
@@ -147915,6 +148026,20 @@ function SelectModeTool(ctx) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
ctx.toolState.selectedMode = selectedMode.name;
|
ctx.toolState.selectedMode = selectedMode.name;
|
||||||
|
if (selectedMode.name === "Plan") {
|
||||||
|
const issueNumber = params.issue_number ?? ctx.payload.event.issue_number;
|
||||||
|
if (issueNumber !== void 0) {
|
||||||
|
const existing = await fetchExistingPlanComment(ctx, issueNumber);
|
||||||
|
if (existing !== null) {
|
||||||
|
ctx.toolState.existingPlanCommentId = existing.commentId;
|
||||||
|
ctx.toolState.previousPlanBody = existing.body;
|
||||||
|
return {
|
||||||
|
...buildOrchestratorGuidance(selectedMode, modeGuidance.PlanEdit),
|
||||||
|
previousPlanBody: existing.body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return buildOrchestratorGuidance(selectedMode);
|
return buildOrchestratorGuidance(selectedMode);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -148723,7 +148848,7 @@ import { join as join12 } from "node:path";
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.177",
|
version: "0.0.178",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
+104
-10
@@ -1,11 +1,43 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { Agent } from "../agents/index.ts";
|
import type { Agent } from "../agents/index.ts";
|
||||||
|
import { apiFetch } from "../utils/apiFetch.ts";
|
||||||
import { getApiUrl } from "../utils/apiUrl.ts";
|
import { getApiUrl } from "../utils/apiUrl.ts";
|
||||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||||
|
import { log } from "../utils/cli.ts";
|
||||||
import { type OctokitWithPlugins, parseRepoContext } from "../utils/github.ts";
|
import { type OctokitWithPlugins, parseRepoContext } from "../utils/github.ts";
|
||||||
|
import { retry } from "../utils/retry.ts";
|
||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.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> {
|
||||||
|
if (ctx.runId === undefined || !ctx.apiToken) return;
|
||||||
|
try {
|
||||||
|
await retry(
|
||||||
|
async () => {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/workflow-run/${ctx.runId}`,
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${ctx.apiToken}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ planCommentNodeId }),
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`PATCH workflow-run: ${response.status}`);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
delayMs: 2000,
|
||||||
|
label: "updatePlanCommentId",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.warning(`updatePlanCommentId exhausted retries: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The prefix text for the initial "leaping into action" comment.
|
* The prefix text for the initial "leaping into action" comment.
|
||||||
* This is used to identify if a comment is still in its initial state
|
* This is used to identify if a comment is still in its initial state
|
||||||
@@ -86,15 +118,21 @@ export async function addFooter(ctx: AddFooterCtx, body: string): Promise<string
|
|||||||
export const Comment = type({
|
export const Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content"),
|
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)."
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CreateCommentTool(ctx: ToolContext) {
|
export function CreateCommentTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "create_issue_comment",
|
name: "create_issue_comment",
|
||||||
description:
|
description:
|
||||||
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
|
"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.",
|
||||||
parameters: Comment,
|
parameters: Comment,
|
||||||
execute: execute(async ({ issueNumber, body }) => {
|
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
||||||
const bodyWithFooter = await addFooter(ctx, body);
|
const bodyWithFooter = await addFooter(ctx, body);
|
||||||
|
|
||||||
const result = await ctx.octokit.rest.issues.createComment({
|
const result = await ctx.octokit.rest.issues.createComment({
|
||||||
@@ -104,6 +142,10 @@ export function CreateCommentTool(ctx: ToolContext) {
|
|||||||
body: bodyWithFooter,
|
body: bodyWithFooter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (commentType === "Plan" && result.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result.data.node_id);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
commentId: result.data.id,
|
commentId: result.data.id,
|
||||||
@@ -147,6 +189,9 @@ export function EditCommentTool(ctx: ToolContext) {
|
|||||||
|
|
||||||
export const ReportProgress = type({
|
export const ReportProgress = type({
|
||||||
body: type.string.describe("the progress update content to share"),
|
body: type.string.describe("the progress update content to share"),
|
||||||
|
"target_plan_comment?": type("boolean").describe(
|
||||||
|
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,13 +206,14 @@ export const ReportProgress = type({
|
|||||||
*/
|
*/
|
||||||
export async function reportProgress(
|
export async function reportProgress(
|
||||||
ctx: ToolContext,
|
ctx: ToolContext,
|
||||||
{ body }: { body: string }
|
params: { body: string; target_plan_comment?: boolean }
|
||||||
): Promise<{
|
): Promise<{
|
||||||
commentId?: number;
|
commentId?: number;
|
||||||
url?: string;
|
url?: string;
|
||||||
body: string;
|
body: string;
|
||||||
action: "created" | "updated" | "skipped";
|
action: "created" | "updated" | "skipped";
|
||||||
}> {
|
}> {
|
||||||
|
const { body, target_plan_comment } = params;
|
||||||
// always track the body for job summary
|
// always track the body for job summary
|
||||||
ctx.toolState.lastProgressBody = body;
|
ctx.toolState.lastProgressBody = body;
|
||||||
|
|
||||||
@@ -177,14 +223,50 @@ export async function reportProgress(
|
|||||||
return { body, action: "skipped" };
|
return { body, action: "skipped" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCommentId = ctx.toolState.progressCommentId;
|
|
||||||
// Explicit issue_number from the payload wins here
|
|
||||||
// This is especially important for manually triggered workflows from trigger links.
|
|
||||||
// Without this, the created "Implement plan" would be created for the wrong issue
|
|
||||||
// if the run happens to research other issues (and thus overwrite the toolState.issueNumber).
|
|
||||||
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
||||||
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
||||||
|
|
||||||
|
// when editing existing plan: update the plan comment from tool state (set by select_mode)
|
||||||
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === undefined) {
|
||||||
|
log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
|
||||||
|
}
|
||||||
|
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== undefined) {
|
||||||
|
const commentId = ctx.toolState.existingPlanCommentId;
|
||||||
|
const customParts =
|
||||||
|
isPlanMode && issueNumber !== undefined
|
||||||
|
? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, commentId)]
|
||||||
|
: undefined;
|
||||||
|
const bodyWithoutFooter = stripExistingFooter(body);
|
||||||
|
const footer = await buildCommentFooter({
|
||||||
|
agent: ctx.agent,
|
||||||
|
octokit: ctx.octokit,
|
||||||
|
customParts,
|
||||||
|
});
|
||||||
|
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
||||||
|
|
||||||
|
const result = await ctx.octokit.rest.issues.updateComment({
|
||||||
|
owner: ctx.repo.owner,
|
||||||
|
repo: ctx.repo.name,
|
||||||
|
comment_id: commentId,
|
||||||
|
body: bodyWithFooter,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
|
if (isPlanMode && result.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result.data.node_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
commentId: result.data.id,
|
||||||
|
url: result.data.html_url,
|
||||||
|
body: result.data.body || "",
|
||||||
|
action: "updated",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCommentId = ctx.toolState.progressCommentId;
|
||||||
|
|
||||||
// if we already have a progress comment, update it
|
// if we already have a progress comment, update it
|
||||||
if (existingCommentId) {
|
if (existingCommentId) {
|
||||||
const customParts =
|
const customParts =
|
||||||
@@ -209,6 +291,10 @@ export async function reportProgress(
|
|||||||
|
|
||||||
ctx.toolState.wasUpdated = true;
|
ctx.toolState.wasUpdated = true;
|
||||||
|
|
||||||
|
if (isPlanMode && result.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, result.data.node_id);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commentId: result.data.id,
|
commentId: result.data.id,
|
||||||
url: result.data.html_url,
|
url: result.data.html_url,
|
||||||
@@ -264,6 +350,10 @@ export async function reportProgress(
|
|||||||
body: bodyWithPlanLink,
|
body: bodyWithPlanLink,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (updateResult.data.node_id) {
|
||||||
|
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commentId: updateResult.data.id,
|
commentId: updateResult.data.id,
|
||||||
url: updateResult.data.html_url,
|
url: updateResult.data.html_url,
|
||||||
@@ -286,8 +376,12 @@ export function ReportProgressTool(ctx: ToolContext) {
|
|||||||
description:
|
description:
|
||||||
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
||||||
parameters: ReportProgress,
|
parameters: ReportProgress,
|
||||||
execute: execute(async ({ body }) => {
|
execute: execute(async (params) => {
|
||||||
const result = await reportProgress(ctx, { body });
|
const reportParams: { body: string; target_plan_comment?: boolean } = { body: params.body };
|
||||||
|
if (params.target_plan_comment !== undefined) {
|
||||||
|
reportParams.target_plan_comment = params.target_plan_comment;
|
||||||
|
}
|
||||||
|
const result = await reportProgress(ctx, reportParams);
|
||||||
|
|
||||||
if (result.action === "skipped") {
|
if (result.action === "skipped") {
|
||||||
// no-op: no comment target, but progress is still tracked for job summary
|
// no-op: no comment target, but progress is still tracked for job summary
|
||||||
|
|||||||
+64
-4
@@ -1,6 +1,7 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { ghPullfrogMcpName } from "../external.ts";
|
import { ghPullfrogMcpName } from "../external.ts";
|
||||||
import type { Mode } from "../modes.ts";
|
import type { Mode } from "../modes.ts";
|
||||||
|
import { apiFetch } from "../utils/apiFetch.ts";
|
||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ export const SelectModeParams = type({
|
|||||||
mode: type.string.describe(
|
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')"
|
||||||
),
|
),
|
||||||
|
"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)"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
function resolveMode(modes: Mode[], modeName: string): Mode | null {
|
function resolveMode(modes: Mode[], modeName: string): Mode | null {
|
||||||
@@ -166,6 +170,22 @@ Use max effort for thorough reviews.`,
|
|||||||
|
|
||||||
### Effort
|
### Effort
|
||||||
|
|
||||||
|
Use mini or auto effort.`,
|
||||||
|
|
||||||
|
PlanEdit: `### Checklist (editing existing plan)
|
||||||
|
|
||||||
|
An existing plan comment was found for this issue. Update that comment with the revised plan — do not create a new plan comment.
|
||||||
|
|
||||||
|
1. Use \`previousPlanBody\` from this response as the plan to revise; do not call \`get_issue\` or \`get_issue_comments\`.
|
||||||
|
2. When delegating, the subagent prompt must contain:
|
||||||
|
- the current plan (\`previousPlanBody\`) and the user's revision request
|
||||||
|
- relevant codebase context (file paths, architecture notes from AGENTS.md)
|
||||||
|
- instructions to produce a structured plan with clear milestones and to return the full plan via \`${ghPullfrogMcpName}/set_output\` as markdown (do not create plan files or save to disk)
|
||||||
|
3. After the subagent completes, call \`${ghPullfrogMcpName}/report_progress\` with the full plan text and \`{ target_plan_comment: true }\` so the revised plan updates the existing plan comment (not the progress comment).
|
||||||
|
4. Then post a short note to the progress comment (e.g. "Plan has been updated in the comment above.") via \`${ghPullfrogMcpName}/report_progress\` so it is not left as "Leaping...".
|
||||||
|
|
||||||
|
### Effort
|
||||||
|
|
||||||
Use mini or auto effort.`,
|
Use mini or auto effort.`,
|
||||||
|
|
||||||
Fix: `### Checklist
|
Fix: `### Checklist
|
||||||
@@ -214,8 +234,8 @@ type OrchestratorGuidance = {
|
|||||||
orchestratorGuidance: string;
|
orchestratorGuidance: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
function buildOrchestratorGuidance(mode: Mode, overrideGuidance?: string): OrchestratorGuidance {
|
||||||
const guidance = modeGuidance[mode.name] ?? "";
|
const guidance = overrideGuidance ?? modeGuidance[mode.name] ?? "";
|
||||||
return {
|
return {
|
||||||
modeName: mode.name,
|
modeName: mode.name,
|
||||||
description: mode.description,
|
description: mode.description,
|
||||||
@@ -223,6 +243,28 @@ function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matches the API response for /repo/[owner]/[repo]/issue/[issueNumber]/plan-comment
|
||||||
|
export type PlanCommentResponsePayload = { error: string } | { commentId: number; body: string };
|
||||||
|
|
||||||
|
async function fetchExistingPlanComment(
|
||||||
|
ctx: ToolContext,
|
||||||
|
issueNumber: number
|
||||||
|
): Promise<Extract<PlanCommentResponsePayload, { commentId: number }> | null> {
|
||||||
|
if (!ctx.apiToken) return null;
|
||||||
|
try {
|
||||||
|
const response = await apiFetch({
|
||||||
|
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issue/${issueNumber}/plan-comment`,
|
||||||
|
method: "GET",
|
||||||
|
headers: { authorization: `Bearer ${ctx.apiToken}` },
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
const data = (await response.json()) as PlanCommentResponsePayload;
|
||||||
|
return response.ok && "commentId" in data ? data : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function SelectModeTool(ctx: ToolContext) {
|
export function SelectModeTool(ctx: ToolContext) {
|
||||||
return tool({
|
return tool({
|
||||||
name: "select_mode",
|
name: "select_mode",
|
||||||
@@ -236,12 +278,14 @@ export function SelectModeTool(ctx: ToolContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedMode = resolveMode(ctx.modes, params.mode);
|
const modeName = params.mode;
|
||||||
|
|
||||||
|
const selectedMode = resolveMode(ctx.modes, modeName);
|
||||||
|
|
||||||
if (!selectedMode) {
|
if (!selectedMode) {
|
||||||
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
||||||
return {
|
return {
|
||||||
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
|
error: `mode "${modeName}" not found. available modes: ${availableModes}`,
|
||||||
availableModes: ctx.modes.map((m) => ({
|
availableModes: ctx.modes.map((m) => ({
|
||||||
name: m.name,
|
name: m.name,
|
||||||
description: m.description,
|
description: m.description,
|
||||||
@@ -250,6 +294,22 @@ export function SelectModeTool(ctx: ToolContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.toolState.selectedMode = selectedMode.name;
|
ctx.toolState.selectedMode = selectedMode.name;
|
||||||
|
|
||||||
|
if (selectedMode.name === "Plan") {
|
||||||
|
const issueNumber = params.issue_number ?? ctx.payload.event.issue_number;
|
||||||
|
if (issueNumber !== undefined) {
|
||||||
|
const existing = await fetchExistingPlanComment(ctx, issueNumber);
|
||||||
|
if (existing !== null) {
|
||||||
|
ctx.toolState.existingPlanCommentId = existing.commentId;
|
||||||
|
ctx.toolState.previousPlanBody = existing.body;
|
||||||
|
return {
|
||||||
|
...buildOrchestratorGuidance(selectedMode, modeGuidance.PlanEdit),
|
||||||
|
previousPlanBody: existing.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return buildOrchestratorGuidance(selectedMode);
|
return buildOrchestratorGuidance(selectedMode);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export interface ToolState {
|
|||||||
progressCommentId: number | null | undefined;
|
progressCommentId: number | null | undefined;
|
||||||
lastProgressBody?: string;
|
lastProgressBody?: string;
|
||||||
wasUpdated?: boolean;
|
wasUpdated?: boolean;
|
||||||
|
// 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;
|
||||||
output?: string;
|
output?: string;
|
||||||
usageEntries: AgentUsage[];
|
usageEntries: AgentUsage[];
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pullfrog/pullfrog",
|
"name": "@pullfrog/pullfrog",
|
||||||
"version": "0.0.177",
|
"version": "0.0.178",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
@@ -41258,14 +41258,20 @@ function createOctokit(token) {
|
|||||||
var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
var LEAPING_INTO_ACTION_PREFIX = "Leaping into action";
|
||||||
var Comment = type({
|
var Comment = type({
|
||||||
issueNumber: type.number.describe("the issue number to comment on"),
|
issueNumber: type.number.describe("the issue number to comment on"),
|
||||||
body: type.string.describe("the comment body content")
|
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)."
|
||||||
|
).optional()
|
||||||
});
|
});
|
||||||
var EditComment = type({
|
var EditComment = type({
|
||||||
commentId: type.number.describe("the ID of the comment to edit"),
|
commentId: type.number.describe("the ID of the comment to edit"),
|
||||||
body: type.string.describe("the new comment body content")
|
body: type.string.describe("the new comment body content")
|
||||||
});
|
});
|
||||||
var ReportProgress = type({
|
var ReportProgress = type({
|
||||||
body: type.string.describe("the progress update content to share")
|
body: type.string.describe("the progress update content to share"),
|
||||||
|
"target_plan_comment?": type("boolean").describe(
|
||||||
|
"when true, update the existing plan comment (from select_mode lookup) instead of the progress comment; use when editing an existing plan"
|
||||||
|
)
|
||||||
});
|
});
|
||||||
var ReplyToReviewComment = type({
|
var ReplyToReviewComment = type({
|
||||||
pull_number: type.number.describe("the pull request number"),
|
pull_number: type.number.describe("the pull request number"),
|
||||||
@@ -41312,7 +41318,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
|||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@pullfrog/pullfrog",
|
name: "@pullfrog/pullfrog",
|
||||||
version: "0.0.177",
|
version: "0.0.178",
|
||||||
type: "module",
|
type: "module",
|
||||||
files: [
|
files: [
|
||||||
"index.js",
|
"index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user