reorder prompt sections: task-first with dynamic TOC (#513)
* reorder prompt sections: task-first with dynamic TOC put the actual task at the top of the prompt for primacy, add a dynamic table of contents, and push system/runtime metadata to the end. new section order: TOC → YOUR TASK → PROCEDURE → EVENT CONTEXT → SYSTEM → LEARNINGS → RUNTIME Made-with: Cursor * enforce clean working tree: continue session if agent leaves uncommitted changes after each agent run, check `git status --porcelain`. if dirty, resume the same session with instructions to commit on a new branch, push, and open a PR. retries up to 3 times before giving up. - claude code: capture session_id from result event, use --resume <id> - opencode: use --continue to resume the last session - remove --no-session-persistence from claude (needed for --resume) - update Task mode to clarify branch/push/PR is the default finalize step Made-with: Cursor * log full prompt in collapsible group for debugging Made-with: Cursor * fix: format tool refs in buildCommitPrompt via formatMcpToolRef * enforce clean git status: general instructions, stop hook, and Task mode Made-with: Cursor * fix: rename stale titleBody references after body leak fix Made-with: Cursor --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
committed by
pullfrog[bot]
parent
6b93e6b368
commit
b9b6503315
+50
-17
@@ -15,7 +15,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { pullfrogMcpName } from "../external.ts";
|
||||
|
||||
import { getIdleMs, markActivity } from "../utils/activity.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
type AgentRunContext,
|
||||
type AgentUsage,
|
||||
agent,
|
||||
buildCommitPrompt,
|
||||
getGitStatus,
|
||||
MAX_COMMIT_RETRIES,
|
||||
MAX_STDERR_LINES,
|
||||
} from "./shared.ts";
|
||||
|
||||
@@ -53,7 +56,7 @@ function writeMcpConfig(ctx: AgentRunContext): string {
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
[ghPullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl },
|
||||
[pullfrogMcpName]: { type: "http", url: ctx.mcpServerUrl },
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -177,12 +180,15 @@ type RunParams = {
|
||||
todoTracker?: TodoTracker | undefined;
|
||||
};
|
||||
|
||||
async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||
type ClaudeRunResult = AgentResult & { sessionId?: string | undefined };
|
||||
|
||||
async function runClaude(params: RunParams): Promise<ClaudeRunResult> {
|
||||
const startTime = performance.now();
|
||||
let eventCount = 0;
|
||||
const thinkingTimer = new ThinkingTimer();
|
||||
|
||||
let finalOutput = "";
|
||||
let sessionId: string | undefined;
|
||||
let accumulatedTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
let tokensLogged = false;
|
||||
|
||||
@@ -271,6 +277,7 @@ async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||
}
|
||||
},
|
||||
result: (event: ClaudeResultEvent) => {
|
||||
if (event.session_id) sessionId = event.session_id;
|
||||
const subtype = event.subtype || "unknown";
|
||||
const numTurns = event.num_turns || 0;
|
||||
|
||||
@@ -431,7 +438,13 @@ async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||
);
|
||||
log.debug(`stdout: ${result.stdout?.substring(0, 500)}`);
|
||||
log.debug(`stderr: ${result.stderr?.substring(0, 500)}`);
|
||||
return { success: false, output: finalOutput || output, error: errorMessage, usage };
|
||||
return {
|
||||
success: false,
|
||||
output: finalOutput || output,
|
||||
error: errorMessage,
|
||||
usage,
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventCount === 0 && lastProviderError) {
|
||||
@@ -440,10 +453,11 @@ async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||
output: finalOutput || output,
|
||||
error: `provider error: ${lastProviderError}`,
|
||||
usage,
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, output: finalOutput || output, usage };
|
||||
return { success: true, output: finalOutput || output, usage, sessionId };
|
||||
} catch (error) {
|
||||
params.todoTracker?.cancel();
|
||||
const duration = performance.now() - startTime;
|
||||
@@ -471,6 +485,7 @@ async function runClaude(params: RunParams): Promise<AgentResult> {
|
||||
output: finalOutput || output,
|
||||
error: `${errorMessage} [${diagnosis}]`,
|
||||
usage: buildUsage(),
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -557,17 +572,15 @@ export const claude = agent({
|
||||
|
||||
installManagedSettings();
|
||||
|
||||
const args = [
|
||||
// base args shared between initial run and continue runs
|
||||
const baseArgs = [
|
||||
cliPath,
|
||||
"-p",
|
||||
ctx.instructions.full,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--dangerously-skip-permissions",
|
||||
"--mcp-config",
|
||||
mcpConfigPath,
|
||||
"--verbose",
|
||||
"--no-session-persistence",
|
||||
"--effort",
|
||||
effort,
|
||||
"--disallowedTools",
|
||||
@@ -576,7 +589,7 @@ export const claude = agent({
|
||||
];
|
||||
|
||||
if (model) {
|
||||
args.push("--model", model);
|
||||
baseArgs.push("--model", model);
|
||||
}
|
||||
|
||||
// agent process gets full env — needs LLM API keys, PATH, locale, etc.
|
||||
@@ -589,15 +602,35 @@ export const claude = agent({
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.info(`» effort: ${effort}`);
|
||||
log.debug(`» starting Pullfrog (Claude Code): node ${args.join(" ")}`);
|
||||
log.debug(`» starting Pullfrog (Claude Code): node ${baseArgs.join(" ")}`);
|
||||
log.debug(`» working directory: ${repoDir}`);
|
||||
|
||||
return runClaude({
|
||||
label: "Pullfrog",
|
||||
args,
|
||||
cwd: repoDir,
|
||||
env,
|
||||
todoTracker: ctx.todoTracker,
|
||||
const runParams = { label: "Pullfrog", cwd: repoDir, env, todoTracker: ctx.todoTracker };
|
||||
|
||||
let result = await runClaude({
|
||||
...runParams,
|
||||
args: [...baseArgs, "-p", ctx.instructions.full],
|
||||
});
|
||||
|
||||
// post-run: if the working tree is dirty, resume the session and ask the agent to commit
|
||||
for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
|
||||
if (!result.success || !result.sessionId) break;
|
||||
const status = getGitStatus();
|
||||
if (!status) break;
|
||||
|
||||
log.info(`» dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):\n${status}`);
|
||||
result = await runClaude({
|
||||
...runParams,
|
||||
args: [
|
||||
...baseArgs,
|
||||
"-p",
|
||||
buildCommitPrompt("claude", status),
|
||||
"--resume",
|
||||
result.sessionId,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
+29
-6
@@ -16,7 +16,7 @@ import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { ghPullfrogMcpName } from "../external.ts";
|
||||
import { pullfrogMcpName } from "../external.ts";
|
||||
import { modelAliases } from "../models.ts";
|
||||
import { getIdleMs, markActivity } from "../utils/activity.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
type AgentRunContext,
|
||||
type AgentUsage,
|
||||
agent,
|
||||
buildCommitPrompt,
|
||||
getGitStatus,
|
||||
MAX_COMMIT_RETRIES,
|
||||
MAX_STDERR_LINES,
|
||||
} from "./shared.ts";
|
||||
|
||||
@@ -66,7 +69,7 @@ function buildSecurityConfig(ctx: AgentRunContext, model: string | undefined): s
|
||||
skill: "allow",
|
||||
},
|
||||
mcp: {
|
||||
[ghPullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl },
|
||||
[pullfrogMcpName]: { type: "remote", url: ctx.mcpServerUrl },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -627,7 +630,8 @@ export const opentoad = agent({
|
||||
agent: "opencode",
|
||||
});
|
||||
|
||||
const args = ["run", ctx.instructions.full, "--format", "json", "--print-logs"];
|
||||
// base args shared between initial run and continue runs
|
||||
const baseArgs = ["run", "--format", "json", "--print-logs"];
|
||||
|
||||
// OPENCODE_PERMISSION has absolute highest precedence (merged after managed/MDM configs).
|
||||
// external_directory gates ALL native filesystem tools (Read, Write, Edit, Glob, Grep, etc.)
|
||||
@@ -647,16 +651,35 @@ export const opentoad = agent({
|
||||
|
||||
const repoDir = process.cwd();
|
||||
|
||||
log.debug(`» starting Pullfrog (OpenCode): ${cliPath} ${args.join(" ")}`);
|
||||
log.debug(`» starting Pullfrog (OpenCode): ${cliPath} ${baseArgs.join(" ")}`);
|
||||
log.debug(`» working directory: ${repoDir}`);
|
||||
|
||||
return runOpenCode({
|
||||
const runParams = {
|
||||
label: "Pullfrog",
|
||||
cliPath,
|
||||
args,
|
||||
cwd: repoDir,
|
||||
env,
|
||||
todoTracker: ctx.todoTracker,
|
||||
};
|
||||
|
||||
let result = await runOpenCode({
|
||||
...runParams,
|
||||
args: [...baseArgs, ctx.instructions.full],
|
||||
});
|
||||
|
||||
// post-run: if the working tree is dirty, continue the session and ask the agent to commit
|
||||
for (let attempt = 0; attempt < MAX_COMMIT_RETRIES; attempt++) {
|
||||
if (!result.success) break;
|
||||
const status = getGitStatus();
|
||||
if (!status) break;
|
||||
|
||||
log.info(`» dirty working tree (attempt ${attempt + 1}/${MAX_COMMIT_RETRIES}):\n${status}`);
|
||||
result = await runOpenCode({
|
||||
...runParams,
|
||||
args: [...baseArgs, "--continue", buildCommitPrompt("opentoad", status)],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
+28
-1
@@ -1,3 +1,5 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import type { AgentId } from "../external.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import type { ResolvedInstructions } from "../utils/instructions.ts";
|
||||
import type { ResolvedPayload } from "../utils/payload.ts";
|
||||
@@ -6,6 +8,31 @@ import type { TodoTracker } from "../utils/todoTracking.ts";
|
||||
// maximum number of stderr lines to keep in the rolling buffer during agent execution
|
||||
export const MAX_STDERR_LINES = 20;
|
||||
|
||||
// ── post-run commit enforcement ─────────────────────────────────────────────────
|
||||
|
||||
export const MAX_COMMIT_RETRIES = 3;
|
||||
|
||||
export function getGitStatus(): string {
|
||||
try {
|
||||
return execFileSync("git", ["status", "--porcelain"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 10_000,
|
||||
}).trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCommitPrompt(_agentId: AgentId, status: string): string {
|
||||
return [
|
||||
`UNCOMMITTED CHANGES — the working tree is dirty. push all changes to a pull request (new or existing). \`git status\` must be clean before you finish.`,
|
||||
"",
|
||||
"```",
|
||||
status,
|
||||
"```",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* token/cost usage data from a single agent run
|
||||
*/
|
||||
@@ -42,7 +69,7 @@ export interface AgentRunContext {
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
name: string;
|
||||
name: AgentId;
|
||||
install: (token?: string) => Promise<string>;
|
||||
run: (ctx: AgentRunContext) => Promise<AgentResult>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user