iterate-plugin 3.5.5 → 3.5.7

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/src/transcript.ts CHANGED
@@ -98,7 +98,10 @@ function normalizeFinding(input: unknown): TranscriptFinding | null {
98
98
  sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low'
99
99
  ? sev
100
100
  : 'low'
101
- const line = typeof f.line === 'number' && Number.isFinite(f.line) ? f.line : 0
101
+ // Only a non-negative integer is a real line anchor; a fractional/malformed
102
+ // value (e.g. 3.7 from a bypassed schema) must not leak into the manifest.
103
+ const line =
104
+ typeof f.line === 'number' && Number.isInteger(f.line) && f.line >= 0 ? f.line : 0
102
105
  return {
103
106
  dimension,
104
107
  file,
@@ -267,7 +270,8 @@ export class ReviewTranscriptBuilder {
267
270
  const att = typeof attempt === 'number' && Number.isFinite(attempt) ? Math.floor(attempt) : 1
268
271
  this.roundStart(this.round)
269
272
  const live = this.rounds[this.round - 1]! as LiveRound | undefined
270
- if (live && this.threadCount(live) < MAX_THREADS_PER_ROUND) {
273
+ if (!live) return
274
+ if (this.threadCount(live) < MAX_THREADS_PER_ROUND) {
271
275
  live.threads.push({
272
276
  dimension: dim || 'review',
273
277
  attempt: att > 0 ? att : 1,
@@ -275,7 +279,86 @@ export class ReviewTranscriptBuilder {
275
279
  readFiles: [],
276
280
  findings: [],
277
281
  })
282
+ } else {
283
+ // Thread cap reached: route the extra dimension/retry into ONE shared
284
+ // "other" overflow thread instead of silently dropping it. A dropped
285
+ // reviewerStart used to leave `currentThread()` pointing at the PREVIOUS
286
+ // dimension's thread, so its findings were mis-attributed to that
287
+ // dimension. The overflow thread stays bounded (at most ONE extra thread
288
+ // per round) while keeping over-cap findings attributable to "other".
289
+ let overflow = live.threads.find((t) => t.dimension === 'other')
290
+ if (!overflow) {
291
+ overflow = {
292
+ dimension: 'other',
293
+ attempt: att > 0 ? att : 1,
294
+ messages: [],
295
+ readFiles: [],
296
+ findings: [],
297
+ }
298
+ live.threads.push(overflow)
299
+ }
300
+ }
301
+ this.touch()
302
+ }
303
+
304
+ /**
305
+ * Restore ONE persisted thread during rehydration, preserving the message
306
+ * ARRAY boundaries (a rehydrated manifest is re-serialized on the next nudge
307
+ * edit, so collapsed messages would silently rewrite the evidence) and the
308
+ * thread's own dimension (never merging it into a previous dimension's
309
+ * thread). Bypasses the live per-round cap — the manifest already enforced
310
+ * it at capture time — but every restored field is hard-bounded so a hostile
311
+ * hand-edited manifest can never blow up memory.
312
+ */
313
+ restoreThread(
314
+ round: number,
315
+ thread: {
316
+ dimension?: unknown
317
+ attempt?: unknown
318
+ messages?: unknown
319
+ readFiles?: unknown
320
+ findings?: unknown
321
+ },
322
+ ): void {
323
+ const r =
324
+ typeof round === 'number' && Number.isFinite(round)
325
+ ? Math.min(Math.floor(round), MAX_ROUNDS)
326
+ : 1
327
+ this.roundStart(r)
328
+ const live = this.rounds[this.round - 1]
329
+ if (!live) return
330
+ if (!thread || typeof thread !== 'object') return
331
+ // Hostile-manifest guard: keep the live hard bound (with headroom for the
332
+ // single overflow thread) and newest-first truncate beyond it.
333
+ if (live.threads.length >= MAX_THREADS_PER_ROUND * 2 + 1) return
334
+ const dim =
335
+ typeof thread.dimension === 'string' && thread.dimension.trim()
336
+ ? thread.dimension.trim()
337
+ : 'review'
338
+ const att =
339
+ typeof thread.attempt === 'number' && Number.isFinite(thread.attempt)
340
+ ? Math.floor(thread.attempt)
341
+ : 1
342
+ const t: LiveThread = {
343
+ dimension: dim,
344
+ attempt: att > 0 ? att : 1,
345
+ messages: [],
346
+ readFiles: [],
347
+ findings: [],
348
+ }
349
+ const messages = Array.isArray(thread.messages) ? thread.messages : []
350
+ for (const m of messages.slice(0, MAX_MESSAGES_PER_THREAD)) {
351
+ if (typeof m === 'string' && m.trim()) t.messages.push(m)
352
+ }
353
+ if (Array.isArray(thread.readFiles)) {
354
+ t.readFiles.push(...dedupePaths(thread.readFiles as string[]))
355
+ }
356
+ if (Array.isArray(thread.findings)) {
357
+ for (const f of thread.findings) {
358
+ if (normalizeFinding(f)) pushThreadFinding(t, f)
359
+ }
278
360
  }
361
+ live.threads.push(t)
279
362
  this.touch()
280
363
  }
281
364
 
package/src/types.ts CHANGED
@@ -18,9 +18,12 @@ export interface IterateConfig {
18
18
  }
19
19
  /**
20
20
  * LLM reasoning effort for review passes ('low' | 'medium' | 'high').
21
- * Absent → follow the provider default. The harness forwards it into the
22
- * OpenAI-compatible request body; the plugin surfaces it in the settings
23
- * panel and review plan (dsh 0.1.1-rc.7+ exposes the same 'low' effort).
21
+ * Absent → follow the provider default. Accepted by `iterate_config` write
22
+ * validation (config-write), exposed in the settings guide, and carried on
23
+ * the review plan (`buildReviewPlan.reasoningEffort`, plus an effort
24
+ * directive injected into every reviewer prompt) so the orchestrating
25
+ * harness can honor it per reviewer subagent. The plugin itself never reads
26
+ * or modifies the provider request body.
24
27
  */
25
28
  reasoning_effort?: 'low' | 'medium' | 'high'
26
29
  reviewer: {
@@ -226,7 +229,9 @@ export interface IterationStatus {
226
229
  findingsCount: number
227
230
  totalDecisionLogEntries: number
228
231
  hasCheckpoint: boolean
229
- /** True when a checkpoint is present — i.e. the previous run was interrupted before finishing. */
232
+ /** True when a checkpoint is present AND no decision-log entry is newer than
233
+ * it — i.e. the previous run left a checkpoint and nothing has logged since
234
+ * (a live run that saved progress keeps `interrupted=false`). */
230
235
  interrupted: boolean
231
236
  /** How many times the current checkpoint has already been resumed. */
232
237
  resumeCount: number