Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:43:26 +02:00
commit 5aa3e8d685
700 files changed
+132801

No files matched your search

+325
View File
@@ -0,0 +1,325 @@
import { z } from "zod";
import type { AppEnv } from "../config/env.js";
import { prisma } from "../db/prisma.js";
import { configureAutonomy, getAutonomyState, importAutonomyRoadmap, normalizeAutonomyState, reportAutonomyWork, resumeAutonomy, retryAutonomyBlockerIfDue } from "../repositories/autonomyRepository.js";
import { claimAutonomyWork } from "../services/autonomyRuntimeControl.js";
import { compactAutonomyState, recordAutonomyExecutorHeartbeat, touchAutonomyExecutorHeartbeat } from "../services/autonomyRuntime.js";
import { syncGitSources } from "../services/gitSourceSync.js";
import { reconcileRepositoryAutonomyRoadmap } from "../services/repositoryAutonomy.js";
import { reconcileGiteaAutonomyExecution } from "../services/giteaAutonomyExecution.js";
import { createProjectBrainMcpServer as createBaseProjectBrainMcpServer } from "./server.js";
const result = (data: Record<string, unknown>, text: string) => ({
content: [{ type: "text" as const, text }],
structuredContent: data,
});
const asRecord = (value: unknown): Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
const autonomyStatuses = ["off", "waiting_roadmap", "running", "blocked", "completed"] as const;
type AutoModeProjectRow = {
status: string;
name: string;
pending_work_items: number;
active_work_kind?: string;
last_run_at?: string;
};
const autoModeQueueRank = (row: AutoModeProjectRow) =>
row.status === "running" && !row.active_work_kind ? 0 : row.active_work_kind ? 1 : row.status === "blocked" ? 2 : 3;
const queueTimestamp = (value?: string) => {
const parsed = value ? Date.parse(value) : 0;
return Number.isFinite(parsed) ? parsed : 0;
};
export const compareAutoModeProjectRows = (left: AutoModeProjectRow, right: AutoModeProjectRow) => {
const rankDifference = autoModeQueueRank(left) - autoModeQueueRank(right);
if (rankDifference) return rankDifference;
if (autoModeQueueRank(left) === 0) {
const ageDifference = queueTimestamp(left.last_run_at) - queueTimestamp(right.last_run_at);
if (ageDifference) return ageDifference;
}
return right.pending_work_items - left.pending_work_items || left.name.localeCompare(right.name);
};
const reportWithConflictRetry = async (
projectId: string,
input: Parameters<typeof reportAutonomyWork>[1],
actor: string,
) => {
try {
return await reportAutonomyWork(projectId, input, actor);
} catch (error) {
if (!(error instanceof Error) || error.message !== "AUTO_MODE_CONFLICT") throw error;
return reportAutonomyWork(projectId, input, actor);
}
};
export const createProjectBrainMcpServer = (env: AppEnv, actor: string) => {
const server = createBaseProjectBrainMcpServer(env, actor);
server.registerTool("list_auto_mode_projects", {
title: "List ProjectBrain Auto Mode projects",
description: "Discover projects that have durable Auto Mode state without scanning every project through separate MCP calls. Intended as the first call for scheduled or newly resumed ChatGPT runs so they can find enabled autonomous roadmaps efficiently.",
inputSchema: {
enabled_only: z.boolean().default(true),
statuses: z.array(z.enum(autonomyStatuses)).max(autonomyStatuses.length).optional(),
max_projects: z.number().int().min(1).max(50).default(20),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ enabled_only, statuses, max_projects }) => {
const projects = await prisma.project.findMany({
where: { deletedAt: null },
orderBy: { updatedAt: "desc" },
select: { slug: true, readableId: true, name: true, workspaceState: { select: { notesJson: true } } },
});
const selectedStatuses = statuses?.length ? new Set(statuses) : undefined;
const rows = projects.flatMap((project) => {
const notes = asRecord(project.workspaceState?.notesJson);
const autonomy = asRecord(notes.autonomy);
if (!Object.keys(autonomy).length) return [];
const state = retryAutonomyBlockerIfDue(normalizeAutonomyState(autonomy));
const enabled = state.enabled;
const status = state.status;
if (enabled_only && !enabled) return [];
if (selectedStatuses && !selectedStatuses.has(status)) return [];
const roadmap = state.roadmap;
const phases = roadmap?.phases ?? [];
const currentPhaseIndex = roadmap?.currentPhaseIndex ?? 0;
const currentPhase = phases[currentPhaseIndex];
const activeLease = state.activeLease && Date.parse(state.activeLease.expiresAt) > Date.now() ? state.activeLease : undefined;
const phaseItems = phases.flatMap((phase) => phase.items);
const lastRun = state.runs.reduce<(typeof state.runs)[number] | undefined>((latest, candidate) =>
!latest || queueTimestamp(candidate.startedAt) > queueTimestamp(latest.startedAt) ? candidate : latest, undefined);
return [{
project_id: project.slug,
readable_id: project.readableId ?? undefined,
name: project.name,
enabled,
status,
roadmap_title: roadmap?.title,
current_phase_index: phases.length ? currentPhaseIndex : undefined,
phase_count: phases.length,
current_phase_title: currentPhase?.title,
pending_work_items: phaseItems.filter((item) => item.status === "pending" || item.status === "in_progress").length,
completed_work_items: phaseItems.filter((item) => item.status === "completed").length,
max_work_items_per_run: state.maxWorkItemsPerRun,
max_run_minutes: state.maxRunMinutes,
last_run_at: lastRun?.startedAt,
active_work_kind: activeLease?.kind,
active_run_id: activeLease?.runId,
lease_expires_at: activeLease?.expiresAt,
blocker_reason: state.blocker?.reason,
blocker_kind: state.blocker?.kind,
retry_at: state.blocker?.retryAt,
}];
}).sort(compareAutoModeProjectRows).slice(0, max_projects);
return result({ projects: rows }, `${rows.length} Auto Mode project(s) matched. Claimable projects are ordered fairly by oldest last run (never-run projects first), so select the first row and keep processing claim_auto_work → execute → report_auto_work until that project's item or time budget is reached, work becomes busy or terminal, or user action is required.`);
});
server.registerTool("heartbeat_auto_executor", {
title: "Report ProjectBrain autonomous executor heartbeat",
description: "Report one bounded scheduled executor wake-up so ProjectBrain can distinguish queued autonomous work from a connected executor. Ordinary ChatGPT web tasks may run different projects concurrently; active_project_id selects the isolated ownership slot. Call at the start and end of every scheduled run. Normal claim/report calls refresh the heartbeat automatically; add an explicit heartbeat only immediately before and after an external wait that may exceed five minutes. A different run cannot replace the current owner of the same project inside the server-enforced run window.",
inputSchema: {
instance_id: z.string().trim().min(3).max(120),
run_id: z.string().trim().min(1).max(160).optional(),
status: z.enum(["idle", "working", "degraded"]),
active_project_id: z.string().trim().min(1).max(240).optional(),
detail: z.string().trim().min(1).max(1_000).optional(),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ instance_id, run_id, status, active_project_id, detail }) => {
const recorded = await recordAutonomyExecutorHeartbeat({
instanceId: instance_id,
...(run_id ? { runId: run_id } : {}),
status,
...(active_project_id ? { activeProjectId: active_project_id } : {}),
...(detail ? { detail } : {}),
});
const heartbeat = recorded.heartbeat;
return result({
accepted: recorded.accepted,
...(recorded.accepted ? {} : { rejection_reason: recorded.reason }),
executor: {
instance_id: heartbeat.instanceId,
run_id: heartbeat.runId ?? undefined,
status: heartbeat.status,
active_project_id: heartbeat.activeProjectId ?? undefined,
detail: heartbeat.detail ?? undefined,
last_seen_at: heartbeat.lastSeenAt.toISOString(),
},
}, recorded.accepted
? `Autonomous executor '${heartbeat.instanceId}' is ${heartbeat.status}; heartbeat recorded.`
: `Heartbeat rejected (${recorded.reason}); active run '${heartbeat.runId ?? "unknown"}' retains executor ownership. Do not claim work for the rejected run.`);
});
server.registerTool("get_auto_mode_state", {
title: "Get ProjectBrain Auto Mode state",
description: "Read the durable Auto Mode controller for one project and reconcile any newly observed canonical Gitea branch/PR/validation/merge evidence first. The default summary omits the large roadmap Markdown and returns bounded current-phase items and recent events. Request full only for targeted diagnostics that genuinely require the complete durable state.",
inputSchema: {
project_id: z.string().min(1),
view: z.enum(["summary", "full"]).default("summary"),
item_limit: z.number().int().min(1).max(25).default(25),
event_limit: z.number().int().min(0).max(20).default(5),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ project_id, view, item_limit, event_limit }) => {
let state = await getAutonomyState(project_id);
try {
state = (await reconcileGiteaAutonomyExecution(env, project_id, "system:mcp-auto-read")).state;
} catch {
// Durable ProjectBrain state remains readable during a transient Gitea read failure.
}
const projected = view === "full" ? state : compactAutonomyState(state, { itemLimit: item_limit, eventLimit: event_limit });
return result({ state: projected as unknown as Record<string, unknown>, view }, `Auto Mode is ${state.enabled ? "enabled" : "disabled"}; state=${state.status}${state.roadmap ? `, roadmap='${state.roadmap.title}', phase ${state.roadmap.currentPhaseIndex + 1}/${state.roadmap.phases.length}` : ""}; view=${view}.`);
});
server.registerTool("configure_auto_mode", {
title: "Configure ProjectBrain Auto Mode",
description: "Enable or disable autonomous roadmap execution for one project and configure the bounded brainstorm/failure/lease policy. When a linked repository exposes ROADMAP.md or a supported structured backlog, enabling Auto Mode synchronizes and bootstraps that repository plan automatically; explicit manually imported plans are preserved.",
inputSchema: {
project_id: z.string().min(1),
profile: z.enum(["conservative", "normal", "autonomous", "custom"]).optional(),
enabled: z.boolean().optional(),
auto_brainstorm: z.boolean().optional(),
max_brainstorm_iterations: z.number().int().min(1).max(10).optional(),
max_consecutive_failures: z.number().int().min(1).max(10).optional(),
lease_minutes: z.number().int().min(5).max(120).optional(),
max_work_items_per_run: z.number().int().min(1).max(20).optional(),
max_run_minutes: z.number().int().min(5).max(55).optional(),
max_recovery_minutes: z.number().int().min(15).max(1_440).optional(),
pause_after_current: z.boolean().optional(),
scheduling_policy: z.enum(["balanced", "downstream-impact"]).optional(),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ project_id, profile, enabled, auto_brainstorm, max_brainstorm_iterations, max_consecutive_failures, lease_minutes, max_work_items_per_run, max_run_minutes, max_recovery_minutes, pause_after_current, scheduling_policy }) => {
let state = await configureAutonomy(project_id, {
...(profile ? { profile } : {}),
...(enabled !== undefined ? { enabled } : {}),
...(auto_brainstorm !== undefined ? { autoBrainstorm: auto_brainstorm } : {}),
...(max_brainstorm_iterations !== undefined ? { maxBrainstormIterations: max_brainstorm_iterations } : {}),
...(max_consecutive_failures !== undefined ? { maxConsecutiveFailures: max_consecutive_failures } : {}),
...(lease_minutes !== undefined ? { leaseMinutes: lease_minutes } : {}),
...(max_work_items_per_run !== undefined ? { maxWorkItemsPerRun: max_work_items_per_run } : {}),
...(max_run_minutes !== undefined ? { maxRunMinutes: max_run_minutes } : {}),
...(max_recovery_minutes !== undefined ? { maxRecoveryMinutes: max_recovery_minutes } : {}),
...(pause_after_current !== undefined ? { pauseAfterCurrent: pause_after_current } : {}),
...(scheduling_policy ? { schedulingPolicy: scheduling_policy } : {}),
}, actor);
let repositorySyncWarning: string | undefined;
if (state.enabled) {
try {
const sourceSync = await syncGitSources(env, { projectId: project_id });
if (sourceSync.errors.length) repositorySyncWarning = sourceSync.errors.map((entry) => `${entry.repository}: ${entry.code}`).join("; ");
} catch (error) {
repositorySyncWarning = error instanceof Error ? error.message : "REPOSITORY_SOURCE_SYNC_FAILED";
}
const repositoryPlan = await reconcileRepositoryAutonomyRoadmap(project_id, actor);
state = repositoryPlan.state;
}
return result({
state: state as unknown as Record<string, unknown>,
...(repositorySyncWarning ? { repository_sync_warning: repositorySyncWarning } : {}),
}, `Auto Mode configuration saved; enabled=${state.enabled}, state=${state.status}.${repositorySyncWarning ? ` Repository source refresh warning: ${repositorySyncWarning}` : ""}`);
});
server.registerTool("import_auto_roadmap", {
title: "Import ProjectBrain autonomous roadmap",
description: "Import a Markdown roadmap into ProjectBrain Auto Mode. H2+ headings become ordered phases and checkbox, bullet or numbered lines become work items. Import replaces the active autonomous roadmap but preserves Auto Mode policy settings.",
inputSchema: { project_id: z.string().min(1), title: z.string().trim().min(2).max(200).optional(), markdown: z.string().trim().min(3).max(100_000) },
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
}, async ({ project_id, title, markdown }) => {
const state = await importAutonomyRoadmap(project_id, { title, markdown }, actor);
const roadmap = state.roadmap!;
return result({ state: state as unknown as Record<string, unknown> }, `Roadmap '${roadmap.title}' imported with ${roadmap.phases.length} phase(s) and ${roadmap.phases.reduce((count, phase) => count + phase.items.length, 0)} item(s).`);
});
server.registerTool("resume_auto_mode", {
title: "Resume blocked ProjectBrain Auto Mode",
description: "Clear the current Auto Mode blocker and make its blocked roadmap phase/item claimable again. The roadmap, completed phases and completed work are preserved; only blocker/retry state for the blocked work is reset.",
inputSchema: { project_id: z.string().min(1) },
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ project_id }) => {
const state = await resumeAutonomy(project_id, actor);
return result({ state: state as unknown as Record<string, unknown> }, `Auto Mode resume applied; enabled=${state.enabled}, state=${state.status}.`);
});
server.registerTool("claim_auto_work", {
title: "Claim next ProjectBrain autonomous work",
description: "Backward-compatible controller claim for one Auto work item. Implementation work returns a canonical Gitea execution target whose deterministic branch is the atomic execution claim. Heartbeat and lease ownership only coordinate legacy controller runs; repository correctness is reconciled from Gitea branch, pull-request, exact-head validation and merge evidence.",
inputSchema: {
project_id: z.string().min(1),
run_id: z.string().trim().min(1).max(160),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ project_id, run_id }) => {
const ownsExecutor = await touchAutonomyExecutorHeartbeat(run_id, project_id);
if (!ownsExecutor) {
const state = await getAutonomyState(project_id);
return result({ state: state as unknown as Record<string, unknown>, work: undefined, busy: true, heartbeat_required: true }, "This run does not own an accepted working executor heartbeat. Do not claim or execute work; another run may be active, or heartbeat_auto_executor must be called first.");
}
let repositorySyncWarning: string | undefined;
const beforeClaimState = await getAutonomyState(project_id);
if (!beforeClaimState.runs.some((run) => run.id === run_id)) {
try {
const sourceSync = await syncGitSources(env, { projectId: project_id });
if (sourceSync.errors.length) repositorySyncWarning = sourceSync.errors.map((entry) => `${entry.repository}: ${entry.code}`).join("; ");
} catch (error) {
repositorySyncWarning = error instanceof Error ? error.message : "REPOSITORY_SOURCE_SYNC_FAILED";
}
}
try {
try {
await reconcileGiteaAutonomyExecution(env, project_id, "system:mcp-auto-claim");
} catch {
// Keep the compatibility claim path available during transient Gitea read failures.
}
const claim = await claimAutonomyWork(project_id, actor, run_id);
const work = claim.work;
return result({ ...claim, state: compactAutonomyState(claim.state, { itemLimit: 5, eventLimit: 2 }), ...(repositorySyncWarning ? { repository_sync_warning: repositorySyncWarning } : {}) } as unknown as Record<string, unknown>, (work ? `${claim.resumed ? "Resumed" : "Claimed"} ${work.kind}: ${work.itemTitle ?? work.phaseTitle}. Execute the returned prompt, report with token ${work.token}, and continue within the run budget.` : claim.budgetReached ? `This run reached its server-enforced ${claim.budgetReason === "item_limit" ? "item" : "time"} budget; stop cleanly and leave the recurring task enabled.` : claim.busy ? `Autonomous work is already leased by another executor run; do not duplicate it. Controller state=${claim.state.status}.` : `No autonomous work is currently claimable; controller state=${claim.state.status}.`) + (repositorySyncWarning ? ` Repository source refresh warning: ${repositorySyncWarning}` : ""));
} catch (error) {
if (!(error instanceof Error) || error.message !== "AUTO_MODE_CONFLICT") throw error;
const claim = await claimAutonomyWork(project_id, actor, run_id);
const work = claim.work;
return result({ ...claim, state: compactAutonomyState(claim.state, { itemLimit: 5, eventLimit: 2 }) } as unknown as Record<string, unknown>, work ? `Concurrent claim resolved by resuming this run's durable ${work.kind} lease for ${work.itemTitle ?? work.phaseTitle}. Report with token ${work.token} and continue within the run budget.` : `Concurrent claim resolved without duplicating another run; controller state=${claim.state.status}.`);
}
});
server.registerTool("report_auto_work", {
title: "Report ProjectBrain autonomous work",
description: "Backward-compatible bookkeeping for a claimed lease. Canonical implementation truth comes from the deterministic Gitea branch, PR, exact-head Actions validation and merge; ProjectBrain reconciles that evidence even when this callback is never sent. delivery_id remains supported for legacy feature deliveries. Use discovered_tasks for repair prerequisites and reserve human blockers for actual owner decisions.",
inputSchema: {
project_id: z.string().min(1), token: z.string().uuid(), outcome: z.enum(["success", "failed", "blocked"]), summary: z.string().trim().min(3).max(12_000),
blocker_type: z.enum(["human", "retryable"]).optional(),
retry_after_minutes: z.number().int().min(5).max(1_440).optional(),
delivery_id: z.string().trim().min(1).max(240).optional(),
discovered_tasks: z.array(z.string().trim().min(2).max(1_000)).max(100).default([]),
brainstorm: z.object({
context_summary: z.string().trim().min(3).max(8_000).optional(),
ideas: z.array(z.object({ id: z.string().min(1).max(160), title: z.string().min(1).max(300), description: z.string().min(1).max(4_000), score: z.number().min(0).max(100).optional() })).max(50).default([]),
outcomes: z.array(z.string().min(1).max(2_000)).max(50).default([]),
selected_idea_ids: z.array(z.string().min(1).max(160)).max(50).default([]),
next_actions: z.array(z.string().trim().min(2).max(1_000)).max(100).default([]),
}).optional(),
},
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ project_id, token, outcome, summary, blocker_type, retry_after_minutes, delivery_id, discovered_tasks, brainstorm }) => {
const reportInput = {
token, outcome, summary, ...(blocker_type ? { blockerType: blocker_type } : {}), ...(retry_after_minutes ? { retryAfterMinutes: retry_after_minutes } : {}), ...(delivery_id ? { deliveryId: delivery_id } : {}), discoveredTasks: discovered_tasks,
...(brainstorm ? { brainstorm: { ...(brainstorm.context_summary ? { contextSummary: brainstorm.context_summary } : {}), ideas: brainstorm.ideas, outcomes: brainstorm.outcomes, selectedIdeaIds: brainstorm.selected_idea_ids, nextActions: brainstorm.next_actions } } : {}),
} satisfies Parameters<typeof reportAutonomyWork>[1];
try {
await reconcileGiteaAutonomyExecution(env, project_id, "system:mcp-auto-report");
} catch {
// A compatibility report can still be recorded if Gitea is temporarily unavailable.
}
const report = await reportWithConflictRetry(project_id, reportInput, actor);
if (report.runId) await touchAutonomyExecutorHeartbeat(report.runId, project_id);
return result({ ...report, state: compactAutonomyState(report.state, { itemLimit: 5, eventLimit: 2 }) } as unknown as Record<string, unknown>, `${report.replayed ? "Replayed" : "Recorded"} autonomous work as ${outcome}; ${report.addedTasks.length} new task(s) added. Controller state=${report.state.status}.`);
});
return server;
};