Repo Intelligence: agent-managed per-repo learnings with revision history (#487)

* add repo learnings feature with edit history

introduces a new "Learnings" section in the repo console where agents can
persist operational knowledge (setup steps, test commands, conventions) at
the end of runs via an MCP tool. users can also edit learnings manually.

- add `learnings` field to Repo model and `LearningsRevision` audit table
- add `update_learnings` MCP tool for agents to persist repo knowledge
- integrate learnings into prompt assembly as REPO LEARNINGS section
- add learnings step to mode guidance (Build, AddressReviews, Plan, Fix, Task)
- add PATCH /api/repo/[owner]/[repo]/learnings endpoint (JWT auth)
- add GET /api/repo/[owner]/[repo]/learnings/history endpoint (Clerk auth)
- add LearningsSection component with textarea, save-on-blur, and history modal
- record revision history with actor tracking (agent vs user) and pruning (50 max)
- gate UI behind owner === "pullfrog" for internal dogfooding

Made-with: Cursor

* fix prisma enum import path for LearningsActor

Made-with: Cursor

* simplify learnings schema: remove LearningsActor enum, store model name directly

the actor/actorName split was unnecessary — learnings are only written by
agents so the revision table just needs a model column. removes all user
editing concepts from schema, API, and frontend.

Made-with: Cursor

* fix migration: add separate migration instead of rewriting existing one

restores original learnings_revisions migration and adds a new migration
that drops actor/actorName columns, backfills model from actorName, and
drops the LearningsActor enum.

Made-with: Cursor

* polish learnings feature: rename to Repo Intelligence, fix atomicity, fix review skip

- rename user-facing "learnings" to "Repo Intelligence" (UI, prompt section, wiki, sidebar)
- simplify description to "Automatically discovered by the agent across runs."
- wrap repo.update + revision create in $transaction for atomicity
- refactor recordLearningsRevision to pruneLearningsRevisions (prune-only)
- fix empty review skip: don't block APPROVE reviews with no body
- fix broken docs anchor: #free-options → #free-models
- update agent guidance to require flat bullet list format with pruning
- add accessibility: aria-expanded, sr-only loading, output element
- add chevron rotation, stale data clear on modal close, max-h scroll
- trim + length-limit model field, remove type cast, restore pre-existing comment
- update wiki prompt examples with actual bullet-formatted content
- update model test snapshot

Made-with: Cursor

* fix stale free model name in docs, rename utility file to match export

- docs/keys.mdx: MiMo V2 Flash → MiMo V2 Pro (matches model code change)
- rename recordLearningsRevision.ts → pruneLearningsRevisions.ts

Made-with: Cursor

* Add skill, .neon

* polish learnings UI and remove verbose log

- learnings code block: read-only appearance with muted text, copy button, rounded corners
- history modal: full-width rows with cursor-pointer, chevron moved to right, no preview text
- drop noisy update_learnings log line

Made-with: Cursor

* inject learningsStep into all modes, drop seed script, soften revision styling

Made-with: Cursor

* Drop seed

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
David Blass
2026-03-25 19:15:43 +00:00
committed by pullfrog[bot]
parent e6d34ee01b
commit 64f2238316
8 changed files with 141 additions and 15 deletions
+65 -9
View File
@@ -146767,6 +146767,40 @@ function AddLabelsTool(ctx) {
});
}
// mcp/learnings.ts
var UpdateLearningsParams = type({
learnings: type.string.describe(
"the FULL merged learnings as a flat bullet list. each line starts with `- `. one discrete, actionable fact per bullet. combine existing bullets from the prompt with your new discoveries. deduplicate \u2014 if an existing bullet covers the same fact, update it in place rather than adding a new one. drop bullets that are clearly wrong or no longer relevant to the current codebase. keep the list focused and concise."
)
});
function UpdateLearningsTool(ctx) {
return tool({
name: "update_learnings",
description: "persist operational learnings about this repository (setup steps, test commands, key conventions, patterns). ONLY call this when you have high confidence the information is correct and broadly useful for future runs \u2014 not for one-off findings or uncertain observations. format: flat bullet list (`- ` per line, one fact per bullet). pass the FULL merged list \u2014 combine existing learnings from the prompt with new discoveries. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.",
parameters: UpdateLearningsParams,
execute: execute(async (params) => {
const response = await apiFetch({
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
method: "PATCH",
headers: {
authorization: `Bearer ${ctx.apiToken}`,
"content-type": "application/json"
},
body: JSON.stringify({
learnings: params.learnings,
model: ctx.toolState.model
}),
signal: AbortSignal.timeout(1e4)
});
if (!response.ok) {
const error49 = await response.text();
throw new Error(`failed to update learnings: ${error49}`);
}
return { success: true };
})
});
}
// mcp/output.ts
var import_ajv3 = __toESM(require_ajv(), 1);
var SetOutputParams = type({
@@ -147014,7 +147048,7 @@ function CreatePullRequestReviewTool(ctx) {
execute: execute(async ({ pull_number, body, approved, commit_id, comments = [] }) => {
if (body) body = fixDoubleEscapedString(body);
ctx.toolState.issueNumber = pull_number;
if (!body && comments.length === 0) {
if (!approved && !body && comments.length === 0) {
log.info(
"review has no body and no inline comments \u2014 skipping submission (no issues found)"
);
@@ -147673,6 +147707,9 @@ var SelectModeParams = type({
function resolveMode(modes2, modeName) {
return modes2.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
}
function learningsStep(n) {
return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${ghPullfrogMcpName}/update_learnings\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt \u2014 pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
}
var modeGuidance = {
Build: `### Checklist
@@ -147694,6 +147731,8 @@ var modeGuidance = {
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
${learningsStep(5)}
### Notes
For simple, well-defined tasks, skip the plan phase and go straight to build.`,
@@ -147738,7 +147777,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`,
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\`
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary`,
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
${learningsStep(6)}`,
Review: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`. Read the diff to identify the major areas of change.
@@ -147788,7 +147829,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`,
2. Produce a structured, actionable plan with clear milestones.
3. Call \`${ghPullfrogMcpName}/report_progress\` with the plan.`,
3. Call \`${ghPullfrogMcpName}/report_progress\` with the plan.
${learningsStep(4)}`,
PlanEdit: `### Checklist (editing existing plan)
An existing plan comment was found for this issue. Update that comment with the revised plan \u2014 do not create a new plan comment.
@@ -147817,7 +147860,9 @@ An existing plan comment was found for this issue. Update that comment with the
5. Finalize:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary`,
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
${learningsStep(6)}`,
Task: `### Checklist
1. Analyze the task. For simple operations (labeling, commenting, answering questions, running a single command), handle directly.
@@ -147831,7 +147876,9 @@ An existing plan comment was found for this issue. Update that comment with the
3. Finalize:
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
${learningsStep(4)}`,
Summarize: `### Checklist
1. Checkout the PR via \`${ghPullfrogMcpName}/checkout_pr\` \u2014 this returns PR metadata and a \`diffPath\`.
@@ -148368,7 +148415,8 @@ function buildOrchestratorTools(ctx, outputSchema) {
PushTagsTool(ctx),
DeleteBranchTool(ctx),
CreatePullRequestTool(ctx),
UpdatePullRequestBodyTool(ctx)
UpdatePullRequestBodyTool(ctx),
UpdateLearningsTool(ctx)
];
}
async function tryStartMcpServer(ctx, tools, port) {
@@ -149849,10 +149897,15 @@ function buildCommonInputs(ctx) {
};
}
function assembleFullPrompt(ctx) {
const learningsSection = ctx.learnings ? `************* REPO INTELLIGENCE *************
${ctx.learnings}` : "";
const rawFull = `************* RUNTIME CONTEXT *************
${ctx.runtime}
${learningsSection}
${ctx.system}
${ctx.contextSections}`;
@@ -149897,7 +149950,8 @@ If the task clearly requires no work, call \`${ghPullfrogMcpName}/report_progres
const full = assembleFullPrompt({
runtime: inputs.runtime,
system,
contextSections
contextSections,
learnings: ctx.learnings
});
return {
full,
@@ -150275,7 +150329,8 @@ var defaultSettings = {
push: "restricted",
shell: "restricted",
prApproveEnabled: false,
modeInstructions: {}
modeInstructions: {},
learnings: null
};
var defaultRunContext = {
settings: defaultSettings,
@@ -150635,7 +150690,8 @@ async function main() {
payload,
repo: runContext.repo,
modes: modes2,
outputSchema
outputSchema,
learnings: runContext.repoSettings.learnings
});
const logParts = [
instructions.eventInstructions ? `EVENT-LEVEL INSTRUCTIONS:
+1
View File
@@ -289,6 +289,7 @@ export async function main(): Promise<MainResult> {
repo: runContext.repo,
modes,
outputSchema,
learnings: runContext.repoSettings.learnings,
});
// log instructions as soon as they are fully resolved
const logParts = [
+41
View File
@@ -0,0 +1,41 @@
import { type } from "arktype";
import { apiFetch } from "../utils/apiFetch.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
const UpdateLearningsParams = type({
learnings: type.string.describe(
"the FULL merged learnings as a flat bullet list. each line starts with `- `. one discrete, actionable fact per bullet. combine existing bullets from the prompt with your new discoveries. deduplicate — if an existing bullet covers the same fact, update it in place rather than adding a new one. drop bullets that are clearly wrong or no longer relevant to the current codebase. keep the list focused and concise."
),
});
export function UpdateLearningsTool(ctx: ToolContext) {
return tool({
name: "update_learnings",
description:
"persist operational learnings about this repository (setup steps, test commands, key conventions, patterns). ONLY call this when you have high confidence the information is correct and broadly useful for future runs — not for one-off findings or uncertain observations. format: flat bullet list (`- ` per line, one fact per bullet). pass the FULL merged list — combine existing learnings from the prompt with new discoveries. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.",
parameters: UpdateLearningsParams,
execute: execute(async (params) => {
const response = await apiFetch({
path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/learnings`,
method: "PATCH",
headers: {
authorization: `Bearer ${ctx.apiToken}`,
"content-type": "application/json",
},
body: JSON.stringify({
learnings: params.learnings,
model: ctx.toolState.model,
}),
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`failed to update learnings: ${error}`);
}
return { success: true };
}),
});
}
+3 -2
View File
@@ -84,8 +84,9 @@ export function CreatePullRequestReviewTool(ctx: ToolContext) {
// set issue context (PRs are issues)
ctx.toolState.issueNumber = pull_number;
// skip empty reviews (no body, no inline comments) — nothing to post
if (!body && comments.length === 0) {
// skip empty COMMENT reviews (no body, no inline comments) — nothing to post.
// APPROVE reviews are never skipped: the approval stamp itself is the content.
if (!approved && !body && comments.length === 0) {
log.info(
"review has no body and no inline comments — skipping submission (no issues found)"
);
+18 -4
View File
@@ -19,6 +19,10 @@ function resolveMode(modes: Mode[], modeName: string): Mode | null {
return modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
}
function learningsStep(n: number): string {
return `${n}. **learnings** (only if high confidence): if you discovered something about repo setup, test commands, conventions, or patterns that you are confident is correct and would reliably help future runs, call \`${ghPullfrogMcpName}/update_learnings\` to persist it. skip this step if you are unsure or the finding is speculative/one-off. format as a flat bullet list (\`- \` per line, one fact per bullet). merge with existing learnings from the prompt — pass the FULL merged list. deduplicate, and drop bullets that are clearly wrong or no longer relevant to the current codebase.`;
}
const modeGuidance: Record<string, string> = {
Build: `### Checklist
@@ -40,6 +44,8 @@ const modeGuidance: Record<string, string> = {
- create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- call \`${ghPullfrogMcpName}/report_progress\` with the final summary including PR link
${learningsStep(5)}
### Notes
For simple, well-defined tasks, skip the plan phase and go straight to build.`,
@@ -86,7 +92,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`,
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- reply to each comment using \`${ghPullfrogMcpName}/reply_to_review_comment\`
- resolve addressed threads via \`${ghPullfrogMcpName}/resolve_review_thread\`
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary`,
- call \`${ghPullfrogMcpName}/report_progress\` with a brief summary
${learningsStep(6)}`,
Review: `### Checklist
@@ -139,7 +147,9 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`,
2. Produce a structured, actionable plan with clear milestones.
3. Call \`${ghPullfrogMcpName}/report_progress\` with the plan.`,
3. Call \`${ghPullfrogMcpName}/report_progress\` with the plan.
${learningsStep(4)}`,
PlanEdit: `### Checklist (editing existing plan)
@@ -170,7 +180,9 @@ An existing plan comment was found for this issue. Update that comment with the
5. Finalize:
- push changes via \`${ghPullfrogMcpName}/push_branch\`
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary`,
- call \`${ghPullfrogMcpName}/report_progress\` with the diagnosis and fix summary
${learningsStep(6)}`,
Task: `### Checklist
@@ -185,7 +197,9 @@ An existing plan comment was found for this issue. Update that comment with the
3. Finalize:
- call \`${ghPullfrogMcpName}/report_progress\` with results
- if the task involved code changes, push via \`${ghPullfrogMcpName}/push_branch\` and create a PR via \`${ghPullfrogMcpName}/create_pull_request\`
- if the task involved labeling, commenting, or other GitHub operations, perform those directly`,
- if the task involved labeling, commenting, or other GitHub operations, perform those directly
${learningsStep(4)}`,
Summarize: `### Checklist
+2
View File
@@ -29,6 +29,7 @@ import { GetIssueCommentsTool } from "./issueComments.ts";
import { GetIssueEventsTool } from "./issueEvents.ts";
import { IssueInfoTool } from "./issueInfo.ts";
import { AddLabelsTool } from "./labels.ts";
import { UpdateLearningsTool } from "./learnings.ts";
import { SetOutputTool } from "./output.ts";
import { CreatePullRequestTool, UpdatePullRequestBodyTool } from "./pr.ts";
import { PullRequestInfoTool } from "./prInfo.ts";
@@ -213,6 +214,7 @@ function buildOrchestratorTools(ctx: ToolContext, outputSchema?: JsonSchema): To
DeleteBranchTool(ctx),
CreatePullRequestTool(ctx),
UpdatePullRequestBodyTool(ctx),
UpdateLearningsTool(ctx),
];
}
+9
View File
@@ -11,6 +11,7 @@ interface InstructionsContext {
repo: RunContextData["repo"];
modes: Mode[];
outputSchema?: Record<string, unknown> | undefined;
learnings: string | null;
}
function buildRuntimeContext(ctx: InstructionsContext): string {
@@ -328,13 +329,20 @@ interface AssembleFullPromptInput {
runtime: string;
system: string;
contextSections: string;
learnings: string | null;
}
function assembleFullPrompt(ctx: AssembleFullPromptInput): string {
const learningsSection = ctx.learnings
? `************* REPO INTELLIGENCE *************\n\n${ctx.learnings}`
: "";
const rawFull = `************* RUNTIME CONTEXT *************
${ctx.runtime}
${learningsSection}
${ctx.system}
${ctx.contextSections}`;
@@ -385,6 +393,7 @@ If the task clearly requires no work, call \`${ghPullfrogMcpName}/report_progres
runtime: inputs.runtime,
system,
contextSections,
learnings: ctx.learnings,
});
return {
+2
View File
@@ -18,6 +18,7 @@ export interface RepoSettings {
shell: ShellPermission;
prApproveEnabled: boolean;
modeInstructions: Record<string, string>;
learnings: string | null;
}
export interface RunContext {
@@ -36,6 +37,7 @@ const defaultSettings: RepoSettings = {
shell: "restricted",
prApproveEnabled: false,
modeInstructions: {},
learnings: null,
};
const defaultRunContext: RunContext = {