Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .claude/agents/duul-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,20 @@ approved_plan: <the full approved plan text>
- 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<verbatim user request>\n\n## Files\n- path/to/file.ts: <change>\n\n## Approach\n<...>\n\n## Edge cases\n<...>",
"workspace_root": "/absolute/path/to/repo",
"user_original_request": "<verbatim user message>",
"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.
14 changes: 12 additions & 2 deletions src/schemas/code-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/schemas/execution-partition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion src/schemas/plan-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
29 changes: 26 additions & 3 deletions src/tools/code-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<your code>", "approved_plan": "<plan text>", "workspace_root": "<absolute path>", "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
Expand Down
29 changes: 26 additions & 3 deletions src/tools/execution-partition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<plan text>", "workspace_root": "<absolute path>" }. ' +
'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
Expand Down
20 changes: 18 additions & 2 deletions src/tools/plan-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<your complete plan text>", "workspace_root": "<absolute path>", "user_original_request": "<verbatim user message>", "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
Expand Down
Loading