diff --git a/.claude/agents/duul-planner.md b/.claude/agents/duul-planner.md index 2b72d03..95012ae 100644 --- a/.claude/agents/duul-planner.md +++ b/.claude/agents/duul-planner.md @@ -58,3 +58,20 @@ approved_plan: - Be thorough in your plan — include file paths, function signatures, data flow, and error handling. - Always include `workspace_root` so the reviewer can explore the codebase. - Write `.duul-state.json` after every review call with: `{ "review_id", "phase": "plan", "verdict", "approved_plan", "iteration", "git_head_sha" }` + +## Tool input rules (CRITICAL) + +When calling `request_plan_review`, your tool input MUST include the `plan` field with the full plan markdown as a string. Do **NOT** send an empty `{}` object — that triggers an MCP validation error (`-32602: plan required`). + +**Minimum valid call:** + +```json +{ + "plan": "## Problem\n\n\n## Files\n- path/to/file.ts: \n\n## Approach\n<...>\n\n## Edge cases\n<...>", + "workspace_root": "/absolute/path/to/repo", + "user_original_request": "", + "iteration_count": 1 +} +``` + +If you find yourself unable to write the plan text in one tool-use turn (e.g. the plan is too long), draft and finalize the plan in your thinking/scratch first, then make a single tool call with the complete `plan` string. **Never call the tool with placeholder, empty, or partial input.** If the tool returns the validation error above, you wrote an empty input — re-read your draft and call again with the full `plan` string populated. diff --git a/src/schemas/code-review.ts b/src/schemas/code-review.ts index 06f40a6..0b6f5b1 100644 --- a/src/schemas/code-review.ts +++ b/src/schemas/code-review.ts @@ -13,11 +13,21 @@ export const DependenciesSchema = z.object({ }); export const CodeReviewInputSchema = z.object({ - code: z.string().min(1, 'code must not be empty').describe('The code to review'), + code: z + .string() + .min(1, 'code must not be empty') + .describe( + 'REQUIRED. The full code being reviewed (markdown code block or raw source). Must NOT be omitted or empty. ' + + 'For multi-file diffs, concatenate all changed code with file headers. ' + + 'Pass actual code content here — never call this tool with an empty object.', + ), approved_plan: z .string() .min(1, 'approved_plan must not be empty') - .describe('The previously approved plan this code implements'), + .describe( + 'REQUIRED. Full text of the plan approved in Phase 1. Must NOT be omitted. ' + + 'Pass the entire approved plan content (markdown) so the reviewer can verify the code matches it.', + ), file_path: z.string().optional().describe('File path for contextual feedback'), dependencies: DependenciesSchema.optional().describe('Related library version info'), relevant_code: z diff --git a/src/schemas/execution-partition.ts b/src/schemas/execution-partition.ts index 4840bff..da3ab4c 100644 --- a/src/schemas/execution-partition.ts +++ b/src/schemas/execution-partition.ts @@ -5,10 +5,17 @@ export const ExecutionPartitionInputSchema = z.object({ approved_plan: z .string() .min(1, 'approved_plan must not be empty') - .describe('The previously approved plan to partition into execution units'), + .describe( + 'REQUIRED. Full text of the approved plan to partition into subtasks. Must NOT be omitted or empty. ' + + 'Pass the entire approved plan markdown so the partitioner can analyze dependencies and split work.', + ), workspace_root: z .string() - .describe('Absolute path to the workspace root directory'), + .min(1, 'workspace_root must not be empty') + .describe( + 'REQUIRED. Absolute path to the workspace root directory. Must NOT be omitted. ' + + 'Example: "/Users/me/project". The partitioner uses this to verify file paths exist.', + ), working_directories: z .array(z.string()) .optional() diff --git a/src/schemas/plan-review.ts b/src/schemas/plan-review.ts index 62406ca..07ae7b5 100644 --- a/src/schemas/plan-review.ts +++ b/src/schemas/plan-review.ts @@ -31,7 +31,14 @@ export const ProjectContextSchema = z.object({ }); export const PlanReviewInputSchema = z.object({ - plan: z.string().min(1, 'plan must not be empty').describe('Detailed implementation plan'), + plan: z + .string() + .min(1, 'plan must not be empty') + .describe( + 'REQUIRED. Full implementation plan text (markdown). Must NOT be omitted or empty. ' + + 'Include: problem statement (quote user request), files to create/modify with paths, ' + + 'approach, edge cases, dependencies. Pass actual plan content here — never call this tool with an empty object.', + ), project_context: ProjectContextSchema.optional().describe('Structured project context'), constraints: z .array(z.string()) diff --git a/src/tools/code-review.ts b/src/tools/code-review.ts index c43a8aa..aa33b7a 100644 --- a/src/tools/code-review.ts +++ b/src/tools/code-review.ts @@ -23,15 +23,38 @@ export function registerCodeReviewTool(server: McpServer): void { { title: 'DUUL Code Review (Strict QA)', description: - 'DUUL Phase 2: Submit code for review by an LLM acting as a Strict QA Engineer. ' + - 'Requires the approved plan for context. Returns blocking issues, vulnerabilities, ' + - 'and optionally an optimized code snippet, or approval.', + 'DUUL Phase 2: Submit code for strict QA review. ' + + 'REQUIRED fields: code (the full code being reviewed — do NOT leave empty), approved_plan (the Phase 1 approved plan text). ' + + 'Optional: workspace_root, file_path, changed_files, artifact_refs, previous_review_id, iteration_count. ' + + 'NEVER call with an empty object — populate code and approved_plan with actual content before invoking. ' + + 'Returns blocking issues, vulnerabilities, optimized snippet, or APPROVE verdict.', inputSchema: CodeReviewInputSchema, outputSchema: CodeReviewMcpOutputSchema, }, async (input) => { try { const args = input as CodeReviewInput; + + if ( + !args || + typeof args.code !== 'string' || + args.code.trim().length < 5 || + typeof args.approved_plan !== 'string' || + args.approved_plan.trim().length < 20 + ) { + const message = + 'ERROR: `code` and `approved_plan` fields are both required. ' + + '`code` must contain the actual code being reviewed (min 5 chars). ' + + '`approved_plan` must contain the full plan text approved in Phase 1 (min 20 chars). ' + + 'You called request_code_review with missing or empty content. ' + + 'Retry with: { "code": "", "approved_plan": "", "workspace_root": "", "iteration_count": 1 }. ' + + 'Do NOT call this tool again with an empty input.'; + console.error(`[duul] code-review rejected: missing/empty code or approved_plan field`); + return { + content: [{ type: 'text' as const, text: message }], + isError: true, + }; + } const iterMeta = computeIterationMeta('code', args.iteration_count, args.max_review_iterations); // Short-circuit if iteration limit exceeded diff --git a/src/tools/execution-partition.ts b/src/tools/execution-partition.ts index d8b64ae..1914752 100644 --- a/src/tools/execution-partition.ts +++ b/src/tools/execution-partition.ts @@ -20,15 +20,38 @@ export function registerExecutionPartitionTool(server: McpServer): void { { title: 'DUUL Execution Partition (Project Manager)', description: - 'DUUL optional: Partition an approved plan into executable subtasks with dependency graph, ' + - 'spawn strategy, and handoff contracts. Use after plan review approval to ' + - 'determine whether work can be parallelized across multiple agents/workspaces.', + 'DUUL optional: Partition an approved plan into executable subtasks. ' + + 'REQUIRED fields: approved_plan (full plan markdown — do NOT leave empty), workspace_root (absolute path). ' + + 'Optional: working_directories, changed_files, entrypoints, artifact_refs, max_parallelism, iteration_count. ' + + 'NEVER call with an empty object — populate approved_plan with actual plan text before invoking. ' + + 'Returns dependency graph, spawn strategy, and handoff contracts.', inputSchema: ExecutionPartitionInputSchema, outputSchema: ExecutionPartitionMcpOutputSchema, }, async (input) => { try { const args = input as ExecutionPartitionInput; + + if ( + !args || + typeof args.approved_plan !== 'string' || + args.approved_plan.trim().length < 20 || + typeof args.workspace_root !== 'string' || + args.workspace_root.trim().length === 0 + ) { + const message = + 'ERROR: `approved_plan` and `workspace_root` fields are both required. ' + + '`approved_plan` must contain the full plan text (min 20 chars). ' + + '`workspace_root` must be an absolute path. ' + + 'You called request_execution_partition with missing or empty content. ' + + 'Retry with: { "approved_plan": "", "workspace_root": "" }. ' + + 'Do NOT call this tool again with an empty input.'; + console.error(`[duul] execution-partition rejected: missing/empty approved_plan or workspace_root`); + return { + content: [{ type: 'text' as const, text: message }], + isError: true, + }; + } const iterMeta = computeIterationMeta('partition', args.iteration_count, args.max_review_iterations); // Short-circuit if iteration limit exceeded diff --git a/src/tools/plan-review.ts b/src/tools/plan-review.ts index 29be45c..1efaa70 100644 --- a/src/tools/plan-review.ts +++ b/src/tools/plan-review.ts @@ -23,14 +23,30 @@ export function registerPlanReviewTool(server: McpServer): void { { title: 'DUUL Plan Review (Senior Architect)', description: - 'DUUL Phase 1: Submit a development plan for review by an LLM acting as a Senior Architect. ' + - 'Returns structured feedback with blocking issues, edge cases, and implementation checklist, or approval.', + 'DUUL Phase 1: Submit an implementation plan for senior-architect review. ' + + 'REQUIRED fields: plan (full plan markdown — do NOT leave empty), workspace_root (absolute path). ' + + 'Optional: project_context, changed_files, artifact_refs, user_original_request, previous_review_id, iteration_count. ' + + 'NEVER call with an empty object — populate plan with your actual plan text before invoking. ' + + 'Returns blocking issues, edge cases, implementation checklist, or APPROVE verdict.', inputSchema: PlanReviewInputSchema, outputSchema: PlanReviewMcpOutputSchema, }, async (input) => { try { const args = input as PlanReviewInput; + + if (!args || typeof args.plan !== 'string' || args.plan.trim().length < 20) { + const message = + 'ERROR: `plan` field is required and must contain the full plan markdown (at least 20 chars). ' + + 'You called request_plan_review with missing or empty plan content. ' + + 'Retry with: { "plan": "", "workspace_root": "", "user_original_request": "", "iteration_count": 1 }. ' + + 'Do NOT call this tool again with an empty input.'; + console.error(`[duul] plan-review rejected: missing/empty plan field`); + return { + content: [{ type: 'text' as const, text: message }], + isError: true, + }; + } const iterMeta = computeIterationMeta('plan', args.iteration_count, args.max_review_iterations); // Short-circuit if iteration limit exceeded