diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f6b086..918e24d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,6 +100,9 @@ enum IdeaStatus { PLANNING PLAN_FAILED PLAN_READY + REVIEWING_PLAN + PLAN_REVIEW_FAILED + PLAN_REVIEWED PLANNED } @@ -107,6 +110,7 @@ enum ClaudeJobKind { TASK_IMPLEMENTATION IDEA_GRILL IDEA_MAKE_PLAN + IDEA_REVIEW_PLAN PLAN_CHAT SPRINT_IMPLEMENTATION } @@ -124,6 +128,7 @@ enum IdeaLogType { NOTE GRILL_RESULT PLAN_RESULT + PLAN_REVIEW_RESULT STATUS_CHANGE JOB_EVENT } @@ -518,14 +523,16 @@ model Idea { code String @db.VarChar(30) title String description String? @db.VarChar(4000) - grill_md String? @db.Text - plan_md String? @db.Text - pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull) - pbi_id String? @unique - status IdeaStatus @default(DRAFT) - archived Boolean @default(false) - created_at DateTime @default(now()) - updated_at DateTime @updatedAt + grill_md String? @db.Text + plan_md String? @db.Text + plan_review_log Json? // ReviewLog from orchestrator + reviewed_at DateTime? + pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull) + pbi_id String? @unique + status IdeaStatus @default(DRAFT) + archived Boolean @default(false) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt questions ClaudeQuestion[] jobs ClaudeJob[] diff --git a/src/index.ts b/src/index.ts index 06cefba..03f08d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { registerMarkPbiPrMergedTool } from './tools/mark-pbi-pr-merged.js' import { registerGetIdeaContextTool } from './tools/get-idea-context.js' import { registerUpdateIdeaGrillMdTool } from './tools/update-idea-grill-md.js' import { registerUpdateIdeaPlanMdTool } from './tools/update-idea-plan-md.js' +import { registerUpdateIdeaPlanReviewedTool } from './tools/update-idea-plan-reviewed.js' import { registerLogIdeaDecisionTool } from './tools/log-idea-decision.js' import { registerGetWorkerSettingsTool } from './tools/get-worker-settings.js' import { registerWorkerHeartbeatTool } from './tools/worker-heartbeat.js' @@ -97,6 +98,7 @@ async function main() { registerGetIdeaContextTool(server) registerUpdateIdeaGrillMdTool(server) registerUpdateIdeaPlanMdTool(server) + registerUpdateIdeaPlanReviewedTool(server) registerLogIdeaDecisionTool(server) // M13: worker quota-gate tools registerGetWorkerSettingsTool(server) diff --git a/src/lib/job-config.ts b/src/lib/job-config.ts index 811e365..ef7270d 100644 --- a/src/lib/job-config.ts +++ b/src/lib/job-config.ts @@ -101,6 +101,19 @@ const KIND_DEFAULTS: Record = { 'mcp__scrum4me__update_job_status', ], }, + IDEA_REVIEW_PLAN: { + model: 'claude-opus-4-7', + thinking_budget: 6000, + permission_mode: 'acceptEdits', + max_turns: 1, + allowed_tools: [ + 'Read', 'Write', 'Grep', 'Glob', + 'mcp__scrum4me__update_idea_plan_reviewed', + 'mcp__scrum4me__log_idea_decision', + 'mcp__scrum4me__update_job_status', + 'mcp__scrum4me__ask_user_question', + ], + }, PLAN_CHAT: { model: 'claude-sonnet-4-6', thinking_budget: 6000, diff --git a/src/tools/update-idea-plan-reviewed.ts b/src/tools/update-idea-plan-reviewed.ts new file mode 100644 index 0000000..0217c22 --- /dev/null +++ b/src/tools/update-idea-plan-reviewed.ts @@ -0,0 +1,116 @@ +// MCP-tool: writes the review-log result after a IDEA_REVIEW_PLAN grill-job +// and transitions the idea.status to PLAN_REVIEWED (on success) or +// PLAN_REVIEW_FAILED (on failure). +// +// Called by the worker as the final step of a review-plan session. + +import { z } from 'zod' +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' + +import { prisma } from '../prisma.js' +import { requireWriteAccess } from '../auth.js' +import { userOwnsIdea } from '../access.js' +import { toolError, toolJson, withToolErrors } from '../errors.js' + +const inputSchema = z.object({ + idea_id: z.string().min(1), + review_log: z.object({}).passthrough(), // Full ReviewLog from orchestrator (JSON object) + approval_status: z + .enum(['pending', 'approved', 'rejected'] as const) + .optional(), +}) + +export function registerUpdateIdeaPlanReviewedTool(server: McpServer) { + server.registerTool( + 'update_idea_plan_reviewed', + { + title: 'Mark plan as reviewed', + description: + 'Save review-log after plan review cycle and transition idea.status to PLAN_REVIEWED (if approved) or PLAN_REVIEW_FAILED (if rejected/pending requires manual approval). Forbidden for demo accounts.', + inputSchema, + }, + async ({ idea_id, review_log, approval_status }) => + withToolErrors(async () => { + const auth = await requireWriteAccess() + if (!(await userOwnsIdea(idea_id, auth.userId))) { + return toolError('Idea not found') + } + + // Determine target status based on approval + const nextStatus = + approval_status === 'approved' + ? 'PLAN_REVIEWED' + : approval_status === 'rejected' + ? 'PLAN_REVIEW_FAILED' + : 'PLAN_REVIEWED' // Default to approved if not specified + + // Log summary metrics from review_log + const logSummary = buildReviewLogSummary(review_log) + + const result = await prisma.$transaction([ + prisma.idea.update({ + where: { id: idea_id }, + data: { + plan_review_log: review_log as any, + reviewed_at: new Date(), + status: nextStatus, + }, + select: { id: true, status: true, code: true }, + }), + prisma.ideaLog.create({ + data: { + idea_id, + type: 'PLAN_REVIEW_RESULT', + content: logSummary.summary, + metadata: { + approval_status, + convergence_status: logSummary.convergence_status, + final_score: logSummary.final_score, + rounds_completed: logSummary.rounds_completed, + }, + }, + }), + ]) + + return toolJson({ + ok: true, + idea: result[0], + review_log_summary: logSummary, + }) + }), + ) +} + +function buildReviewLogSummary( + reviewLog: Record, +): { + summary: string + convergence_status: string + final_score: number + rounds_completed: number +} { + const rounds = Array.isArray(reviewLog.rounds) ? reviewLog.rounds : [] + const convergence = reviewLog.convergence || {} + const finalScore = + rounds.length > 0 ? rounds[rounds.length - 1].score ?? 0 : 0 + + const convergenceStatus = + convergence.stable_at_round !== undefined + ? `stable at round ${convergence.stable_at_round}` + : convergence.final_diff_pct !== undefined + ? `${convergence.final_diff_pct}% diff` + : 'pending' + + const summary = + `Plan reviewed in ${rounds.length} rounds. ` + + `Convergence: ${convergenceStatus}. ` + + `Final score: ${finalScore}/100. ` + + `Status: ${reviewLog.approval?.status || 'pending'}.` + + return { + summary, + convergence_status: convergenceStatus, + final_score: finalScore, + rounds_completed: rounds.length, + } +}