refactor instructions to return object with full/system/user/event/runtime properties, fix duplicate modes and json prompt extraction (#110)

This commit is contained in:
Colin McDonnell
2026-01-16 21:43:54 +00:00
committed by pullfrog[bot]
parent 410b11db71
commit cb925556e8
16 changed files with 1843 additions and 1952 deletions
+7 -45
View File
@@ -1,59 +1,21 @@
import type { ToolState } from "../mcp/server.ts";
import { createOctokit, parseRepoContext } from "./github.ts";
import { getGitHubInstallationToken } from "./token.ts";
import { fetchWorkflowRunInfo } from "./workflowRun.ts";
/**
* Get progress comment ID from environment variable or database.
*/
function getProgressCommentIdFromEnv(): number | null {
const envCommentId = process.env.PULLFROG_PROGRESS_COMMENT_ID;
if (envCommentId) {
const parsed = parseInt(envCommentId, 10);
if (!Number.isNaN(parsed)) {
return parsed;
}
}
return null;
}
export async function reportErrorToComment({
error,
title,
}: {
interface ReportErrorParams {
toolState: ToolState;
error: string;
title?: string;
}): Promise<void> {
const formattedError = title ? `${title}\n\n${error}` : error;
}
// try to get comment ID from env var first, then from database if needed
let commentId = getProgressCommentIdFromEnv();
export async function reportErrorToComment(ctx: ReportErrorParams): Promise<void> {
const formattedError = ctx.title ? `${ctx.title}\n\n${ctx.error}` : ctx.error;
// if not in env var, try fetching from database using run ID
if (!commentId) {
const runId = process.env.GITHUB_RUN_ID;
if (runId) {
try {
const workflowRunInfo = await fetchWorkflowRunInfo(runId);
if (workflowRunInfo.progressCommentId) {
const parsed = parseInt(workflowRunInfo.progressCommentId, 10);
if (!Number.isNaN(parsed)) {
commentId = parsed;
// cache it in env var for future use
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
}
}
} catch {
// database fetch failed, continue without comment ID
}
}
}
// if no comment ID available, can't update comment
const commentId = ctx.toolState.progressComment.id;
if (!commentId) {
return;
}
// update comment directly using GitHub API
const repoContext = parseRepoContext();
const octokit = createOctokit(getGitHubInstallationToken());
+47 -39
View File
@@ -1,20 +1,17 @@
import { execSync } from "node:child_process";
import { encode as toonEncode } from "@toon-format/toon";
import { ghPullfrogMcpName } from "../external.ts";
import { computeModes, type Mode } from "../modes.ts";
import type { Mode } from "../modes.ts";
import type { ResolvedPayload } from "./payload.ts";
import type { RepoData } from "./repoData.ts";
type BashPermission = "disabled" | "restricted" | "enabled";
interface InstructionsInput {
prompt: string;
event: { trigger: string; [key: string]: unknown };
interface InstructionsContext {
payload: ResolvedPayload;
repoData: RepoData;
modes: Mode[];
bash: BashPermission;
}
function buildRuntimeContext(input: InstructionsInput): string {
function buildRuntimeContext(ctx: InstructionsContext): string {
const lines: string[] = [];
lines.push(`working_directory: ${process.cwd()}`);
@@ -27,8 +24,8 @@ function buildRuntimeContext(input: InstructionsInput): string {
// git not available or not in a repo
}
lines.push(`repo: ${input.repoData.owner}/${input.repoData.name}`);
lines.push(`default_branch: ${input.repoData.repo.default_branch}`);
lines.push(`repo: ${ctx.repoData.owner}/${ctx.repoData.name}`);
lines.push(`default_branch: ${ctx.repoData.repo.default_branch}`);
const ghVars: Record<string, string | undefined> = {
github_event_name: process.env.GITHUB_EVENT_NAME,
@@ -47,7 +44,7 @@ function buildRuntimeContext(input: InstructionsInput): string {
return lines.join("\n");
}
function getShellInstructions(bash: BashPermission): string {
function getShellInstructions(bash: ResolvedPayload["bash"]): string {
switch (bash) {
case "disabled":
return `**Shell commands**: Shell command execution is DISABLED. Do not attempt to run shell commands.`;
@@ -62,21 +59,35 @@ function getShellInstructions(bash: BashPermission): string {
}
}
export function resolveInstructions(input: InstructionsInput): string {
let encodedEvent = "";
export interface ResolvedInstructions {
full: string;
system: string;
user: string;
event: string;
runtime: string;
}
const eventKeys = Object.keys(input.event);
if (eventKeys.length === 1 && eventKeys[0] === "trigger") {
// no meaningful event data to encode
} else {
encodedEvent = toonEncode(input.event);
}
export function resolveInstructions(ctx: InstructionsContext): ResolvedInstructions {
const event = toonEncode({
agent: ctx.payload.agent,
effort: ctx.payload.effort,
permissions: {
web: ctx.payload.web,
search: ctx.payload.search,
write: ctx.payload.write,
bash: ctx.payload.bash,
},
event: ctx.payload.event,
});
const runtimeContext = buildRuntimeContext(input);
const runtime = buildRuntimeContext(ctx);
return (
`
***********************************************
const user = ctx.payload.prompt
.split("\n")
.map((line) => `> ${line}`)
.join("\n");
const system = `***********************************************
************* SYSTEM INSTRUCTIONS *************
***********************************************
@@ -121,7 +132,7 @@ Tool names may be formatted as \`(server name)/(tool name)\`, for example: \`${g
**Efficiency**: Trust the tools - do not repeatedly verify file contents or git status after operations. If a tool reports success, proceed to the next step. Only verify if you encounter an actual error.
${getShellInstructions(input.bash)}
${getShellInstructions(ctx.payload.bash)}
**Command execution**: Never use \`sleep\` to wait for commands to complete. Commands run synchronously - when the bash tool returns, the command has finished.
@@ -142,33 +153,30 @@ ${getShellInstructions(input.bash)}
### Available modes
${[...computeModes({ hasProgressComment: true }), ...input.modes].map((w) => ` - "${w.name}": ${w.description}`).join("\n")}
${ctx.modes.map((m) => `- "${m.name}": ${m.description}`).join("\n")}
### Following the mode instructions
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.
Eagerly inspect the MCP tools available to you via the \`${ghPullfrogMcpName}\` MCP server. These are VITALLY IMPORTANT to completing your task.`;
const full = `
${system}
************* USER PROMPT *************
${input.prompt
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}
${user}
${
encodedEvent
? `************* EVENT DATA *************
************* EVENT DATA *************
The following is structured data about the GitHub event that triggered this run (e.g., issue body, PR details, comment content). Use this context to understand the full situation.
The following is structured data about the context of this run (agent, effort level, permissions, and the GitHub event that triggered it). Use this context to understand the full situation.
${encodedEvent}`
: ""
}
${event}
************* RUNTIME CONTEXT *************
${runtimeContext}`
);
${runtime}`;
return { full, system, user, event, runtime };
}
+3 -1
View File
@@ -108,7 +108,9 @@ export function resolvePayload(repoSettings: RepoSettings) {
return {
"~pullfrog": true as const,
agent: resolvedAgent,
prompt: inputs.prompt ?? jsonPayload?.prompt,
// 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,
event,
effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
cwd: resolveCwd(inputs.cwd),
+3 -3
View File
@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
import { mkdtemp } from "node:fs/promises";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { PayloadEvent } from "../external.ts";
@@ -16,8 +16,8 @@ export interface SetupOptions {
/**
* Create a shared temp directory for the action
*/
export async function createTempDirectory(): Promise<string> {
const sharedTempDir = await mkdtemp(join(tmpdir(), "pullfrog-"));
export function createTempDirectory(): string {
const sharedTempDir = mkdtempSync(join(tmpdir(), "pullfrog-"));
process.env.PULLFROG_TEMP_DIR = sharedTempDir;
log.info(`» created temp dir at ${sharedTempDir}`);
return sharedTempDir;
-1
View File
@@ -27,7 +27,6 @@ export async function resolveRun(params: ResolveRunParams): Promise<ResolveRunRe
const workflowRunInfo = runId ? await fetchWorkflowRunInfo(runId) : { progressCommentId: null };
if (workflowRunInfo.progressCommentId) {
process.env.PULLFROG_PROGRESS_COMMENT_ID = workflowRunInfo.progressCommentId;
log.info(`» using pre-created progress comment: ${workflowRunInfo.progressCommentId}`);
}