@planningo/duul 1.0.0 → 1.1.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.
@@ -14,9 +14,16 @@ export const ReviewerConfigSchema = z.object({
14
14
  .optional()
15
15
  .describe('Review provider. Default: env REVIEW_PROVIDER or "openai".'),
16
16
  model: z
17
- .string()
17
+ .union([
18
+ z.string(),
19
+ z.object({
20
+ plan: z.string().optional(),
21
+ code: z.string().optional(),
22
+ partition: z.string().optional(),
23
+ }),
24
+ ])
18
25
  .optional()
19
- .describe('Model to use. Default: env REVIEW_MODEL or provider default.'),
26
+ .describe('Model to use. Either a single string applied to all tools, or an object with per-tool overrides (plan/code/partition). Default: env REVIEW_MODEL or provider default.'),
20
27
  base_url: z
21
28
  .string()
22
29
  .optional()
@@ -45,6 +52,13 @@ export const IterationMetaOutputSchema = z.object({
45
52
  iteration_count: z.number().describe('Current iteration number (1-based) as reported by the caller.'),
46
53
  iteration_limit: z.number().describe('Maximum iterations allowed for this phase.'),
47
54
  iteration_limit_reached: z.boolean().describe('Whether the iteration limit has been reached.'),
55
+ cost_warning: z
56
+ .string()
57
+ .optional()
58
+ .nullable()
59
+ .describe('Soft warning string emitted once iteration_count crosses ~60% of iteration_limit. ' +
60
+ 'Includes the current round cost so the orchestrator can decide whether to accept a near-verdict or escalate. ' +
61
+ 'Null when below the threshold.'),
48
62
  });
49
63
  /**
50
64
  * Token usage fields — added to MCP output for cost/usage tracking.
@@ -1,7 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  export declare const ExecutionPartitionInputSchema: z.ZodObject<{
3
- approved_plan: z.ZodString;
4
- workspace_root: z.ZodString;
3
+ approved_plan: z.ZodOptional<z.ZodString>;
4
+ approved_plan_file: z.ZodOptional<z.ZodString>;
5
+ workspace_root: z.ZodOptional<z.ZodString>;
5
6
  working_directories: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
6
7
  changed_files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
7
8
  entrypoints: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -25,32 +26,51 @@ export declare const ExecutionPartitionInputSchema: z.ZodObject<{
25
26
  max_review_iterations: z.ZodOptional<z.ZodNumber>;
26
27
  reviewer_config: z.ZodOptional<z.ZodObject<{
27
28
  provider: z.ZodOptional<z.ZodEnum<["openai", "anthropic", "google", "openrouter", "compatible"]>>;
28
- model: z.ZodOptional<z.ZodString>;
29
+ model: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
30
+ plan: z.ZodOptional<z.ZodString>;
31
+ code: z.ZodOptional<z.ZodString>;
32
+ partition: z.ZodOptional<z.ZodString>;
33
+ }, "strip", z.ZodTypeAny, {
34
+ code?: string | undefined;
35
+ plan?: string | undefined;
36
+ partition?: string | undefined;
37
+ }, {
38
+ code?: string | undefined;
39
+ plan?: string | undefined;
40
+ partition?: string | undefined;
41
+ }>]>>;
29
42
  base_url: z.ZodOptional<z.ZodString>;
30
43
  api_key: z.ZodOptional<z.ZodString>;
31
44
  temperature: z.ZodOptional<z.ZodNumber>;
32
45
  top_p: z.ZodOptional<z.ZodNumber>;
33
46
  }, "strip", z.ZodTypeAny, {
34
47
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
35
- model?: string | undefined;
48
+ model?: string | {
49
+ code?: string | undefined;
50
+ plan?: string | undefined;
51
+ partition?: string | undefined;
52
+ } | undefined;
36
53
  base_url?: string | undefined;
37
54
  api_key?: string | undefined;
38
55
  temperature?: number | undefined;
39
56
  top_p?: number | undefined;
40
57
  }, {
41
58
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
42
- model?: string | undefined;
59
+ model?: string | {
60
+ code?: string | undefined;
61
+ plan?: string | undefined;
62
+ partition?: string | undefined;
63
+ } | undefined;
43
64
  base_url?: string | undefined;
44
65
  api_key?: string | undefined;
45
66
  temperature?: number | undefined;
46
67
  top_p?: number | undefined;
47
68
  }>>;
48
69
  }, "strip", z.ZodTypeAny, {
49
- workspace_root: string;
50
- approved_plan: string;
51
70
  iteration_count?: number | undefined;
52
71
  changed_files?: string[] | undefined;
53
72
  constraints?: string[] | undefined;
73
+ workspace_root?: string | undefined;
54
74
  working_directories?: string[] | undefined;
55
75
  entrypoints?: string[] | undefined;
56
76
  artifact_refs?: {
@@ -62,19 +82,24 @@ export declare const ExecutionPartitionInputSchema: z.ZodObject<{
62
82
  max_review_iterations?: number | undefined;
63
83
  reviewer_config?: {
64
84
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
65
- model?: string | undefined;
85
+ model?: string | {
86
+ code?: string | undefined;
87
+ plan?: string | undefined;
88
+ partition?: string | undefined;
89
+ } | undefined;
66
90
  base_url?: string | undefined;
67
91
  api_key?: string | undefined;
68
92
  temperature?: number | undefined;
69
93
  top_p?: number | undefined;
70
94
  } | undefined;
95
+ approved_plan?: string | undefined;
96
+ approved_plan_file?: string | undefined;
71
97
  max_parallelism?: number | undefined;
72
98
  }, {
73
- workspace_root: string;
74
- approved_plan: string;
75
99
  iteration_count?: number | undefined;
76
100
  changed_files?: string[] | undefined;
77
101
  constraints?: string[] | undefined;
102
+ workspace_root?: string | undefined;
78
103
  working_directories?: string[] | undefined;
79
104
  entrypoints?: string[] | undefined;
80
105
  artifact_refs?: {
@@ -86,12 +111,18 @@ export declare const ExecutionPartitionInputSchema: z.ZodObject<{
86
111
  max_review_iterations?: number | undefined;
87
112
  reviewer_config?: {
88
113
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
89
- model?: string | undefined;
114
+ model?: string | {
115
+ code?: string | undefined;
116
+ plan?: string | undefined;
117
+ partition?: string | undefined;
118
+ } | undefined;
90
119
  base_url?: string | undefined;
91
120
  api_key?: string | undefined;
92
121
  temperature?: number | undefined;
93
122
  top_p?: number | undefined;
94
123
  } | undefined;
124
+ approved_plan?: string | undefined;
125
+ approved_plan_file?: string | undefined;
95
126
  max_parallelism?: number | undefined;
96
127
  }>;
97
128
  export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
@@ -154,13 +185,6 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
154
185
  review_focus: z.ZodArray<z.ZodString, "many">;
155
186
  risk_level: z.ZodEnum<["high", "medium", "low"]>;
156
187
  }, "strip", z.ZodTypeAny, {
157
- id: string;
158
- title: string;
159
- goal: string;
160
- can_run_in_parallel: boolean;
161
- depends_on: string[];
162
- workspace_name_hint: string;
163
- spawn_strategy: "new_workspace" | "reuse_workspace";
164
188
  scope: {
165
189
  changed_files?: string[] | undefined;
166
190
  working_directories?: string[] | undefined;
@@ -172,11 +196,6 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
172
196
  priority: "high" | "medium" | "low";
173
197
  }[] | undefined;
174
198
  };
175
- handoff_contract: string[];
176
- completion_criteria: string[];
177
- review_focus: string[];
178
- risk_level: "high" | "medium" | "low";
179
- }, {
180
199
  id: string;
181
200
  title: string;
182
201
  goal: string;
@@ -184,6 +203,11 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
184
203
  depends_on: string[];
185
204
  workspace_name_hint: string;
186
205
  spawn_strategy: "new_workspace" | "reuse_workspace";
206
+ handoff_contract: string[];
207
+ completion_criteria: string[];
208
+ review_focus: string[];
209
+ risk_level: "high" | "medium" | "low";
210
+ }, {
187
211
  scope: {
188
212
  changed_files?: string[] | undefined;
189
213
  working_directories?: string[] | undefined;
@@ -195,6 +219,13 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
195
219
  priority: "high" | "medium" | "low";
196
220
  }[] | undefined;
197
221
  };
222
+ id: string;
223
+ title: string;
224
+ goal: string;
225
+ can_run_in_parallel: boolean;
226
+ depends_on: string[];
227
+ workspace_name_hint: string;
228
+ spawn_strategy: "new_workspace" | "reuse_workspace";
198
229
  handoff_contract: string[];
199
230
  completion_criteria: string[];
200
231
  review_focus: string[];
@@ -233,13 +264,6 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
233
264
  handoff_artifact_pattern: string;
234
265
  subtask_result_schema_version: "1.0";
235
266
  subtasks: {
236
- id: string;
237
- title: string;
238
- goal: string;
239
- can_run_in_parallel: boolean;
240
- depends_on: string[];
241
- workspace_name_hint: string;
242
- spawn_strategy: "new_workspace" | "reuse_workspace";
243
267
  scope: {
244
268
  changed_files?: string[] | undefined;
245
269
  working_directories?: string[] | undefined;
@@ -251,6 +275,13 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
251
275
  priority: "high" | "medium" | "low";
252
276
  }[] | undefined;
253
277
  };
278
+ id: string;
279
+ title: string;
280
+ goal: string;
281
+ can_run_in_parallel: boolean;
282
+ depends_on: string[];
283
+ workspace_name_hint: string;
284
+ spawn_strategy: "new_workspace" | "reuse_workspace";
254
285
  handoff_contract: string[];
255
286
  completion_criteria: string[];
256
287
  review_focus: string[];
@@ -275,13 +306,6 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
275
306
  handoff_artifact_pattern: string;
276
307
  subtask_result_schema_version: "1.0";
277
308
  subtasks: {
278
- id: string;
279
- title: string;
280
- goal: string;
281
- can_run_in_parallel: boolean;
282
- depends_on: string[];
283
- workspace_name_hint: string;
284
- spawn_strategy: "new_workspace" | "reuse_workspace";
285
309
  scope: {
286
310
  changed_files?: string[] | undefined;
287
311
  working_directories?: string[] | undefined;
@@ -293,6 +317,13 @@ export declare const ExecutionPartitionOutputSchema: z.ZodObject<{
293
317
  priority: "high" | "medium" | "low";
294
318
  }[] | undefined;
295
319
  };
320
+ id: string;
321
+ title: string;
322
+ goal: string;
323
+ can_run_in_parallel: boolean;
324
+ depends_on: string[];
325
+ workspace_name_hint: string;
326
+ spawn_strategy: "new_workspace" | "reuse_workspace";
296
327
  handoff_contract: string[];
297
328
  completion_criteria: string[];
298
329
  review_focus: string[];
@@ -369,13 +400,6 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
369
400
  review_focus: z.ZodArray<z.ZodString, "many">;
370
401
  risk_level: z.ZodEnum<["high", "medium", "low"]>;
371
402
  }, "strip", z.ZodTypeAny, {
372
- id: string;
373
- title: string;
374
- goal: string;
375
- can_run_in_parallel: boolean;
376
- depends_on: string[];
377
- workspace_name_hint: string;
378
- spawn_strategy: "new_workspace" | "reuse_workspace";
379
403
  scope: {
380
404
  changed_files?: string[] | undefined;
381
405
  working_directories?: string[] | undefined;
@@ -387,11 +411,6 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
387
411
  priority: "high" | "medium" | "low";
388
412
  }[] | undefined;
389
413
  };
390
- handoff_contract: string[];
391
- completion_criteria: string[];
392
- review_focus: string[];
393
- risk_level: "high" | "medium" | "low";
394
- }, {
395
414
  id: string;
396
415
  title: string;
397
416
  goal: string;
@@ -399,6 +418,11 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
399
418
  depends_on: string[];
400
419
  workspace_name_hint: string;
401
420
  spawn_strategy: "new_workspace" | "reuse_workspace";
421
+ handoff_contract: string[];
422
+ completion_criteria: string[];
423
+ review_focus: string[];
424
+ risk_level: "high" | "medium" | "low";
425
+ }, {
402
426
  scope: {
403
427
  changed_files?: string[] | undefined;
404
428
  working_directories?: string[] | undefined;
@@ -410,6 +434,13 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
410
434
  priority: "high" | "medium" | "low";
411
435
  }[] | undefined;
412
436
  };
437
+ id: string;
438
+ title: string;
439
+ goal: string;
440
+ can_run_in_parallel: boolean;
441
+ depends_on: string[];
442
+ workspace_name_hint: string;
443
+ spawn_strategy: "new_workspace" | "reuse_workspace";
413
444
  handoff_contract: string[];
414
445
  completion_criteria: string[];
415
446
  review_focus: string[];
@@ -445,6 +476,7 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
445
476
  iteration_count: z.ZodNumber;
446
477
  iteration_limit: z.ZodNumber;
447
478
  iteration_limit_reached: z.ZodBoolean;
479
+ cost_warning: z.ZodNullable<z.ZodOptional<z.ZodString>>;
448
480
  } & {
449
481
  token_usage: z.ZodObject<{
450
482
  input_tokens: z.ZodNumber;
@@ -501,13 +533,6 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
501
533
  handoff_artifact_pattern: string;
502
534
  subtask_result_schema_version: "1.0";
503
535
  subtasks: {
504
- id: string;
505
- title: string;
506
- goal: string;
507
- can_run_in_parallel: boolean;
508
- depends_on: string[];
509
- workspace_name_hint: string;
510
- spawn_strategy: "new_workspace" | "reuse_workspace";
511
536
  scope: {
512
537
  changed_files?: string[] | undefined;
513
538
  working_directories?: string[] | undefined;
@@ -519,6 +544,13 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
519
544
  priority: "high" | "medium" | "low";
520
545
  }[] | undefined;
521
546
  };
547
+ id: string;
548
+ title: string;
549
+ goal: string;
550
+ can_run_in_parallel: boolean;
551
+ depends_on: string[];
552
+ workspace_name_hint: string;
553
+ spawn_strategy: "new_workspace" | "reuse_workspace";
522
554
  handoff_contract: string[];
523
555
  completion_criteria: string[];
524
556
  review_focus: string[];
@@ -534,6 +566,7 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
534
566
  on_blocker: "escalate_to_human";
535
567
  on_max_retries_exceeded: "abort_subtask_and_report";
536
568
  };
569
+ cost_warning?: string | null | undefined;
537
570
  }, {
538
571
  iteration_count: number;
539
572
  iteration_limit: number;
@@ -558,13 +591,6 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
558
591
  handoff_artifact_pattern: string;
559
592
  subtask_result_schema_version: "1.0";
560
593
  subtasks: {
561
- id: string;
562
- title: string;
563
- goal: string;
564
- can_run_in_parallel: boolean;
565
- depends_on: string[];
566
- workspace_name_hint: string;
567
- spawn_strategy: "new_workspace" | "reuse_workspace";
568
594
  scope: {
569
595
  changed_files?: string[] | undefined;
570
596
  working_directories?: string[] | undefined;
@@ -576,6 +602,13 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
576
602
  priority: "high" | "medium" | "low";
577
603
  }[] | undefined;
578
604
  };
605
+ id: string;
606
+ title: string;
607
+ goal: string;
608
+ can_run_in_parallel: boolean;
609
+ depends_on: string[];
610
+ workspace_name_hint: string;
611
+ spawn_strategy: "new_workspace" | "reuse_workspace";
579
612
  handoff_contract: string[];
580
613
  completion_criteria: string[];
581
614
  review_focus: string[];
@@ -591,6 +624,7 @@ export declare const ExecutionPartitionMcpOutputSchema: z.ZodObject<{
591
624
  on_blocker: "escalate_to_human";
592
625
  on_max_retries_exceeded: "abort_subtask_and_report";
593
626
  };
627
+ cost_warning?: string | null | undefined;
594
628
  }>;
595
629
  export type ExecutionPartitionInput = z.infer<typeof ExecutionPartitionInputSchema>;
596
630
  export type ExecutionPartitionOutput = z.infer<typeof ExecutionPartitionOutputSchema>;
@@ -3,11 +3,21 @@ import { ArtifactRefSchema, ReviewerConfigSchema, IterationMetaOutputSchema, Tok
3
3
  export const ExecutionPartitionInputSchema = z.object({
4
4
  approved_plan: z
5
5
  .string()
6
- .min(1, 'approved_plan must not be empty')
7
- .describe('The previously approved plan to partition into execution units'),
6
+ .optional()
7
+ .describe('Full text of the approved plan to partition into subtasks. REQUIRED unless approved_plan_file is provided. ' +
8
+ 'Pass the entire approved plan markdown so the partitioner can analyze dependencies and split work. ' +
9
+ 'If the plan is large, prefer approved_plan_file — inlining a very large string here can make the tool call fail to serialize.'),
10
+ approved_plan_file: z
11
+ .string()
12
+ .optional()
13
+ .describe('Relative path (within workspace_root) to a markdown file containing the approved plan, ' +
14
+ 'e.g. ".duul/plan.md". Use this instead of inlining `approved_plan` when it is large. ' +
15
+ 'Exactly one of `approved_plan` or `approved_plan_file` is required. Must be a relative path.'),
8
16
  workspace_root: z
9
17
  .string()
10
- .describe('Absolute path to the workspace root directory'),
18
+ .optional()
19
+ .describe('Absolute path to the workspace root directory. REQUIRED (enforced by the handler). ' +
20
+ 'Example: "/Users/me/project". The partitioner uses this to verify file paths exist.'),
11
21
  working_directories: z
12
22
  .array(z.string())
13
23
  .optional()
@@ -31,7 +31,8 @@ export declare const ProjectContextSchema: z.ZodObject<{
31
31
  }[] | undefined;
32
32
  }>;
33
33
  export declare const PlanReviewInputSchema: z.ZodObject<{
34
- plan: z.ZodString;
34
+ plan: z.ZodOptional<z.ZodString>;
35
+ plan_file: z.ZodOptional<z.ZodString>;
35
36
  project_context: z.ZodOptional<z.ZodObject<{
36
37
  file_tree: z.ZodOptional<z.ZodString>;
37
38
  changed_files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -99,30 +100,51 @@ export declare const PlanReviewInputSchema: z.ZodObject<{
99
100
  max_review_iterations: z.ZodOptional<z.ZodNumber>;
100
101
  reviewer_config: z.ZodOptional<z.ZodObject<{
101
102
  provider: z.ZodOptional<z.ZodEnum<["openai", "anthropic", "google", "openrouter", "compatible"]>>;
102
- model: z.ZodOptional<z.ZodString>;
103
+ model: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
104
+ plan: z.ZodOptional<z.ZodString>;
105
+ code: z.ZodOptional<z.ZodString>;
106
+ partition: z.ZodOptional<z.ZodString>;
107
+ }, "strip", z.ZodTypeAny, {
108
+ code?: string | undefined;
109
+ plan?: string | undefined;
110
+ partition?: string | undefined;
111
+ }, {
112
+ code?: string | undefined;
113
+ plan?: string | undefined;
114
+ partition?: string | undefined;
115
+ }>]>>;
103
116
  base_url: z.ZodOptional<z.ZodString>;
104
117
  api_key: z.ZodOptional<z.ZodString>;
105
118
  temperature: z.ZodOptional<z.ZodNumber>;
106
119
  top_p: z.ZodOptional<z.ZodNumber>;
107
120
  }, "strip", z.ZodTypeAny, {
108
121
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
109
- model?: string | undefined;
122
+ model?: string | {
123
+ code?: string | undefined;
124
+ plan?: string | undefined;
125
+ partition?: string | undefined;
126
+ } | undefined;
110
127
  base_url?: string | undefined;
111
128
  api_key?: string | undefined;
112
129
  temperature?: number | undefined;
113
130
  top_p?: number | undefined;
114
131
  }, {
115
132
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
116
- model?: string | undefined;
133
+ model?: string | {
134
+ code?: string | undefined;
135
+ plan?: string | undefined;
136
+ partition?: string | undefined;
137
+ } | undefined;
117
138
  base_url?: string | undefined;
118
139
  api_key?: string | undefined;
119
140
  temperature?: number | undefined;
120
141
  top_p?: number | undefined;
121
142
  }>>;
122
143
  }, "strip", z.ZodTypeAny, {
123
- plan: string;
144
+ plan?: string | undefined;
124
145
  iteration_count?: number | undefined;
125
146
  changed_files?: string[] | undefined;
147
+ plan_file?: string | undefined;
126
148
  project_context?: {
127
149
  file_tree?: string | undefined;
128
150
  changed_files?: string[] | undefined;
@@ -158,16 +180,21 @@ export declare const PlanReviewInputSchema: z.ZodObject<{
158
180
  max_review_iterations?: number | undefined;
159
181
  reviewer_config?: {
160
182
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
161
- model?: string | undefined;
183
+ model?: string | {
184
+ code?: string | undefined;
185
+ plan?: string | undefined;
186
+ partition?: string | undefined;
187
+ } | undefined;
162
188
  base_url?: string | undefined;
163
189
  api_key?: string | undefined;
164
190
  temperature?: number | undefined;
165
191
  top_p?: number | undefined;
166
192
  } | undefined;
167
193
  }, {
168
- plan: string;
194
+ plan?: string | undefined;
169
195
  iteration_count?: number | undefined;
170
196
  changed_files?: string[] | undefined;
197
+ plan_file?: string | undefined;
171
198
  project_context?: {
172
199
  file_tree?: string | undefined;
173
200
  changed_files?: string[] | undefined;
@@ -203,7 +230,11 @@ export declare const PlanReviewInputSchema: z.ZodObject<{
203
230
  max_review_iterations?: number | undefined;
204
231
  reviewer_config?: {
205
232
  provider?: "openai" | "anthropic" | "google" | "openrouter" | "compatible" | undefined;
206
- model?: string | undefined;
233
+ model?: string | {
234
+ code?: string | undefined;
235
+ plan?: string | undefined;
236
+ partition?: string | undefined;
237
+ } | undefined;
207
238
  base_url?: string | undefined;
208
239
  api_key?: string | undefined;
209
240
  temperature?: number | undefined;
@@ -389,6 +420,7 @@ export declare const PlanReviewMcpOutputSchema: z.ZodObject<{
389
420
  iteration_count: z.ZodNumber;
390
421
  iteration_limit: z.ZodNumber;
391
422
  iteration_limit_reached: z.ZodBoolean;
423
+ cost_warning: z.ZodNullable<z.ZodOptional<z.ZodString>>;
392
424
  } & {
393
425
  token_usage: z.ZodObject<{
394
426
  input_tokens: z.ZodNumber;
@@ -469,6 +501,7 @@ export declare const PlanReviewMcpOutputSchema: z.ZodObject<{
469
501
  symptom_match_notes: string | null;
470
502
  gates_tripped: string[] | null;
471
503
  review_id: string;
504
+ cost_warning?: string | null | undefined;
472
505
  }, {
473
506
  iteration_count: number;
474
507
  iteration_limit: number;
@@ -517,6 +550,7 @@ export declare const PlanReviewMcpOutputSchema: z.ZodObject<{
517
550
  symptom_match_notes: string | null;
518
551
  gates_tripped: string[] | null;
519
552
  review_id: string;
553
+ cost_warning?: string | null | undefined;
520
554
  }>;
521
555
  export type PlanReviewInput = z.infer<typeof PlanReviewInputSchema>;
522
556
  export type PlanReviewOutput = z.infer<typeof PlanReviewOutputSchema>;
@@ -25,7 +25,21 @@ export const ProjectContextSchema = z.object({
25
25
  'that are relevant to the plan but not part of the change itself.'),
26
26
  });
27
27
  export const PlanReviewInputSchema = z.object({
28
- plan: z.string().min(1, 'plan must not be empty').describe('Detailed implementation plan'),
28
+ plan: z
29
+ .string()
30
+ .optional()
31
+ .describe('Full implementation plan text (markdown). REQUIRED unless plan_file is provided. ' +
32
+ 'Include: problem statement (quote user request), files to create/modify with paths, ' +
33
+ 'approach, edge cases, dependencies. ' +
34
+ 'If the plan is large, prefer writing it to a file and passing plan_file instead — ' +
35
+ 'inlining a very large plan string here can make the tool call fail to serialize.'),
36
+ plan_file: z
37
+ .string()
38
+ .optional()
39
+ .describe('Relative path (within workspace_root) to a markdown file containing the full plan, ' +
40
+ 'e.g. ".duul/plan.md". Use this instead of inlining `plan` when the plan is large: ' +
41
+ 'write the plan to the file first, then pass its path here. ' +
42
+ 'Exactly one of `plan` or `plan_file` is required. Requires workspace_root. Must be a relative path.'),
29
43
  project_context: ProjectContextSchema.optional().describe('Structured project context'),
30
44
  constraints: z
31
45
  .array(z.string())
@@ -3,4 +3,22 @@
3
3
  * Handles all 8 standard tools + get_git_diff.
4
4
  */
5
5
  import { type WorkspaceScope } from './filesystem.js';
6
- export declare function executeFilesystemTool(projectRoot: string, toolName: string, args: Record<string, unknown>, scope?: WorkspaceScope | null): Promise<string>;
6
+ /**
7
+ * Mutable per-review byte counter. Passed by reference into executeFilesystemTool
8
+ * so every successful tool return adds to `used`, and calls short-circuit once
9
+ * `used >= cap`.
10
+ */
11
+ export interface ReviewerByteBudget {
12
+ used: number;
13
+ cap: number;
14
+ }
15
+ /**
16
+ * Resolve the reviewer file-read cap from env. Opt-in: if DUUL_MAX_REVIEWER_BYTES
17
+ * is unset/invalid, returns Infinity (no cap). Measurements showed a 200KB default
18
+ * was too tight — ~1/3 of code reviews hit the cap and spent extra rounds.
19
+ * Cost-conscious setups can opt in explicitly; 200000–500000 is a reasonable
20
+ * starting range, tune based on typical review complexity (see README).
21
+ */
22
+ export declare function getMaxReviewerBytes(): number;
23
+ export declare function createReviewerByteBudget(cap?: number): ReviewerByteBudget;
24
+ export declare function executeFilesystemTool(projectRoot: string, toolName: string, args: Record<string, unknown>, scope?: WorkspaceScope | null, budget?: ReviewerByteBudget): Promise<string>;
@@ -3,35 +3,72 @@
3
3
  * Handles all 8 standard tools + get_git_diff.
4
4
  */
5
5
  import { readProjectFile, listProjectDirectory, searchInFiles, readProjectFileRange, statProjectFile, readJsonValue, listTrackedFiles, getGitDiff, } from './filesystem.js';
6
- export async function executeFilesystemTool(projectRoot, toolName, args, scope) {
6
+ /**
7
+ * Resolve the reviewer file-read cap from env. Opt-in: if DUUL_MAX_REVIEWER_BYTES
8
+ * is unset/invalid, returns Infinity (no cap). Measurements showed a 200KB default
9
+ * was too tight — ~1/3 of code reviews hit the cap and spent extra rounds.
10
+ * Cost-conscious setups can opt in explicitly; 200000–500000 is a reasonable
11
+ * starting range, tune based on typical review complexity (see README).
12
+ */
13
+ export function getMaxReviewerBytes() {
14
+ const raw = process.env.DUUL_MAX_REVIEWER_BYTES;
15
+ if (!raw)
16
+ return Infinity;
17
+ const parsed = parseInt(raw, 10);
18
+ if (isNaN(parsed) || parsed <= 0)
19
+ return Infinity;
20
+ return parsed;
21
+ }
22
+ export function createReviewerByteBudget(cap) {
23
+ return { used: 0, cap: cap ?? getMaxReviewerBytes() };
24
+ }
25
+ function budgetExhaustedMessage(budget) {
26
+ return `Reviewer file budget exhausted (used ${budget.used} / cap ${budget.cap} bytes). Rely on context already gathered. Do NOT request more files — submit your verdict.`;
27
+ }
28
+ export async function executeFilesystemTool(projectRoot, toolName, args, scope, budget) {
29
+ if (budget && budget.used >= budget.cap) {
30
+ return budgetExhaustedMessage(budget);
31
+ }
7
32
  try {
33
+ let result;
8
34
  switch (toolName) {
9
35
  case 'read_file': {
10
- const result = await readProjectFile(projectRoot, args.path, scope);
11
- if (result.length > 50_000) {
12
- return `\u26a0\ufe0f This file is large (${result.length} chars). Consider using read_file_range or search_in_files instead.\n\n${result}`;
13
- }
14
- return result;
36
+ const content = await readProjectFile(projectRoot, args.path, scope);
37
+ result = content.length > 50_000
38
+ ? `\u26a0\ufe0f This file is large (${content.length} chars). Consider using read_file_range or search_in_files instead.\n\n${content}`
39
+ : content;
40
+ break;
15
41
  }
16
42
  case 'list_directory':
17
- return await listProjectDirectory(projectRoot, args.path, scope);
43
+ result = await listProjectDirectory(projectRoot, args.path, scope);
44
+ break;
18
45
  case 'search_in_files':
19
- return await searchInFiles(projectRoot, args.query, args.paths, args.glob, scope?.trackedOnly, scope?.workingDirectories, scope);
46
+ result = await searchInFiles(projectRoot, args.query, args.paths, args.glob, scope?.trackedOnly, scope?.workingDirectories, scope);
47
+ break;
20
48
  case 'read_file_range':
21
- return await readProjectFileRange(projectRoot, args.path, args.start_line, args.end_line, scope);
49
+ result = await readProjectFileRange(projectRoot, args.path, args.start_line, args.end_line, scope);
50
+ break;
22
51
  case 'stat_file':
23
- return await statProjectFile(projectRoot, args.path, scope);
52
+ result = await statProjectFile(projectRoot, args.path, scope);
53
+ break;
24
54
  case 'read_json':
25
- return await readJsonValue(projectRoot, args.path, args.json_pointer, scope);
55
+ result = await readJsonValue(projectRoot, args.path, args.json_pointer, scope);
56
+ break;
26
57
  case 'list_tracked_files': {
27
58
  const files = await listTrackedFiles(projectRoot, args.prefix, scope);
28
- return files.join('\n') || 'No tracked files found.';
59
+ result = files.join('\n') || 'No tracked files found.';
60
+ break;
29
61
  }
30
62
  case 'get_git_diff':
31
- return await getGitDiff(projectRoot, args.base, args.paths, scope);
63
+ result = await getGitDiff(projectRoot, args.base, args.paths, scope);
64
+ break;
32
65
  default:
33
66
  return `Unknown tool: ${toolName}`;
34
67
  }
68
+ if (budget && Number.isFinite(budget.cap)) {
69
+ budget.used += Buffer.byteLength(result, 'utf8');
70
+ }
71
+ return result;
35
72
  }
36
73
  catch (error) {
37
74
  return `Error: ${error instanceof Error ? error.message : String(error)}`;