@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,175 @@
1
+ import { z } from 'zod';
2
+ import { ArtifactRefSchema, ReviewerConfigSchema, IterationMetaOutputSchema, TokenUsageOutputSchema } from './common.js';
3
+ export const ProjectContextSchema = z.object({
4
+ file_tree: z
5
+ .string()
6
+ .max(2000, 'file_tree must be at most 2000 characters')
7
+ .optional()
8
+ .describe('Project file tree summary (top-level dirs + changed files only, max 2000 chars)'),
9
+ changed_files: z
10
+ .array(z.string())
11
+ .optional()
12
+ .describe('List of files related to this change'),
13
+ package_versions: z
14
+ .record(z.string(), z.string())
15
+ .optional()
16
+ .describe('Key package versions, e.g. { "express": "4.18.2" }'),
17
+ relevant_code: z
18
+ .array(z.object({
19
+ file_path: z.string().describe('File path'),
20
+ code: z.string().describe('Relevant code snippet (types, interfaces, functions, classes)'),
21
+ }))
22
+ .optional()
23
+ .describe('Related code snippets the reviewer should see for full context. ' +
24
+ 'Include type definitions, interfaces, data models, and surrounding code ' +
25
+ 'that are relevant to the plan but not part of the change itself.'),
26
+ });
27
+ export const PlanReviewInputSchema = z.object({
28
+ plan: z.string().min(1, 'plan must not be empty').describe('Detailed implementation plan'),
29
+ project_context: ProjectContextSchema.optional().describe('Structured project context'),
30
+ constraints: z
31
+ .array(z.string())
32
+ .optional()
33
+ .describe('Special constraints: performance, memory, security, etc.'),
34
+ notes_to_reviewer: z
35
+ .string()
36
+ .optional()
37
+ .describe('Context or rebuttals from the caller to the reviewer. ' +
38
+ 'Use this to explain codebase-specific facts the reviewer cannot see, ' +
39
+ 'or to respond to blocking issues from a previous round.'),
40
+ user_original_request: z
41
+ .string()
42
+ .max(4000)
43
+ .optional()
44
+ .describe("The user's original, unedited problem statement (not paraphrased by the caller). " +
45
+ 'Used by the reviewer to verify the plan actually addresses the reported symptom.'),
46
+ // --- Workspace-aware scope fields ---
47
+ workspace_root: z
48
+ .string()
49
+ .optional()
50
+ .describe('Absolute path to the workspace root directory. Preferred over project_root. ' +
51
+ 'When provided, the reviewer gains file exploration tools scoped to this workspace.'),
52
+ project_root: z
53
+ .string()
54
+ .optional()
55
+ .describe('[DEPRECATED — use workspace_root] Absolute path to the project root directory. ' +
56
+ 'Used as fallback when workspace_root is not provided.'),
57
+ working_directories: z
58
+ .array(z.string())
59
+ .optional()
60
+ .describe('Subdirectories within workspace_root to restrict file access to. ' +
61
+ 'Acts as a sparse-checkout-like allowlist. If omitted, entire workspace_root is accessible.'),
62
+ linked_roots: z
63
+ .array(z.string())
64
+ .max(5)
65
+ .optional()
66
+ .describe('Additional read-only workspace roots the reviewer can access. ' +
67
+ 'Each must be a valid absolute path (3+ depth). Max 5.'),
68
+ changed_files: z
69
+ .array(z.string())
70
+ .optional()
71
+ .describe('Files changed in this review scope (top-level, separate from project_context)'),
72
+ entrypoints: z
73
+ .array(z.string())
74
+ .optional()
75
+ .describe('Entry point files the reviewer should start from'),
76
+ artifact_refs: z
77
+ .array(ArtifactRefSchema)
78
+ .max(30)
79
+ .optional()
80
+ .describe('References to important files with reason and priority. Max 30.'),
81
+ tracked_only: z
82
+ .boolean()
83
+ .optional()
84
+ .describe('When true, only git-tracked files are accessible to the reviewer.'),
85
+ // --- Git metadata ---
86
+ git_head_sha: z.string().optional().describe('Current git HEAD SHA for this review'),
87
+ previous_git_head_sha: z.string().optional().describe('Git HEAD SHA from the previous review round. Used with git_head_sha to detect stale context.'),
88
+ workspace_name: z.string().optional().describe('Name of the workspace (for logging/identification)'),
89
+ // --- Setup/Run metadata ---
90
+ setup_script_present: z.boolean().optional().describe('Whether a setup script exists in the workspace'),
91
+ run_script_present: z.boolean().optional().describe('Whether a run/start script exists in the workspace'),
92
+ environment_files_expected: z.array(z.string()).optional().describe('Environment files expected but not tracked (e.g. [".env", ".env.local"]). Prevents false positives.'),
93
+ // --- Git diff ---
94
+ git_diff: z
95
+ .string()
96
+ .optional()
97
+ .describe('Pre-computed git diff to include in review context. If omitted and workspace_root + changed_files are provided, ' +
98
+ 'the diff is auto-generated.'),
99
+ git_diff_base: z
100
+ .string()
101
+ .optional()
102
+ .describe('Base ref for auto-generated git diff (e.g. "HEAD", "main"). Default: "HEAD".'),
103
+ previous_review_id: z
104
+ .string()
105
+ .optional()
106
+ .describe('Response ID from a previous review call. Pass this to maintain reviewer context ' +
107
+ 'across rounds — the reviewer will remember all files it read, previous feedback, ' +
108
+ 'and the full conversation history.'),
109
+ // --- Iteration tracking ---
110
+ iteration_count: z
111
+ .number()
112
+ .min(1)
113
+ .optional()
114
+ .describe('Current iteration number (1-based). The caller MUST increment this on each call. ' +
115
+ 'When this reaches the iteration limit, the server returns requires_human_review: true.'),
116
+ max_review_iterations: z
117
+ .number()
118
+ .min(1)
119
+ .max(20)
120
+ .optional()
121
+ .describe('Override the maximum iterations for this phase. Default: env MAX_PLAN_REVIEW_ITERATIONS or 7.'),
122
+ // --- Reviewer config ---
123
+ reviewer_config: ReviewerConfigSchema.optional().describe('Per-request reviewer configuration. Overrides env defaults.'),
124
+ });
125
+ const BlockingIssueSchema = z.object({
126
+ description: z.string().describe('What the issue is'),
127
+ suggestion: z.string().describe('How to fix it'),
128
+ });
129
+ export const PlanReviewOutputSchema = z.object({
130
+ verdict: z.enum(['APPROVE', 'REVISE']).describe('Final verdict'),
131
+ review_status: z.enum(['completed', 'incomplete']).describe('Whether the review was fully completed. "incomplete" means the tool loop was exhausted before the reviewer could finish.'),
132
+ confidence: z.number().min(0).max(1).describe('Confidence in the verdict (0-1), advisory only'),
133
+ requires_human_review: z.boolean().describe('Whether a human should review this'),
134
+ architectural_analysis: z.string().describe('Structural pros/cons analysis'),
135
+ blocking_issues: z
136
+ .array(BlockingIssueSchema)
137
+ .describe('Issues that must be fixed before proceeding'),
138
+ merge_blockers: z
139
+ .array(BlockingIssueSchema)
140
+ .nullable()
141
+ .describe('Subset of blocking_issues that should block merge. Null if same as blocking_issues.'),
142
+ non_blocking_suggestions: z
143
+ .array(z.string())
144
+ .describe('Optional improvement suggestions'),
145
+ edge_cases: z.array(z.string()).describe('Unconsidered edge cases'),
146
+ checklist_for_implementation: z
147
+ .array(z.string())
148
+ .describe('Must-follow checklist for implementation'),
149
+ follow_up_todos: z.array(z.string()).nullable().describe('Follow-up tasks after implementation'),
150
+ missing_context: z.array(z.string()).nullable().describe('Files or context the reviewer could not access'),
151
+ evidence_files: z.array(z.string()).nullable().describe('Files the reviewer examined as evidence'),
152
+ used_tools: z.array(z.string()).nullable().describe('Tool calls made during review'),
153
+ tool_exhaustion_reason: z.enum(['budget', 'repeat', 'round_limit']).nullable().describe('If review_status is incomplete, the reason why the tool loop was exhausted'),
154
+ parallelization_hint: z.enum(['serial', 'parallel', 'hybrid']).nullable().describe('Hint from reviewer: can this plan be parallelized?'),
155
+ coordination_risks: z.array(z.string()).nullable().describe('Coordination risks if parallelized'),
156
+ recommended_subtask_boundaries: z.array(z.string()).nullable().describe('Suggested subtask split boundaries'),
157
+ user_original_request_echo: z.string().nullable().describe('Verbatim echo of user_original_request so the reviewer commits to what it was asked to solve. Null only if the caller omitted user_original_request.'),
158
+ symptom_impact: z
159
+ .object({
160
+ before: z.string().describe('Observable symptom the user reported, in their own terms.'),
161
+ after: z.string().describe('What the user will observe after this plan is implemented.'),
162
+ causal_chain: z.string().describe("Why the planned change causes 'before' → 'after'."),
163
+ })
164
+ .nullable()
165
+ .describe('How the plan is expected to change the user-visible symptom. Null only if user_original_request was not supplied.'),
166
+ symptom_match_notes: z.string().nullable().describe('If plan does NOT clearly address the reported symptom, explain the gap here. Null if fully addressed.'),
167
+ gates_tripped: z.array(z.string()).nullable().describe('Server-populated list of post-LLM gate names that fired. Reviewer should leave null.'),
168
+ });
169
+ // Extended output with server-added fields (not sent to the reviewer model, used for MCP response)
170
+ export const PlanReviewMcpOutputSchema = PlanReviewOutputSchema
171
+ .extend({
172
+ review_id: z.string().describe('Response ID for maintaining reviewer context across rounds. Pass as previous_review_id on the next call.'),
173
+ })
174
+ .merge(IterationMetaOutputSchema)
175
+ .merge(TokenUsageOutputSchema);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared filesystem tool executor used by all provider implementations.
3
+ * Handles all 8 standard tools + get_git_diff.
4
+ */
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>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared filesystem tool executor used by all provider implementations.
3
+ * Handles all 8 standard tools + get_git_diff.
4
+ */
5
+ import { readProjectFile, listProjectDirectory, searchInFiles, readProjectFileRange, statProjectFile, readJsonValue, listTrackedFiles, getGitDiff, } from './filesystem.js';
6
+ export async function executeFilesystemTool(projectRoot, toolName, args, scope) {
7
+ try {
8
+ switch (toolName) {
9
+ 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;
15
+ }
16
+ case 'list_directory':
17
+ return await listProjectDirectory(projectRoot, args.path, scope);
18
+ case 'search_in_files':
19
+ return await searchInFiles(projectRoot, args.query, args.paths, args.glob, scope?.trackedOnly, scope?.workingDirectories, scope);
20
+ case 'read_file_range':
21
+ return await readProjectFileRange(projectRoot, args.path, args.start_line, args.end_line, scope);
22
+ case 'stat_file':
23
+ return await statProjectFile(projectRoot, args.path, scope);
24
+ case 'read_json':
25
+ return await readJsonValue(projectRoot, args.path, args.json_pointer, scope);
26
+ case 'list_tracked_files': {
27
+ const files = await listTrackedFiles(projectRoot, args.prefix, scope);
28
+ return files.join('\n') || 'No tracked files found.';
29
+ }
30
+ case 'get_git_diff':
31
+ return await getGitDiff(projectRoot, args.base, args.paths, scope);
32
+ default:
33
+ return `Unknown tool: ${toolName}`;
34
+ }
35
+ }
36
+ catch (error) {
37
+ return `Error: ${error instanceof Error ? error.message : String(error)}`;
38
+ }
39
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Validates that project_root is a reasonable absolute path,
3
+ * not the filesystem root or a system directory.
4
+ */
5
+ export declare function validateProjectRoot(projectRoot: string): void;
6
+ /**
7
+ * List git-tracked files under a given prefix.
8
+ * Respects working_directories and supports linked roots via "linked:<index>:<prefix>" syntax.
9
+ */
10
+ export declare function listTrackedFiles(root: string, prefix?: string, scope?: WorkspaceScope | null): Promise<string[]>;
11
+ /**
12
+ * Check if a file is git-tracked.
13
+ */
14
+ export declare function isTrackedFile(root: string, filePath: string): Promise<boolean>;
15
+ /**
16
+ * Workspace scope resolution result.
17
+ */
18
+ export interface WorkspaceScope {
19
+ root: string;
20
+ workingDirectories: string[] | null;
21
+ linkedRoots: string[];
22
+ trackedOnly: boolean;
23
+ }
24
+ /**
25
+ * Resolves workspace scope from input fields following precedence rules:
26
+ * 1. workspace_root (preferred)
27
+ * 2. project_root (deprecated fallback)
28
+ * 3. working_directories (allowlist intersection)
29
+ * 4. linked_roots (separate read-only scopes)
30
+ */
31
+ export declare function resolveWorkspaceScope(input: {
32
+ workspace_root?: string;
33
+ project_root?: string;
34
+ working_directories?: string[];
35
+ linked_roots?: string[];
36
+ tracked_only?: boolean;
37
+ }): WorkspaceScope | null;
38
+ export declare function readProjectFile(projectRoot: string, filePath: string, scope?: WorkspaceScope | null): Promise<string>;
39
+ export declare function listProjectDirectory(projectRoot: string, dirPath: string, scope?: WorkspaceScope | null): Promise<string>;
40
+ /**
41
+ * Search for a pattern in files using rg (preferred), git grep, or grep fallback.
42
+ * Supports searching across primary root and linked roots.
43
+ */
44
+ export declare function searchInFiles(root: string, query: string, paths?: string[], glob?: string, trackedOnly?: boolean, workingDirectories?: string[] | null, scope?: WorkspaceScope | null): Promise<string>;
45
+ /**
46
+ * Read a specific line range from a file.
47
+ */
48
+ export declare function readProjectFileRange(root: string, filePath: string, startLine: number, endLine: number, scope?: WorkspaceScope | null): Promise<string>;
49
+ /**
50
+ * Get file metadata (size, type, modified time).
51
+ */
52
+ export declare function statProjectFile(root: string, filePath: string, scope?: WorkspaceScope | null): Promise<string>;
53
+ /**
54
+ * Read a JSON file, optionally extracting a value at a JSON pointer path.
55
+ * Pointer format: "/key/subkey/0" (RFC 6901 simplified).
56
+ */
57
+ export declare function readJsonValue(root: string, filePath: string, pointer?: string, scope?: WorkspaceScope | null): Promise<string>;
58
+ /**
59
+ * Run git diff within the workspace scope.
60
+ * Returns the diff output, capped at MAX_GIT_DIFF_BYTES.
61
+ *
62
+ * Defaults to `HEAD` (staged + unstaged vs last commit) rather than `HEAD~1`,
63
+ * so the reviewer sees only the current workspace changes.
64
+ *
65
+ * For untracked (new) files listed in `paths`, appends a synthetic diff
66
+ * generated via `git diff --no-index /dev/null <file>`, so newly added files
67
+ * are visible to the reviewer.
68
+ */
69
+ export declare function getGitDiff(root: string, base?: string | null, paths?: string[] | null, scope?: WorkspaceScope | null): Promise<string>;