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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/cordis.patch.yml +9 -0
- package/package.json +59 -0
- package/src/config-loader.ts +162 -0
- package/src/index.ts +50 -0
- package/src/meta-review.ts +289 -0
- package/src/review.ts +360 -0
- package/src/skill-prompt.ts +227 -0
- package/src/tools/config.ts +90 -0
- package/src/tools/context.ts +170 -0
- package/src/tools/decision-log.ts +163 -0
- package/src/tools/review.ts +174 -0
- package/src/tools/validate.ts +159 -0
- package/src/types.ts +100 -0
|
@@ -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
|
+
}
|