iterate-plugin 3.5.2 → 3.5.4
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 +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/session-hooks.js +48 -4
- package/dist/skill-prompt.js +63 -29
- package/dist/tools/checkpoint.js +39 -5
- package/dist/tools/config.js +11 -2
- package/dist/tools/context.js +5 -1
- package/dist/tools/defense-events.js +48 -9
- package/dist/tools/defense-store.js +63 -5
- package/dist/tools/experience-bank.js +14 -11
- package/dist/tools/experience-store.js +79 -14
- package/dist/tools/fix.js +30 -10
- package/dist/tools/history.js +5 -3
- package/dist/tools/prune.js +197 -29
- package/dist/tools/quality-gate.js +3 -0
- package/dist/tools/quality-store.js +8 -2
- package/dist/tools/review.js +17 -3
- package/dist/tools/transcript.js +72 -15
- package/dist/tools/triage.js +35 -2
- package/dist/transcript.js +30 -7
- package/lib/client.js +89 -21
- package/lib/parse.js +27 -2
- package/package.json +9 -7
- package/src/client/index.ts +53 -8
- package/src/session-hooks.ts +50 -4
- package/src/skill-prompt.ts +63 -29
- package/src/tools/checkpoint.ts +45 -6
- package/src/tools/config.ts +11 -2
- package/src/tools/context.ts +5 -1
- package/src/tools/defense-events.ts +50 -9
- package/src/tools/defense-store.ts +64 -8
- package/src/tools/experience-bank.ts +14 -11
- package/src/tools/experience-store.ts +88 -23
- package/src/tools/fix.ts +32 -12
- package/src/tools/history.ts +5 -3
- package/src/tools/prune.ts +204 -28
- package/src/tools/quality-gate.ts +4 -0
- package/src/tools/quality-store.ts +7 -2
- package/src/tools/review.ts +16 -3
- package/src/tools/transcript.ts +71 -14
- package/src/tools/triage.ts +36 -2
- package/src/transcript.ts +32 -7
- package/src/types.ts +2 -0
package/src/tools/review.ts
CHANGED
|
@@ -265,9 +265,22 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
265
265
|
const coverageEnabled = config.reviewer?.coverage_validation !== false
|
|
266
266
|
let coverage: CoverageResult | null = null
|
|
267
267
|
if (coverageEnabled) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
// Build the assigned inventory the SAME way `plan` builds it: a
|
|
269
|
+
// changed-only scope uses the resolved git-diff file set, and
|
|
270
|
+
// falls back to the FULL walk when git is unavailable or nothing
|
|
271
|
+
// changed (mirrors buildReviewPlan's effective-scope decision), so
|
|
272
|
+
// the coverage ratio reflects the files reviewers were actually
|
|
273
|
+
// told to read — never a vacuous 1.0 on an empty inventory.
|
|
274
|
+
let assigned: string[]
|
|
275
|
+
if (config.review?.scope === 'changed-only') {
|
|
276
|
+
const gitScope = await resolveChangedFiles(projectRoot, config.git?.target_branch ?? 'main')
|
|
277
|
+
assigned =
|
|
278
|
+
gitScope.changedFiles.length > 0
|
|
279
|
+
? collectScopeFiles(projectRoot, { scope: 'changed-only', changedFiles: gitScope.changedFiles })
|
|
280
|
+
: collectScopeFiles(projectRoot, { scope: 'full' })
|
|
281
|
+
} else {
|
|
282
|
+
assigned = collectScopeFiles(projectRoot, { scope: 'full' })
|
|
283
|
+
}
|
|
271
284
|
const readFiles = Array.isArray((source as unknown as { readFiles?: unknown }).readFiles)
|
|
272
285
|
? ((source as unknown as { readFiles?: unknown }).readFiles as string[])
|
|
273
286
|
: null
|
package/src/tools/transcript.ts
CHANGED
|
@@ -44,7 +44,10 @@ function captureRound(builder: ReviewTranscriptBuilder, round: unknown): void {
|
|
|
44
44
|
findings?: unknown
|
|
45
45
|
readFiles?: unknown
|
|
46
46
|
}
|
|
47
|
-
const roundNo = typeof r.round === 'number' ? Math.floor(r.round) : 0
|
|
47
|
+
const roundNo = typeof r.round === 'number' && Number.isFinite(r.round) ? Math.floor(r.round) : 0
|
|
48
|
+
// NaN round values (`Math.floor(NaN) === NaN`, and `NaN <= 0` is false) would
|
|
49
|
+
// fall through the guard below; a finite check rejects them outright so a
|
|
50
|
+
// malformed round can never collapse into round 1 via the builder's coercion.
|
|
48
51
|
if (roundNo <= 0) return
|
|
49
52
|
builder.roundStart(roundNo)
|
|
50
53
|
const findings = Array.isArray(r.findings) ? r.findings : []
|
|
@@ -70,14 +73,16 @@ function captureRound(builder: ReviewTranscriptBuilder, round: unknown): void {
|
|
|
70
73
|
function normalizeCheckpoint(input: unknown): TranscriptManifest['checkpoint'] {
|
|
71
74
|
if (!input || typeof input !== 'object') return null
|
|
72
75
|
const c = input as Record<string, unknown>
|
|
73
|
-
|
|
76
|
+
// Number.isFinite: a NaN round must not pass `round <= 0` (which is false for
|
|
77
|
+
// NaN) and later coerce into a phantom round 1 row.
|
|
78
|
+
const round = typeof c.round === 'number' && Number.isFinite(c.round) ? c.round : 0
|
|
74
79
|
if (round <= 0) return null
|
|
75
80
|
return {
|
|
76
81
|
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : 'normal',
|
|
77
82
|
round,
|
|
78
|
-
maxRounds: typeof c.maxRounds === 'number' ? c.maxRounds : 0,
|
|
79
|
-
fixedCount: typeof c.fixedCount === 'number' ? c.fixedCount : 0,
|
|
80
|
-
resumeCount: typeof c.resumeCount === 'number' ? c.resumeCount : 0,
|
|
83
|
+
maxRounds: typeof c.maxRounds === 'number' && Number.isFinite(c.maxRounds) ? c.maxRounds : 0,
|
|
84
|
+
fixedCount: typeof c.fixedCount === 'number' && Number.isFinite(c.fixedCount) ? c.fixedCount : 0,
|
|
85
|
+
resumeCount: typeof c.resumeCount === 'number' && Number.isFinite(c.resumeCount) ? c.resumeCount : 0,
|
|
81
86
|
updatedAt: typeof c.updatedAt === 'string' ? c.updatedAt : new Date().toISOString(),
|
|
82
87
|
}
|
|
83
88
|
}
|
|
@@ -92,7 +97,7 @@ function normalizeFix(input: unknown): TranscriptFix | null {
|
|
|
92
97
|
return {
|
|
93
98
|
id,
|
|
94
99
|
timestamp: typeof f.timestamp === 'string' ? f.timestamp : new Date().toISOString(),
|
|
95
|
-
round: typeof f.round === 'number' ? f.round : 0,
|
|
100
|
+
round: typeof f.round === 'number' && Number.isFinite(f.round) ? f.round : 0,
|
|
96
101
|
file,
|
|
97
102
|
summary: typeof f.summary === 'string' ? f.summary : '',
|
|
98
103
|
linesAdded: typeof f.linesAdded === 'number' ? f.linesAdded : 0,
|
|
@@ -155,6 +160,7 @@ export function registerTranscriptTool(ctx: {
|
|
|
155
160
|
description: 'For `capture`: array of applied fixes [{id, file, round, summary, linesAdded, linesRemoved, success}].',
|
|
156
161
|
},
|
|
157
162
|
refReadFiles: { type: 'json', description: 'For `capture`: flat array of all read files across rounds (optional).' },
|
|
163
|
+
stoppedReason: { type: 'string', description: 'For `capture`: why the run ended — "converged" | "max_rounds_reached" | "aborted_by_validation". When omitted, derived from the convergence trend (trailing 0 = converged, otherwise = max_rounds_reached once a round ran).' },
|
|
158
164
|
text: { type: 'string', description: 'For `nudge`: steering text to set (or null to clear).' },
|
|
159
165
|
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
160
166
|
},
|
|
@@ -224,11 +230,35 @@ export function registerTranscriptTool(ctx: {
|
|
|
224
230
|
manifest = null
|
|
225
231
|
}
|
|
226
232
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
// Rehydrating a parsed-but-malformed manifest (e.g. `rounds: [null]`
|
|
234
|
+
// or a non-object) must never crash the nudge — degrade to a fresh
|
|
235
|
+
// builder so the steering text still lands. The fallback must keep
|
|
236
|
+
// the ORIGINAL run identity (mode/taskMode/goal/maxRounds) so a
|
|
237
|
+
// stray malformed field can never silently rewrite a dry-run run
|
|
238
|
+
// into a `normal` one.
|
|
239
|
+
const fallbackOpts = {
|
|
240
|
+
project: projectRoot,
|
|
241
|
+
mode: (manifest?.mode === 'dry-run' || manifest?.mode === 'normal' ? manifest.mode : null) as 'dry-run' | 'normal' | null,
|
|
242
|
+
taskMode: (manifest?.taskMode === 'code' || manifest?.taskMode === 'iterate' ? manifest.taskMode : null) as 'code' | 'iterate' | null,
|
|
243
|
+
approval,
|
|
244
|
+
goal: typeof manifest?.goal === 'string' ? manifest.goal : '',
|
|
245
|
+
maxRounds: typeof manifest?.maxRounds === 'number' ? manifest.maxRounds : 0,
|
|
246
|
+
}
|
|
247
|
+
let builder: ReviewTranscriptBuilder
|
|
248
|
+
if (manifest) {
|
|
249
|
+
try {
|
|
250
|
+
builder = rehydrateBuilder(manifest, approval)
|
|
251
|
+
} catch {
|
|
252
|
+
builder = new ReviewTranscriptBuilder(fallbackOpts)
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
builder = new ReviewTranscriptBuilder({ project: projectRoot, mode: 'normal', approval })
|
|
256
|
+
}
|
|
230
257
|
builder.setNudge(typeof args.text === 'string' && args.text.trim() ? args.text : null)
|
|
231
|
-
await
|
|
258
|
+
const persisted = await persistChecked(file, builder.serialize())
|
|
259
|
+
if (!persisted.ok) {
|
|
260
|
+
return { operation: 'nudge', updated: false, error: persisted.error }
|
|
261
|
+
}
|
|
232
262
|
return {
|
|
233
263
|
operation: 'nudge',
|
|
234
264
|
updated: true,
|
|
@@ -287,11 +317,23 @@ export function registerTranscriptTool(ctx: {
|
|
|
287
317
|
if (record) builder.fix(record)
|
|
288
318
|
}
|
|
289
319
|
}
|
|
290
|
-
//
|
|
320
|
+
// End state: an explicit stoppedReason wins; otherwise derive it from
|
|
321
|
+
// the trend. A run that settled closes as "converged"; a run that did
|
|
322
|
+
// real work but never trended to 0 closes as "max_rounds_reached";
|
|
323
|
+
// a capture with no rounds stays "active" (nothing recorded yet).
|
|
324
|
+
const explicitReason =
|
|
325
|
+
typeof args.stoppedReason === 'string' && args.stoppedReason.trim()
|
|
326
|
+
? args.stoppedReason.trim()
|
|
327
|
+
: ''
|
|
291
328
|
const last = convergence[convergence.length - 1]
|
|
292
|
-
if (
|
|
329
|
+
if (explicitReason) builder.finish(explicitReason)
|
|
330
|
+
else if (convergence.length > 0 && last === 0) builder.finish('converged')
|
|
331
|
+
else if (roundsExecuted > 0) builder.finish('max_rounds_reached')
|
|
293
332
|
|
|
294
|
-
await
|
|
333
|
+
const persisted = await persistChecked(file, builder.serialize())
|
|
334
|
+
if (!persisted.ok) {
|
|
335
|
+
return { operation: 'capture', found: true, updated: false, error: persisted.error }
|
|
336
|
+
}
|
|
295
337
|
const live = await readLive(projectRoot)
|
|
296
338
|
return {
|
|
297
339
|
operation: 'capture',
|
|
@@ -332,7 +374,7 @@ function rehydrateBuilder(manifest: TranscriptManifest, approval: 'ask' | 'deny'
|
|
|
332
374
|
if (Array.isArray(manifest.fixes)) for (const fx of manifest.fixes) builder.fix(fx as TranscriptFix)
|
|
333
375
|
if (Array.isArray(manifest.timeline)) for (const e of manifest.timeline) builder.decision(e)
|
|
334
376
|
builder.setNudge(manifest.nudge?.text ?? null)
|
|
335
|
-
if (!manifest.active) builder.finish()
|
|
377
|
+
if (!manifest.active) builder.finish(manifest.stoppedReason ?? undefined)
|
|
336
378
|
return builder
|
|
337
379
|
}
|
|
338
380
|
|
|
@@ -342,6 +384,21 @@ async function persist(file: string, manifest: TranscriptManifest): Promise<void
|
|
|
342
384
|
await writeTextAtomicAsync(file, JSON.stringify(manifest, null, 2))
|
|
343
385
|
}
|
|
344
386
|
|
|
387
|
+
/** persist wrapped with a structured error channel so disk failures surface
|
|
388
|
+
* as a structured tool result instead of rejecting the whole execute (matches
|
|
389
|
+
* every other write path in this codebase). */
|
|
390
|
+
async function persistChecked(
|
|
391
|
+
file: string,
|
|
392
|
+
manifest: TranscriptManifest,
|
|
393
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
394
|
+
try {
|
|
395
|
+
await persist(file, manifest)
|
|
396
|
+
return { ok: true }
|
|
397
|
+
} catch (err) {
|
|
398
|
+
return { ok: false, error: `failed to persist transcript: ${err instanceof Error ? err.message : String(err)}` }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
345
402
|
/**
|
|
346
403
|
* Mark one applied fix as rolled back in the PERSISTED transcript, so the
|
|
347
404
|
* client observatory (F4 fix/rollback tab) reflects the reversal instead of
|
package/src/tools/triage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
1
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join, dirname, basename } from 'node:path'
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
4
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
5
5
|
import yaml from 'js-yaml'
|
|
@@ -16,6 +16,9 @@ const KNOWN_INTENTIONAL_KEY = 'known_intentional'
|
|
|
16
16
|
/** Max entries per single `apply` call. */
|
|
17
17
|
const MAX_ENTRIES = 500
|
|
18
18
|
|
|
19
|
+
/** How many timestamped config backups are retained (older ones are removed). */
|
|
20
|
+
export const MAX_TRIAGE_BACKUPS = 5
|
|
21
|
+
|
|
19
22
|
/** Whole-file marker line (matches review.ts filterKnownIntentional semantics). */
|
|
20
23
|
const WHOLE_FILE_LINE = 0
|
|
21
24
|
|
|
@@ -171,6 +174,33 @@ export function backupSuffix(now = new Date()): string {
|
|
|
171
174
|
return now.toISOString().replace(/[:.]/g, '-')
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Bound the timestamped config backups: after a fresh one is written, delete
|
|
179
|
+
* every older `config.bak-*` file beyond the newest `keep`. Best-effort — a
|
|
180
|
+
* filesystem failure here must never fail the apply that just succeeded
|
|
181
|
+
* (backups are a safety net, not a requirement).
|
|
182
|
+
* @returns the absolute paths of the backups that were removed.
|
|
183
|
+
*/
|
|
184
|
+
export function pruneOldConfigBackups(configPath: string, keep = MAX_TRIAGE_BACKUPS): string[] {
|
|
185
|
+
const removed: string[] = []
|
|
186
|
+
try {
|
|
187
|
+
const dir = dirname(configPath)
|
|
188
|
+
const prefix = `${basename(configPath)}.bak-`
|
|
189
|
+
const matches = readdirSync(dir)
|
|
190
|
+
.filter((f) => f.startsWith(prefix))
|
|
191
|
+
.sort()
|
|
192
|
+
// Keep the newest `keep`; remove everything older.
|
|
193
|
+
const doomed = matches.slice(0, Math.max(0, matches.length - keep))
|
|
194
|
+
for (const f of doomed) {
|
|
195
|
+
rmSync(join(dir, f), { force: true })
|
|
196
|
+
removed.push(join(dir, f))
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// Best-effort cleanup — never surface a cleanup failure.
|
|
200
|
+
}
|
|
201
|
+
return removed
|
|
202
|
+
}
|
|
203
|
+
|
|
174
204
|
// ─── File I/O ───────────────────────────────────────────────────────────────
|
|
175
205
|
|
|
176
206
|
/** Load the raw config object (empty when the file is missing). */
|
|
@@ -248,6 +278,10 @@ function applyEntries(
|
|
|
248
278
|
}
|
|
249
279
|
}
|
|
250
280
|
|
|
281
|
+
// Success: bound the accumulation of timestamped backups so a long-lived
|
|
282
|
+
// project never collects an unbounded pile of config snapshots.
|
|
283
|
+
if (backupPath) pruneOldConfigBackups(configPath)
|
|
284
|
+
|
|
251
285
|
return { ok: true, added, skipped, count: merged.length, configPath, backupPath }
|
|
252
286
|
}
|
|
253
287
|
|
package/src/transcript.ts
CHANGED
|
@@ -53,6 +53,11 @@ const MAX_TIMELINE = 500
|
|
|
53
53
|
* grow the manifest payload without limit). */
|
|
54
54
|
const MAX_FIXES = 200
|
|
55
55
|
|
|
56
|
+
/** Max round number accepted by the builder. A model-authored/manifest-backed
|
|
57
|
+
* round value is attacker-influenced JSON: `roundStart(1e9)` / `snapshotConvergence`
|
|
58
|
+
* would otherwise preallocate arrays of that size and OOM the host. */
|
|
59
|
+
const MAX_ROUNDS = 1000
|
|
60
|
+
|
|
56
61
|
/** Thresholds applied when reducing a string list under a cap. */
|
|
57
62
|
function clampStringList(source: string[], cap: number): string[] {
|
|
58
63
|
const out: string[] = []
|
|
@@ -137,6 +142,7 @@ export class ReviewTranscriptBuilder {
|
|
|
137
142
|
private round = 0
|
|
138
143
|
private maxRounds = 0
|
|
139
144
|
private active = true
|
|
145
|
+
private stoppedReason: string | null = null
|
|
140
146
|
private readonly rounds: LiveRound[] = []
|
|
141
147
|
private readonly convergence: number[] = []
|
|
142
148
|
private readonly globalFindings: TranscriptFinding[] = []
|
|
@@ -198,18 +204,27 @@ export class ReviewTranscriptBuilder {
|
|
|
198
204
|
this.touch()
|
|
199
205
|
}
|
|
200
206
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
207
|
+
/**
|
|
208
|
+
* End the run (stops the "active" pulsing in the UI). `reason` records WHY
|
|
209
|
+
* it ended, so a run stopped by max-rounds or validation is never mistaken
|
|
210
|
+
* for a live run NOR for a clean convergence.
|
|
211
|
+
*/
|
|
212
|
+
finish(reason?: string): void {
|
|
203
213
|
this.active = false
|
|
214
|
+
if (typeof reason === 'string' && reason.trim()) this.stoppedReason = reason.trim()
|
|
204
215
|
this.touch()
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
/** Open a review round, capturing the current round index. */
|
|
208
219
|
roundStart(round: number, maxRounds?: number): void {
|
|
209
220
|
const r = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1
|
|
210
|
-
|
|
221
|
+
// Clamp to MAX_ROUNDS: `this.rounds.length < this.round` below preallocates
|
|
222
|
+
// an array of size `round` — an unbounded model-controlled value (e.g. 1e9)
|
|
223
|
+
// would OOM the host. Values above the cap are folded into the cap so the
|
|
224
|
+
// real rounds are never silently dropped.
|
|
225
|
+
this.round = Math.min(r > 0 ? r : 1, MAX_ROUNDS)
|
|
211
226
|
if (typeof maxRounds === 'number' && Number.isFinite(maxRounds) && maxRounds >= 0) {
|
|
212
|
-
this.maxRounds = Math.floor(maxRounds)
|
|
227
|
+
this.maxRounds = Math.min(Math.floor(maxRounds), MAX_ROUNDS)
|
|
213
228
|
}
|
|
214
229
|
while (this.rounds.length < this.round) {
|
|
215
230
|
this.rounds.push({ round: this.rounds.length + 1, threads: [] })
|
|
@@ -294,7 +309,10 @@ export class ReviewTranscriptBuilder {
|
|
|
294
309
|
|
|
295
310
|
/** Record a round's new-finding count for the convergence series. */
|
|
296
311
|
snapshotConvergence(round: number, newCount: number): void {
|
|
297
|
-
const
|
|
312
|
+
const rawR = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1
|
|
313
|
+
// Clamp (mirrors roundStart): the loop below preallocates `r` slots, so an
|
|
314
|
+
// unbounded model-controlled round number would OOM the host.
|
|
315
|
+
const r = Math.min(rawR > 0 ? rawR : 1, MAX_ROUNDS)
|
|
298
316
|
const n = typeof newCount === 'number' && Number.isFinite(newCount) ? newCount : 0
|
|
299
317
|
while (this.convergence.length < r) this.convergence.push(-1)
|
|
300
318
|
this.convergence[r - 1] = Math.floor(n)
|
|
@@ -312,7 +330,10 @@ export class ReviewTranscriptBuilder {
|
|
|
312
330
|
this.fixes.push({
|
|
313
331
|
id,
|
|
314
332
|
timestamp: typeof record.timestamp === 'string' ? record.timestamp : isoNow(),
|
|
315
|
-
round:
|
|
333
|
+
round:
|
|
334
|
+
typeof record.round === 'number' && Number.isFinite(record.round)
|
|
335
|
+
? Math.floor(record.round)
|
|
336
|
+
: this.round,
|
|
316
337
|
file,
|
|
317
338
|
summary: typeof record.summary === 'string' ? record.summary : '',
|
|
318
339
|
linesAdded:
|
|
@@ -363,7 +384,10 @@ export class ReviewTranscriptBuilder {
|
|
|
363
384
|
const type = typeof entry.type === 'string' ? entry.type : 'decision'
|
|
364
385
|
this.timeline.push({
|
|
365
386
|
timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : isoNow(),
|
|
366
|
-
round:
|
|
387
|
+
round:
|
|
388
|
+
typeof entry.round === 'number' && Number.isFinite(entry.round)
|
|
389
|
+
? Math.floor(entry.round)
|
|
390
|
+
: this.round,
|
|
367
391
|
type,
|
|
368
392
|
data:
|
|
369
393
|
entry.data && typeof entry.data === 'object'
|
|
@@ -425,6 +449,7 @@ export class ReviewTranscriptBuilder {
|
|
|
425
449
|
checkpoint: this.checkpoint,
|
|
426
450
|
timeline: this.timeline,
|
|
427
451
|
nudge: this.nudge,
|
|
452
|
+
stoppedReason: this.stoppedReason,
|
|
428
453
|
approval: {
|
|
429
454
|
active: this.approval !== 'allow',
|
|
430
455
|
policy: this.approval,
|
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 ──────────────────────────────────────────────────────
|