diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index df7cf7d..890b767 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -1118,6 +1118,28 @@ export default function Settings() {
+
+
+
);
})}
diff --git a/docs/assets/PatchDeck-Settings.png b/docs/assets/PatchDeck-Settings.png
index 56bd128..b0bd5f0 100644
Binary files a/docs/assets/PatchDeck-Settings.png and b/docs/assets/PatchDeck-Settings.png differ
diff --git a/server/babysitter.test.ts b/server/babysitter.test.ts
index 206ac06..0e6570c 100644
--- a/server/babysitter.test.ts
+++ b/server/babysitter.test.ts
@@ -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",
@@ -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 /);
} finally {
delete process.env.CODEFACTORY_HOME;
diff --git a/server/babysitter.ts b/server/babysitter.ts
index b45a90c..4a8b343 100644
--- a/server/babysitter.ts
+++ b/server/babysitter.ts
@@ -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.",
@@ -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),
"",
@@ -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}.`,
@@ -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}`),
@@ -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
@@ -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,
"",
@@ -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,
@@ -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:",
@@ -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({
@@ -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) {
@@ -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",
@@ -4680,6 +4711,7 @@ export class PRBabysitter {
pullSummary,
remoteName,
conflictFiles: normalizedConflictFiles,
+ agentInstructions: repoAgentInstructions,
});
await queueLog(pr.id, "info", `Launching ${agent} to resolve merge conflicts`, {
@@ -4818,6 +4850,7 @@ export class PRBabysitter {
commentTasks: effectiveCommentTasks,
statusTasks,
docsTaskSummary,
+ agentInstructions: repoAgentInstructions,
});
await updateRunRecord({
diff --git a/server/backgroundJobHandlers.test.ts b/server/backgroundJobHandlers.test.ts
index bba4c9d..fb31f82 100644
--- a/server/backgroundJobHandlers.test.ts
+++ b/server/backgroundJobHandlers.test.ts
@@ -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",
@@ -332,7 +335,7 @@ test("work_issue handler opens a PR after a successful repair run", async () =>
);
const pullsCreated: Array> = [];
const commentsCreated: Array> = [];
- 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 () => ({
@@ -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,
@@ -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");
diff --git a/server/backgroundJobHandlers.ts b/server/backgroundJobHandlers.ts
index b5154bd..1918501 100644
--- a/server/backgroundJobHandlers.ts
+++ b/server/backgroundJobHandlers.ts
@@ -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) {
diff --git a/server/issueWorkAgent.test.ts b/server/issueWorkAgent.test.ts
index 267a083..2daa757 100644
--- a/server/issueWorkAgent.test.ts
+++ b/server/issueWorkAgent.test.ts
@@ -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 = "";
diff --git a/server/issueWorkAgent.ts b/server/issueWorkAgent.ts
index 5cab862..4bf4ed5 100644
--- a/server/issueWorkAgent.ts
+++ b/server/issueWorkAgent.ts
@@ -20,6 +20,7 @@ export type IssueWorkPromptInput = {
agent: CodingAgent;
agentSettings?: AgentRuntimeSettings;
contributionGuidance?: string | null;
+ agentInstructions?: string | null;
subtasks?: IssueSubtask[];
};
@@ -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:",
@@ -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
@@ -145,6 +155,8 @@ export function buildIssueWorkPrompt(input: IssueWorkPromptInput): string {
"",
guidance,
"",
+ customInstructions,
+ "",
"Fallback response template:",
"```",
"## Summary",
@@ -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",
diff --git a/server/memoryStorage.test.ts b/server/memoryStorage.test.ts
index 081c761..5a4a845 100644
--- a/server/memoryStorage.test.ts
+++ b/server/memoryStorage.test.ts
@@ -493,6 +493,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
}]);
});
@@ -516,6 +517,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
const config = await storage.getConfig();
@@ -534,6 +536,7 @@ describe("MemStorage", () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
});
});
diff --git a/server/memoryStorage.ts b/server/memoryStorage.ts
index 96b9ede..0298caf 100644
--- a/server/memoryStorage.ts
+++ b/server/memoryStorage.ts
@@ -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);
@@ -453,6 +454,7 @@ export class MemStorage implements IStorage {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
}
}
diff --git a/server/routes.test.ts b/server/routes.test.ts
index b9e4d1f..3832ed5 100644
--- a/server/routes.test.ts
+++ b/server/routes.test.ts
@@ -1352,6 +1352,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: "default" | "low" | "medium" | "high" | "xhigh" | null;
claudeModel: string | null;
claudeEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max" | null;
+ agentInstructions: string;
}>;
assert.deepEqual(initial, [{
repo: "acme/widgets",
@@ -1365,6 +1366,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
}]);
const updateResponse = await fetch(`${harness.baseUrl}/api/repos/settings`, {
@@ -1382,6 +1384,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: "high",
claudeModel: "sonnet",
claudeEffort: "xhigh",
+ agentInstructions: "Prefer the repo's pnpm scripts before npm.",
}),
});
assert.equal(updateResponse.status, 200);
@@ -1397,6 +1400,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: "default" | "low" | "medium" | "high" | "xhigh" | null;
claudeModel: string | null;
claudeEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max" | null;
+ agentInstructions: string;
};
// Enabling auto-work implicitly enables auto-evaluate — they're dependent settings,
// not independent flags. Verifies coercion in applyWatchedRepoUpdate.
@@ -1412,6 +1416,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: "high",
claudeModel: "sonnet",
claudeEffort: "xhigh",
+ agentInstructions: "Prefer the repo's pnpm scripts before npm.",
});
const persisted = await harness.storage.getRepoSettings("acme/widgets");
@@ -1427,6 +1432,7 @@ test("GET/PATCH /api/repos/settings exposes repo-level settings", async () => {
codexReasoningEffort: "high",
claudeModel: "sonnet",
claudeEffort: "xhigh",
+ agentInstructions: "Prefer the repo's pnpm scripts before npm.",
});
} finally {
await harness.close();
@@ -1465,6 +1471,7 @@ test("PATCH /api/repos/settings can update only ownPrsOnly", async () => {
codexReasoningEffort: "default" | "low" | "medium" | "high" | "xhigh" | null;
claudeModel: string | null;
claudeEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max" | null;
+ agentInstructions: string;
};
assert.deepEqual(updated, {
repo: "acme/widgets",
@@ -1478,6 +1485,7 @@ test("PATCH /api/repos/settings can update only ownPrsOnly", async () => {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
} finally {
await harness.close();
diff --git a/server/routes.ts b/server/routes.ts
index 753c204..3df62e5 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -429,6 +429,7 @@ export async function registerRoutes(
codexReasoningEffort: codexReasoningEffortSchema.nullable().optional(),
claudeModel: z.string().nullable().optional(),
claudeEffort: claudeEffortSchema.nullable().optional(),
+ agentInstructions: z.string().optional(),
}).refine(
(value) => (
value.autoCreateReleases !== undefined
@@ -441,6 +442,7 @@ export async function registerRoutes(
|| value.codexReasoningEffort !== undefined
|| value.claudeModel !== undefined
|| value.claudeEffort !== undefined
+ || value.agentInstructions !== undefined
),
"At least one repository setting must be provided",
).parse(req.body);
diff --git a/server/sqliteStorage.ts b/server/sqliteStorage.ts
index e5e6f97..46af77c 100644
--- a/server/sqliteStorage.ts
+++ b/server/sqliteStorage.ts
@@ -183,6 +183,7 @@ type WatchedRepoRow = {
codex_reasoning_effort: Config["codexReasoningEffort"] | null;
claude_model: string | null;
claude_effort: Config["claudeEffort"] | null;
+ agent_instructions: string | null;
};
type FeedbackItemRow = {
@@ -604,7 +605,8 @@ export class SqliteStorage implements IStorage {
codex_model TEXT,
codex_reasoning_effort TEXT,
claude_model TEXT,
- claude_effort TEXT
+ claude_effort TEXT,
+ agent_instructions TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS prs (
@@ -969,6 +971,7 @@ export class SqliteStorage implements IStorage {
this.ensureColumn("watched_repos", "codex_reasoning_effort", "TEXT");
this.ensureColumn("watched_repos", "claude_model", "TEXT");
this.ensureColumn("watched_repos", "claude_effort", "TEXT");
+ this.ensureColumn("watched_repos", "agent_instructions", "TEXT NOT NULL DEFAULT ''");
this.exec("UPDATE watched_repos SET issue_auto_evaluate = 1 WHERE issue_auto_work = 1 AND issue_auto_evaluate = 0");
this.ensureColumn("config", "max_concurrent_issue_evaluations", "INTEGER NOT NULL DEFAULT 2");
this.ensureColumn("config", "max_concurrent_issue_work", "INTEGER NOT NULL DEFAULT 1");
@@ -1239,7 +1242,7 @@ export class SqliteStorage implements IStorage {
for (const repo of config.watchedRepos) {
const settings = watchedRepoSettings.get(repo);
this.run(
- "INSERT INTO watched_repos (repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ "INSERT INTO watched_repos (repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort, agent_instructions) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
repo,
settings?.auto_create_releases ?? 0,
settings?.own_prs_only ?? 1,
@@ -1251,6 +1254,7 @@ export class SqliteStorage implements IStorage {
settings?.codex_reasoning_effort ?? null,
settings?.claude_model ?? null,
settings?.claude_effort ?? null,
+ settings?.agent_instructions ?? "",
);
}
});
@@ -1269,12 +1273,13 @@ export class SqliteStorage implements IStorage {
codexReasoningEffort: row.codex_reasoning_effort ?? null,
claudeModel: row.claude_model ?? null,
claudeEffort: row.claude_effort ?? null,
+ agentInstructions: row.agent_instructions ?? "",
};
}
private getWatchedRepoRows(): WatchedRepoRow[] {
return this.all(
- "SELECT repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort FROM watched_repos ORDER BY repo ASC",
+ "SELECT repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort, agent_instructions FROM watched_repos ORDER BY repo ASC",
);
}
@@ -2057,7 +2062,7 @@ export class SqliteStorage implements IStorage {
async getRepoSettings(repo: string): Promise {
const row = this.get(
- "SELECT repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort FROM watched_repos WHERE repo = ?",
+ "SELECT repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort, agent_instructions FROM watched_repos WHERE repo = ?",
repo,
);
return row ? this.parseWatchedRepoRow(row) : undefined;
@@ -2079,14 +2084,15 @@ export class SqliteStorage implements IStorage {
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
};
const next = applyWatchedRepoUpdate(existing, updates);
this.withWriteTransaction(() => {
this.run(
`
- INSERT INTO watched_repos (repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ INSERT INTO watched_repos (repo, auto_create_releases, own_prs_only, issue_auto_evaluate, issue_auto_work, pr_auto_monitor, coding_agent_override, codex_model, codex_reasoning_effort, claude_model, claude_effort, agent_instructions)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(repo) DO UPDATE SET
auto_create_releases = excluded.auto_create_releases,
own_prs_only = excluded.own_prs_only,
@@ -2097,7 +2103,8 @@ export class SqliteStorage implements IStorage {
codex_model = excluded.codex_model,
codex_reasoning_effort = excluded.codex_reasoning_effort,
claude_model = excluded.claude_model,
- claude_effort = excluded.claude_effort
+ claude_effort = excluded.claude_effort,
+ agent_instructions = excluded.agent_instructions
`,
next.repo,
Number(next.autoCreateReleases),
@@ -2110,6 +2117,7 @@ export class SqliteStorage implements IStorage {
next.codexReasoningEffort,
next.claudeModel,
next.claudeEffort,
+ next.agentInstructions,
);
});
diff --git a/server/storage.test.ts b/server/storage.test.ts
index 2678a87..655ae6c 100644
--- a/server/storage.test.ts
+++ b/server/storage.test.ts
@@ -254,6 +254,7 @@ test("SqliteStorage reloads config and PR state from the same root", async () =>
codexReasoningEffort: "medium",
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
assert.equal(runtime.drainMode, true);
assert.equal(runtime.drainRequestedAt, "2026-03-18T10:00:00.000Z");
@@ -478,6 +479,10 @@ test("SqliteStorage schema defaults automatic release creation off", async () =>
watchedRepoColumns.find((column) => column.name === "issue_auto_work")?.dflt_value,
"0",
);
+ assert.equal(
+ watchedRepoColumns.find((column) => column.name === "agent_instructions")?.dflt_value,
+ "''",
+ );
} finally {
db.close();
storage.close();
@@ -528,6 +533,7 @@ test("SqliteStorage updateRepoSettings tracks a previously untracked repo", asyn
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
const after = await storage.getConfig();
@@ -546,6 +552,7 @@ test("SqliteStorage updateRepoSettings tracks a previously untracked repo", asyn
codexReasoningEffort: null,
claudeModel: null,
claudeEffort: null,
+ agentInstructions: "",
});
} finally {
storage.close();
diff --git a/shared/models.ts b/shared/models.ts
index 296bcc6..363a50e 100644
--- a/shared/models.ts
+++ b/shared/models.ts
@@ -423,10 +423,14 @@ export function applyWatchedRepoUpdate(
existing: WatchedRepo,
updates: Partial>,
): WatchedRepo {
+ const agentInstructions = updates.agentInstructions === undefined
+ ? existing.agentInstructions
+ : updates.agentInstructions.trim();
const merged = {
...existing,
...updates,
repo: existing.repo,
+ agentInstructions,
};
if (merged.issueAutoWork) {
merged.issueAutoEvaluate = true;
diff --git a/shared/schema.ts b/shared/schema.ts
index 88a8334..2fffb37 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -750,6 +750,7 @@ export const watchedRepoSchema = z.object({
codexReasoningEffort: codexReasoningEffortSchema.nullable(),
claudeModel: z.string().nullable(),
claudeEffort: claudeEffortSchema.nullable(),
+ agentInstructions: z.string().default(""),
});
export type WatchedRepo = z.infer;