iterate-plugin 3.5.3 → 3.5.5

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.
Files changed (53) hide show
  1. package/dist/atomic-fs.js +22 -4
  2. package/dist/config-loader.js +8 -3
  3. package/dist/config-write.js +50 -9
  4. package/dist/evidence.js +28 -1
  5. package/dist/git-scope.js +14 -5
  6. package/dist/jobs.js +15 -1
  7. package/dist/live.js +39 -22
  8. package/dist/meta-review.js +2 -2
  9. package/dist/review-scope.js +6 -2
  10. package/dist/review.js +7 -6
  11. package/dist/session-hooks.js +48 -4
  12. package/dist/skill-prompt.js +66 -32
  13. package/dist/tools/context.js +5 -1
  14. package/dist/tools/decision-log.js +142 -17
  15. package/dist/tools/defense-events.js +7 -3
  16. package/dist/tools/defense-store.js +14 -7
  17. package/dist/tools/experience-bank.js +6 -3
  18. package/dist/tools/experience-store.js +24 -12
  19. package/dist/tools/fix.js +24 -3
  20. package/dist/tools/prune.js +185 -17
  21. package/dist/tools/review.js +17 -3
  22. package/dist/tools/transcript.js +28 -6
  23. package/dist/tools/triage.js +38 -4
  24. package/dist/transcript.js +68 -34
  25. package/lib/client.js +41 -11
  26. package/lib/parse.js +30 -2
  27. package/package.json +10 -7
  28. package/src/atomic-fs.ts +20 -4
  29. package/src/client/index.ts +23 -3
  30. package/src/config-loader.ts +8 -3
  31. package/src/config-write.ts +59 -9
  32. package/src/evidence.ts +29 -1
  33. package/src/git-scope.ts +14 -3
  34. package/src/jobs.ts +15 -1
  35. package/src/live.ts +41 -22
  36. package/src/meta-review.ts +2 -2
  37. package/src/review-scope.ts +6 -2
  38. package/src/review.ts +7 -6
  39. package/src/session-hooks.ts +50 -4
  40. package/src/skill-prompt.ts +66 -32
  41. package/src/tools/context.ts +5 -1
  42. package/src/tools/decision-log.ts +136 -16
  43. package/src/tools/defense-events.ts +7 -3
  44. package/src/tools/defense-store.ts +14 -7
  45. package/src/tools/experience-bank.ts +6 -3
  46. package/src/tools/experience-store.ts +24 -12
  47. package/src/tools/fix.ts +25 -3
  48. package/src/tools/prune.ts +194 -17
  49. package/src/tools/review.ts +16 -3
  50. package/src/tools/transcript.ts +26 -5
  51. package/src/tools/triage.ts +39 -4
  52. package/src/transcript.ts +68 -31
  53. package/src/types.ts +2 -0
package/src/transcript.ts CHANGED
@@ -71,6 +71,20 @@ function clampStringList(source: string[], cap: number): string[] {
71
71
  return out
72
72
  }
73
73
 
74
+ /**
75
+ * Deep-clone a decision-timeline payload so it is decoupled from the caller's
76
+ * live object. structuredClone is available in every supported Node release;
77
+ * on the off chance it is missing, fall back to a JSON round-trip (decision
78
+ * payloads are JSON-serializable by construction).
79
+ */
80
+ function cloneDecisionData(data: Record<string, unknown>): Record<string, unknown> {
81
+ try {
82
+ return structuredClone(data)
83
+ } catch {
84
+ return JSON.parse(JSON.stringify(data)) as Record<string, unknown>
85
+ }
86
+ }
87
+
74
88
  /** Normalize a single finding, dropping malformed entries. */
75
89
  function normalizeFinding(input: unknown): TranscriptFinding | null {
76
90
  if (!input || typeof input !== 'object') return null
@@ -113,6 +127,19 @@ interface LiveRound {
113
127
  threads: LiveThread[]
114
128
  }
115
129
 
130
+ /**
131
+ * Append one raw finding to a thread's list, bound to MAX_FINDINGS_PER_THREAD
132
+ * with NEWEST-WINS eviction (drop the oldest findings once the cap is hit).
133
+ * A single oversized fixer agent could otherwise OOM the client with an
134
+ * unbounded finding dump inside one thread.
135
+ */
136
+ function pushThreadFinding(thread: LiveThread, raw: unknown): void {
137
+ thread.findings.push(raw)
138
+ if (thread.findings.length > MAX_FINDINGS_PER_THREAD) {
139
+ thread.findings.splice(0, thread.findings.length - MAX_FINDINGS_PER_THREAD)
140
+ }
141
+ }
142
+
116
143
  /** Merge a report snapshot's findings/readFiles into a thread by dimension. */
117
144
  function mergeReportIntoThread(
118
145
  thread: LiveThread,
@@ -120,8 +147,7 @@ function mergeReportIntoThread(
120
147
  readFiles: readonly unknown[] | undefined,
121
148
  ): void {
122
149
  for (const raw of findings) {
123
- const f = normalizeFinding(raw)
124
- if (f) thread.findings.push(raw)
150
+ if (normalizeFinding(raw)) pushThreadFinding(thread, raw)
125
151
  }
126
152
  for (const r of readFiles ?? []) {
127
153
  if (typeof r === 'string') thread.readFiles.push(r)
@@ -142,9 +168,11 @@ export class ReviewTranscriptBuilder {
142
168
  private round = 0
143
169
  private maxRounds = 0
144
170
  private active = true
171
+ private stoppedReason: string | null = null
145
172
  private readonly rounds: LiveRound[] = []
146
173
  private readonly convergence: number[] = []
147
174
  private readonly globalFindings: TranscriptFinding[] = []
175
+ private readonly globalSeenKeys = new Set<string>()
148
176
  private readonly fixes: TranscriptFix[] = []
149
177
  private checkpoint: TranscriptCheckpoint | null = null
150
178
  private readonly timeline: TranscriptEntry[] = []
@@ -203,9 +231,14 @@ export class ReviewTranscriptBuilder {
203
231
  this.touch()
204
232
  }
205
233
 
206
- /** End the run (stops the "active" pulsing in the UI). */
207
- finish(): void {
234
+ /**
235
+ * End the run (stops the "active" pulsing in the UI). `reason` records WHY
236
+ * it ended, so a run stopped by max-rounds or validation is never mistaken
237
+ * for a live run NOR for a clean convergence.
238
+ */
239
+ finish(reason?: string): void {
208
240
  this.active = false
241
+ if (typeof reason === 'string' && reason.trim()) this.stoppedReason = reason.trim()
209
242
  this.touch()
210
243
  }
211
244
 
@@ -276,12 +309,11 @@ export class ReviewTranscriptBuilder {
276
309
  for (const raw of findings) {
277
310
  const f = normalizeFinding(raw)
278
311
  if (f) {
279
- thread.findings.push(raw)
280
- this.globalFindings.push(f)
312
+ pushThreadFinding(thread, raw)
313
+ this.addGlobal(f)
281
314
  }
282
315
  }
283
316
  }
284
- this.reevaluateGlobal()
285
317
  this.touch()
286
318
  }
287
319
 
@@ -293,9 +325,8 @@ export class ReviewTranscriptBuilder {
293
325
  mergeReportIntoThread(thread, findings, readFiles)
294
326
  for (const raw of findings) {
295
327
  const f = normalizeFinding(raw)
296
- if (f) this.globalFindings.push(f)
328
+ if (f) this.addGlobal(f)
297
329
  }
298
- this.reevaluateGlobal()
299
330
  this.touch()
300
331
  }
301
332
 
@@ -384,8 +415,12 @@ export class ReviewTranscriptBuilder {
384
415
  : this.round,
385
416
  type,
386
417
  data:
387
- entry.data && typeof entry.data === 'object'
388
- ? (entry.data as Record<string, unknown>)
418
+ entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
419
+ // Deep clone so a caller mutating its own object later can never
420
+ // alias into the timeline (serialize would then emit the mutated
421
+ // values instead of what was actually decided). Decision-log payloads
422
+ // are small JSON, so the clone cost is negligible.
423
+ ? cloneDecisionData(entry.data as Record<string, unknown>)
389
424
  : {},
390
425
  })
391
426
  if (this.timeline.length > MAX_TIMELINE) {
@@ -435,14 +470,14 @@ export class ReviewTranscriptBuilder {
435
470
  maxRounds: this.maxRounds,
436
471
  rounds,
437
472
  convergence: this.convergence,
438
- findings:
439
- this.globalFindings.length > MAX_FINDINGS_TOTAL
440
- ? this.globalFindings.slice(0, MAX_FINDINGS_TOTAL)
441
- : this.globalFindings,
473
+ // addGlobal maintains the newest-wins cap incrementally, so the array is
474
+ // already ≤ MAX_FINDINGS_TOTAL — no post-hoc slice needed.
475
+ findings: this.globalFindings,
442
476
  fixes: this.fixes,
443
477
  checkpoint: this.checkpoint,
444
478
  timeline: this.timeline,
445
479
  nudge: this.nudge,
480
+ stoppedReason: this.stoppedReason,
446
481
  approval: {
447
482
  active: this.approval !== 'allow',
448
483
  policy: this.approval,
@@ -470,22 +505,24 @@ export class ReviewTranscriptBuilder {
470
505
  return live.threads.length
471
506
  }
472
507
 
473
- /** Recompute the global finding list from per-thread findings (dedup). */
474
- private reevaluateGlobal(): void {
475
- // Rebuild from threads to derive a deterministic global list.
476
- this.globalFindings.length = 0
477
- const seen = new Set<string>()
478
- for (const r of this.rounds) {
479
- for (const t of r.threads) {
480
- for (const raw of t.findings) {
481
- const f = normalizeFinding(raw)
482
- if (!f) continue
483
- const key = `${f.file}\u0000${f.line ?? 0}\u0000${f.dimension}\u0000${f.summary}`
484
- if (seen.has(key)) continue
485
- seen.add(key)
486
- this.globalFindings.push(f)
487
- if (this.globalFindings.length >= MAX_FINDINGS_TOTAL) return
488
- }
508
+ /**
509
+ * Insert one deduplicated finding into the compact global list.
510
+ * NEWEST-WINS: when the global cap (MAX_FINDINGS_TOTAL) is hit, the oldest
511
+ * kept finding is evicted so fresh, still-relevant findings survive — this
512
+ * is the documented contract. Amortized O(1): dedupe via a seen-key set and
513
+ * eviction is one `shift` per overflow insert.
514
+ */
515
+ private addGlobal(f: TranscriptFinding): void {
516
+ const key = `${f.file}\u0000${f.line ?? 0}\u0000${f.dimension}\u0000${f.summary}`
517
+ if (this.globalSeenKeys.has(key)) return
518
+ this.globalSeenKeys.add(key)
519
+ this.globalFindings.push(f)
520
+ if (this.globalFindings.length > MAX_FINDINGS_TOTAL) {
521
+ const evicted = this.globalFindings.shift()
522
+ if (evicted) {
523
+ this.globalSeenKeys.delete(
524
+ `${evicted.file}\u0000${evicted.line ?? 0}\u0000${evicted.dimension}\u0000${evicted.summary}`,
525
+ )
489
526
  }
490
527
  }
491
528
  }
package/src/types.ts CHANGED
@@ -349,6 +349,8 @@ export interface TranscriptManifest {
349
349
  }
350
350
  /** v3.0: task_mode indicator from harness status */
351
351
  taskMode?: 'code' | 'iterate' | null
352
+ /** Why the run ended: converged / max_rounds_reached / aborted_by_validation. */
353
+ stoppedReason?: string | null
352
354
  }
353
355
 
354
356
  // ─── v3.0: Quality Gate ──────────────────────────────────────────────────────