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
|
||||
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",
|
||||
|
||||
+104
-10
@@ -1,11 +1,43 @@
|
||||
import { type } from "arktype";
|
||||
import type { Agent } from "../agents/index.ts";
|
||||
import { apiFetch } from "../utils/apiFetch.ts";
|
||||
import { getApiUrl } from "../utils/apiUrl.ts";
|
||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { type OctokitWithPlugins, parseRepoContext } from "../utils/github.ts";
|
||||
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> {
|
||||
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.
|
||||
* 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({
|
||||
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)."
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export function CreateCommentTool(ctx: ToolContext) {
|
||||
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.",
|
||||
"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({
|
||||
@@ -104,6 +142,10 @@ export function CreateCommentTool(ctx: ToolContext) {
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
if (commentType === "Plan" && result.data.node_id) {
|
||||
await updatePlanCommentId(ctx, result.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
@@ -147,6 +189,9 @@ export function EditCommentTool(ctx: ToolContext) {
|
||||
|
||||
export const ReportProgress = type({
|
||||
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(
|
||||
ctx: ToolContext,
|
||||
{ body }: { body: string }
|
||||
params: { body: string; target_plan_comment?: boolean }
|
||||
): Promise<{
|
||||
commentId?: number;
|
||||
url?: string;
|
||||
body: string;
|
||||
action: "created" | "updated" | "skipped";
|
||||
}> {
|
||||
const { body, target_plan_comment } = params;
|
||||
// always track the body for job summary
|
||||
ctx.toolState.lastProgressBody = body;
|
||||
|
||||
@@ -177,14 +223,50 @@ export async function reportProgress(
|
||||
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 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 (existingCommentId) {
|
||||
const customParts =
|
||||
@@ -209,6 +291,10 @@ export async function reportProgress(
|
||||
|
||||
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,
|
||||
@@ -264,6 +350,10 @@ export async function reportProgress(
|
||||
body: bodyWithPlanLink,
|
||||
});
|
||||
|
||||
if (updateResult.data.node_id) {
|
||||
await updatePlanCommentId(ctx, updateResult.data.node_id);
|
||||
}
|
||||
|
||||
return {
|
||||
commentId: updateResult.data.id,
|
||||
url: updateResult.data.html_url,
|
||||
@@ -286,8 +376,12 @@ export function ReportProgressTool(ctx: ToolContext) {
|
||||
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: 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") {
|
||||
// no-op: no comment target, but progress is still tracked for job summary
|
||||
|
||||
+64
-4
@@ -1,6 +1,7 @@
|
||||
import { type } from "arktype";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import type { Mode } from "../modes.ts";
|
||||
import { apiFetch } from "../utils/apiFetch.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
@@ -8,6 +9,9 @@ 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')"
|
||||
),
|
||||
"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 {
|
||||
@@ -166,6 +170,22 @@ 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 — 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
|
||||
@@ -214,8 +234,8 @@ type OrchestratorGuidance = {
|
||||
orchestratorGuidance: string;
|
||||
};
|
||||
|
||||
function buildOrchestratorGuidance(mode: Mode): OrchestratorGuidance {
|
||||
const guidance = modeGuidance[mode.name] ?? "";
|
||||
function buildOrchestratorGuidance(mode: Mode, overrideGuidance?: string): OrchestratorGuidance {
|
||||
const guidance = overrideGuidance ?? modeGuidance[mode.name] ?? "";
|
||||
return {
|
||||
modeName: mode.name,
|
||||
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) {
|
||||
return tool({
|
||||
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) {
|
||||
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,
|
||||
@@ -250,6 +294,22 @@ export function SelectModeTool(ctx: ToolContext) {
|
||||
}
|
||||
|
||||
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);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -64,6 +64,9 @@ export interface ToolState {
|
||||
progressCommentId: number | null | undefined;
|
||||
lastProgressBody?: string;
|
||||
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;
|
||||
usageEntries: AgentUsage[];
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pullfrog/pullfrog",
|
||||
"version": "0.0.177",
|
||||
"version": "0.0.178",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"index.js",
|
||||
|
||||
@@ -41258,14 +41258,20 @@ function createOctokit(token) {
|
||||
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")
|
||||
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({
|
||||
commentId: type.number.describe("the ID of the comment to edit"),
|
||||
body: type.string.describe("the new comment body content")
|
||||
});
|
||||
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({
|
||||
pull_number: type.number.describe("the pull request number"),
|
||||
@@ -41312,7 +41318,7 @@ var Effort = type.enumerated("mini", "auto", "max");
|
||||
// package.json
|
||||
var package_default = {
|
||||
name: "@pullfrog/pullfrog",
|
||||
version: "0.0.177",
|
||||
version: "0.0.178",
|
||||
type: "module",
|
||||
files: [
|
||||
"index.js",
|
||||
|
||||
Reference in New Issue
Block a user