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:
Anna Bocharova
2026-03-10 20:34:54 +00:00
committed by pullfrog[bot]
parent 6c9747585f
commit 9c99bcbbac
6 changed files with 319 additions and 31 deletions
+138 -13
View File
@@ -145074,6 +145074,33 @@ function stripExistingFooter(body) {
}
// 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({
agent: agent2,
octokit,
@@ -145117,14 +145144,17 @@ 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")
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) {
return tool({
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,
execute: execute(async ({ issueNumber, body }) => {
execute: execute(async ({ issueNumber, body, type: commentType }) => {
const bodyWithFooter = await addFooter(ctx, body);
const result = await ctx.octokit.rest.issues.createComment({
owner: ctx.repo.owner,
@@ -145132,6 +145162,9 @@ function CreateCommentTool(ctx) {
issue_number: issueNumber,
body: bodyWithFooter
});
if (commentType === "Plan" && result.data.node_id) {
await updatePlanCommentId(ctx, result.data.node_id);
}
return {
success: true,
commentId: result.data.id,
@@ -145169,16 +145202,50 @@ function EditCommentTool(ctx) {
});
}
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;
if (ctx.payload.event.silent) {
return { body, action: "skipped" };
}
const existingCommentId = ctx.toolState.progressCommentId;
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
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) {
const customParts = isPlanMode && issueNumber !== void 0 ? [buildImplementPlanLink(ctx.repo.owner, ctx.repo.name, issueNumber, existingCommentId)] : void 0;
const bodyWithoutFooter = stripExistingFooter(body);
@@ -145195,6 +145262,9 @@ async function reportProgress(ctx, { body }) {
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,
@@ -145234,6 +145304,9 @@ async function reportProgress(ctx, { body }) {
comment_id: result.data.id,
body: bodyWithPlanLink
});
if (updateResult.data.node_id) {
await updatePlanCommentId(ctx, updateResult.data.node_id);
}
return {
commentId: updateResult.data.id,
url: updateResult.data.html_url,
@@ -145253,8 +145326,12 @@ function ReportProgressTool(ctx) {
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.",
parameters: ReportProgress,
execute: execute(async ({ body }) => {
const result = await reportProgress(ctx, { body });
execute: execute(async (params) => {
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") {
return {
success: true,
@@ -147693,6 +147770,9 @@ function ResolveReviewThreadTool(ctx) {
var SelectModeParams = type({
mode: type.string.describe(
"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) {
@@ -147845,6 +147925,21 @@ Use max effort for thorough reviews.`,
### 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.`,
Fix: `### Checklist
@@ -147884,14 +147979,29 @@ Use auto effort.`,
- 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.`
};
function buildOrchestratorGuidance(mode) {
const guidance = modeGuidance[mode.name] ?? "";
function buildOrchestratorGuidance(mode, overrideGuidance) {
const guidance = overrideGuidance ?? modeGuidance[mode.name] ?? "";
return {
modeName: mode.name,
description: mode.description,
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) {
return tool({
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.`
};
}
const selectedMode = resolveMode(ctx.modes, params.mode);
const modeName = params.mode;
const selectedMode = resolveMode(ctx.modes, modeName);
if (!selectedMode) {
const availableModes = ctx.modes.map((m) => m.name).join(", ");
return {
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
error: `mode "${modeName}" not found. available modes: ${availableModes}`,
availableModes: ctx.modes.map((m) => ({
name: m.name,
description: m.description
@@ -147915,6 +148026,20 @@ function SelectModeTool(ctx) {
};
}
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);
})
});
@@ -148723,7 +148848,7 @@ import { join as join12 } from "node:path";
// package.json
var package_default = {
name: "@pullfrog/pullfrog",
version: "0.0.177",
version: "0.0.178",
type: "module",
files: [
"index.js",