Make prompt construction more disciplined (#173)

* Make prompt construction more disciplined

* Clean up

* Tweaks
This commit is contained in:
Colin McDonnell
2026-01-24 18:49:47 +00:00
committed by pullfrog[bot]
parent 54279e313b
commit 210084a3b6
7 changed files with 170 additions and 87 deletions
+9 -1
View File
@@ -33,7 +33,15 @@ export const agent = <const input extends AgentInput>(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}`);
+71 -41
View File
@@ -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",
+3 -1
View File
@@ -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 */
-1
View File
@@ -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,
};
+8 -6
View File
@@ -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.:
+76 -36
View File
@@ -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 };
}
+3 -1
View File
@@ -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",