iterate-plugin 2.7.2 → 2.8.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.
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/dist/config-loader.js +1 -1
- package/dist/evidence.js +143 -0
- package/dist/meta-review.js +29 -1
- package/dist/review.js +10 -2
- package/dist/skill-prompt.js +4 -2
- package/dist/tools/review.js +10 -1
- package/lib/client.js +1474 -734
- package/package.json +6 -2
- package/src/client/index.ts +1218 -0
- package/src/config-loader.ts +1 -1
- package/src/evidence.ts +194 -0
- package/src/meta-review.ts +34 -1
- package/src/review.ts +12 -2
- package/src/skill-prompt.ts +4 -2
- package/src/tools/review.ts +10 -1
- package/src/types.ts +1 -1
package/src/config-loader.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function defaultConfig(): IterateConfig {
|
|
|
49
49
|
auto_merge: false,
|
|
50
50
|
},
|
|
51
51
|
validation: { command_whitelist: [], commands: {} },
|
|
52
|
-
reviewer: { output_schema_validation: true },
|
|
52
|
+
reviewer: { output_schema_validation: true, evidence_validation: true },
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
package/src/evidence.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic code-evidence verification for review findings.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `iterate_harness/iterate/evidence.py` for the iterate-plugin.
|
|
5
|
+
*
|
|
6
|
+
* The iterate review loop requires that reviewer subagent findings ANCHOR to
|
|
7
|
+
* real code instead of speculating. This module enforces it:
|
|
8
|
+
*
|
|
9
|
+
* - a finding's `file` must resolve to an existing file under the project root
|
|
10
|
+
* (traversal-safe), otherwise evidence is poisoned (`file_not_found`);
|
|
11
|
+
* - a finding with an explicit line must reference a line that actually exists
|
|
12
|
+
* in that file (`line_out_of_range`);
|
|
13
|
+
* - a whole-file finding (line 0 / undefined) must still reference an existing
|
|
14
|
+
* file, so even structural findings cannot point at nothing;
|
|
15
|
+
* - `readVerified` is a best-effort, NON-gating hint: the plugin's reviewers are
|
|
16
|
+
* subagents whose reads are not aggregated here, so it is only set when a
|
|
17
|
+
* read set is explicitly provided and never fails the audit.
|
|
18
|
+
*
|
|
19
|
+
* Gate rule (user preference): ANY localizable finding with poisoned evidence
|
|
20
|
+
* flips the whole audit to `passed: false`, so the meta-review forces revision.
|
|
21
|
+
*
|
|
22
|
+
* The pure math (`countLines`, `verifyLineBounds`) is separated from the
|
|
23
|
+
* filesystem half (`verifyFinding`) to stay unit-testable without touching disk.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
27
|
+
import { resolve, sep } from 'node:path'
|
|
28
|
+
import type { ReviewFinding } from './types.ts'
|
|
29
|
+
|
|
30
|
+
/** Sentinel for whole-file findings (line 0 or omitted means the whole file). */
|
|
31
|
+
export const WHOLE_FILE_LINE = 0
|
|
32
|
+
|
|
33
|
+
export type EvidenceError = 'file_not_found' | 'line_out_of_range'
|
|
34
|
+
|
|
35
|
+
/** Per-finding attestation result. */
|
|
36
|
+
export interface FindingEvidence {
|
|
37
|
+
file: string
|
|
38
|
+
line: number | null
|
|
39
|
+
lineTotal: number | null
|
|
40
|
+
resolvedPath: string | null
|
|
41
|
+
verified: boolean
|
|
42
|
+
error?: EvidenceError
|
|
43
|
+
/** True/False only when a read-set is supplied; undefined = not checkable. */
|
|
44
|
+
readVerified?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Aggregate attestation over a findings list. */
|
|
48
|
+
export interface EvidenceAudit {
|
|
49
|
+
checked: number
|
|
50
|
+
results: FindingEvidence[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A single finding object that exposes `file` / `line` (for verification). */
|
|
54
|
+
interface Locatable {
|
|
55
|
+
file?: string
|
|
56
|
+
line?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Number of physical lines in `text`. A trailing newline does not add a line. */
|
|
60
|
+
export function countLines(text: string): number {
|
|
61
|
+
if (text === '') return 0
|
|
62
|
+
const parts = text.split(/\r\n|\r|\n/)
|
|
63
|
+
// A trailing newline leaves an empty final element that is NOT a line
|
|
64
|
+
// (mirrors Python `str.splitlines()` used by the harness).
|
|
65
|
+
if (parts[parts.length - 1] === '') return parts.length - 1
|
|
66
|
+
return parts.length
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve `root/rel` and reject any path escaping `root` (returns null). */
|
|
70
|
+
export function resolveWithin(root: string, rel: string): string | null {
|
|
71
|
+
const resolved = resolve(root, rel)
|
|
72
|
+
const rootResolved = resolve(root)
|
|
73
|
+
if (resolved === rootResolved) return resolved
|
|
74
|
+
const prefix = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep
|
|
75
|
+
if (!resolved.startsWith(prefix)) return null
|
|
76
|
+
return resolved
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pure check that `line` (if anchored) exists in `text`.
|
|
81
|
+
* Whole-file findings (undefined/0) are always bounds-valid.
|
|
82
|
+
*/
|
|
83
|
+
export function verifyLineBounds(
|
|
84
|
+
line: number | null | undefined,
|
|
85
|
+
text: string,
|
|
86
|
+
): { inBounds: boolean; lineTotal: number } {
|
|
87
|
+
const lineTotal = countLines(text)
|
|
88
|
+
if (line === undefined || line === null || line === WHOLE_FILE_LINE) {
|
|
89
|
+
return { inBounds: true, lineTotal }
|
|
90
|
+
}
|
|
91
|
+
if (line < 1) return { inBounds: false, lineTotal }
|
|
92
|
+
return { inBounds: line <= lineTotal, lineTotal }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Verify a single finding's location against the real filesystem. */
|
|
96
|
+
export function verifyFinding(
|
|
97
|
+
root: string,
|
|
98
|
+
input: Locatable,
|
|
99
|
+
opts: { readSet?: Set<string> } = {},
|
|
100
|
+
): FindingEvidence {
|
|
101
|
+
const relFile = input.file ?? ''
|
|
102
|
+
const line = typeof input.line === 'number' ? input.line : null
|
|
103
|
+
const resolved = resolveWithin(root, relFile)
|
|
104
|
+
|
|
105
|
+
if (resolved === null || !existsSync(resolved)) {
|
|
106
|
+
return {
|
|
107
|
+
file: relFile,
|
|
108
|
+
line,
|
|
109
|
+
lineTotal: null,
|
|
110
|
+
resolvedPath: resolved,
|
|
111
|
+
verified: false,
|
|
112
|
+
error: 'file_not_found',
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let text: string
|
|
117
|
+
try {
|
|
118
|
+
text = readFileSync(resolved, 'utf-8')
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
file: relFile,
|
|
122
|
+
line,
|
|
123
|
+
lineTotal: null,
|
|
124
|
+
resolvedPath: resolved,
|
|
125
|
+
verified: false,
|
|
126
|
+
error: 'file_not_found',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { inBounds, lineTotal } = verifyLineBounds(line, text)
|
|
131
|
+
if (!inBounds) {
|
|
132
|
+
return {
|
|
133
|
+
file: relFile,
|
|
134
|
+
line,
|
|
135
|
+
lineTotal,
|
|
136
|
+
resolvedPath: resolved,
|
|
137
|
+
verified: false,
|
|
138
|
+
error: 'line_out_of_range',
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const outcome: FindingEvidence = {
|
|
143
|
+
file: relFile,
|
|
144
|
+
line,
|
|
145
|
+
lineTotal,
|
|
146
|
+
resolvedPath: resolved,
|
|
147
|
+
verified: true,
|
|
148
|
+
}
|
|
149
|
+
if (opts.readSet !== undefined) {
|
|
150
|
+
outcome.readVerified = opts.readSet.has(resolved)
|
|
151
|
+
}
|
|
152
|
+
return outcome
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Attest every finding in a list. */
|
|
156
|
+
export function verifyFindings(
|
|
157
|
+
root: string,
|
|
158
|
+
findings: Locatable[],
|
|
159
|
+
opts: { readSet?: Set<string> } = {},
|
|
160
|
+
): EvidenceAudit {
|
|
161
|
+
const results = findings.map((f) => verifyFinding(root, f, opts))
|
|
162
|
+
return { checked: results.length, results }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `passed` is true only when no real existence failure exists (read is a hint). */
|
|
166
|
+
export function evidencePassed(audit: EvidenceAudit): boolean {
|
|
167
|
+
return audit.results.every((r) => r.error === undefined)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Violating (non-grounded) results. */
|
|
171
|
+
export function evidenceViolations(audit: EvidenceAudit): FindingEvidence[] {
|
|
172
|
+
return audit.results.filter((r) => r.error !== undefined)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Serialize an audit for tool payloads (pure). */
|
|
176
|
+
export function evidenceToPlain(audit: EvidenceAudit): Record<string, unknown> {
|
|
177
|
+
const computable = audit.results.filter((r) => r.readVerified !== undefined)
|
|
178
|
+
const readRatio =
|
|
179
|
+
computable.length === 0
|
|
180
|
+
? null
|
|
181
|
+
: Number(
|
|
182
|
+
(
|
|
183
|
+
computable.filter((r) => r.readVerified === true).length / computable.length
|
|
184
|
+
).toFixed(3),
|
|
185
|
+
)
|
|
186
|
+
return {
|
|
187
|
+
checked: audit.checked,
|
|
188
|
+
passed: evidencePassed(audit),
|
|
189
|
+
violations: audit.results
|
|
190
|
+
.filter((r) => r.error !== undefined)
|
|
191
|
+
.map((r) => ({ file: r.file, line: r.line, lineTotal: r.lineTotal, verified: r.verified, error: r.error })),
|
|
192
|
+
readVerifiedRatio: readRatio,
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/meta-review.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ReviewFinding, ReviewReport } from './types.ts'
|
|
18
|
+
import type { EvidenceAudit } from './evidence.ts'
|
|
18
19
|
import { sortFindings } from './review.ts'
|
|
19
20
|
|
|
20
21
|
/** A single defect found while auditing a review report. */
|
|
@@ -278,9 +279,41 @@ export function metaReviewReport(report: ReviewReport): MetaReviewResult {
|
|
|
278
279
|
/**
|
|
279
280
|
* Build the final review report: pair the source report with its meta-review
|
|
280
281
|
* verdict and a rolled-up summary. Pure and deterministic.
|
|
282
|
+
*
|
|
283
|
+
* `evidence` (an EvidenceAudit produced against the real repo) is the hard
|
|
284
|
+
* code-evidence gate: every finding whose file/line does not resolve to
|
|
285
|
+
* existing code is emitted as a critical EVIDENCE_VIOLATION and flips the
|
|
286
|
+
* verdict to `needs_revision`. The audit itself reads the filesystem; this
|
|
287
|
+
* function only folds the (pure, precomputed) result in.
|
|
281
288
|
*/
|
|
282
|
-
export function buildFinalReviewReport(
|
|
289
|
+
export function buildFinalReviewReport(
|
|
290
|
+
report: ReviewReport,
|
|
291
|
+
opts: { evidence?: EvidenceAudit | null } = {},
|
|
292
|
+
): FinalReviewReport {
|
|
283
293
|
const meta = metaReviewReport(report)
|
|
294
|
+
const evidence = opts.evidence ?? null
|
|
295
|
+
if (evidence !== null) {
|
|
296
|
+
meta.checksRun += 1
|
|
297
|
+
if (evidence.results.some((r) => r.error !== undefined)) {
|
|
298
|
+
for (const violation of evidence.results) {
|
|
299
|
+
if (violation.error === undefined) continue
|
|
300
|
+
const detail =
|
|
301
|
+
violation.error === 'line_out_of_range'
|
|
302
|
+
? `${violation.line} is beyond this file's ${violation.lineTotal} lines`
|
|
303
|
+
: `${violation.file} does not exist at all (verifiable read required)`
|
|
304
|
+
meta.issues.push({
|
|
305
|
+
code: 'EVIDENCE_VIOLATION',
|
|
306
|
+
severity: 'critical',
|
|
307
|
+
summary:
|
|
308
|
+
`Finding references non-existent code: ${violation.file}` +
|
|
309
|
+
(violation.line ? `:${violation.line}` : ''),
|
|
310
|
+
detail: detail + '. Review results must anchor to real, read code.',
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
meta.passed = false
|
|
314
|
+
meta.verdict = 'revise'
|
|
315
|
+
}
|
|
316
|
+
}
|
|
284
317
|
const summary = report?.summary ?? {}
|
|
285
318
|
const verdict: FinalReviewVerdict = meta.passed ? 'approved' : 'needs_revision'
|
|
286
319
|
return {
|
package/src/review.ts
CHANGED
|
@@ -351,11 +351,21 @@ export function reviewerTaskPrompt(input: {
|
|
|
351
351
|
} else {
|
|
352
352
|
parts.push('This is round 1 — report every issue you find in this dimension.')
|
|
353
353
|
}
|
|
354
|
+
parts.push(
|
|
355
|
+
'EVIDENCE RULE (mandatory): read every file you report on with the ' +
|
|
356
|
+
'read_file tool BEFORE judging it. NEVER report a location you did not ' +
|
|
357
|
+
'actually read — speculation about code you never inspected is a ' +
|
|
358
|
+
'disqualifying failure, and fabricated line numbers are treated as ' +
|
|
359
|
+
'poisoned evidence. Anchor every finding to real code.',
|
|
360
|
+
)
|
|
354
361
|
parts.push(
|
|
355
362
|
`Return a JSON object: {"findings": [...]}.`,
|
|
356
363
|
`Each finding: dimension (must be "${input.dimension}"), file (relative path), ` +
|
|
357
|
-
'line (
|
|
358
|
-
'
|
|
364
|
+
'line (REQUIRED positive integer — the exact line you READ for an ' +
|
|
365
|
+
'anchored, line-targeted issue; use 0 for whole-file/module-level ' +
|
|
366
|
+
'issues), severity (critical/high/medium/low), summary (one line), ' +
|
|
367
|
+
'failure_scenario (how/when it fails, backed by the code you actually ' +
|
|
368
|
+
'read), suggested_fix (the concrete fix), ' +
|
|
359
369
|
`is_atomic (true if the fix is <= ${input.maxLines} lines within a SINGLE file/function, else false).`,
|
|
360
370
|
`Write summaries and details in ${input.outputLanguage}.`,
|
|
361
371
|
)
|
package/src/skill-prompt.ts
CHANGED
|
@@ -122,10 +122,11 @@ return {
|
|
|
122
122
|
|
|
123
123
|
Key rules for dry-run:
|
|
124
124
|
- **NEVER call a fixer / never edit files / never create branches or worktree.** Reviewers read only.
|
|
125
|
+
- **Every reviewer MUST actually read each file it reports on (read_file) BEFORE judging it, and anchor every finding to a real location. Fabricated file paths or invented line numbers are poisoned evidence and fail the run.** Subagents never report on code they didn't inspect.
|
|
125
126
|
- Each round feeds the already-known findings to reviewers so they hunt NEW issues only → that is what drives convergence.
|
|
126
127
|
- Stop when a round reports 0 new findings (converged) or maxReviewRounds is reached.
|
|
127
128
|
- The report (with per-round convergence stats + suggested fix priorities) is the deliverable.
|
|
128
|
-
- **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The \`finalReport.verdict\` is \`approved\` only when the report passes every check; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
|
|
129
|
+
- **Meta-review**: after building the report, audit it with \`iterate_review({operation:"meta-review"})\` for internal consistency (counts, severity buckets, dimension sums, sort order, convergence math). The meta-review ALSO runs the hard code-evidence gate (default on): every finding's file/line is validated against real files on disk, so any fabricated location surfaces as a critical \`EVIDENCE_VIOLATION\` and flips the verdict to \`needs_revision\`. The \`finalReport.verdict\` is \`approved\` only when the report passes every check AND every finding anchors to real, read code; otherwise \`needs_revision\`. Surface the final report and its verdict as the closing deliverable.
|
|
129
130
|
- Only a single \`report\` entry may be appended to the decision log; nothing else is written.
|
|
130
131
|
|
|
131
132
|
### Normal-mode workflow (autonomous closed loop)
|
|
@@ -329,11 +330,12 @@ Key rules for normal mode:
|
|
|
329
330
|
- Close with \`iterate_status\` metrics and surface the convergence indicators (fixed count, remaining architectural count, abort reason) in the final summary.
|
|
330
331
|
|
|
331
332
|
### Finding schema (for reviewer agents)
|
|
332
|
-
{ "dimension": string, "file": string (relative path), "line": number (
|
|
333
|
+
{ "dimension": string, "file": string (relative path), "line": number (REQUIRED for line-targeted issues — the exact line you READ; use 0 for whole-file/module-level issues),
|
|
333
334
|
"severity": "critical" | "high" | "medium" | "low", "summary": string (one line),
|
|
334
335
|
"failure_scenario": string (how/when it fails), "suggested_fix": string (the concrete fix),
|
|
335
336
|
"is_atomic": boolean (true if fix ≤ max_lines within a single file/function) }
|
|
336
337
|
Atomic = is_atomic true (single file, single function, ≤ config.atomic.max_lines lines change). Architectural = everything else.
|
|
338
|
+
Every finding MUST reference a file the reviewer actually read (read_file) and a real location — never speculate about code that was never inspected. Fabricated paths/lines are poisoned evidence and fail the meta-review evidence gate.
|
|
337
339
|
|
|
338
340
|
### Workflow meta
|
|
339
341
|
Always pass \`meta: { name: "iterate", description: "Autonomous iterate loop" }\`.
|
package/src/tools/review.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
|
3
3
|
import { loadEffectiveConfig, resolveProjectRoot } from '../config-loader.ts'
|
|
4
4
|
import { buildReviewPlan, buildReviewReport } from '../review.ts'
|
|
5
5
|
import { buildFinalReviewReport, metaReviewReport } from '../meta-review.ts'
|
|
6
|
+
import { evidenceToPlain, verifyFindings } from '../evidence.ts'
|
|
6
7
|
import type { KnownIntentional, ReviewFinding, ReviewReport, ReviewRound } from '../types.ts'
|
|
7
8
|
|
|
8
9
|
/** Default round cap when neither the arg nor config provides one. */
|
|
@@ -94,6 +95,7 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
94
95
|
found: { type: 'boolean' },
|
|
95
96
|
plan: { type: 'json' },
|
|
96
97
|
report: { type: 'json' },
|
|
98
|
+
evidence: { type: 'json' },
|
|
97
99
|
finalReport: { type: 'json' },
|
|
98
100
|
error: { type: 'string' },
|
|
99
101
|
},
|
|
@@ -165,12 +167,19 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
const audit = metaReviewReport(source)
|
|
168
|
-
|
|
170
|
+
// Hard code-evidence gate (default on): every finding's file/line is
|
|
171
|
+
// validated against real files on disk before folding into the final
|
|
172
|
+
// verdict. Disable via config `reviewer.evidence_validation: false`.
|
|
173
|
+
const evidenceEnabled = config.reviewer?.evidence_validation !== false
|
|
174
|
+
const findings: ReviewFinding[] = Array.isArray(source.findings) ? source.findings : []
|
|
175
|
+
const evidence = evidenceEnabled ? verifyFindings(projectRoot, findings) : null
|
|
176
|
+
const finalReport = buildFinalReviewReport(source, { evidence })
|
|
169
177
|
return {
|
|
170
178
|
operation: 'meta-review',
|
|
171
179
|
mode,
|
|
172
180
|
found: true,
|
|
173
181
|
report: audit as unknown as JsonValue,
|
|
182
|
+
evidence: evidence ? (evidenceToPlain(evidence) as unknown as JsonValue) : null,
|
|
174
183
|
finalReport: finalReport as unknown as JsonValue,
|
|
175
184
|
}
|
|
176
185
|
}
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface IterateConfig {
|
|
|
16
16
|
command_whitelist: string[]
|
|
17
17
|
commands: Record<string, string[]>
|
|
18
18
|
}
|
|
19
|
-
reviewer: { output_schema_validation: boolean }
|
|
19
|
+
reviewer: { output_schema_validation: boolean; evidence_validation: boolean }
|
|
20
20
|
onboarding?: Record<string, unknown>
|
|
21
21
|
personalization?: Record<string, unknown>
|
|
22
22
|
}
|