iterate-plugin 2.3.6

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.
@@ -0,0 +1,174 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from '@deepseek-ai/dsh-session'
3
+ import { loadEffectiveConfig } from '../config-loader.ts'
4
+ import { buildReviewPlan, buildReviewReport } from '../review.ts'
5
+ import { buildFinalReviewReport, metaReviewReport } from '../meta-review.ts'
6
+ import type { KnownIntentional, ReviewFinding, ReviewReport, ReviewRound } from '../types.ts'
7
+
8
+ /** Default round cap when neither the arg nor config provides one. */
9
+ const DEFAULT_MAX_REVIEW_ROUNDS = 3
10
+
11
+ /**
12
+ * Register the `iterate_review` tool.
13
+ *
14
+ * Two operations:
15
+ * - `plan`: deterministic review plan for a mode (normal | dry-run).
16
+ * Returns the goal, scope, per-dimension reviewer prompts,
17
+ * the findings schema, and the max round cap. The orchestrator
18
+ * uses this instead of inventing prompts ad hoc.
19
+ * - `aggregate`: deterministic aggregation of raw per-round findings.
20
+ * Applies known_intentional filtering, cross-round dedupe,
21
+ * severity sort, and convergence stats; returns a ReviewReport.
22
+ * Purely computational — NEVER touches the filesystem.
23
+ */
24
+ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
25
+ ctx.tools.register(
26
+ defineTool({
27
+ name: 'iterate_review',
28
+ description:
29
+ 'Deterministic review engine for the iterate workflow. ' +
30
+ 'Use `plan` to generate the review plan (dimensions, reviewer prompts, findings schema, round cap) ' +
31
+ 'for normal or dry-run mode. Use `aggregate` to merge raw per-round findings into a deduped, ' +
32
+ 'severity-sorted report with multi-round convergence statistics, and to audit that report ' +
33
+ '(`meta-review`) producing a final review report. ' +
34
+ '`aggregate`/`meta-review` are purely computational — they never modify any file.',
35
+
36
+ parameters: {
37
+ operation: {
38
+ type: 'string',
39
+ required: true,
40
+ description: '"plan" to build the review plan, "aggregate" to merge findings, "meta-review" to audit a report.',
41
+ enum: ['plan', 'aggregate', 'meta-review'],
42
+ },
43
+ mode: {
44
+ type: 'string',
45
+ description: 'Review mode: "dry-run" (pure review, no fixes) or "normal" (autonomous loop). Default: dry-run.',
46
+ enum: ['dry-run', 'normal'],
47
+ },
48
+ rounds: {
49
+ type: 'json',
50
+ description:
51
+ 'For `aggregate`: array of per-round findings, e.g. ' +
52
+ '[{"round":1,"findings":[...]},{"round":2,"findings":[...]}]. Each finding: ' +
53
+ '{dimension,file,line?,severity,summary,failure_scenario,suggested_fix,is_atomic}.',
54
+ },
55
+ maxReviewRounds: {
56
+ type: 'integer',
57
+ description: 'Round cap for dry-run convergence. Default: config.max_rounds, else 3.',
58
+ },
59
+ goal: {
60
+ type: 'string',
61
+ description: 'Optional goal override for `aggregate` (defaults to config goal).',
62
+ },
63
+ knownIntentional: {
64
+ type: 'json',
65
+ description:
66
+ 'For `aggregate`: known-intentional entries to filter out, e.g. ' +
67
+ '[{"file":"db/queries.py","line":42,"dimension":"security","reason":"..."}]. line=0/omitted = whole file.',
68
+ },
69
+ report: {
70
+ type: 'json',
71
+ description:
72
+ 'For `meta-review`: the ReviewReport JSON (as returned by `aggregate`) to audit for ' +
73
+ 'internal consistency and produce the final review report.',
74
+ },
75
+ path: {
76
+ type: 'string',
77
+ description: 'Project root directory (default: current working directory).',
78
+ },
79
+ },
80
+
81
+ output: {
82
+ schema: {
83
+ type: 'object',
84
+ additionalProperties: false,
85
+ properties: {
86
+ operation: { type: 'string', required: true },
87
+ mode: { type: 'string' },
88
+ found: { type: 'boolean' },
89
+ plan: { type: 'json' },
90
+ report: { type: 'json' },
91
+ finalReport: { type: 'json' },
92
+ error: { type: 'string' },
93
+ },
94
+ },
95
+ render: (_args, value) => [
96
+ { type: 'text', text: JSON.stringify(value, null, 2) },
97
+ ],
98
+ },
99
+
100
+ async execute(args) {
101
+ const projectRoot = args.path ?? process.cwd()
102
+ // Effective config = defaults merged with project overrides. Never
103
+ // null, so `plan`/`aggregate` work even without a config file.
104
+ const { config } = loadEffectiveConfig(projectRoot)
105
+ const mode = args.mode ?? 'dry-run'
106
+
107
+ if (args.operation === 'plan') {
108
+ const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS
109
+ const knownIntentional = (config.personalization as { known_intentional?: KnownIntentional[] } | undefined)
110
+ ?.known_intentional
111
+ const plan = buildReviewPlan({ config, mode, maxReviewRounds, knownIntentional })
112
+ return { operation: 'plan', mode, found: true, plan: plan as unknown as JsonValue }
113
+ }
114
+
115
+ if (args.operation === 'aggregate') {
116
+ const rawRounds = Array.isArray(args.rounds) ? args.rounds : []
117
+ const rounds: ReviewRound[] = rawRounds
118
+ .map((r) => {
119
+ const rr = r as { round?: number; findings?: unknown }
120
+ const findings = Array.isArray(rr?.findings) ? (rr.findings as ReviewFinding[]) : []
121
+ return { round: typeof rr?.round === 'number' ? rr.round : 0, findings }
122
+ })
123
+ .filter((r: ReviewRound) => r.round > 0)
124
+
125
+ if (rounds.length === 0) {
126
+ return {
127
+ operation: 'aggregate',
128
+ mode,
129
+ error: 'rounds must be a non-empty array of {round, findings}.',
130
+ }
131
+ }
132
+
133
+ const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS
134
+ const goal = args.goal ?? config.goal ?? ''
135
+ const dimensions = config.dimensions ?? []
136
+ const report = buildReviewReport({
137
+ mode,
138
+ goal,
139
+ dimensions,
140
+ maxReviewRounds,
141
+ rounds,
142
+ knownIntentional: args.knownIntentional as KnownIntentional[] | undefined,
143
+ })
144
+ return { operation: 'aggregate', mode, report: report as unknown as JsonValue }
145
+ }
146
+
147
+ if (args.operation === 'meta-review') {
148
+ const source = args.report as ReviewReport | undefined
149
+ if (!source || typeof source !== 'object') {
150
+ return {
151
+ operation: 'meta-review',
152
+ mode,
153
+ error: 'report must be a ReviewReport JSON object (as returned by `aggregate`).',
154
+ }
155
+ }
156
+ const audit = metaReviewReport(source)
157
+ const finalReport = buildFinalReviewReport(source)
158
+ return {
159
+ operation: 'meta-review',
160
+ mode,
161
+ found: true,
162
+ report: audit as unknown as JsonValue,
163
+ finalReport: finalReport as unknown as JsonValue,
164
+ }
165
+ }
166
+
167
+ return {
168
+ operation: args.operation,
169
+ error: `Unknown operation "${args.operation}". Use "plan", "aggregate", or "meta-review".`,
170
+ }
171
+ },
172
+ }),
173
+ )
174
+ }
@@ -0,0 +1,159 @@
1
+ import { exec } from 'node:child_process'
2
+ import { defineTool } from '@deepseek-ai/dsh-tools'
3
+ import { loadEffectiveConfig, isCommandAllowed, flattenCommands } from '../config-loader.ts'
4
+ import type { ValidationResult } from '../types.ts'
5
+
6
+ const DEFAULT_TIMEOUT_MS = 120_000
7
+
8
+ /**
9
+ * Run a single shell command with timeout and return structured results.
10
+ * Pure function (no side effects beyond the exec call).
11
+ */
12
+ async function runCommand(
13
+ command: string,
14
+ cwd: string,
15
+ timeoutMs: number,
16
+ ): Promise<ValidationResult> {
17
+ const start = performance.now()
18
+ return new Promise<ValidationResult>((resolve) => {
19
+ exec(
20
+ command,
21
+ {
22
+ cwd,
23
+ timeout: timeoutMs,
24
+ maxBuffer: 10 * 1024 * 1024, // 10 MB
25
+ env: { ...process.env, PAGER: 'cat' },
26
+ },
27
+ (error, stdout, stderr) => {
28
+ const durationMs = Math.round(performance.now() - start)
29
+ // error.code is the exit code when the command ran; error.killed means timeout
30
+ resolve({
31
+ command,
32
+ exitCode: error?.code ?? (error ? 1 : 0),
33
+ stdout: stdout ?? '',
34
+ stderr: stderr ?? '',
35
+ timedOut: error?.killed === true,
36
+ durationMs,
37
+ })
38
+ },
39
+ )
40
+ })
41
+ }
42
+
43
+ /**
44
+ * Register the `iterate_validate` tool.
45
+ * Runs validation commands defined in iterate.config.yaml `validation.commands`.
46
+ * Enforces exact-match — a command not listed there (exactly) is rejected.
47
+ */
48
+ export function registerValidateTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
49
+ ctx.tools.register(
50
+ defineTool({
51
+ name: 'iterate_validate',
52
+ description:
53
+ 'Run a validation command that is PRECONFIGURED in iterate.config.yaml `validation.commands`. ' +
54
+ 'The command must exactly match one of the configured commands (they are the only ones the user trusts). ' +
55
+ 'Returns exit code, stdout, stderr, and duration. ' +
56
+ 'Use this after making fixes to verify correctness.',
57
+
58
+ parameters: {
59
+ command: {
60
+ type: 'string',
61
+ required: true,
62
+ description: 'One of the commands listed in iterate.config.yaml validation.commands (exact match required, e.g. "pytest tests/ -x -q").',
63
+ },
64
+ path: {
65
+ type: 'string',
66
+ description: 'Project root directory (default: current working directory).',
67
+ },
68
+ timeout: {
69
+ type: 'integer',
70
+ description: 'Timeout in milliseconds (default: 120000).',
71
+ },
72
+ },
73
+
74
+ output: {
75
+ schema: {
76
+ type: 'object',
77
+ additionalProperties: false,
78
+ properties: {
79
+ allowed: { type: 'boolean', required: true },
80
+ command: { type: 'string', required: true },
81
+ exitCode: { type: 'integer', required: true },
82
+ stdout: { type: 'string', required: true },
83
+ stderr: { type: 'string', required: true },
84
+ timedOut: { type: 'boolean', required: true },
85
+ durationMs: { type: 'integer', required: true },
86
+ rejectReason: { type: 'string' },
87
+ },
88
+ },
89
+ render: (_args, value) => [
90
+ {
91
+ type: 'text',
92
+ text: value.allowed
93
+ ? [
94
+ `Command: ${value.command}`,
95
+ `Exit code: ${value.exitCode}`,
96
+ `Duration: ${value.durationMs}ms`,
97
+ value.timedOut ? '⚠ Timed out' : '',
98
+ '',
99
+ value.stdout ? `[stdout]\n${value.stdout}` : '',
100
+ value.stderr ? `[stderr]\n${value.stderr}` : '',
101
+ ]
102
+ .filter(Boolean)
103
+ .join('\n')
104
+ : `Command rejected: ${value.rejectReason}`,
105
+ },
106
+ ],
107
+ },
108
+
109
+ async execute(args) {
110
+ const projectRoot = args.path ?? process.cwd()
111
+ // Effective config = defaults merged with project overrides. Never null.
112
+ const { config, source } = loadEffectiveConfig(projectRoot)
113
+ const timeout = args.timeout ?? DEFAULT_TIMEOUT_MS
114
+
115
+ // Only commands predefined in validation.commands may run — the
116
+ // user trusts exactly these, and nothing else. This replaces the
117
+ // old prefix-match whitelist, which let e.g. `python3 -c "..."`
118
+ // slip through on a `python3` prefix.
119
+ const predefinedCommands = flattenCommands(config.validation.commands)
120
+ if (predefinedCommands.length === 0) {
121
+ return {
122
+ allowed: false,
123
+ command: args.command,
124
+ exitCode: -1,
125
+ stdout: '',
126
+ stderr: '',
127
+ timedOut: false,
128
+ durationMs: 0,
129
+ rejectReason:
130
+ (source === 'defaults'
131
+ ? 'No iterate.config.yaml at project root — running on built-in defaults, which configure NO trusted validation commands. '
132
+ : 'No validation.commands configured in iterate.config.yaml. ') +
133
+ 'Nothing can be validated until you define trusted commands in `validation.commands`.',
134
+ }
135
+ }
136
+ if (!isCommandAllowed(args.command, predefinedCommands)) {
137
+ return {
138
+ allowed: false,
139
+ command: args.command,
140
+ exitCode: -1,
141
+ stdout: '',
142
+ stderr: '',
143
+ timedOut: false,
144
+ durationMs: 0,
145
+ rejectReason:
146
+ `Command must exactly match a command predefined in iterate.config.yaml validation.commands. ` +
147
+ `Allowed commands: ${predefinedCommands.join(' | ')}`,
148
+ }
149
+ }
150
+
151
+ const result = await runCommand(args.command, projectRoot, timeout)
152
+ return {
153
+ allowed: true,
154
+ ...result,
155
+ }
156
+ },
157
+ }),
158
+ )
159
+ }
package/src/types.ts ADDED
@@ -0,0 +1,100 @@
1
+ /** Parsed iterate.config.yaml */
2
+ export interface IterateConfig {
3
+ goal: string
4
+ max_rounds: number
5
+ language: 'zh' | 'en'
6
+ dimensions: string[]
7
+ review: { scope: 'full' | 'changed-only' }
8
+ atomic: { max_lines: number; max_adjacent_methods: number }
9
+ git: {
10
+ target_branch: string
11
+ use_worktree: boolean
12
+ push_per_round: boolean
13
+ auto_merge: boolean
14
+ }
15
+ validation: {
16
+ command_whitelist: string[]
17
+ commands: Record<string, string[]>
18
+ }
19
+ reviewer: { output_schema_validation: boolean }
20
+ onboarding?: Record<string, unknown>
21
+ personalization?: Record<string, unknown>
22
+ }
23
+
24
+ /** One entry in the append-only decision log */
25
+ export interface DecisionLogEntry {
26
+ timestamp: string
27
+ round: number
28
+ type:
29
+ | 'round_start'
30
+ | 'review_result'
31
+ | 'atomic_fix'
32
+ | 'architectural_fix'
33
+ | 'revert'
34
+ | 'validation'
35
+ | 'decision'
36
+ | 'report'
37
+ data: Record<string, unknown>
38
+ }
39
+
40
+ /** Result of running a validation command */
41
+ export interface ValidationResult {
42
+ command: string
43
+ exitCode: number
44
+ stdout: string
45
+ stderr: string
46
+ timedOut: boolean
47
+ durationMs: number
48
+ }
49
+
50
+ /** A single finding from a dimension review */
51
+ export interface ReviewFinding {
52
+ dimension: string
53
+ file: string
54
+ line?: number
55
+ severity: 'critical' | 'high' | 'medium' | 'low'
56
+ summary: string
57
+ failure_scenario: string
58
+ suggested_fix: string
59
+ /** true if the fix fits within atomic thresholds (single file, single function, ≤ max_lines). */
60
+ is_atomic: boolean
61
+ }
62
+
63
+ /** Review report (dry-run or normal mode) */
64
+ export interface ReviewReport {
65
+ mode: 'dry-run' | 'normal'
66
+ goal: string
67
+ dimensions: string[]
68
+ maxReviewRounds: number
69
+ rounds: ReviewRound[]
70
+ /** Globally deduped, known-intentional-filtered, severity-sorted findings. */
71
+ findings: ReviewFinding[]
72
+ convergence: {
73
+ totalRounds: number
74
+ findingsByRound: number[]
75
+ converged: boolean
76
+ stoppedReason: 'converged' | 'max_rounds_reached'
77
+ }
78
+ summary: {
79
+ totalFindings: number
80
+ critical: number
81
+ high: number
82
+ medium: number
83
+ low: number
84
+ byDimension: Record<string, number>
85
+ }
86
+ }
87
+
88
+ /** One round of review */
89
+ export interface ReviewRound {
90
+ round: number
91
+ findings: ReviewFinding[]
92
+ }
93
+
94
+ /** Known intentional entry (filtered out from findings) */
95
+ export interface KnownIntentional {
96
+ file: string
97
+ line?: number
98
+ dimension: string
99
+ reason: string
100
+ }