From 210084a3b6083af86da07843546177d156dbdf64 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Sat, 24 Jan 2026 18:49:47 +0000 Subject: [PATCH] Make prompt construction more disciplined (#173) * Make prompt construction more disciplined * Clean up * Tweaks --- agents/shared.ts | 10 +++- entry | 112 ++++++++++++++++++++++++++---------------- external.ts | 4 +- mcp/issueComments.ts | 1 - modes.ts | 14 +++--- utils/instructions.ts | 112 ++++++++++++++++++++++++++++-------------- utils/payload.ts | 4 +- 7 files changed, 170 insertions(+), 87 deletions(-) diff --git a/agents/shared.ts b/agents/shared.ts index d9b9d4a..7f7a08a 100644 --- a/agents/shared.ts +++ b/agents/shared.ts @@ -33,7 +33,15 @@ export const agent = (input: input): defineAgent const search = ctx.payload.search; const write = ctx.payload.write; log.info(`» running ${input.name} with effort=${ctx.payload.effort}...`); - log.box(ctx.instructions.user.trim() + "\n\n" + ctx.instructions.event.trim(), { + // build log box content: eventInstructions (if any) + user request (if any) + event data + const logParts = [ + ctx.instructions.eventInstructions + ? `EVENT-LEVEL INSTRUCTIONS:\n${ctx.instructions.eventInstructions}` + : null, + ctx.instructions.user ? `USER REQUEST:\n${ctx.instructions.user}` : null, + ctx.instructions.event, + ].filter(Boolean); + log.box(logParts.join("\n\n---\n\n"), { title: "Instructions", }); log.info(`» tool permissions: web=${web}, search=${search}, write=${write}, bash=${bash}`); diff --git a/entry b/entry index 7b599db..81280dd 100755 --- a/entry +++ b/entry @@ -138323,8 +138323,7 @@ function GetIssueCommentsTool(ctx) { comments: comments.map((comment) => ({ id: comment.id, body: comment.body, - user: comment.user?.login, - author_association: comment.author_association + user: comment.user?.login })), count: comments.length }; @@ -139239,7 +139238,7 @@ function computeModes() { { name: "Build", description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details", - prompt: `Follow these steps. THINK HARDER. + prompt: `Follow these steps exactly. 1. Determine whether to work on the current branch or create a new one: - **PR event, modifying the existing PR**: The PR branch is probably already checked out. Continue on this branch. - **PR event, but user wants a NEW branch/PR**: Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch from the current HEAD. @@ -139253,17 +139252,19 @@ function computeModes() { 4. Understand the requirements and any existing plan -5. Make the necessary code changes using file operations. Then use ${ghPullfrogMcpName}/commit_files to commit your changes, and ${ghPullfrogMcpName}/push_branch to push the branch. Do NOT use git commands like \`git commit\` or \`git push\` directly. +5. Make the necessary code changes using file operations. You should change the minimum amount of code necessary to accomplish your task. Emphasize code quality and elegance. -6. Test your changes to ensure they work correctly +6. Then use ${ghPullfrogMcpName}/commit_files to commit your changes, and ${ghPullfrogMcpName}/push_branch to push the branch. Do NOT use git commands like \`git commit\` or \`git push\` directly. -7. ${reportProgressInstruction} +7. Test your changes to ensure they work correctly -8. Determine whether to create a PR: +8. ${reportProgressInstruction} + +9. Determine whether to create a PR (if not already on a PR branch): - **Default behavior**: Create a PR using ${ghPullfrogMcpName}/create_pull_request with an informative title and body. If relevant, indicate which issue the PR addresses (e.g. "Fixes #123"). - **Branch-only request**: If the user explicitly asks for a branch without a PR (e.g. "don't create a PR", "branch only", "just create a branch"), do NOT create a PR. Simply push the branch and report the branch link. -9. Call report_progress one final time ONLY if you haven't already included all the important information (PR links, branch links, summary) in a previous report_progress call. If you already called report_progress with complete information including PR links after creating the PR, you do NOT need to call it again. Only make a final call if you need to add missing information. When making the final call, ensure it includes: +10. Call report_progress one final time ONLY if you haven't already included all the important information (PR links, branch links, summary) in a previous report_progress call. If you already called report_progress with complete information including PR links after creating the PR, you do NOT need to call it again. Only make a final call if you need to add missing information. When making the final call, ensure it includes: - A summary of what was accomplished - Links to any artifacts created (PRs, branches, issues) - If you created a PR, ALWAYS include the PR link. e.g.: @@ -156190,7 +156191,14 @@ var agent = (input) => { const search2 = ctx.payload.search; const write = ctx.payload.write; log.info(`\xBB running ${input.name} with effort=${ctx.payload.effort}...`); - log.box(ctx.instructions.user.trim() + "\n\n" + ctx.instructions.event.trim(), { + const logParts = [ + ctx.instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS: +${ctx.instructions.eventInstructions}` : null, + ctx.instructions.user ? `USER REQUEST: +${ctx.instructions.user}` : null, + ctx.instructions.event + ].filter(Boolean); + log.box(logParts.join("\n\n---\n\n"), { title: "Instructions" }); log.info(`\xBB tool permissions: web=${web}, search=${search2}, write=${write}, bash=${bash}`); @@ -157885,6 +157893,7 @@ function buildRuntimeContext(ctx) { const { "~pullfrog": _, prompt: _p, + eventInstructions: _ei, repoInstructions: _r, event: _e, ...payloadRest @@ -157911,24 +157920,26 @@ function buildRuntimeContext(ctx) { const filtered = Object.fromEntries(Object.entries(data).filter(([_2, v]) => v !== void 0)); return encode(filtered); } -function buildEventData(event) { - const { title, body, trigger, ...rest } = event; +function buildEventTitleBody(event) { const sections = []; - const trimmedTitle = typeof title === "string" ? title.trim() : ""; - const trimmedBody = typeof body === "string" ? body.trim() : ""; + const trimmedTitle = typeof event.title === "string" ? event.title.trim() : ""; + const trimmedBody = typeof event.body === "string" ? event.body.trim() : ""; if (trimmedTitle) { sections.push(`# ${trimmedTitle}`); } if (trimmedBody) { sections.push(trimmedBody); } - const restWithTrigger = trigger === "workflow_dispatch" ? rest : { trigger, ...rest }; - if (Object.keys(restWithTrigger).length > 0) { - if (sections.length > 0) sections.push("---"); - sections.push(encode(restWithTrigger)); - } return sections.join("\n\n"); } +function buildEventMetadata(event) { + const { title: _t, body: _b, trigger, ...rest } = event; + const restWithTrigger = trigger === "workflow_dispatch" ? rest : { trigger, ...rest }; + if (Object.keys(restWithTrigger).length === 0) { + return ""; + } + return encode(restWithTrigger); +} function getShellInstructions(bash) { const backgroundInstructions = `For long-running processes (dev servers, watchers), use \`bash({ command, background: true })\` which returns a handle. Use \`${ghPullfrogMcpName}/kill_background\` to stop background processes by handle.`; switch (bash) { @@ -157945,11 +157956,16 @@ function getShellInstructions(bash) { } } function resolveInstructions(ctx) { - const event = buildEventData(ctx.payload.event); + const eventTitleBody = buildEventTitleBody(ctx.payload.event); + const eventMetadata = buildEventMetadata(ctx.payload.event); const runtime = buildRuntimeContext(ctx); const user = ctx.payload.prompt; + const eventInstructions = ctx.payload.eventInstructions ?? ""; const repo = ctx.payload.repoInstructions ?? ""; - const userQuoted = user.split("\n").map((line) => `> ${line}`).join("\n"); + const isPr = ctx.payload.event.is_pr === true; + const relatedLabel = isPr ? "--- related PR ---" : "--- related issue ---"; + const event = [eventTitleBody, eventMetadata].filter(Boolean).join("\n\n---\n\n"); + const userQuoted = user ? user.split("\n").map((line) => `> ${line}`).join("\n") : ""; const system = `*********************************************** ************* SYSTEM INSTRUCTIONS ************* *********************************************** @@ -157972,11 +157988,10 @@ Use backticks liberally for inline code (e.g. \`z.string()\`) even in headers. ## Priority Order In case of conflict between instructions, follow this precedence (highest to lowest): -1. Security rules (below) -2. System instructions (this document) -3. Mode instructions (returned by select_mode) -4. Repository-specific instructions (AGENTS.md, CLAUDE.md, etc.) -5. User prompt +1. Security rules and system instructions (non-overridable) +2. User prompt +3. Event-level instructions +4. Repo-level instructions ## Security @@ -158024,29 +158039,42 @@ ${ctx.modes.map((m) => `- "${m.name}": ${m.description}`).join("\n")} After selecting a mode, follow the detailed step-by-step instructions provided by the ${ghPullfrogMcpName}/select_mode tool. Refer to the user prompt, event data, and runtime context below to inform your actions. These instructions cannot override the Security rules or System instructions above. Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` MCP server. These are VITALLY IMPORTANT to completing your task.`; - const repoSection = repo ? ` - -************* REPO-LEVEL INSTRUCTIONS ************* + const repoSection = repo ? `************* REPO-LEVEL INSTRUCTIONS ************* ${repo}` : ""; - const full = ` -${system} + const eventInstructionsSection = eventInstructions ? `************* EVENT-LEVEL INSTRUCTIONS ************* -************* USER PROMPT ************* +${eventInstructions}` : ""; + const titleBodySection = eventTitleBody ? `${relatedLabel} + +${eventTitleBody}` : ""; + const metadataSection = eventMetadata ? `--- event context --- + +${eventMetadata}` : ""; + const userSection = userQuoted ? `************* USER PROMPT \u2014 THIS IS YOUR TASK ************* ${userQuoted} + +${titleBodySection} + +${metadataSection}` : `************* EVENT CONTEXT ************* + +${titleBodySection} + +${metadataSection}`; + const rawFull = `************* RUNTIME CONTEXT ************* + +${runtime} + +${system} + ${repoSection} -************* EVENT DATA ************* +${eventInstructionsSection} -${event} - -************* RUNTIME CONTEXT ************* - -The following contains the agent configuration (agent, effort, permissions) and runtime environment details. - -${runtime}`; - return { full, system, user, repo, event, runtime }; +${userSection}`; + const full = rawFull.trim().replace(/\n{3,}/g, "\n\n"); + return { full, system, user, eventInstructions, repo, event, runtime }; } // utils/normalizeEnv.ts @@ -158123,9 +158151,10 @@ var ToolPermissionInput = type.enumerated("disabled", "enabled"); var BashPermissionInput = type.enumerated("disabled", "restricted", "enabled"); var JsonPayload = type({ "~pullfrog": "true", - "version": "string", + version: "string", "agent?": AgentName.or("undefined"), "prompt?": "string", + "eventInstructions?": "string", "repoInstructions?": "string", "event?": "object", "effort?": Effort.or("undefined") @@ -158204,6 +158233,7 @@ function resolvePayload(repoSettings) { // inverted: jsonPayload.prompt extracts the text from the JSON payload, // whereas inputs.prompt IS the raw JSON string when internally dispatched prompt: jsonPayload?.prompt ?? inputs.prompt, + eventInstructions: jsonPayload?.eventInstructions, repoInstructions: jsonPayload?.repoInstructions, event, effort: inputs.effort ?? jsonPayload?.effort ?? "auto", diff --git a/external.ts b/external.ts index bb723b9..03f357f 100644 --- a/external.ts +++ b/external.ts @@ -245,8 +245,10 @@ export interface WriteablePayload { version: string; /** agent slug identifier (e.g., "claude", "codex", "gemini") */ agent?: AgentName | undefined; - /** the prompt/instructions for the agent to execute (body if @pullfrog tagged + per-trigger instructions) */ + /** the user's actual request (body if @pullfrog tagged) */ prompt: string; + /** event-level instructions for this trigger type (macro-expanded server-side) */ + eventInstructions?: string | undefined; /** repo-level instructions (macro-expanded server-side) */ repoInstructions?: string | undefined; /** event data from webhook payload - discriminated union based on trigger field */ diff --git a/mcp/issueComments.ts b/mcp/issueComments.ts index 53fe689..9d2c8b1 100644 --- a/mcp/issueComments.ts +++ b/mcp/issueComments.ts @@ -28,7 +28,6 @@ export function GetIssueCommentsTool(ctx: ToolContext) { id: comment.id, body: comment.body, user: comment.user?.login, - author_association: comment.author_association, })), count: comments.length, }; diff --git a/modes.ts b/modes.ts index dc41241..929db6e 100644 --- a/modes.ts +++ b/modes.ts @@ -24,7 +24,7 @@ export function computeModes(): Mode[] { name: "Build", description: "Implement, build, create, or develop code changes; make specific changes to files or features; execute a plan; or handle tasks with specific implementation details", - prompt: `Follow these steps. THINK HARDER. + prompt: `Follow these steps exactly. 1. Determine whether to work on the current branch or create a new one: - **PR event, modifying the existing PR**: The PR branch is probably already checked out. Continue on this branch. - **PR event, but user wants a NEW branch/PR**: Use \`${ghPullfrogMcpName}/create_branch\` to create a new branch from the current HEAD. @@ -38,17 +38,19 @@ export function computeModes(): Mode[] { 4. Understand the requirements and any existing plan -5. Make the necessary code changes using file operations. Then use ${ghPullfrogMcpName}/commit_files to commit your changes, and ${ghPullfrogMcpName}/push_branch to push the branch. Do NOT use git commands like \`git commit\` or \`git push\` directly. +5. Make the necessary code changes using file operations. You should change the minimum amount of code necessary to accomplish your task. Emphasize code quality and elegance. -6. Test your changes to ensure they work correctly +6. Then use ${ghPullfrogMcpName}/commit_files to commit your changes, and ${ghPullfrogMcpName}/push_branch to push the branch. Do NOT use git commands like \`git commit\` or \`git push\` directly. -7. ${reportProgressInstruction} +7. Test your changes to ensure they work correctly -8. Determine whether to create a PR: +8. ${reportProgressInstruction} + +9. Determine whether to create a PR (if not already on a PR branch): - **Default behavior**: Create a PR using ${ghPullfrogMcpName}/create_pull_request with an informative title and body. If relevant, indicate which issue the PR addresses (e.g. "Fixes #123"). - **Branch-only request**: If the user explicitly asks for a branch without a PR (e.g. "don't create a PR", "branch only", "just create a branch"), do NOT create a PR. Simply push the branch and report the branch link. -9. Call report_progress one final time ONLY if you haven't already included all the important information (PR links, branch links, summary) in a previous report_progress call. If you already called report_progress with complete information including PR links after creating the PR, you do NOT need to call it again. Only make a final call if you need to add missing information. When making the final call, ensure it includes: +10. Call report_progress one final time ONLY if you haven't already included all the important information (PR links, branch links, summary) in a previous report_progress call. If you already called report_progress with complete information including PR links after creating the PR, you do NOT need to call it again. Only make a final call if you need to add missing information. When making the final call, ensure it includes: - A summary of what was accomplished - Links to any artifacts created (PRs, branches, issues) - If you created a PR, ALWAYS include the PR link. e.g.: diff --git a/utils/instructions.ts b/utils/instructions.ts index 4074b4c..4b141a4 100644 --- a/utils/instructions.ts +++ b/utils/instructions.ts @@ -12,10 +12,11 @@ interface InstructionsContext { } function buildRuntimeContext(ctx: InstructionsContext): string { - // extract payload fields excluding prompt/repoInstructions/event + // extract payload fields excluding prompt/instructions/event (those are rendered separately) const { "~pullfrog": _, prompt: _p, + eventInstructions: _ei, repoInstructions: _r, event: _e, ...payloadRest @@ -50,13 +51,12 @@ function buildRuntimeContext(ctx: InstructionsContext): string { return toonEncode(filtered); } -function buildEventData(event: PayloadEvent): string { - const { title, body, trigger, ...rest } = event; +function buildEventTitleBody(event: PayloadEvent): string { const sections: string[] = []; // render title + body as markdown - const trimmedTitle = typeof title === "string" ? title.trim() : ""; - const trimmedBody = typeof body === "string" ? body.trim() : ""; + const trimmedTitle = typeof event.title === "string" ? event.title.trim() : ""; + const trimmedBody = typeof event.body === "string" ? event.body.trim() : ""; if (trimmedTitle) { sections.push(`# ${trimmedTitle}`); @@ -66,16 +66,20 @@ function buildEventData(event: PayloadEvent): string { sections.push(trimmedBody); } + return sections.join("\n\n"); +} + +function buildEventMetadata(event: PayloadEvent): string { + const { title: _t, body: _b, trigger, ...rest } = event; + // include trigger in rest unless it's workflow_dispatch (not informative) const restWithTrigger = trigger === "workflow_dispatch" ? rest : { trigger, ...rest }; - // separator and toon-encoded remaining fields - if (Object.keys(restWithTrigger).length > 0) { - if (sections.length > 0) sections.push("---"); - sections.push(toonEncode(restWithTrigger)); + if (Object.keys(restWithTrigger).length === 0) { + return ""; } - return sections.join("\n\n"); + return toonEncode(restWithTrigger); } function getShellInstructions(bash: ResolvedPayload["bash"]): string { @@ -99,25 +103,41 @@ export interface ResolvedInstructions { full: string; system: string; user: string; + eventInstructions: string; repo: string; event: string; runtime: string; } export function resolveInstructions(ctx: InstructionsContext): ResolvedInstructions { - const event = buildEventData(ctx.payload.event); + const eventTitleBody = buildEventTitleBody(ctx.payload.event); + const eventMetadata = buildEventMetadata(ctx.payload.event); const runtime = buildRuntimeContext(ctx); - // user prompt is constructed server-side (body if @pullfrog tagged + per-trigger instructions) + // user prompt is the user's actual request (body if @pullfrog tagged) const user = ctx.payload.prompt; - // repo-level instructions are macro-expanded server-side and passed separately + // event-level instructions are trigger-specific (macro-expanded server-side) + // note: server only sends these when there's no user prompt (user request has precedence) + const eventInstructions = ctx.payload.eventInstructions ?? ""; + + // repo-level instructions are macro-expanded server-side const repo = ctx.payload.repoInstructions ?? ""; + // determine if this is a PR or issue for labeling + const isPr = ctx.payload.event.is_pr === true; + const relatedLabel = isPr ? "--- related PR ---" : "--- related issue ---"; + + // combined event data for backwards compatibility + const event = [eventTitleBody, eventMetadata].filter(Boolean).join("\n\n---\n\n"); + + // quote user prompt with "> " to distinguish user-written content const userQuoted = user - .split("\n") - .map((line) => `> ${line}`) - .join("\n"); + ? user + .split("\n") + .map((line) => `> ${line}`) + .join("\n") + : ""; const system = `*********************************************** ************* SYSTEM INSTRUCTIONS ************* @@ -141,11 +161,10 @@ Use backticks liberally for inline code (e.g. \`z.string()\`) even in headers. ## Priority Order In case of conflict between instructions, follow this precedence (highest to lowest): -1. Security rules (below) -2. System instructions (this document) -3. Mode instructions (returned by select_mode) -4. Repository-specific instructions (AGENTS.md, CLAUDE.md, etc.) -5. User prompt +1. Security rules and system instructions (non-overridable) +2. User prompt +3. Event-level instructions +4. Repo-level instructions ## Security @@ -194,32 +213,53 @@ After selecting a mode, follow the detailed step-by-step instructions provided b Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` MCP server. These are VITALLY IMPORTANT to completing your task.`; - // build repo instructions section (only if non-empty) + // build optional sections (only if non-empty) const repoSection = repo - ? ` - -************* REPO-LEVEL INSTRUCTIONS ************* + ? `************* REPO-LEVEL INSTRUCTIONS ************* ${repo}` : ""; - const full = ` -${system} + const eventInstructionsSection = eventInstructions + ? `************* EVENT-LEVEL INSTRUCTIONS ************* -************* USER PROMPT ************* +${eventInstructions}` + : ""; + + // build the task/context section + // - if user gave direct @pullfrog request: show as USER PROMPT with event as context + // - if automatic trigger: show as EVENT CONTEXT (eventInstructions section has the task) + const titleBodySection = eventTitleBody ? `${relatedLabel}\n\n${eventTitleBody}` : ""; + const metadataSection = eventMetadata ? `--- event context ---\n\n${eventMetadata}` : ""; + + const userSection = userQuoted + ? `************* USER PROMPT — THIS IS YOUR TASK ************* ${userQuoted} + +${titleBodySection} + +${metadataSection}` + : `************* EVENT CONTEXT ************* + +${titleBodySection} + +${metadataSection}`; + + const rawFull = `************* RUNTIME CONTEXT ************* + +${runtime} + +${system} + ${repoSection} -************* EVENT DATA ************* +${eventInstructionsSection} -${event} +${userSection}`; -************* RUNTIME CONTEXT ************* + // normalize spacing: trim and collapse 3+ consecutive newlines to 2 + const full = rawFull.trim().replace(/\n{3,}/g, "\n\n"); -The following contains the agent configuration (agent, effort, permissions) and runtime environment details. - -${runtime}`; - - return { full, system, user, repo, event, runtime }; + return { full, system, user, eventInstructions, repo, event, runtime }; } diff --git a/utils/payload.ts b/utils/payload.ts index a143456..d1d0363 100644 --- a/utils/payload.ts +++ b/utils/payload.ts @@ -15,9 +15,10 @@ const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled") // permissions are derived from event.authorPermission instead export const JsonPayload = type({ "~pullfrog": "true", - "version": "string", + version: "string", "agent?": AgentName.or("undefined"), "prompt?": "string", + "eventInstructions?": "string", "repoInstructions?": "string", "event?": "object", "effort?": Effort.or("undefined"), @@ -140,6 +141,7 @@ export function resolvePayload(repoSettings: RepoSettings) { // inverted: jsonPayload.prompt extracts the text from the JSON payload, // whereas inputs.prompt IS the raw JSON string when internally dispatched prompt: jsonPayload?.prompt ?? inputs.prompt, + eventInstructions: jsonPayload?.eventInstructions, repoInstructions: jsonPayload?.repoInstructions, event, effort: inputs.effort ?? jsonPayload?.effort ?? "auto",