iterate-plugin 3.5.4 → 3.5.6
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/dist/atomic-fs.js +22 -4
- package/dist/config-loader.js +30 -5
- package/dist/config-write.js +78 -9
- package/dist/evidence.js +33 -2
- package/dist/git-scope.js +14 -5
- package/dist/jobs.js +15 -1
- package/dist/live.js +39 -22
- package/dist/meta-review.js +7 -3
- package/dist/review-scope.js +6 -2
- package/dist/review.js +34 -15
- package/dist/skill-prompt.js +63 -24
- package/dist/tools/checkpoint.js +11 -4
- package/dist/tools/decision-log.js +142 -17
- package/dist/tools/fix.js +24 -3
- package/dist/tools/history.js +1 -1
- package/dist/tools/prune.js +57 -26
- package/dist/tools/transcript.js +17 -6
- package/dist/tools/triage.js +3 -2
- package/dist/transcript.js +136 -34
- package/lib/client.js +67 -31
- package/lib/parse.js +34 -0
- package/package.json +12 -8
- package/src/atomic-fs.ts +20 -4
- package/src/client/index.ts +45 -24
- package/src/config-loader.ts +28 -5
- package/src/config-write.ts +91 -9
- package/src/evidence.ts +34 -2
- package/src/git-scope.ts +14 -3
- package/src/jobs.ts +15 -1
- package/src/live.ts +41 -22
- package/src/meta-review.ts +7 -3
- package/src/review-scope.ts +6 -2
- package/src/review.ts +47 -14
- package/src/skill-prompt.ts +63 -24
- package/src/tools/checkpoint.ts +12 -4
- package/src/tools/decision-log.ts +136 -16
- package/src/tools/fix.ts +25 -3
- package/src/tools/history.ts +1 -1
- package/src/tools/prune.ts +51 -23
- package/src/tools/transcript.ts +16 -5
- package/src/tools/triage.ts +3 -2
- package/src/transcript.ts +144 -31
- package/src/types.ts +9 -4
package/dist/tools/transcript.js
CHANGED
|
@@ -148,7 +148,7 @@ export function registerTranscriptTool(ctx) {
|
|
|
148
148
|
description: 'For `capture`: array of applied fixes [{id, file, round, summary, linesAdded, linesRemoved, success}].',
|
|
149
149
|
},
|
|
150
150
|
refReadFiles: { type: 'json', description: 'For `capture`: flat array of all read files across rounds (optional).' },
|
|
151
|
-
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).' },
|
|
151
|
+
stoppedReason: { type: 'string', description: 'For `capture`: why the run ended — "converged" | "max_rounds_reached" | "aborted_by_validation" | "aborted_by_config" | "schema_invalid" | "no_usable_reviewer_output" | "inconclusive". When omitted, derived from the convergence trend (trailing 0 = converged, otherwise = max_rounds_reached once a round ran).' },
|
|
152
152
|
text: { type: 'string', description: 'For `nudge`: steering text to set (or null to clear).' },
|
|
153
153
|
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
154
154
|
},
|
|
@@ -344,13 +344,24 @@ function rehydrateBuilder(manifest, approval) {
|
|
|
344
344
|
maxRounds: manifest.maxRounds,
|
|
345
345
|
});
|
|
346
346
|
for (const r of Array.isArray(manifest.rounds) ? manifest.rounds : []) {
|
|
347
|
+
// A malformed/null round row must be skipped, never a throw (the caller
|
|
348
|
+
// wraps rehydrate in a fallback, but preserving sibling rows is better).
|
|
349
|
+
if (!r || typeof r !== 'object')
|
|
350
|
+
continue;
|
|
347
351
|
builder.roundStart(r.round, manifest.maxRounds);
|
|
348
352
|
for (const t of Array.isArray(r.threads) ? r.threads : []) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
// restoreThread (not reviewerStart + reviewerMessage(join)) preserves the
|
|
354
|
+
// message ARRAY boundaries and the thread's own dimension: rehydration
|
|
355
|
+
// previously collapsed each thread's messages into one string and, once a
|
|
356
|
+
// round hit the per-round thread cap, silently DROPPED the extra threads
|
|
357
|
+
// / mis-merged their findings into the previous dimension's thread.
|
|
358
|
+
builder.restoreThread(r.round, {
|
|
359
|
+
dimension: t.dimension,
|
|
360
|
+
attempt: t.attempt,
|
|
361
|
+
messages: t.messages,
|
|
362
|
+
readFiles: t.readFiles,
|
|
363
|
+
findings: t.findings,
|
|
364
|
+
});
|
|
354
365
|
}
|
|
355
366
|
}
|
|
356
367
|
for (let idx = 0; idx < (manifest.convergence ?? []).length; idx += 1) {
|
package/dist/tools/triage.js
CHANGED
|
@@ -184,8 +184,9 @@ function readConfigFile(configPath) {
|
|
|
184
184
|
return {};
|
|
185
185
|
const content = readFileSync(configPath, 'utf-8');
|
|
186
186
|
const parsed = yaml.load(content);
|
|
187
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
188
|
-
// A config that exists but is
|
|
187
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
188
|
+
// A config that exists but is NOT a YAML mapping (including a YAML list,
|
|
189
|
+
// which `typeof [] === 'object'` alone would accept) must NOT be silently
|
|
189
190
|
// treated as empty: writing over it would destroy user data. Callers
|
|
190
191
|
// surface this as an error and refuse to write.
|
|
191
192
|
throw new Error('existing iterate.config.yaml is not a valid YAML mapping');
|
package/dist/transcript.js
CHANGED
|
@@ -54,6 +54,20 @@ function clampStringList(source, cap) {
|
|
|
54
54
|
}
|
|
55
55
|
return out;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Deep-clone a decision-timeline payload so it is decoupled from the caller's
|
|
59
|
+
* live object. structuredClone is available in every supported Node release;
|
|
60
|
+
* on the off chance it is missing, fall back to a JSON round-trip (decision
|
|
61
|
+
* payloads are JSON-serializable by construction).
|
|
62
|
+
*/
|
|
63
|
+
function cloneDecisionData(data) {
|
|
64
|
+
try {
|
|
65
|
+
return structuredClone(data);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return JSON.parse(JSON.stringify(data));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
/** Normalize a single finding, dropping malformed entries. */
|
|
58
72
|
function normalizeFinding(input) {
|
|
59
73
|
if (!input || typeof input !== 'object')
|
|
@@ -68,7 +82,9 @@ function normalizeFinding(input) {
|
|
|
68
82
|
const severity = sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low'
|
|
69
83
|
? sev
|
|
70
84
|
: 'low';
|
|
71
|
-
|
|
85
|
+
// Only a non-negative integer is a real line anchor; a fractional/malformed
|
|
86
|
+
// value (e.g. 3.7 from a bypassed schema) must not leak into the manifest.
|
|
87
|
+
const line = typeof f.line === 'number' && Number.isInteger(f.line) && f.line >= 0 ? f.line : 0;
|
|
72
88
|
return {
|
|
73
89
|
dimension,
|
|
74
90
|
file,
|
|
@@ -81,12 +97,23 @@ function normalizeFinding(input) {
|
|
|
81
97
|
acknowledged: typeof f.acknowledged === 'boolean' ? f.acknowledged : undefined,
|
|
82
98
|
};
|
|
83
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Append one raw finding to a thread's list, bound to MAX_FINDINGS_PER_THREAD
|
|
102
|
+
* with NEWEST-WINS eviction (drop the oldest findings once the cap is hit).
|
|
103
|
+
* A single oversized fixer agent could otherwise OOM the client with an
|
|
104
|
+
* unbounded finding dump inside one thread.
|
|
105
|
+
*/
|
|
106
|
+
function pushThreadFinding(thread, raw) {
|
|
107
|
+
thread.findings.push(raw);
|
|
108
|
+
if (thread.findings.length > MAX_FINDINGS_PER_THREAD) {
|
|
109
|
+
thread.findings.splice(0, thread.findings.length - MAX_FINDINGS_PER_THREAD);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
84
112
|
/** Merge a report snapshot's findings/readFiles into a thread by dimension. */
|
|
85
113
|
function mergeReportIntoThread(thread, findings, readFiles) {
|
|
86
114
|
for (const raw of findings) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
thread.findings.push(raw);
|
|
115
|
+
if (normalizeFinding(raw))
|
|
116
|
+
pushThreadFinding(thread, raw);
|
|
90
117
|
}
|
|
91
118
|
for (const r of readFiles ?? []) {
|
|
92
119
|
if (typeof r === 'string')
|
|
@@ -111,6 +138,7 @@ export class ReviewTranscriptBuilder {
|
|
|
111
138
|
rounds = [];
|
|
112
139
|
convergence = [];
|
|
113
140
|
globalFindings = [];
|
|
141
|
+
globalSeenKeys = new Set();
|
|
114
142
|
fixes = [];
|
|
115
143
|
checkpoint = null;
|
|
116
144
|
timeline = [];
|
|
@@ -193,7 +221,9 @@ export class ReviewTranscriptBuilder {
|
|
|
193
221
|
const att = typeof attempt === 'number' && Number.isFinite(attempt) ? Math.floor(attempt) : 1;
|
|
194
222
|
this.roundStart(this.round);
|
|
195
223
|
const live = this.rounds[this.round - 1];
|
|
196
|
-
if (live
|
|
224
|
+
if (!live)
|
|
225
|
+
return;
|
|
226
|
+
if (this.threadCount(live) < MAX_THREADS_PER_ROUND) {
|
|
197
227
|
live.threads.push({
|
|
198
228
|
dimension: dim || 'review',
|
|
199
229
|
attempt: att > 0 ? att : 1,
|
|
@@ -202,6 +232,78 @@ export class ReviewTranscriptBuilder {
|
|
|
202
232
|
findings: [],
|
|
203
233
|
});
|
|
204
234
|
}
|
|
235
|
+
else {
|
|
236
|
+
// Thread cap reached: route the extra dimension/retry into ONE shared
|
|
237
|
+
// "other" overflow thread instead of silently dropping it. A dropped
|
|
238
|
+
// reviewerStart used to leave `currentThread()` pointing at the PREVIOUS
|
|
239
|
+
// dimension's thread, so its findings were mis-attributed to that
|
|
240
|
+
// dimension. The overflow thread stays bounded (at most ONE extra thread
|
|
241
|
+
// per round) while keeping over-cap findings attributable to "other".
|
|
242
|
+
let overflow = live.threads.find((t) => t.dimension === 'other');
|
|
243
|
+
if (!overflow) {
|
|
244
|
+
overflow = {
|
|
245
|
+
dimension: 'other',
|
|
246
|
+
attempt: att > 0 ? att : 1,
|
|
247
|
+
messages: [],
|
|
248
|
+
readFiles: [],
|
|
249
|
+
findings: [],
|
|
250
|
+
};
|
|
251
|
+
live.threads.push(overflow);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.touch();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Restore ONE persisted thread during rehydration, preserving the message
|
|
258
|
+
* ARRAY boundaries (a rehydrated manifest is re-serialized on the next nudge
|
|
259
|
+
* edit, so collapsed messages would silently rewrite the evidence) and the
|
|
260
|
+
* thread's own dimension (never merging it into a previous dimension's
|
|
261
|
+
* thread). Bypasses the live per-round cap — the manifest already enforced
|
|
262
|
+
* it at capture time — but every restored field is hard-bounded so a hostile
|
|
263
|
+
* hand-edited manifest can never blow up memory.
|
|
264
|
+
*/
|
|
265
|
+
restoreThread(round, thread) {
|
|
266
|
+
const r = typeof round === 'number' && Number.isFinite(round)
|
|
267
|
+
? Math.min(Math.floor(round), MAX_ROUNDS)
|
|
268
|
+
: 1;
|
|
269
|
+
this.roundStart(r);
|
|
270
|
+
const live = this.rounds[this.round - 1];
|
|
271
|
+
if (!live)
|
|
272
|
+
return;
|
|
273
|
+
if (!thread || typeof thread !== 'object')
|
|
274
|
+
return;
|
|
275
|
+
// Hostile-manifest guard: keep the live hard bound (with headroom for the
|
|
276
|
+
// single overflow thread) and newest-first truncate beyond it.
|
|
277
|
+
if (live.threads.length >= MAX_THREADS_PER_ROUND * 2 + 1)
|
|
278
|
+
return;
|
|
279
|
+
const dim = typeof thread.dimension === 'string' && thread.dimension.trim()
|
|
280
|
+
? thread.dimension.trim()
|
|
281
|
+
: 'review';
|
|
282
|
+
const att = typeof thread.attempt === 'number' && Number.isFinite(thread.attempt)
|
|
283
|
+
? Math.floor(thread.attempt)
|
|
284
|
+
: 1;
|
|
285
|
+
const t = {
|
|
286
|
+
dimension: dim,
|
|
287
|
+
attempt: att > 0 ? att : 1,
|
|
288
|
+
messages: [],
|
|
289
|
+
readFiles: [],
|
|
290
|
+
findings: [],
|
|
291
|
+
};
|
|
292
|
+
const messages = Array.isArray(thread.messages) ? thread.messages : [];
|
|
293
|
+
for (const m of messages.slice(0, MAX_MESSAGES_PER_THREAD)) {
|
|
294
|
+
if (typeof m === 'string' && m.trim())
|
|
295
|
+
t.messages.push(m);
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(thread.readFiles)) {
|
|
298
|
+
t.readFiles.push(...dedupePaths(thread.readFiles));
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(thread.findings)) {
|
|
301
|
+
for (const f of thread.findings) {
|
|
302
|
+
if (normalizeFinding(f))
|
|
303
|
+
pushThreadFinding(t, f);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
live.threads.push(t);
|
|
205
307
|
this.touch();
|
|
206
308
|
}
|
|
207
309
|
/** Append narration (assistant text) to the current reviewer thread. */
|
|
@@ -237,12 +339,11 @@ export class ReviewTranscriptBuilder {
|
|
|
237
339
|
for (const raw of findings) {
|
|
238
340
|
const f = normalizeFinding(raw);
|
|
239
341
|
if (f) {
|
|
240
|
-
thread
|
|
241
|
-
this.
|
|
342
|
+
pushThreadFinding(thread, raw);
|
|
343
|
+
this.addGlobal(f);
|
|
242
344
|
}
|
|
243
345
|
}
|
|
244
346
|
}
|
|
245
|
-
this.reevaluateGlobal();
|
|
246
347
|
this.touch();
|
|
247
348
|
}
|
|
248
349
|
/** Merge a round-level report snapshot (findings + readFiles) into a thread. */
|
|
@@ -255,9 +356,8 @@ export class ReviewTranscriptBuilder {
|
|
|
255
356
|
for (const raw of findings) {
|
|
256
357
|
const f = normalizeFinding(raw);
|
|
257
358
|
if (f)
|
|
258
|
-
this.
|
|
359
|
+
this.addGlobal(f);
|
|
259
360
|
}
|
|
260
|
-
this.reevaluateGlobal();
|
|
261
361
|
this.touch();
|
|
262
362
|
}
|
|
263
363
|
// ─── Convergence (F2) ───────────────────────────────────────────────────
|
|
@@ -337,8 +437,12 @@ export class ReviewTranscriptBuilder {
|
|
|
337
437
|
? Math.floor(entry.round)
|
|
338
438
|
: this.round,
|
|
339
439
|
type,
|
|
340
|
-
data: entry.data && typeof entry.data === 'object'
|
|
341
|
-
|
|
440
|
+
data: entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
|
|
441
|
+
// Deep clone so a caller mutating its own object later can never
|
|
442
|
+
// alias into the timeline (serialize would then emit the mutated
|
|
443
|
+
// values instead of what was actually decided). Decision-log payloads
|
|
444
|
+
// are small JSON, so the clone cost is negligible.
|
|
445
|
+
? cloneDecisionData(entry.data)
|
|
342
446
|
: {},
|
|
343
447
|
});
|
|
344
448
|
if (this.timeline.length > MAX_TIMELINE) {
|
|
@@ -385,9 +489,9 @@ export class ReviewTranscriptBuilder {
|
|
|
385
489
|
maxRounds: this.maxRounds,
|
|
386
490
|
rounds,
|
|
387
491
|
convergence: this.convergence,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
492
|
+
// addGlobal maintains the newest-wins cap incrementally, so the array is
|
|
493
|
+
// already ≤ MAX_FINDINGS_TOTAL — no post-hoc slice needed.
|
|
494
|
+
findings: this.globalFindings,
|
|
391
495
|
fixes: this.fixes,
|
|
392
496
|
checkpoint: this.checkpoint,
|
|
393
497
|
timeline: this.timeline,
|
|
@@ -416,25 +520,23 @@ export class ReviewTranscriptBuilder {
|
|
|
416
520
|
threadCount(live) {
|
|
417
521
|
return live.threads.length;
|
|
418
522
|
}
|
|
419
|
-
/**
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
523
|
+
/**
|
|
524
|
+
* Insert one deduplicated finding into the compact global list.
|
|
525
|
+
* NEWEST-WINS: when the global cap (MAX_FINDINGS_TOTAL) is hit, the oldest
|
|
526
|
+
* kept finding is evicted so fresh, still-relevant findings survive — this
|
|
527
|
+
* is the documented contract. Amortized O(1): dedupe via a seen-key set and
|
|
528
|
+
* eviction is one `shift` per overflow insert.
|
|
529
|
+
*/
|
|
530
|
+
addGlobal(f) {
|
|
531
|
+
const key = `${f.file}\u0000${f.line ?? 0}\u0000${f.dimension}\u0000${f.summary}`;
|
|
532
|
+
if (this.globalSeenKeys.has(key))
|
|
533
|
+
return;
|
|
534
|
+
this.globalSeenKeys.add(key);
|
|
535
|
+
this.globalFindings.push(f);
|
|
536
|
+
if (this.globalFindings.length > MAX_FINDINGS_TOTAL) {
|
|
537
|
+
const evicted = this.globalFindings.shift();
|
|
538
|
+
if (evicted) {
|
|
539
|
+
this.globalSeenKeys.delete(`${evicted.file}\u0000${evicted.line ?? 0}\u0000${evicted.dimension}\u0000${evicted.summary}`);
|
|
438
540
|
}
|
|
439
541
|
}
|
|
440
542
|
}
|
package/lib/client.js
CHANGED
|
@@ -465,6 +465,10 @@ function normalizeTranscript(manifest) {
|
|
|
465
465
|
phases: asArray(safeGet(src, "phases")).map((p) => typeof p === "string" ? p : ""),
|
|
466
466
|
round: asNum(safeGet(src, "round")),
|
|
467
467
|
maxRounds: asNum(safeGet(src, "maxRounds")),
|
|
468
|
+
// v3.5: why a FINISHED run stopped (converged / max_rounds_reached /
|
|
469
|
+
// aborted_by_validation / aborted_by_config). Required for the
|
|
470
|
+
// observatory badge to render the stop reason instead of a bare "已结束".
|
|
471
|
+
stoppedReason: src.stoppedReason === null || src.stoppedReason === void 0 ? null : asStr(safeGet(src, "stoppedReason")) || null,
|
|
468
472
|
rounds,
|
|
469
473
|
convergence: asArray(safeGet(src, "convergence")).map((n) => asCount(n)),
|
|
470
474
|
findings: asArray(safeGet(src, "findings")).map((f) => ({ ...f })),
|
|
@@ -1207,6 +1211,26 @@ function trendMax(points) {
|
|
|
1207
1211
|
}
|
|
1208
1212
|
return max;
|
|
1209
1213
|
}
|
|
1214
|
+
function stoppedReasonLabel(reason) {
|
|
1215
|
+
switch (reason) {
|
|
1216
|
+
case "converged":
|
|
1217
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u5DF2\u6536\u655B\uFF08\u65E0\u65B0\u53D1\u73B0\uFF09";
|
|
1218
|
+
case "max_rounds_reached":
|
|
1219
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u8FBE\u5230\u8F6E\u6570\u4E0A\u9650";
|
|
1220
|
+
case "aborted_by_validation":
|
|
1221
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA\u505C\u6B62";
|
|
1222
|
+
case "aborted_by_config":
|
|
1223
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u547D\u4EE4\u4E0D\u5728\u767D\u540D\u5355\uFF08\u914D\u7F6E\u9700\u4FEE\u590D\uFF09";
|
|
1224
|
+
case "inconclusive":
|
|
1225
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u8F6E\u6B21\u7ED3\u8BBA\u4E0D\u660E\uFF08\u5BA1\u67E5\u8F93\u51FA\u65E0\u6548\uFF09";
|
|
1226
|
+
case "schema_invalid":
|
|
1227
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u5BA1\u67E5\u8F93\u51FA\u8FDE\u7EED schema \u6821\u9A8C\u5931\u8D25";
|
|
1228
|
+
case "no_usable_reviewer_output":
|
|
1229
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u65E0\u53EF\u7528\u5BA1\u67E5\u8F93\u51FA";
|
|
1230
|
+
default:
|
|
1231
|
+
return reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F";
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1210
1234
|
function buildCompletionSummary(report) {
|
|
1211
1235
|
const conv = (
|
|
1212
1236
|
/** @type {Record<string, unknown>} */
|
|
@@ -1225,6 +1249,7 @@ var CONFIG_EDIT_FIELDS = [
|
|
|
1225
1249
|
{ key: "dimensions", label: "\u5BA1\u67E5\u7EF4\u5EA6", hint: '\u6570\u7EC4\uFF0C\u5982 ["correctness","security"]' },
|
|
1226
1250
|
{ key: "max_rounds", label: "\u6700\u5927\u8F6E\u6570", hint: "\u6B63\u6574\u6570" },
|
|
1227
1251
|
{ key: "review.scope", label: "\u5BA1\u67E5\u8303\u56F4", hint: '"full" \u6216 "changed-only"' },
|
|
1252
|
+
{ key: "reasoning_effort", label: "\u5BA1\u67E5\u63A8\u7406\u5F3A\u5EA6", hint: '"low" / "medium" / "high"\uFF0C\u7F3A\u7701\u8DDF\u968F\u6A21\u578B\u9ED8\u8BA4' },
|
|
1228
1253
|
{ key: "atomic.max_lines", label: "\u539F\u5B50\u4FEE\u590D\u4E0A\u9650\u884C\u6570", hint: "\u6B63\u6574\u6570" },
|
|
1229
1254
|
{ key: "git.push_per_round", label: "\u6BCF\u8F6E\u63A8\u9001", hint: "true / false" }
|
|
1230
1255
|
];
|
|
@@ -1720,20 +1745,6 @@ function severityColor(severity) {
|
|
|
1720
1745
|
function severityLabel(severity) {
|
|
1721
1746
|
return SEVERITY_LABEL[coerceSeverity(severity)];
|
|
1722
1747
|
}
|
|
1723
|
-
function stoppedReasonLabel(reason) {
|
|
1724
|
-
switch (reason) {
|
|
1725
|
-
case "converged":
|
|
1726
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u5DF2\u6536\u655B\uFF08\u65E0\u65B0\u53D1\u73B0\uFF09";
|
|
1727
|
-
case "max_rounds_reached":
|
|
1728
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u8FBE\u5230\u8F6E\u6570\u4E0A\u9650";
|
|
1729
|
-
case "aborted_by_validation":
|
|
1730
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA\u505C\u6B62";
|
|
1731
|
-
case "aborted_by_config":
|
|
1732
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u547D\u4EE4\u4E0D\u5728\u767D\u540D\u5355\uFF08\u914D\u7F6E\u9700\u4FEE\u590D\uFF09";
|
|
1733
|
-
default:
|
|
1734
|
-
return reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F";
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
1748
|
function runStatusText(live, manifest) {
|
|
1738
1749
|
if (live) return "\u8FD0\u884C\u4E2D";
|
|
1739
1750
|
const reason = manifest && typeof manifest.stoppedReason === "string" ? manifest.stoppedReason : "";
|
|
@@ -1763,6 +1774,7 @@ var slotsSvc = void 0;
|
|
|
1763
1774
|
var themeSvc = void 0;
|
|
1764
1775
|
var storage = null;
|
|
1765
1776
|
var themeDisposer = null;
|
|
1777
|
+
var themeListener = null;
|
|
1766
1778
|
var themeEnabled = true;
|
|
1767
1779
|
var roundPulseListeners = [];
|
|
1768
1780
|
function emitRoundPulse(round, converged) {
|
|
@@ -1784,6 +1796,31 @@ function clearThemeSkin() {
|
|
|
1784
1796
|
themeDisposer = null;
|
|
1785
1797
|
}
|
|
1786
1798
|
}
|
|
1799
|
+
function registerThemeListener(ctx) {
|
|
1800
|
+
if (typeof ctx.on !== "function") return;
|
|
1801
|
+
if (themeListener) {
|
|
1802
|
+
try {
|
|
1803
|
+
themeListener();
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
themeListener = null;
|
|
1807
|
+
}
|
|
1808
|
+
const off = ctx.on("theme/change", () => {
|
|
1809
|
+
if (themeEnabled) applyThemeSkin();
|
|
1810
|
+
});
|
|
1811
|
+
themeListener = typeof off === "function" ? off : null;
|
|
1812
|
+
if (typeof ctx.effect === "function") {
|
|
1813
|
+
ctx.effect(() => {
|
|
1814
|
+
if (themeListener) {
|
|
1815
|
+
try {
|
|
1816
|
+
themeListener();
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
themeListener = null;
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1787
1824
|
function setThemeEnabled(enabled) {
|
|
1788
1825
|
themeEnabled = enabled;
|
|
1789
1826
|
if (storage) storage.set(THEME_STORAGE_KEY, enabled ? "1" : "0");
|
|
@@ -3870,18 +3907,21 @@ function apply(ctx) {
|
|
|
3870
3907
|
themeEnabled = savedTheme === null ? true : savedTheme === "1";
|
|
3871
3908
|
if (themeEnabled) applyThemeSkin();
|
|
3872
3909
|
if (typeof document !== "undefined" && document.createElement && document.head) {
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3910
|
+
if (!document.head.querySelector('style[data-iterate-lock="1"]')) {
|
|
3911
|
+
const style = document.createElement("style");
|
|
3912
|
+
style.dataset.iterateLock = "1";
|
|
3913
|
+
style.dataset.plugin = PLUGIN_TAG;
|
|
3914
|
+
style.dataset.pluginCss = "iterate-main";
|
|
3915
|
+
style.textContent = ITERATE_CSS;
|
|
3916
|
+
document.head.appendChild(style);
|
|
3917
|
+
if (typeof ctx.effect === "function") {
|
|
3918
|
+
ctx.effect(() => {
|
|
3919
|
+
try {
|
|
3920
|
+
style.remove();
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3885
3925
|
}
|
|
3886
3926
|
}
|
|
3887
3927
|
if (slotsSvc === void 0) {
|
|
@@ -3925,11 +3965,7 @@ function apply(ctx) {
|
|
|
3925
3965
|
)
|
|
3926
3966
|
);
|
|
3927
3967
|
}
|
|
3928
|
-
|
|
3929
|
-
ctx.on("theme/change", () => {
|
|
3930
|
-
if (themeEnabled) applyThemeSkin();
|
|
3931
|
-
});
|
|
3932
|
-
}
|
|
3968
|
+
registerThemeListener(ctx);
|
|
3933
3969
|
}
|
|
3934
3970
|
return module.exports; } });
|
|
3935
3971
|
|
package/lib/parse.js
CHANGED
|
@@ -581,6 +581,12 @@ export function normalizeTranscript(manifest) {
|
|
|
581
581
|
phases: asArray(safeGet(src, 'phases')).map((p) => (typeof p === 'string' ? p : '')),
|
|
582
582
|
round: asNum(safeGet(src, 'round')),
|
|
583
583
|
maxRounds: asNum(safeGet(src, 'maxRounds')),
|
|
584
|
+
// v3.5: why a FINISHED run stopped (converged / max_rounds_reached /
|
|
585
|
+
// aborted_by_validation / aborted_by_config). Required for the
|
|
586
|
+
// observatory badge to render the stop reason instead of a bare "已结束".
|
|
587
|
+
stoppedReason: src.stoppedReason === null || src.stoppedReason === undefined
|
|
588
|
+
? null
|
|
589
|
+
: asStr(safeGet(src, 'stoppedReason')) || null,
|
|
584
590
|
rounds,
|
|
585
591
|
convergence: asArray(safeGet(src, 'convergence')).map((n) => asCount(n)),
|
|
586
592
|
findings: asArray(safeGet(src, 'findings')).map((f) => ({ ...f })),
|
|
@@ -1681,6 +1687,33 @@ export function trendMax(points) {
|
|
|
1681
1687
|
return max
|
|
1682
1688
|
}
|
|
1683
1689
|
|
|
1690
|
+
// ─── Stop reason badges (v3.5.5) ─────────────────────────────────────────────
|
|
1691
|
+
//
|
|
1692
|
+
// Every value the iterate loop can report as `stoppedReason` — including the
|
|
1693
|
+
// schema-retry batch (schema_invalid / no_usable_reviewer_output / inconclusive)
|
|
1694
|
+
// — maps to a concrete Chinese label. Unknown values still get a readable
|
|
1695
|
+
// default instead of a bare "已结束". Kept in parse.js so both the client
|
|
1696
|
+
// observatory badge and Node unit tests share one source of truth.
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Chinese badge label for a run's `stoppedReason`, or null/undefined-safe.
|
|
1700
|
+
*
|
|
1701
|
+
* @param {string | null | undefined} reason
|
|
1702
|
+
* @returns {string}
|
|
1703
|
+
*/
|
|
1704
|
+
export function stoppedReasonLabel(reason) {
|
|
1705
|
+
switch (reason) {
|
|
1706
|
+
case 'converged': return '已结束 · 已收敛(无新发现)'
|
|
1707
|
+
case 'max_rounds_reached': return '已结束 · 达到轮数上限'
|
|
1708
|
+
case 'aborted_by_validation': return '已结束 · 验证失败后回滚停止'
|
|
1709
|
+
case 'aborted_by_config': return '已结束 · 验证命令不在白名单(配置需修复)'
|
|
1710
|
+
case 'inconclusive': return '已结束 · 轮次结论不明(审查输出无效)'
|
|
1711
|
+
case 'schema_invalid': return '已结束 · 审查输出连续 schema 校验失败'
|
|
1712
|
+
case 'no_usable_reviewer_output': return '已结束 · 无可用审查输出'
|
|
1713
|
+
default: return reason ? `已结束 · ${reason}` : '已结束'
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1684
1717
|
// ─── Completion notification ────────────────────────────────────────────────
|
|
1685
1718
|
|
|
1686
1719
|
/**
|
|
@@ -1711,6 +1744,7 @@ export const CONFIG_EDIT_FIELDS = [
|
|
|
1711
1744
|
{ key: 'dimensions', label: '审查维度', hint: '数组,如 ["correctness","security"]' },
|
|
1712
1745
|
{ key: 'max_rounds', label: '最大轮数', hint: '正整数' },
|
|
1713
1746
|
{ key: 'review.scope', label: '审查范围', hint: '"full" 或 "changed-only"' },
|
|
1747
|
+
{ key: 'reasoning_effort', label: '审查推理强度', hint: '"low" / "medium" / "high",缺省跟随模型默认' },
|
|
1714
1748
|
{ key: 'atomic.max_lines', label: '原子修复上限行数', hint: '正整数' },
|
|
1715
1749
|
{ key: 'git.push_per_round', label: '每轮推送', hint: 'true / false' },
|
|
1716
1750
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.6",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
"0.1.5-alpha.2": "compatible",
|
|
39
39
|
"0.1.5-rc.1": "compatible",
|
|
40
40
|
"0.1.5-rc.2": "compatible",
|
|
41
|
-
"0.1.6-alpha.1": "compatible"
|
|
41
|
+
"0.1.6-alpha.1": "compatible",
|
|
42
|
+
"0.1.6-alpha.2": "compatible",
|
|
43
|
+
"0.1.7-alpha.1": "compatible",
|
|
44
|
+
"0.1.7-alpha.2": "compatible",
|
|
45
|
+
"0.1.7-rc.1": "compatible"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"client": {
|
|
@@ -75,15 +79,15 @@
|
|
|
75
79
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
76
80
|
},
|
|
77
81
|
"dependencies": {
|
|
78
|
-
"@deepseek-ai/cordis": "4.0.
|
|
79
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
80
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
82
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
83
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.1",
|
|
81
85
|
"js-yaml": "4.3.2"
|
|
82
86
|
},
|
|
83
87
|
"devDependencies": {
|
|
84
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
85
|
-
"@deepseek-ai/dsh-jobs": "
|
|
86
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
88
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-jobs": "0.1.7-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.1",
|
|
87
91
|
"@types/js-yaml": "4.0.9",
|
|
88
92
|
"@types/node": "22.15.0",
|
|
89
93
|
"@types/react": "19.2.2",
|
package/src/atomic-fs.ts
CHANGED
|
@@ -54,11 +54,19 @@ function tempPathFor(filePath: string): string {
|
|
|
54
54
|
*/
|
|
55
55
|
export function writeTextAtomic(filePath: string, data: string): void {
|
|
56
56
|
const tmp = tempPathFor(filePath)
|
|
57
|
-
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(tmp, data, 'utf-8')
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// The write itself failed (disk full, permission, …): the temp file may
|
|
61
|
+
// already be partially written, so remove it before propagating — a
|
|
62
|
+
// failed write must never litter the directory either.
|
|
63
|
+
cleanupTemp(tmp)
|
|
64
|
+
throw err
|
|
65
|
+
}
|
|
58
66
|
try {
|
|
59
67
|
renameSync(tmp, filePath)
|
|
60
68
|
} catch (err) {
|
|
61
|
-
// Best-effort temp cleanup so a failed
|
|
69
|
+
// Best-effort temp cleanup so a failed rename never litters the directory.
|
|
62
70
|
cleanupTemp(tmp)
|
|
63
71
|
throw err
|
|
64
72
|
}
|
|
@@ -67,11 +75,19 @@ export function writeTextAtomic(filePath: string, data: string): void {
|
|
|
67
75
|
/** Async variant of {@link writeTextAtomic} (same temp naming convention). */
|
|
68
76
|
export async function writeTextAtomicAsync(filePath: string, data: string): Promise<void> {
|
|
69
77
|
const tmp = tempPathFor(filePath)
|
|
70
|
-
|
|
78
|
+
try {
|
|
79
|
+
await writeFile(tmp, data, 'utf-8')
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// The write itself failed (disk full, permission, …): the temp file may
|
|
82
|
+
// already be partially written, so remove it before propagating — a
|
|
83
|
+
// failed write must never litter the directory either.
|
|
84
|
+
await cleanupTempAsync(tmp)
|
|
85
|
+
throw err
|
|
86
|
+
}
|
|
71
87
|
try {
|
|
72
88
|
await rename(tmp, filePath)
|
|
73
89
|
} catch (err) {
|
|
74
|
-
// Best-effort temp cleanup so a failed
|
|
90
|
+
// Best-effort temp cleanup so a failed rename never litters the directory.
|
|
75
91
|
await cleanupTempAsync(tmp)
|
|
76
92
|
throw err
|
|
77
93
|
}
|