@planningo/duul 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +438 -0
  3. package/README.md +463 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +18 -0
  6. package/build/prompts/code-review-system.d.ts +9 -0
  7. package/build/prompts/code-review-system.js +116 -0
  8. package/build/prompts/execution-partition-system.d.ts +11 -0
  9. package/build/prompts/execution-partition-system.js +76 -0
  10. package/build/prompts/plan-review-system.d.ts +29 -0
  11. package/build/prompts/plan-review-system.js +175 -0
  12. package/build/schemas/code-review.d.ts +514 -0
  13. package/build/schemas/code-review.js +175 -0
  14. package/build/schemas/common.d.ts +118 -0
  15. package/build/schemas/common.js +64 -0
  16. package/build/schemas/execution-partition.d.ts +597 -0
  17. package/build/schemas/execution-partition.js +107 -0
  18. package/build/schemas/plan-review.d.ts +523 -0
  19. package/build/schemas/plan-review.js +175 -0
  20. package/build/services/filesystem-tools.d.ts +6 -0
  21. package/build/services/filesystem-tools.js +39 -0
  22. package/build/services/filesystem.d.ts +69 -0
  23. package/build/services/filesystem.js +609 -0
  24. package/build/services/pricing.d.ts +8 -0
  25. package/build/services/pricing.js +105 -0
  26. package/build/services/providers/anthropic.d.ts +28 -0
  27. package/build/services/providers/anthropic.js +431 -0
  28. package/build/services/providers/google.d.ts +28 -0
  29. package/build/services/providers/google.js +358 -0
  30. package/build/services/providers/openai.d.ts +22 -0
  31. package/build/services/providers/openai.js +395 -0
  32. package/build/services/providers/types.d.ts +82 -0
  33. package/build/services/providers/types.js +1 -0
  34. package/build/services/review-gates.d.ts +83 -0
  35. package/build/services/review-gates.js +200 -0
  36. package/build/services/review-limits.d.ts +36 -0
  37. package/build/services/review-limits.js +65 -0
  38. package/build/services/reviewer.d.ts +30 -0
  39. package/build/services/reviewer.js +243 -0
  40. package/build/services/usage-logger.d.ts +2 -0
  41. package/build/services/usage-logger.js +42 -0
  42. package/build/tools/code-review.d.ts +2 -0
  43. package/build/tools/code-review.js +178 -0
  44. package/build/tools/execution-partition.d.ts +2 -0
  45. package/build/tools/execution-partition.js +146 -0
  46. package/build/tools/plan-review.d.ts +2 -0
  47. package/build/tools/plan-review.js +183 -0
  48. package/package.json +65 -0
@@ -0,0 +1,178 @@
1
+ import { CodeReviewInputSchema, CodeReviewOutputSchema, CodeReviewMcpOutputSchema, } from '../schemas/code-review.js';
2
+ import { getCodeReviewSystemPrompt, formatCodeReviewUserMessage } from '../prompts/code-review-system.js';
3
+ import { callReview } from '../services/reviewer.js';
4
+ import { resolveWorkspaceScope, getGitDiff } from '../services/filesystem.js';
5
+ import { computeIterationMeta, isIterationLimitExceeded } from '../services/review-limits.js';
6
+ import { logUsage } from '../services/usage-logger.js';
7
+ import { applyGates } from '../services/review-gates.js';
8
+ const MAX_INLINE_DIFF_CHARS = 50_000;
9
+ const ZERO_USAGE = { input_tokens: 0, output_tokens: 0, total_tokens: 0, api_calls: 0, provider: 'none', model: 'none', estimated_cost_usd: null };
10
+ export function registerCodeReviewTool(server) {
11
+ server.registerTool('request_code_review', {
12
+ title: 'DUUL Code Review (Strict QA)',
13
+ description: 'DUUL Phase 2: Submit code for review by an LLM acting as a Strict QA Engineer. ' +
14
+ 'Requires the approved plan for context. Returns blocking issues, vulnerabilities, ' +
15
+ 'and optionally an optimized code snippet, or approval.',
16
+ inputSchema: CodeReviewInputSchema,
17
+ outputSchema: CodeReviewMcpOutputSchema,
18
+ }, async (input) => {
19
+ try {
20
+ const args = input;
21
+ const iterMeta = computeIterationMeta('code', args.iteration_count, args.max_review_iterations);
22
+ // Short-circuit if iteration limit exceeded
23
+ if (isIterationLimitExceeded('code', args.iteration_count, args.max_review_iterations)) {
24
+ console.error(`[duul] Code review iteration limit exceeded: ${args.iteration_count} > ${iterMeta.iteration_limit}`);
25
+ const limitResult = {
26
+ verdict: 'REVISE',
27
+ review_status: 'incomplete',
28
+ confidence: 0,
29
+ requires_human_review: true,
30
+ logic_validation: `Iteration limit reached (${iterMeta.iteration_count}/${iterMeta.iteration_limit}). Human review required to continue.`,
31
+ blocking_issues: [],
32
+ merge_blockers: null,
33
+ non_blocking_suggestions: [],
34
+ vulnerabilities: [],
35
+ optimized_snippet: null,
36
+ follow_up_todos: null,
37
+ missing_context: null,
38
+ evidence_files: null,
39
+ used_tools: null,
40
+ tool_exhaustion_reason: null,
41
+ user_original_request_echo: null,
42
+ symptom_impact: null,
43
+ symptom_match_notes: null,
44
+ gates_tripped: null,
45
+ review_id: '',
46
+ ...iterMeta,
47
+ token_usage: ZERO_USAGE,
48
+ };
49
+ return {
50
+ content: [{ type: 'text', text: JSON.stringify(limitResult, null, 2) }],
51
+ structuredContent: limitResult,
52
+ };
53
+ }
54
+ const scope = resolveWorkspaceScope(args);
55
+ // Auto-generate git diff if not provided
56
+ let gitDiff = args.git_diff;
57
+ if (!gitDiff && scope?.root && args.changed_files?.length) {
58
+ try {
59
+ const diffResult = await getGitDiff(scope.root, args.git_diff_base ?? 'HEAD', args.changed_files, scope);
60
+ if (diffResult && !diffResult.startsWith('Error') && !diffResult.startsWith('No differences')) {
61
+ gitDiff = diffResult.length > MAX_INLINE_DIFF_CHARS
62
+ ? diffResult.slice(0, MAX_INLINE_DIFF_CHARS) + `\n\n[truncated — diff exceeded ${MAX_INLINE_DIFF_CHARS} chars]`
63
+ : diffResult;
64
+ }
65
+ }
66
+ catch {
67
+ // Diff generation is best-effort, continue without it
68
+ }
69
+ }
70
+ const systemPrompt = getCodeReviewSystemPrompt();
71
+ const userMessage = formatCodeReviewUserMessage(args.code, args.approved_plan, args.file_path, args.dependencies, args.relevant_code, args.notes_to_reviewer, {
72
+ workingDirectories: args.working_directories,
73
+ linkedRoots: args.linked_roots,
74
+ changedFiles: args.changed_files,
75
+ entrypoints: args.entrypoints,
76
+ artifactRefs: args.artifact_refs,
77
+ gitHeadSha: args.git_head_sha,
78
+ previousGitHeadSha: args.previous_git_head_sha,
79
+ workspaceName: args.workspace_name,
80
+ setupScriptPresent: args.setup_script_present,
81
+ runScriptPresent: args.run_script_present,
82
+ environmentFilesExpected: args.environment_files_expected,
83
+ gitDiff,
84
+ }, args.user_original_request);
85
+ const { parsed, reviewId, usage } = await callReview({
86
+ systemPrompt,
87
+ userMessage,
88
+ schemaName: 'code_review_output',
89
+ outputSchema: CodeReviewOutputSchema,
90
+ workspaceScope: scope,
91
+ previousReviewId: args.previous_review_id,
92
+ reviewerConfig: args.reviewer_config,
93
+ createFallback: (reason, usedTools) => ({
94
+ verdict: 'REVISE',
95
+ review_status: 'incomplete',
96
+ confidence: 0,
97
+ requires_human_review: true,
98
+ logic_validation: `Review could not be completed — tool loop exhausted (${reason}).`,
99
+ blocking_issues: [],
100
+ merge_blockers: null,
101
+ non_blocking_suggestions: [],
102
+ vulnerabilities: [],
103
+ optimized_snippet: null,
104
+ follow_up_todos: null,
105
+ missing_context: usedTools.length > 0 ? usedTools : ['No tools were called'],
106
+ evidence_files: null,
107
+ used_tools: usedTools,
108
+ tool_exhaustion_reason: reason,
109
+ user_original_request_echo: null,
110
+ symptom_impact: null,
111
+ symptom_match_notes: null,
112
+ gates_tripped: null,
113
+ }),
114
+ });
115
+ // Invariant: APPROVE with blocking_issues is always wrong — override to REVISE
116
+ let verdict = parsed.verdict === 'APPROVE' && parsed.blocking_issues?.length > 0
117
+ ? 'REVISE'
118
+ : parsed.verdict;
119
+ if (verdict !== parsed.verdict) {
120
+ console.error(`[duul] Verdict overridden: APPROVE → REVISE (${parsed.blocking_issues.length} blocking issues)`);
121
+ }
122
+ // Post-LLM gates
123
+ const gates = applyGates({
124
+ phase: 'code',
125
+ userOriginalRequest: args.user_original_request,
126
+ notesToReviewer: args.notes_to_reviewer,
127
+ changedFiles: args.changed_files,
128
+ gitDiff,
129
+ artifactRefs: args.artifact_refs,
130
+ symptomImpact: parsed.symptom_impact,
131
+ });
132
+ let requires_human_review = parsed.requires_human_review;
133
+ let blocking_issues = parsed.blocking_issues;
134
+ if (gates.tripped.length > 0) {
135
+ if (gates.forcedVerdict === 'REVISE')
136
+ verdict = 'REVISE';
137
+ if (gates.forcedHumanReview)
138
+ requires_human_review = true;
139
+ blocking_issues = [...blocking_issues, ...gates.extraBlockingIssues];
140
+ console.error(`[duul] Code gates tripped: ${gates.tripped.join(', ')}`);
141
+ }
142
+ const result = {
143
+ ...parsed,
144
+ verdict,
145
+ requires_human_review,
146
+ blocking_issues,
147
+ gates_tripped: gates.tripped.length > 0 ? gates.tripped : null,
148
+ review_id: reviewId,
149
+ ...iterMeta,
150
+ token_usage: usage,
151
+ };
152
+ logUsage('code_review', result.token_usage, {
153
+ verdict,
154
+ review_id: reviewId,
155
+ iteration_count: iterMeta.iteration_count,
156
+ workspace_name: args.workspace_name,
157
+ gates_tripped: result.gates_tripped,
158
+ });
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text',
163
+ text: JSON.stringify(result, null, 2),
164
+ },
165
+ ],
166
+ structuredContent: result,
167
+ };
168
+ }
169
+ catch (error) {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ console.error(`[duul] code-review error: ${message}`);
172
+ return {
173
+ content: [{ type: 'text', text: `Code review failed: ${message}` }],
174
+ isError: true,
175
+ };
176
+ }
177
+ });
178
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerExecutionPartitionTool(server: McpServer): void;
@@ -0,0 +1,146 @@
1
+ import { ExecutionPartitionInputSchema, ExecutionPartitionOutputSchema, ExecutionPartitionMcpOutputSchema, } from '../schemas/execution-partition.js';
2
+ import { getExecutionPartitionSystemPrompt, formatExecutionPartitionUserMessage } from '../prompts/execution-partition-system.js';
3
+ import { callReview } from '../services/reviewer.js';
4
+ import { resolveWorkspaceScope } from '../services/filesystem.js';
5
+ import { computeIterationMeta, isIterationLimitExceeded } from '../services/review-limits.js';
6
+ import { logUsage } from '../services/usage-logger.js';
7
+ const ZERO_USAGE = { input_tokens: 0, output_tokens: 0, total_tokens: 0, api_calls: 0, provider: 'none', model: 'none', estimated_cost_usd: null };
8
+ export function registerExecutionPartitionTool(server) {
9
+ server.registerTool('request_execution_partition', {
10
+ title: 'DUUL Execution Partition (Project Manager)',
11
+ description: 'DUUL optional: Partition an approved plan into executable subtasks with dependency graph, ' +
12
+ 'spawn strategy, and handoff contracts. Use after plan review approval to ' +
13
+ 'determine whether work can be parallelized across multiple agents/workspaces.',
14
+ inputSchema: ExecutionPartitionInputSchema,
15
+ outputSchema: ExecutionPartitionMcpOutputSchema,
16
+ }, async (input) => {
17
+ try {
18
+ const args = input;
19
+ const iterMeta = computeIterationMeta('partition', args.iteration_count, args.max_review_iterations);
20
+ // Short-circuit if iteration limit exceeded
21
+ if (isIterationLimitExceeded('partition', args.iteration_count, args.max_review_iterations)) {
22
+ console.error(`[duul] Partition iteration limit exceeded: ${args.iteration_count} > ${iterMeta.iteration_limit}`);
23
+ const limitResult = {
24
+ execution_mode: 'serial',
25
+ rationale: `Iteration limit reached (${iterMeta.iteration_count}/${iterMeta.iteration_limit}). Human review required to continue.`,
26
+ requires_human_checkpoint: true,
27
+ human_checkpoint_reasons: [`Iteration limit reached — ${iterMeta.iteration_count}/${iterMeta.iteration_limit} iterations used.`],
28
+ spawn_strategy: 'reuse_workspace',
29
+ handoff_artifact_pattern: '.context/subtasks/{subtask_id}.json',
30
+ subtask_result_schema_version: '1.0',
31
+ subtasks: [{
32
+ id: 'full-plan',
33
+ title: 'Execute full plan serially (limit reached)',
34
+ goal: args.approved_plan.slice(0, 200) + '...',
35
+ can_run_in_parallel: false,
36
+ depends_on: [],
37
+ workspace_name_hint: 'main',
38
+ spawn_strategy: 'reuse_workspace',
39
+ scope: {
40
+ working_directories: args.working_directories,
41
+ changed_files: args.changed_files,
42
+ entrypoints: args.entrypoints,
43
+ },
44
+ handoff_contract: [],
45
+ completion_criteria: ['All items in the approved plan are implemented'],
46
+ review_focus: ['Full plan implementation'],
47
+ risk_level: 'medium',
48
+ }],
49
+ global_checkpoints: [],
50
+ merge_order: ['full-plan'],
51
+ retry_policy: {
52
+ max_retries: 2,
53
+ on_review_revise: 'retry_subtask',
54
+ on_tool_exhaustion: 'retry_with_narrower_scope',
55
+ on_conflict: 'serialize_and_retry',
56
+ on_blocker: 'escalate_to_human',
57
+ on_max_retries_exceeded: 'abort_subtask_and_report',
58
+ },
59
+ review_id: '',
60
+ ...iterMeta,
61
+ token_usage: ZERO_USAGE,
62
+ };
63
+ return {
64
+ content: [{ type: 'text', text: JSON.stringify(limitResult, null, 2) }],
65
+ structuredContent: limitResult,
66
+ };
67
+ }
68
+ const scope = resolveWorkspaceScope(args);
69
+ const systemPrompt = getExecutionPartitionSystemPrompt();
70
+ const userMessage = formatExecutionPartitionUserMessage(args.approved_plan, args.constraints, {
71
+ changedFiles: args.changed_files,
72
+ entrypoints: args.entrypoints,
73
+ artifactRefs: args.artifact_refs,
74
+ workingDirectories: args.working_directories,
75
+ }, args.max_parallelism);
76
+ const { parsed, reviewId, usage } = await callReview({
77
+ systemPrompt,
78
+ userMessage,
79
+ schemaName: 'execution_partition_output',
80
+ outputSchema: ExecutionPartitionOutputSchema,
81
+ workspaceScope: scope,
82
+ previousReviewId: args.previous_review_id,
83
+ reviewerConfig: args.reviewer_config,
84
+ createFallback: (_reason, _usedTools) => ({
85
+ execution_mode: 'serial',
86
+ rationale: 'Partition could not be completed — tool loop exhausted. Defaulting to serial execution.',
87
+ requires_human_checkpoint: true,
88
+ human_checkpoint_reasons: ['Partition was incomplete — human should verify before proceeding.'],
89
+ spawn_strategy: 'reuse_workspace',
90
+ handoff_artifact_pattern: '.context/subtasks/{subtask_id}.json',
91
+ subtask_result_schema_version: '1.0',
92
+ subtasks: [{
93
+ id: 'full-plan',
94
+ title: 'Execute full plan serially',
95
+ goal: args.approved_plan.slice(0, 200) + '...',
96
+ can_run_in_parallel: false,
97
+ depends_on: [],
98
+ workspace_name_hint: 'main',
99
+ spawn_strategy: 'reuse_workspace',
100
+ scope: {
101
+ working_directories: args.working_directories,
102
+ changed_files: args.changed_files,
103
+ entrypoints: args.entrypoints,
104
+ },
105
+ handoff_contract: [],
106
+ completion_criteria: ['All items in the approved plan are implemented'],
107
+ review_focus: ['Full plan implementation'],
108
+ risk_level: 'medium',
109
+ }],
110
+ global_checkpoints: [],
111
+ merge_order: ['full-plan'],
112
+ retry_policy: {
113
+ max_retries: 2,
114
+ on_review_revise: 'retry_subtask',
115
+ on_tool_exhaustion: 'retry_with_narrower_scope',
116
+ on_conflict: 'serialize_and_retry',
117
+ on_blocker: 'escalate_to_human',
118
+ on_max_retries_exceeded: 'abort_subtask_and_report',
119
+ },
120
+ }),
121
+ });
122
+ const result = { ...parsed, review_id: reviewId, ...iterMeta, token_usage: usage };
123
+ logUsage('execution_partition', result.token_usage, {
124
+ review_id: reviewId,
125
+ iteration_count: iterMeta.iteration_count,
126
+ });
127
+ return {
128
+ content: [
129
+ {
130
+ type: 'text',
131
+ text: JSON.stringify(result, null, 2),
132
+ },
133
+ ],
134
+ structuredContent: result,
135
+ };
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ console.error(`[duul] execution-partition error: ${message}`);
140
+ return {
141
+ content: [{ type: 'text', text: `Execution partition failed: ${message}` }],
142
+ isError: true,
143
+ };
144
+ }
145
+ });
146
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerPlanReviewTool(server: McpServer): void;
@@ -0,0 +1,183 @@
1
+ import { PlanReviewInputSchema, PlanReviewOutputSchema, PlanReviewMcpOutputSchema, } from '../schemas/plan-review.js';
2
+ import { getPlanReviewSystemPrompt, formatPlanReviewUserMessage } from '../prompts/plan-review-system.js';
3
+ import { callReview } from '../services/reviewer.js';
4
+ import { resolveWorkspaceScope, getGitDiff } from '../services/filesystem.js';
5
+ import { computeIterationMeta, isIterationLimitExceeded } from '../services/review-limits.js';
6
+ import { logUsage } from '../services/usage-logger.js';
7
+ import { applyGates } from '../services/review-gates.js';
8
+ const MAX_INLINE_DIFF_CHARS = 50_000;
9
+ const ZERO_USAGE = { input_tokens: 0, output_tokens: 0, total_tokens: 0, api_calls: 0, provider: 'none', model: 'none', estimated_cost_usd: null };
10
+ export function registerPlanReviewTool(server) {
11
+ server.registerTool('request_plan_review', {
12
+ title: 'DUUL Plan Review (Senior Architect)',
13
+ description: 'DUUL Phase 1: Submit a development plan for review by an LLM acting as a Senior Architect. ' +
14
+ 'Returns structured feedback with blocking issues, edge cases, and implementation checklist, or approval.',
15
+ inputSchema: PlanReviewInputSchema,
16
+ outputSchema: PlanReviewMcpOutputSchema,
17
+ }, async (input) => {
18
+ try {
19
+ const args = input;
20
+ const iterMeta = computeIterationMeta('plan', args.iteration_count, args.max_review_iterations);
21
+ // Short-circuit if iteration limit exceeded
22
+ if (isIterationLimitExceeded('plan', args.iteration_count, args.max_review_iterations)) {
23
+ console.error(`[duul] Plan review iteration limit exceeded: ${args.iteration_count} > ${iterMeta.iteration_limit}`);
24
+ const limitResult = {
25
+ verdict: 'REVISE',
26
+ review_status: 'incomplete',
27
+ confidence: 0,
28
+ requires_human_review: true,
29
+ architectural_analysis: `Iteration limit reached (${iterMeta.iteration_count}/${iterMeta.iteration_limit}). Human review required to continue.`,
30
+ blocking_issues: [],
31
+ merge_blockers: null,
32
+ non_blocking_suggestions: [],
33
+ edge_cases: [],
34
+ checklist_for_implementation: [],
35
+ follow_up_todos: null,
36
+ missing_context: null,
37
+ evidence_files: null,
38
+ used_tools: null,
39
+ tool_exhaustion_reason: null,
40
+ parallelization_hint: null,
41
+ coordination_risks: null,
42
+ recommended_subtask_boundaries: null,
43
+ user_original_request_echo: null,
44
+ symptom_impact: null,
45
+ symptom_match_notes: null,
46
+ gates_tripped: null,
47
+ review_id: '',
48
+ ...iterMeta,
49
+ token_usage: ZERO_USAGE,
50
+ };
51
+ return {
52
+ content: [{ type: 'text', text: JSON.stringify(limitResult, null, 2) }],
53
+ structuredContent: limitResult,
54
+ };
55
+ }
56
+ const scope = resolveWorkspaceScope(args);
57
+ // Auto-generate git diff if not provided
58
+ let gitDiff = args.git_diff;
59
+ if (!gitDiff && scope?.root && args.changed_files?.length) {
60
+ try {
61
+ const diffResult = await getGitDiff(scope.root, args.git_diff_base ?? 'HEAD', args.changed_files, scope);
62
+ if (diffResult && !diffResult.startsWith('Error') && !diffResult.startsWith('No differences')) {
63
+ gitDiff = diffResult.length > MAX_INLINE_DIFF_CHARS
64
+ ? diffResult.slice(0, MAX_INLINE_DIFF_CHARS) + `\n\n[truncated — diff exceeded ${MAX_INLINE_DIFF_CHARS} chars]`
65
+ : diffResult;
66
+ }
67
+ }
68
+ catch {
69
+ // Diff generation is best-effort, continue without it
70
+ }
71
+ }
72
+ const systemPrompt = getPlanReviewSystemPrompt();
73
+ const userMessage = formatPlanReviewUserMessage(args.plan, args.project_context, args.constraints, args.notes_to_reviewer, {
74
+ workingDirectories: args.working_directories,
75
+ linkedRoots: args.linked_roots,
76
+ changedFiles: args.changed_files,
77
+ entrypoints: args.entrypoints,
78
+ artifactRefs: args.artifact_refs,
79
+ gitHeadSha: args.git_head_sha,
80
+ previousGitHeadSha: args.previous_git_head_sha,
81
+ workspaceName: args.workspace_name,
82
+ setupScriptPresent: args.setup_script_present,
83
+ runScriptPresent: args.run_script_present,
84
+ environmentFilesExpected: args.environment_files_expected,
85
+ gitDiff,
86
+ }, args.user_original_request);
87
+ const { parsed, reviewId, usage } = await callReview({
88
+ systemPrompt,
89
+ userMessage,
90
+ schemaName: 'plan_review_output',
91
+ outputSchema: PlanReviewOutputSchema,
92
+ workspaceScope: scope,
93
+ previousReviewId: args.previous_review_id,
94
+ reviewerConfig: args.reviewer_config,
95
+ createFallback: (reason, usedTools) => ({
96
+ verdict: 'REVISE',
97
+ review_status: 'incomplete',
98
+ confidence: 0,
99
+ requires_human_review: true,
100
+ architectural_analysis: `Review could not be completed — tool loop exhausted (${reason}).`,
101
+ blocking_issues: [],
102
+ merge_blockers: null,
103
+ non_blocking_suggestions: [],
104
+ edge_cases: [],
105
+ checklist_for_implementation: [],
106
+ follow_up_todos: null,
107
+ missing_context: usedTools.length > 0 ? usedTools : ['No tools were called'],
108
+ evidence_files: null,
109
+ used_tools: usedTools,
110
+ tool_exhaustion_reason: reason,
111
+ parallelization_hint: null,
112
+ coordination_risks: null,
113
+ recommended_subtask_boundaries: null,
114
+ user_original_request_echo: null,
115
+ symptom_impact: null,
116
+ symptom_match_notes: null,
117
+ gates_tripped: null,
118
+ }),
119
+ });
120
+ // Invariant: APPROVE with blocking_issues is always wrong — override to REVISE
121
+ let verdict = parsed.verdict === 'APPROVE' && parsed.blocking_issues?.length > 0
122
+ ? 'REVISE'
123
+ : parsed.verdict;
124
+ if (verdict !== parsed.verdict) {
125
+ console.error(`[duul] Verdict overridden: APPROVE → REVISE (${parsed.blocking_issues.length} blocking issues)`);
126
+ }
127
+ // Post-LLM gates
128
+ const gates = applyGates({
129
+ phase: 'plan',
130
+ userOriginalRequest: args.user_original_request,
131
+ notesToReviewer: args.notes_to_reviewer,
132
+ changedFiles: args.changed_files,
133
+ gitDiff,
134
+ artifactRefs: args.artifact_refs,
135
+ symptomImpact: parsed.symptom_impact,
136
+ });
137
+ let requires_human_review = parsed.requires_human_review;
138
+ let blocking_issues = parsed.blocking_issues;
139
+ if (gates.tripped.length > 0) {
140
+ if (gates.forcedVerdict === 'REVISE')
141
+ verdict = 'REVISE';
142
+ if (gates.forcedHumanReview)
143
+ requires_human_review = true;
144
+ blocking_issues = [...blocking_issues, ...gates.extraBlockingIssues];
145
+ console.error(`[duul] Plan gates tripped: ${gates.tripped.join(', ')}`);
146
+ }
147
+ const result = {
148
+ ...parsed,
149
+ verdict,
150
+ requires_human_review,
151
+ blocking_issues,
152
+ gates_tripped: gates.tripped.length > 0 ? gates.tripped : null,
153
+ review_id: reviewId,
154
+ ...iterMeta,
155
+ token_usage: usage,
156
+ };
157
+ logUsage('plan_review', result.token_usage, {
158
+ verdict,
159
+ review_id: reviewId,
160
+ iteration_count: iterMeta.iteration_count,
161
+ workspace_name: args.workspace_name,
162
+ gates_tripped: result.gates_tripped,
163
+ });
164
+ return {
165
+ content: [
166
+ {
167
+ type: 'text',
168
+ text: JSON.stringify(result, null, 2),
169
+ },
170
+ ],
171
+ structuredContent: result,
172
+ };
173
+ }
174
+ catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ console.error(`[duul] plan-review error: ${message}`);
177
+ return {
178
+ content: [{ type: 'text', text: `Plan review failed: ${message}` }],
179
+ isError: true,
180
+ };
181
+ }
182
+ });
183
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@planningo/duul",
3
+ "version": "1.0.0",
4
+ "description": "DUUL — Dual-phase Upfront-plan & Unit-verify Loop. MCP server for LLM peer review of plans and code.",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "duul": "build/index.js"
9
+ },
10
+ "files": [
11
+ "build",
12
+ "!build/__tests__",
13
+ "!build/**/*.test.js",
14
+ "!build/**/*.test.d.ts",
15
+ "README.md",
16
+ "README.ko.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "start": "node build/index.js",
22
+ "test": "tsc && node --test build/services/*.test.js build/__tests__/*.test.js",
23
+ "token-report": "node scripts/token-report.mjs",
24
+ "prepublishOnly": "npm run build",
25
+ "release": "changeset publish"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "claude",
31
+ "claude-code",
32
+ "anthropic",
33
+ "openai",
34
+ "llm",
35
+ "code-review",
36
+ "peer-review",
37
+ "duul"
38
+ ],
39
+ "author": "Planningo",
40
+ "license": "MIT",
41
+ "homepage": "https://github.com/Planningo/duul#readme",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/Planningo/duul.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/Planningo/duul/issues"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.29.0",
57
+ "openai": "^6.1.0",
58
+ "zod": "^3.25.0"
59
+ },
60
+ "devDependencies": {
61
+ "@changesets/cli": "^2.31.0",
62
+ "@types/node": "^22.0.0",
63
+ "typescript": "^5.8.0"
64
+ }
65
+ }