iterate-plugin 2.12.1 → 2.12.3

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.
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { loadEffectiveConfig, resolveProjectRootForExec } from "../config-loader.js";
3
+ import { runWithJob } from "../jobs.js";
3
4
  import { buildReviewPlan, buildReviewReport, sanitizeRounds, validateRoundsSchema, } from "../review.js";
4
5
  import { buildFinalReviewReport, metaReviewReport } from "../meta-review.js";
5
6
  import { evidenceToPlain, verifyFindings } from "../evidence.js";
@@ -65,6 +66,15 @@ export function registerReviewTool(ctx) {
65
66
  description: 'For `meta-review`: the ReviewReport JSON (as returned by `aggregate`) to audit for ' +
66
67
  'internal consistency and produce the final review report.',
67
68
  },
69
+ attachments: {
70
+ type: 'json',
71
+ description: 'Optional (plan only): image/visual attachments to thread into the review, e.g. ' +
72
+ '[{"path":"screens/hits.png","caption":"reproduced layout bug"}]. Each entry: ' +
73
+ '{path?, data?, media_type?, caption?} — path resolves relative to the project root, ' +
74
+ 'data is a base64 payload (media_type e.g. image/png), caption gives human context. ' +
75
+ 'Injected as a mandatory clause into every dimension reviewer prompt so screenshots/' +
76
+ 'mockups/failure repros are weighed alongside the code.',
77
+ },
68
78
  fixedCount: {
69
79
  type: 'integer',
70
80
  description: 'For `aggregate` (normal mode only): number of atomic fixes applied so far. ' +
@@ -107,134 +117,145 @@ export function registerReviewTool(ctx) {
107
117
  ],
108
118
  },
109
119
  async execute(args, exec) {
110
- const resolved = resolveProjectRootForExec(exec, args.path);
111
- if (!resolved.ok) {
112
- return { operation: args.operation, error: resolved.reason };
113
- }
114
- const projectRoot = resolved.root;
115
- // Effective config = defaults merged with project overrides. Never
116
- // null, so `plan`/`aggregate` work even without a config file.
117
- const { config } = loadEffectiveConfig(projectRoot);
118
- const mode = args.mode ?? 'dry-run';
119
- if (args.operation === 'plan') {
120
- const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
121
- const knownIntentional = config.personalization
122
- ?.known_intentional;
123
- // changed-only scope: resolve the changed-file set against
124
- // git.target_branch before building the plan so reviewers get the
125
- // concrete file list (and the plan auto-falls back to full when there
126
- // are no changes). git failures degrade to a full-scope plan.
127
- let changedFiles;
128
- if (config.review?.scope === 'changed-only') {
129
- const gitScope = await resolveChangedFiles(projectRoot, config.git?.target_branch ?? 'main');
130
- changedFiles = gitScope.changedFiles;
120
+ const { result } = await runWithJob(ctx, 'iterate-review', `iterate_review ${String(args.operation ?? '')} (${String(args.mode ?? 'dry-run')})`, async () => {
121
+ const resolved = resolveProjectRootForExec(exec, args.path);
122
+ if (!resolved.ok) {
123
+ return { operation: args.operation, error: resolved.reason };
131
124
  }
132
- // Full-codebase review: pre-collect the source inventory so
133
- // buildReviewPlan can batch it into per-chunk reviewer tasks
134
- // (coverage enforcement).
135
- let scopeFiles;
136
- if (config.review?.scope === 'full') {
137
- scopeFiles = collectScopeFiles(projectRoot, { scope: 'full' });
138
- }
139
- const plan = buildReviewPlan({ config, mode, maxReviewRounds, knownIntentional, changedFiles, scopeFiles });
140
- return { operation: 'plan', mode, found: true, plan: plan };
141
- }
142
- if (args.operation === 'aggregate') {
143
- const rawRounds = Array.isArray(args.rounds) ? args.rounds : [];
144
- const rounds = rawRounds
145
- .map((r) => {
146
- const rr = r;
147
- const findings = Array.isArray(rr?.findings) ? rr.findings : [];
148
- const readFiles = Array.isArray(rr?.readFiles)
149
- ? rr.readFiles.filter((f) => typeof f === 'string')
125
+ const projectRoot = resolved.root;
126
+ // Effective config = defaults merged with project overrides. Never
127
+ // null, so `plan`/`aggregate` work even without a config file.
128
+ const { config } = loadEffectiveConfig(projectRoot);
129
+ const mode = args.mode ?? 'dry-run';
130
+ if (args.operation === 'plan') {
131
+ const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
132
+ const knownIntentional = config.personalization
133
+ ?.known_intentional;
134
+ // changed-only scope: resolve the changed-file set against
135
+ // git.target_branch before building the plan so reviewers get the
136
+ // concrete file list (and the plan auto-falls back to full when there
137
+ // are no changes). git failures degrade to a full-scope plan.
138
+ let changedFiles;
139
+ if (config.review?.scope === 'changed-only') {
140
+ const gitScope = await resolveChangedFiles(projectRoot, config.git?.target_branch ?? 'main');
141
+ changedFiles = gitScope.changedFiles;
142
+ }
143
+ // Full-codebase review: pre-collect the source inventory so
144
+ // buildReviewPlan can batch it into per-chunk reviewer tasks
145
+ // (coverage enforcement).
146
+ let scopeFiles;
147
+ if (config.review?.scope === 'full') {
148
+ scopeFiles = collectScopeFiles(projectRoot, { scope: 'full' });
149
+ }
150
+ // Thread image/visual attachments (screenshots/mockups/failure repros)
151
+ // into the plan so every reviewer prompt weighs them alongside code.
152
+ const attachments = Array.isArray(args.attachments)
153
+ ? args.attachments.filter((a) => Boolean(a) &&
154
+ typeof a === 'object' &&
155
+ ((typeof a.path === 'string' && a.path.length > 0) ||
156
+ (typeof a.data === 'string' && a.data.length > 0)))
150
157
  : [];
151
- return { round: typeof rr?.round === 'number' ? rr.round : 0, findings, readFiles };
152
- })
153
- .filter((r) => r.round > 0);
154
- if (rounds.length === 0) {
158
+ const plan = buildReviewPlan({ config, mode, maxReviewRounds, knownIntentional, changedFiles, scopeFiles, attachments });
159
+ return { operation: 'plan', mode, found: true, plan: plan };
160
+ }
161
+ if (args.operation === 'aggregate') {
162
+ const rawRounds = Array.isArray(args.rounds) ? args.rounds : [];
163
+ const rounds = rawRounds
164
+ .map((r) => {
165
+ const rr = r;
166
+ const findings = Array.isArray(rr?.findings) ? rr.findings : [];
167
+ const readFiles = Array.isArray(rr?.readFiles)
168
+ ? rr.readFiles.filter((f) => typeof f === 'string')
169
+ : [];
170
+ return { round: typeof rr?.round === 'number' ? rr.round : 0, findings, readFiles };
171
+ })
172
+ .filter((r) => r.round > 0);
173
+ if (rounds.length === 0) {
174
+ return {
175
+ operation: 'aggregate',
176
+ mode,
177
+ error: 'rounds must be a non-empty array of {round, findings}.',
178
+ };
179
+ }
180
+ const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
181
+ const goal = args.goal ?? config.goal ?? '';
182
+ const dimensions = config.dimensions ?? [];
183
+ // Output schema validation gate (reviewer.output_schema_validation,
184
+ // default true): validate every round's findings against the findings
185
+ // schema, then drop schema-invalid entries before the deterministic
186
+ // core so malformed reviewer output can never crash dedupe/sort or
187
+ // leak into fixes. The `schemaValidation` array is surfaced so the
188
+ // workflow can retry failing rounds (≤2 times) with a strict-JSON
189
+ // nudge. When disabled, non-object entries are still dropped for
190
+ // crash-safety.
191
+ const schemaEnabled = config.reviewer?.output_schema_validation !== false;
192
+ const schemaValidation = schemaEnabled ? validateRoundsSchema(rounds) : null;
193
+ const cleanRounds = sanitizeRounds(rounds, schemaValidation);
194
+ const report = buildReviewReport({
195
+ mode,
196
+ goal,
197
+ dimensions,
198
+ maxReviewRounds,
199
+ rounds: cleanRounds,
200
+ knownIntentional: args.knownIntentional,
201
+ fixedCount: typeof args.fixedCount === 'number' ? args.fixedCount : undefined,
202
+ });
155
203
  return {
156
204
  operation: 'aggregate',
157
205
  mode,
158
- error: 'rounds must be a non-empty array of {round, findings}.',
206
+ report: report,
207
+ schemaValidation: (schemaValidation ?? null),
159
208
  };
160
209
  }
161
- const maxReviewRounds = args.maxReviewRounds ?? config.max_rounds ?? DEFAULT_MAX_REVIEW_ROUNDS;
162
- const goal = args.goal ?? config.goal ?? '';
163
- const dimensions = config.dimensions ?? [];
164
- // Output schema validation gate (reviewer.output_schema_validation,
165
- // default true): validate every round's findings against the findings
166
- // schema, then drop schema-invalid entries before the deterministic
167
- // core so malformed reviewer output can never crash dedupe/sort or
168
- // leak into fixes. The `schemaValidation` array is surfaced so the
169
- // workflow can retry failing rounds (≤2 times) with a strict-JSON
170
- // nudge. When disabled, non-object entries are still dropped for
171
- // crash-safety.
172
- const schemaEnabled = config.reviewer?.output_schema_validation !== false;
173
- const schemaValidation = schemaEnabled ? validateRoundsSchema(rounds) : null;
174
- const cleanRounds = sanitizeRounds(rounds, schemaValidation);
175
- const report = buildReviewReport({
176
- mode,
177
- goal,
178
- dimensions,
179
- maxReviewRounds,
180
- rounds: cleanRounds,
181
- knownIntentional: args.knownIntentional,
182
- fixedCount: typeof args.fixedCount === 'number' ? args.fixedCount : undefined,
183
- });
184
- return {
185
- operation: 'aggregate',
186
- mode,
187
- report: report,
188
- schemaValidation: (schemaValidation ?? null),
189
- };
190
- }
191
- if (args.operation === 'meta-review') {
192
- const source = args.report;
193
- if (!source || typeof source !== 'object') {
210
+ if (args.operation === 'meta-review') {
211
+ const source = args.report;
212
+ if (!source || typeof source !== 'object') {
213
+ return {
214
+ operation: 'meta-review',
215
+ mode,
216
+ error: 'report must be a ReviewReport JSON object (as returned by `aggregate`).',
217
+ };
218
+ }
219
+ const audit = metaReviewReport(source);
220
+ // Hard code-evidence gate (default on): every finding's file/line is
221
+ // validated against real files on disk before folding into the final
222
+ // verdict. Disable via config `reviewer.evidence_validation: false`.
223
+ const evidenceEnabled = config.reviewer?.evidence_validation !== false;
224
+ const findings = Array.isArray(source.findings) ? source.findings : [];
225
+ const evidence = evidenceEnabled ? verifyFindings(projectRoot, findings) : null;
226
+ // Prompt-informative coverage: compare the reviewer's self-reported
227
+ // reads against the assigned scope inventory (never flips the
228
+ // verdict). Disable via config `reviewer.coverage_validation: false`.
229
+ const coverageEnabled = config.reviewer?.coverage_validation !== false;
230
+ let coverage = null;
231
+ if (coverageEnabled) {
232
+ const assigned = collectScopeFiles(projectRoot, {
233
+ scope: config.review?.scope === 'changed-only' ? 'changed-only' : 'full',
234
+ });
235
+ const readFiles = Array.isArray(source.readFiles)
236
+ ? source.readFiles
237
+ : null;
238
+ if (readFiles && readFiles.length > 0) {
239
+ coverage = computeCoverage(assigned, readFiles);
240
+ }
241
+ }
242
+ const finalReport = buildFinalReviewReport(source, { evidence, coverage });
194
243
  return {
195
244
  operation: 'meta-review',
196
245
  mode,
197
- error: 'report must be a ReviewReport JSON object (as returned by `aggregate`).',
246
+ found: true,
247
+ report: audit,
248
+ evidence: evidence ? evidenceToPlain(evidence) : null,
249
+ coverage: coverage ? coverageToDict(coverage) : null,
250
+ finalReport: finalReport,
198
251
  };
199
252
  }
200
- const audit = metaReviewReport(source);
201
- // Hard code-evidence gate (default on): every finding's file/line is
202
- // validated against real files on disk before folding into the final
203
- // verdict. Disable via config `reviewer.evidence_validation: false`.
204
- const evidenceEnabled = config.reviewer?.evidence_validation !== false;
205
- const findings = Array.isArray(source.findings) ? source.findings : [];
206
- const evidence = evidenceEnabled ? verifyFindings(projectRoot, findings) : null;
207
- // Prompt-informative coverage: compare the reviewer's self-reported
208
- // reads against the assigned scope inventory (never flips the
209
- // verdict). Disable via config `reviewer.coverage_validation: false`.
210
- const coverageEnabled = config.reviewer?.coverage_validation !== false;
211
- let coverage = null;
212
- if (coverageEnabled) {
213
- const assigned = collectScopeFiles(projectRoot, {
214
- scope: config.review?.scope === 'changed-only' ? 'changed-only' : 'full',
215
- });
216
- const readFiles = Array.isArray(source.readFiles)
217
- ? source.readFiles
218
- : null;
219
- if (readFiles && readFiles.length > 0) {
220
- coverage = computeCoverage(assigned, readFiles);
221
- }
222
- }
223
- const finalReport = buildFinalReviewReport(source, { evidence, coverage });
224
253
  return {
225
- operation: 'meta-review',
226
- mode,
227
- found: true,
228
- report: audit,
229
- evidence: evidence ? evidenceToPlain(evidence) : null,
230
- coverage: coverage ? coverageToDict(coverage) : null,
231
- finalReport: finalReport,
254
+ operation: args.operation,
255
+ error: `Unknown operation "${args.operation}". Use "plan", "aggregate", or "meta-review".`,
232
256
  };
233
- }
234
- return {
235
- operation: args.operation,
236
- error: `Unknown operation "${args.operation}". Use "plan", "aggregate", or "meta-review".`,
237
- };
257
+ });
258
+ return result;
238
259
  },
239
260
  }));
240
261
  }