Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,28 @@ export default function Settings() {
</select>
</div>
</div>
<div className="mt-4 grid gap-2 border-t border-border pt-4">
<label htmlFor={`tracked-repo-agent-instructions-${id}`} className="text-label uppercase tracking-wider text-muted-foreground">
Agent instructions
</label>
<textarea
id={`tracked-repo-agent-instructions-${id}`}
defaultValue={repo.agentInstructions}
onBlur={(e) => {
const next = e.currentTarget.value;
if (next !== repo.agentInstructions) {
updateRepoSettingsMutation.mutate({
repo: repo.repo,
agentInstructions: next,
});
}
}}
disabled={updateRepoSettingsMutation.isPending}
rows={4}
placeholder="Repository-specific instructions for issue work and PR babysitter runs."
className="min-h-24 resize-y border border-border bg-transparent px-3 py-2 text-body leading-relaxed focus:border-primary focus:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:opacity-50"
/>
</div>
</div>
);
})}
Expand Down
Binary file modified docs/assets/PatchDeck-Settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions server/babysitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5023,6 +5023,9 @@ test("babysitPR retries docs assessment for same-SHA failed state", async () =>

test("babysitPR runs agent for docs-only work when docs assessment says needed", async () => {
const storage = new MemStorage();
await storage.updateRepoSettings("alex-morgan-o/lolodex", {
agentInstructions: "Use pnpm docs:check before finishing.",
});
const pr = await storage.addPR({
number: 106,
title: "Docs-only remediation",
Expand Down Expand Up @@ -5092,6 +5095,7 @@ test("babysitPR runs agent for docs-only work when docs assessment says needed",
assert.equal(updated?.docsAssessment?.status, "needed");
assert.match(capturedPrompt, /Approved documentation tasks:/);
assert.match(capturedPrompt, /README and API docs need updates/);
assert.match(capturedPrompt, /Use pnpm docs:check before finishing\./);
assert.match(capturedPrompt, /DOCS_SUMMARY_START <changed\|no_change>/);
} finally {
delete process.env.CODEFACTORY_HOME;
Expand Down
41 changes: 37 additions & 4 deletions server/babysitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,14 +390,29 @@ function truncateForPrompt(input: string, maxChars: number): string {
return `${input.slice(0, maxChars)}\n... (truncated)`;
}

function formatRepoAgentInstructions(instructions?: string | null): string[] {
const trimmed = instructions?.trim();
if (!trimmed) {
return ["Repository agent instructions: none"];
}

return [
"Repository agent instructions:",
"```",
truncateForPrompt(trimmed, 4000),
"```",
];
}

function buildDocumentationAssessmentPrompt(params: {
pr: PR;
pullSummary: GitHubPullSummary;
changedFiles: string;
diffStat: string;
diffPreview: string;
agentInstructions?: string | null;
}): string {
const { pr, pullSummary, changedFiles, diffStat, diffPreview } = params;
const { pr, pullSummary, changedFiles, diffStat, diffPreview, agentInstructions } = params;

return [
"You are deciding whether a pull request requires repository documentation updates.",
Expand All @@ -408,6 +423,8 @@ function buildDocumentationAssessmentPrompt(params: {
`Base branch: ${pullSummary.baseRef}`,
`Head branch: ${pullSummary.headRef}`,
"",
...formatRepoAgentInstructions(agentInstructions),
"",
"Changed files (git diff --name-only origin/base...HEAD):",
truncateForPrompt(changedFiles || "None", 4000),
"",
Expand Down Expand Up @@ -452,8 +469,9 @@ function buildConflictResolutionPrompt(params: {
pullSummary: GitHubPullSummary;
remoteName: string;
conflictFiles: string[];
agentInstructions?: string | null;
}): string {
const { pr, pullSummary, remoteName, conflictFiles } = params;
const { pr, pullSummary, remoteName, conflictFiles, agentInstructions } = params;

return [
`You are acting as an autonomous PR babysitter for ${pr.repo} PR #${pr.number}.`,
Expand All @@ -465,6 +483,8 @@ function buildConflictResolutionPrompt(params: {
`Head remote: ${remoteName}`,
"You are running inside an isolated app-owned worktree under ~/.patchdeck.",
"",
...formatRepoAgentInstructions(agentInstructions),
"",
"A merge from the base branch into the head branch has been started but has conflicts.",
"The following files have merge conflicts:",
...conflictFiles.map((f) => ` - ${f}`),
Expand All @@ -490,8 +510,9 @@ function buildAgentFixPrompt(params: {
commentTasks: FeedbackItem[];
statusTasks: { context: string; description: string; targetUrl: string | null }[];
docsTaskSummary: string | null;
agentInstructions?: string | null;
}): string {
const { pr, pullSummary, remoteName, commentTasks, statusTasks, docsTaskSummary } = params;
const { pr, pullSummary, remoteName, commentTasks, statusTasks, docsTaskSummary, agentInstructions } = params;

const commentSection = commentTasks.length
? commentTasks
Expand Down Expand Up @@ -543,6 +564,8 @@ function buildAgentFixPrompt(params: {
"GitHub follow-up replies and review-thread resolution will be handled by the babysitter after your run.",
"If a task is invalid after inspection, explain it in your final response and include the exact audit token.",
"",
...formatRepoAgentInstructions(agentInstructions),
"",
"Approved review-comment tasks:",
commentSection,
"",
Expand Down Expand Up @@ -571,8 +594,9 @@ function buildCodeOwnerFallbackPrompt(params: {
pr: PR;
pullSummary: GitHubPullSummary;
remoteName: string;
agentInstructions?: string | null;
}): string {
const { pr, pullSummary, remoteName } = params;
const { pr, pullSummary, remoteName, agentInstructions } = params;

return [
pr.url,
Expand All @@ -589,6 +613,8 @@ function buildCodeOwnerFallbackPrompt(params: {
"- Review the latest PR review comments, unresolved review threads, issue comments, and failing checks.",
"- Treat reviewer feedback as actionable by default, but validate it against the current code before changing anything. You can reject the feedback if it's not a valid feedback.",
"",
...formatRepoAgentInstructions(agentInstructions),
"",
"Task:",
"1. Fetch and inspect the current PR state.",
"2. For each review comment/thread:",
Expand Down Expand Up @@ -3376,10 +3402,12 @@ export class PRBabysitter {
});
await ensureGitIdentity(worktreePath, this.runtime.runCommand);

const fallbackRepoSettings = await this.storage.getRepoSettings(pr.repo);
const prompt = buildCodeOwnerFallbackPrompt({
pr,
pullSummary,
remoteName,
agentInstructions: fallbackRepoSettings?.agentInstructions ?? "",
});

await updateRunRecord({
Expand Down Expand Up @@ -3559,6 +3587,8 @@ export class PRBabysitter {
await updateRunRecord({
resolvedAgent: agent,
});
const repoSettings = await this.storage.getRepoSettings(pr.repo);
const repoAgentInstructions = repoSettings?.agentInstructions ?? "";
const parsedRepo = parseRepoSlug(pr.repo);

if (!parsedRepo) {
Expand Down Expand Up @@ -4479,6 +4509,7 @@ export class PRBabysitter {
changedFiles: changedFilesResult.stdout.trim(),
diffStat: diffStatResult.stdout.trim(),
diffPreview: diffPreviewResult.stdout.trim(),
agentInstructions: repoAgentInstructions,
});
await queueLog(pr.id, "info", `Evaluating documentation needs with ${agent}`, {
phase: "evaluate.docs",
Expand Down Expand Up @@ -4680,6 +4711,7 @@ export class PRBabysitter {
pullSummary,
remoteName,
conflictFiles: normalizedConflictFiles,
agentInstructions: repoAgentInstructions,
});

await queueLog(pr.id, "info", `Launching ${agent} to resolve merge conflicts`, {
Expand Down Expand Up @@ -4818,6 +4850,7 @@ export class PRBabysitter {
commentTasks: effectiveCommentTasks,
statusTasks,
docsTaskSummary,
agentInstructions: repoAgentInstructions,
});

await updateRunRecord({
Expand Down
7 changes: 6 additions & 1 deletion server/backgroundJobHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ test("work_issue handler opens a PR after a successful repair run", async () =>
codingAgent: "claude",
postGitHubProgressReplies: true,
});
await storage.updateRepoSettings("acme/widgets", {
agentInstructions: "Use pnpm for this repository.",
});
const queue = new BackgroundJobQueue(storage);
const job = await queue.enqueue(
"work_issue",
Expand All @@ -332,7 +335,7 @@ test("work_issue handler opens a PR after a successful repair run", async () =>
);
const pullsCreated: Array<Record<string, unknown>> = [];
const commentsCreated: Array<Record<string, unknown>> = [];
const repairCalls: Array<{ repo: string; issueNumber: number; baseBranch: string; repoCloneUrl: string }> = [];
const repairCalls: Array<{ repo: string; issueNumber: number; baseBranch: string; repoCloneUrl: string; agentInstructions?: string | null }> = [];
const octokit = {
issues: {
get: async () => ({
Expand Down Expand Up @@ -375,6 +378,7 @@ test("work_issue handler opens a PR after a successful repair run", async () =>
issueNumber: input.issueNumber,
baseBranch: input.baseBranch,
repoCloneUrl: input.repoCloneUrl,
agentInstructions: input.agentInstructions,
});
return {
accepted: true,
Expand All @@ -394,6 +398,7 @@ test("work_issue handler opens a PR after a successful repair run", async () =>
issueNumber: 17,
baseBranch: "main",
repoCloneUrl: "https://x-access-token:gho_token@github.com/acme/widgets.git",
agentInstructions: "Use pnpm for this repository.",
}]);
assert.equal(pullsCreated.length, 1);
assert.equal(pullsCreated[0]?.head, "issue/17-fix-the-toggle-123");
Expand Down
1 change: 1 addition & 0 deletions server/backgroundJobHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ export function createBackgroundJobHandlers(params: {
repoCloneUrl: buildGitHubCloneUrl(issue.repoFullName, githubToken),
agent,
agentSettings,
agentInstructions: repoSettings?.agentInstructions ?? "",
subtasks: subtasks.length >= 2 ? subtasks : undefined,
});
} catch (error) {
Expand Down
18 changes: 18 additions & 0 deletions server/issueWorkAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ test("buildIssueWorkPrompt includes repository contribution guidance when presen
assert.match(prompt, /Keep changes small and update tests\./);
});

test("buildIssueWorkPrompt includes custom repository agent instructions", () => {
const prompt = buildIssueWorkPrompt({
repo: "acme/widgets",
issueNumber: 17,
issueTitle: "Fix the toggle",
issueUrl: "https://github.com/acme/widgets/issues/17",
issueBody: "The toggle is stuck",
labels: [],
author: "alice",
baseBranch: "main",
agent: "claude",
agentInstructions: "Use pnpm test for focused verification.",
});

assert.match(prompt, /Repository agent instructions:/);
assert.match(prompt, /Use pnpm test for focused verification\./);
});

test("runIssueWorkRepair commits, pushes, and verifies the issue branch", async () => {
const calls: Array<{ command: string; args: string[]; cwd?: string }> = [];
let agentPrompt = "";
Expand Down
13 changes: 13 additions & 0 deletions server/issueWorkAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type IssueWorkPromptInput = {
agent: CodingAgent;
agentSettings?: AgentRuntimeSettings;
contributionGuidance?: string | null;
agentInstructions?: string | null;
subtasks?: IssueSubtask[];
};

Expand Down Expand Up @@ -77,6 +78,7 @@ export function buildIssueWorkPrompt(input: IssueWorkPromptInput): string {
const labels = input.labels.length > 0 ? input.labels.join(", ") : "none";
const author = input.author || "unknown";
const contributionGuidance = input.contributionGuidance?.trim();
const agentInstructions = input.agentInstructions?.trim();
const guidance = contributionGuidance
? [
"Repository contribution guidance:",
Expand All @@ -89,6 +91,14 @@ export function buildIssueWorkPrompt(input: IssueWorkPromptInput): string {
"- No CONTRIBUTING.md was found in the repository.",
"- Use the concise issue-reply and PR-body templates below.",
].join("\n");
const customInstructions = agentInstructions
? [
"Repository agent instructions:",
"```",
trimText(agentInstructions, 4000),
"```",
].join("\n")
: "Repository agent instructions: none";

const hasSubtasks = (input.subtasks?.length ?? 0) >= 2;
const subtaskSection = hasSubtasks
Expand Down Expand Up @@ -145,6 +155,8 @@ export function buildIssueWorkPrompt(input: IssueWorkPromptInput): string {
"",
guidance,
"",
customInstructions,
"",
"Fallback response template:",
"```",
"## Summary",
Expand Down Expand Up @@ -448,6 +460,7 @@ export async function runIssueWorkRepair(
repoContributionGuidance ? `CONTRIBUTING.md:\n${repoContributionGuidance}` : null,
repoPullRequestTemplate ? `.github/pull_request_template.md:\n${repoPullRequestTemplate}` : null,
].filter((value): value is string => Boolean(value)).join("\n\n") || null,
agentInstructions: input.agentInstructions,
});
const branchCreate = await deps.runCommand(
"git",
Expand Down
3 changes: 3 additions & 0 deletions server/memoryStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
agentInstructions: "",
}]);
});

Expand All @@ -516,6 +517,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
agentInstructions: "",
});

const config = await storage.getConfig();
Expand All @@ -534,6 +536,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
agentInstructions: "",
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions server/memoryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export class MemStorage implements IStorage {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
agentInstructions: "",
};
const next = applyWatchedRepoUpdate(existing, updates);
this.repoSettings.set(repo, next);
Expand Down Expand Up @@ -453,6 +454,7 @@ export class MemStorage implements IStorage {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
agentInstructions: "",
});
}
}
Expand Down
Loading
Loading