iterate-plugin 3.5.1 → 3.5.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.
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/tools/checkpoint.js +39 -5
- package/dist/tools/config.js +11 -2
- package/dist/tools/context.js +39 -5
- package/dist/tools/decision-log.js +8 -1
- package/dist/tools/defense-events.js +41 -6
- package/dist/tools/defense-store.js +53 -2
- package/dist/tools/experience-bank.js +8 -8
- package/dist/tools/experience-store.js +58 -5
- package/dist/tools/fix.js +52 -19
- package/dist/tools/history.js +5 -3
- package/dist/tools/prune.js +29 -15
- package/dist/tools/quality-gate.js +3 -0
- package/dist/tools/quality-store.js +8 -2
- package/dist/tools/transcript.js +46 -11
- package/dist/transcript.js +20 -5
- package/lib/client.js +52 -10
- package/lib/parse.js +3 -0
- package/package.json +7 -6
- package/src/client/index.ts +30 -5
- package/src/tools/checkpoint.ts +45 -6
- package/src/tools/config.ts +11 -2
- package/src/tools/context.ts +36 -4
- package/src/tools/decision-log.ts +10 -1
- package/src/tools/defense-events.ts +43 -6
- package/src/tools/defense-store.ts +54 -5
- package/src/tools/experience-bank.ts +8 -8
- package/src/tools/experience-store.ts +67 -14
- package/src/tools/fix.ts +53 -21
- package/src/tools/history.ts +5 -3
- package/src/tools/prune.ts +25 -14
- package/src/tools/quality-gate.ts +4 -0
- package/src/tools/quality-store.ts +7 -2
- package/src/tools/transcript.ts +47 -11
- package/src/transcript.ts +23 -5
package/dist/tools/fix.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
20
20
|
import { writeJsonAtomic, writeTextAtomic } from "../atomic-fs.js";
|
|
21
|
-
import { join, sep } from 'node:path';
|
|
21
|
+
import { dirname, join, sep } from 'node:path';
|
|
22
22
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
23
23
|
import { loadEffectiveConfig, resolveProjectRootForExec } from "../config-loader.js";
|
|
24
24
|
import { runWithJob } from "../jobs.js";
|
|
@@ -120,9 +120,22 @@ export function readRegistry(projectRoot) {
|
|
|
120
120
|
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
121
121
|
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.rounds))
|
|
122
122
|
return emptyRegistry();
|
|
123
|
-
// Defensive: a hand-edited or partially-written registry may
|
|
124
|
-
// round without a `records` array
|
|
125
|
-
|
|
123
|
+
// Defensive normalization: a hand-edited or partially-written registry may
|
|
124
|
+
// contain a round without a `records` array, or records that are missing
|
|
125
|
+
// their id / finding object — all of which would make readers
|
|
126
|
+
// (findFixRecord / recordsForFile / iterate_diff) throw or sum NaN.
|
|
127
|
+
parsed.rounds = parsed.rounds
|
|
128
|
+
.filter((r) => r && typeof r === 'object' && Array.isArray(r.records))
|
|
129
|
+
.map((r) => ({
|
|
130
|
+
...r,
|
|
131
|
+
round: typeof r.round === 'number' && Number.isFinite(r.round) ? Math.floor(r.round) : 0,
|
|
132
|
+
records: r.records.filter((rec) => !!rec &&
|
|
133
|
+
typeof rec === 'object' &&
|
|
134
|
+
typeof rec.id === 'string' &&
|
|
135
|
+
!!rec.finding &&
|
|
136
|
+
typeof rec.finding === 'object'),
|
|
137
|
+
}))
|
|
138
|
+
.filter((r) => typeof r.round === 'number' && Number.isFinite(r.round));
|
|
126
139
|
return parsed;
|
|
127
140
|
}
|
|
128
141
|
catch {
|
|
@@ -198,15 +211,26 @@ export function resolveProjectFile(projectRoot, file) {
|
|
|
198
211
|
return { ok: false, reason: 'file resolves outside the project root' };
|
|
199
212
|
}
|
|
200
213
|
// Symlink containment: the lexical prefix check above does not resolve
|
|
201
|
-
// symlinks.
|
|
202
|
-
//
|
|
203
|
-
// a
|
|
204
|
-
|
|
214
|
+
// symlinks. Verify the REAL path of the target (when it exists) AND of its
|
|
215
|
+
// nearest existing ancestor directory (when it does not — e.g. a fix that
|
|
216
|
+
// creates a new file) stays inside the REAL project root, so a symlinked
|
|
217
|
+
// directory inside the repo can never route a fix (write/rollback/diff)
|
|
218
|
+
// outside the project. The probe stops at the project root itself: the root
|
|
219
|
+
// is harness-provided (not model-controlled), and a path with no existing
|
|
220
|
+
// entry inside the repo cannot hide a symlink.
|
|
221
|
+
let probe = resolved;
|
|
222
|
+
while (!existsSync(probe) && probe !== projectRoot) {
|
|
223
|
+
const parent = dirname(probe);
|
|
224
|
+
if (parent === probe)
|
|
225
|
+
break;
|
|
226
|
+
probe = parent;
|
|
227
|
+
}
|
|
228
|
+
if (existsSync(probe)) {
|
|
205
229
|
let rootReal;
|
|
206
230
|
let real;
|
|
207
231
|
try {
|
|
208
232
|
rootReal = realpathSync(projectRoot);
|
|
209
|
-
real = realpathSync(
|
|
233
|
+
real = realpathSync(probe);
|
|
210
234
|
}
|
|
211
235
|
catch {
|
|
212
236
|
return { ok: false, reason: 'failed to resolve real path for containment check' };
|
|
@@ -485,9 +509,9 @@ export function registerFixTool(ctx) {
|
|
|
485
509
|
// Registry write failed → the file was already modified but no record
|
|
486
510
|
// exists, so a later rollback/diff could never see it and a retry would
|
|
487
511
|
// back up the already-fixed content as "original". Restore the file
|
|
488
|
-
// from the backup to leave the tree exactly as it was.
|
|
512
|
+
// from the backup atomically to leave the tree exactly as it was.
|
|
489
513
|
try {
|
|
490
|
-
|
|
514
|
+
writeTextAtomic(target.resolved, readFileSync(backupPath, 'utf-8'));
|
|
491
515
|
}
|
|
492
516
|
catch (restoreErr) {
|
|
493
517
|
return {
|
|
@@ -600,20 +624,27 @@ export function registerDiffTool(ctx) {
|
|
|
600
624
|
for (const r of round.records) {
|
|
601
625
|
if (!r.success)
|
|
602
626
|
continue;
|
|
603
|
-
const
|
|
627
|
+
const file = typeof r.finding.file === 'string' ? r.finding.file : '';
|
|
628
|
+
if (!file)
|
|
629
|
+
continue;
|
|
630
|
+
// Coerce defensively: a hand-edited registry record missing the
|
|
631
|
+
// numeric fields must not produce NaN in the accumulated summary.
|
|
632
|
+
const added = Number(r.linesAdded) || 0;
|
|
633
|
+
const removed = Number(r.linesRemoved) || 0;
|
|
634
|
+
const existing = files.find((f) => f.file === file);
|
|
604
635
|
if (existing) {
|
|
605
|
-
existing.linesAdded +=
|
|
606
|
-
existing.linesRemoved +=
|
|
636
|
+
existing.linesAdded += added;
|
|
637
|
+
existing.linesRemoved += removed;
|
|
607
638
|
// Recompute the summary from the summed counts so a multi-fix
|
|
608
639
|
// file's text does not contradict its accumulated numbers.
|
|
609
640
|
existing.diffSummary = `+${existing.linesAdded}/-${existing.linesRemoved} lines`;
|
|
610
641
|
}
|
|
611
642
|
else {
|
|
612
643
|
files.push({
|
|
613
|
-
file
|
|
614
|
-
diffSummary: r.diffSummary
|
|
615
|
-
linesAdded:
|
|
616
|
-
linesRemoved:
|
|
644
|
+
file,
|
|
645
|
+
diffSummary: typeof r.diffSummary === 'string' ? r.diffSummary : `+${added}/-${removed} lines`,
|
|
646
|
+
linesAdded: added,
|
|
647
|
+
linesRemoved: removed,
|
|
617
648
|
});
|
|
618
649
|
}
|
|
619
650
|
}
|
|
@@ -691,7 +722,9 @@ export function registerRollbackTool(ctx) {
|
|
|
691
722
|
if (!target.ok)
|
|
692
723
|
return { ok: false, error: target.reason };
|
|
693
724
|
try {
|
|
694
|
-
|
|
725
|
+
// Atomic restore: never leave a truncated source file if we crash
|
|
726
|
+
// mid-restore (matches the writeTextAtomic guarantee used by apply).
|
|
727
|
+
writeTextAtomic(target.resolved, readFileSync(record.backupPath, 'utf-8'));
|
|
695
728
|
}
|
|
696
729
|
catch (err) {
|
|
697
730
|
return { ok: false, error: `failed to restore backup: ${String(err)}` };
|
package/dist/tools/history.js
CHANGED
|
@@ -46,8 +46,10 @@ export function filterDecisionEntries(entries, opts) {
|
|
|
46
46
|
export function summarizeFixRegistry(registry) {
|
|
47
47
|
const rounds = (registry.rounds ?? []).map((r) => ({
|
|
48
48
|
round: r.round,
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Coerce defensively so a hand-edited registry round missing either count
|
|
50
|
+
// can never propagate NaN into the integer output fields.
|
|
51
|
+
fixedCount: Number(r.fixedCount) || 0,
|
|
52
|
+
failedCount: Number(r.failedCount) || 0,
|
|
51
53
|
}));
|
|
52
54
|
return {
|
|
53
55
|
totalFixed: rounds.reduce((s, r) => s + r.fixedCount, 0),
|
|
@@ -74,7 +76,7 @@ export function registerHistoryTool(ctx) {
|
|
|
74
76
|
type: {
|
|
75
77
|
type: 'string',
|
|
76
78
|
description: 'Optional entry-type filter: round_start, review_result, atomic_fix, architectural_fix, ' +
|
|
77
|
-
'revert, validation, decision, report.',
|
|
79
|
+
'revert, round_failed, validation, decision, report, resume.',
|
|
78
80
|
},
|
|
79
81
|
since: {
|
|
80
82
|
type: 'string',
|
package/dist/tools/prune.js
CHANGED
|
@@ -87,17 +87,25 @@ export function inspectPrune(projectRoot, retainDays) {
|
|
|
87
87
|
}
|
|
88
88
|
const staleBackups = [];
|
|
89
89
|
const fixDir = fixesDir(projectRoot);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
// existsSync does not prove readability — a permissions failure must degrade
|
|
91
|
+
// to "nothing listable" (report skipped) instead of throwing out of execute.
|
|
92
|
+
let fixEntries = [];
|
|
93
|
+
try {
|
|
94
|
+
if (existsSync(fixDir))
|
|
95
|
+
fixEntries = readdirSync(fixDir);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
fixEntries = [];
|
|
99
|
+
}
|
|
100
|
+
for (const entry of fixEntries) {
|
|
101
|
+
if (!entry.endsWith('.bak'))
|
|
102
|
+
continue;
|
|
103
|
+
// Extract the fix-id prefix (up to the first underscore after the id).
|
|
104
|
+
// e.g. "fix-abc123_2026-08-17T00-00-00-000Z.bak" → "fix-abc123"
|
|
105
|
+
const match = entry.match(/^(fix-[a-z0-9]+)_/);
|
|
106
|
+
const id = match?.[1];
|
|
107
|
+
if (id && !activeIds.has(id)) {
|
|
108
|
+
staleBackups.push(entry);
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
// 4. Empty rounds (rounds with 0 records).
|
|
@@ -106,10 +114,16 @@ export function inspectPrune(projectRoot, retainDays) {
|
|
|
106
114
|
.map((r) => r.round);
|
|
107
115
|
// 5. Stray atomic-write temp files left behind by a crashed writer. The
|
|
108
116
|
// unique prefix makes them recognizable and safe to delete — no live writer
|
|
109
|
-
// ever reads another writer's temp file.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// ever reads another writer's temp file. A permissions failure listing the
|
|
118
|
+
// dir degrades to "none found" (report-only) rather than throwing.
|
|
119
|
+
let iterateEntries = [];
|
|
120
|
+
try {
|
|
121
|
+
iterateEntries = existsSync(iterateDir(projectRoot)) ? readdirSync(iterateDir(projectRoot)) : [];
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
iterateEntries = [];
|
|
125
|
+
}
|
|
126
|
+
const staleTemps = iterateEntries.filter((f) => isPrunableTemp(f)).sort();
|
|
113
127
|
return {
|
|
114
128
|
oldLogEntries,
|
|
115
129
|
hasCheckpoint,
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
10
10
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
11
11
|
import { readQualityGate, writeQualityGate, computeQualityGate, clearQualityGate } from "./quality-store.js";
|
|
12
|
+
/** Valid severity values (kept in sync with QualityGateSnapshot counting). */
|
|
13
|
+
const VALID_SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
|
|
12
14
|
/** Validate a single finding object; returns true when well-formed. */
|
|
13
15
|
function isValidFinding(raw) {
|
|
14
16
|
if (!raw || typeof raw !== 'object')
|
|
@@ -16,6 +18,7 @@ function isValidFinding(raw) {
|
|
|
16
18
|
const f = raw;
|
|
17
19
|
return (typeof f.dimension === 'string' &&
|
|
18
20
|
typeof f.severity === 'string' &&
|
|
21
|
+
VALID_SEVERITIES.has(f.severity) &&
|
|
19
22
|
typeof f.file === 'string' &&
|
|
20
23
|
(f.line === undefined || typeof f.line === 'number'));
|
|
21
24
|
}
|
|
@@ -200,13 +200,19 @@ export function computeQualityGate(opts) {
|
|
|
200
200
|
const overallScore = dimensionGates.length > 0
|
|
201
201
|
? Math.round(dimensionGates.reduce((sum, d) => sum + d.score, 0) / dimensionGates.length)
|
|
202
202
|
: 0;
|
|
203
|
-
// Determine overall status
|
|
203
|
+
// Determine overall status. An empty review (no dimensions, no findings, no
|
|
204
|
+
// validation results) is NOT a failure — there is simply nothing to gate —
|
|
205
|
+
// so it reports `pending` (not-applicable) instead of a fabricated FAIL.
|
|
204
206
|
const hasCritical = criticalCount > 0;
|
|
205
207
|
const hasHighFail = dimensionGates.some((d) => d.status === 'fail');
|
|
206
208
|
const verificationFails = totalChecks > 0 && failedChecks > 0;
|
|
209
|
+
const noData = dimensionGates.length === 0 && totalFindings === 0 && totalChecks === 0;
|
|
207
210
|
let overallStatus = 'pass';
|
|
208
211
|
let failReason;
|
|
209
|
-
if (
|
|
212
|
+
if (noData) {
|
|
213
|
+
overallStatus = 'pending';
|
|
214
|
+
}
|
|
215
|
+
else if (hasCritical) {
|
|
210
216
|
overallStatus = 'fail';
|
|
211
217
|
failReason = `${criticalCount} critical findings present`;
|
|
212
218
|
}
|
package/dist/tools/transcript.js
CHANGED
|
@@ -31,7 +31,10 @@ function captureRound(builder, round) {
|
|
|
31
31
|
if (!round || typeof round !== 'object')
|
|
32
32
|
return;
|
|
33
33
|
const r = round;
|
|
34
|
-
const roundNo = typeof r.round === 'number' ? Math.floor(r.round) : 0;
|
|
34
|
+
const roundNo = typeof r.round === 'number' && Number.isFinite(r.round) ? Math.floor(r.round) : 0;
|
|
35
|
+
// NaN round values (`Math.floor(NaN) === NaN`, and `NaN <= 0` is false) would
|
|
36
|
+
// fall through the guard below; a finite check rejects them outright so a
|
|
37
|
+
// malformed round can never collapse into round 1 via the builder's coercion.
|
|
35
38
|
if (roundNo <= 0)
|
|
36
39
|
return;
|
|
37
40
|
builder.roundStart(roundNo);
|
|
@@ -61,15 +64,17 @@ function normalizeCheckpoint(input) {
|
|
|
61
64
|
if (!input || typeof input !== 'object')
|
|
62
65
|
return null;
|
|
63
66
|
const c = input;
|
|
64
|
-
|
|
67
|
+
// Number.isFinite: a NaN round must not pass `round <= 0` (which is false for
|
|
68
|
+
// NaN) and later coerce into a phantom round 1 row.
|
|
69
|
+
const round = typeof c.round === 'number' && Number.isFinite(c.round) ? c.round : 0;
|
|
65
70
|
if (round <= 0)
|
|
66
71
|
return null;
|
|
67
72
|
return {
|
|
68
73
|
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : 'normal',
|
|
69
74
|
round,
|
|
70
|
-
maxRounds: typeof c.maxRounds === 'number' ? c.maxRounds : 0,
|
|
71
|
-
fixedCount: typeof c.fixedCount === 'number' ? c.fixedCount : 0,
|
|
72
|
-
resumeCount: typeof c.resumeCount === 'number' ? c.resumeCount : 0,
|
|
75
|
+
maxRounds: typeof c.maxRounds === 'number' && Number.isFinite(c.maxRounds) ? c.maxRounds : 0,
|
|
76
|
+
fixedCount: typeof c.fixedCount === 'number' && Number.isFinite(c.fixedCount) ? c.fixedCount : 0,
|
|
77
|
+
resumeCount: typeof c.resumeCount === 'number' && Number.isFinite(c.resumeCount) ? c.resumeCount : 0,
|
|
73
78
|
updatedAt: typeof c.updatedAt === 'string' ? c.updatedAt : new Date().toISOString(),
|
|
74
79
|
};
|
|
75
80
|
}
|
|
@@ -85,7 +90,7 @@ function normalizeFix(input) {
|
|
|
85
90
|
return {
|
|
86
91
|
id,
|
|
87
92
|
timestamp: typeof f.timestamp === 'string' ? f.timestamp : new Date().toISOString(),
|
|
88
|
-
round: typeof f.round === 'number' ? f.round : 0,
|
|
93
|
+
round: typeof f.round === 'number' && Number.isFinite(f.round) ? f.round : 0,
|
|
89
94
|
file,
|
|
90
95
|
summary: typeof f.summary === 'string' ? f.summary : '',
|
|
91
96
|
linesAdded: typeof f.linesAdded === 'number' ? f.linesAdded : 0,
|
|
@@ -211,11 +216,26 @@ export function registerTranscriptTool(ctx) {
|
|
|
211
216
|
manifest = null;
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
// Rehydrating a parsed-but-malformed manifest (e.g. `rounds: [null]`
|
|
220
|
+
// or a non-object) must never crash the nudge — degrade to a fresh
|
|
221
|
+
// builder so the steering text still lands.
|
|
222
|
+
let builder;
|
|
223
|
+
if (manifest) {
|
|
224
|
+
try {
|
|
225
|
+
builder = rehydrateBuilder(manifest, approval);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
builder = new ReviewTranscriptBuilder({ project: projectRoot, mode: 'normal', approval });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
builder = new ReviewTranscriptBuilder({ project: projectRoot, mode: 'normal', approval });
|
|
233
|
+
}
|
|
217
234
|
builder.setNudge(typeof args.text === 'string' && args.text.trim() ? args.text : null);
|
|
218
|
-
await
|
|
235
|
+
const persisted = await persistChecked(file, builder.serialize());
|
|
236
|
+
if (!persisted.ok) {
|
|
237
|
+
return { operation: 'nudge', updated: false, error: persisted.error };
|
|
238
|
+
}
|
|
219
239
|
return {
|
|
220
240
|
operation: 'nudge',
|
|
221
241
|
updated: true,
|
|
@@ -276,7 +296,10 @@ export function registerTranscriptTool(ctx) {
|
|
|
276
296
|
const last = convergence[convergence.length - 1];
|
|
277
297
|
if (convergence.length > 0 && last === 0)
|
|
278
298
|
builder.finish();
|
|
279
|
-
await
|
|
299
|
+
const persisted = await persistChecked(file, builder.serialize());
|
|
300
|
+
if (!persisted.ok) {
|
|
301
|
+
return { operation: 'capture', found: true, updated: false, error: persisted.error };
|
|
302
|
+
}
|
|
280
303
|
const live = await readLive(projectRoot);
|
|
281
304
|
return {
|
|
282
305
|
operation: 'capture',
|
|
@@ -331,6 +354,18 @@ async function persist(file, manifest) {
|
|
|
331
354
|
await mkdir(dirname(file), { recursive: true });
|
|
332
355
|
await writeTextAtomicAsync(file, JSON.stringify(manifest, null, 2));
|
|
333
356
|
}
|
|
357
|
+
/** persist wrapped with a structured error channel so disk failures surface
|
|
358
|
+
* as a structured tool result instead of rejecting the whole execute (matches
|
|
359
|
+
* every other write path in this codebase). */
|
|
360
|
+
async function persistChecked(file, manifest) {
|
|
361
|
+
try {
|
|
362
|
+
await persist(file, manifest);
|
|
363
|
+
return { ok: true };
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
return { ok: false, error: `failed to persist transcript: ${err instanceof Error ? err.message : String(err)}` };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
334
369
|
/**
|
|
335
370
|
* Mark one applied fix as rolled back in the PERSISTED transcript, so the
|
|
336
371
|
* client observatory (F4 fix/rollback tab) reflects the reversal instead of
|
package/dist/transcript.js
CHANGED
|
@@ -35,6 +35,10 @@ const MAX_TIMELINE = 500;
|
|
|
35
35
|
/** Max applied-fix records kept (newest wins; bounded so a long run cannot
|
|
36
36
|
* grow the manifest payload without limit). */
|
|
37
37
|
const MAX_FIXES = 200;
|
|
38
|
+
/** Max round number accepted by the builder. A model-authored/manifest-backed
|
|
39
|
+
* round value is attacker-influenced JSON: `roundStart(1e9)` / `snapshotConvergence`
|
|
40
|
+
* would otherwise preallocate arrays of that size and OOM the host. */
|
|
41
|
+
const MAX_ROUNDS = 1000;
|
|
38
42
|
/** Thresholds applied when reducing a string list under a cap. */
|
|
39
43
|
function clampStringList(source, cap) {
|
|
40
44
|
const out = [];
|
|
@@ -162,9 +166,13 @@ export class ReviewTranscriptBuilder {
|
|
|
162
166
|
/** Open a review round, capturing the current round index. */
|
|
163
167
|
roundStart(round, maxRounds) {
|
|
164
168
|
const r = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1;
|
|
165
|
-
|
|
169
|
+
// Clamp to MAX_ROUNDS: `this.rounds.length < this.round` below preallocates
|
|
170
|
+
// an array of size `round` — an unbounded model-controlled value (e.g. 1e9)
|
|
171
|
+
// would OOM the host. Values above the cap are folded into the cap so the
|
|
172
|
+
// real rounds are never silently dropped.
|
|
173
|
+
this.round = Math.min(r > 0 ? r : 1, MAX_ROUNDS);
|
|
166
174
|
if (typeof maxRounds === 'number' && Number.isFinite(maxRounds) && maxRounds >= 0) {
|
|
167
|
-
this.maxRounds = Math.floor(maxRounds);
|
|
175
|
+
this.maxRounds = Math.min(Math.floor(maxRounds), MAX_ROUNDS);
|
|
168
176
|
}
|
|
169
177
|
while (this.rounds.length < this.round) {
|
|
170
178
|
this.rounds.push({ round: this.rounds.length + 1, threads: [] });
|
|
@@ -248,7 +256,10 @@ export class ReviewTranscriptBuilder {
|
|
|
248
256
|
// ─── Convergence (F2) ───────────────────────────────────────────────────
|
|
249
257
|
/** Record a round's new-finding count for the convergence series. */
|
|
250
258
|
snapshotConvergence(round, newCount) {
|
|
251
|
-
const
|
|
259
|
+
const rawR = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1;
|
|
260
|
+
// Clamp (mirrors roundStart): the loop below preallocates `r` slots, so an
|
|
261
|
+
// unbounded model-controlled round number would OOM the host.
|
|
262
|
+
const r = Math.min(rawR > 0 ? rawR : 1, MAX_ROUNDS);
|
|
252
263
|
const n = typeof newCount === 'number' && Number.isFinite(newCount) ? newCount : 0;
|
|
253
264
|
while (this.convergence.length < r)
|
|
254
265
|
this.convergence.push(-1);
|
|
@@ -267,7 +278,9 @@ export class ReviewTranscriptBuilder {
|
|
|
267
278
|
this.fixes.push({
|
|
268
279
|
id,
|
|
269
280
|
timestamp: typeof record.timestamp === 'string' ? record.timestamp : isoNow(),
|
|
270
|
-
round: typeof record.round === 'number'
|
|
281
|
+
round: typeof record.round === 'number' && Number.isFinite(record.round)
|
|
282
|
+
? Math.floor(record.round)
|
|
283
|
+
: this.round,
|
|
271
284
|
file,
|
|
272
285
|
summary: typeof record.summary === 'string' ? record.summary : '',
|
|
273
286
|
linesAdded: typeof record.linesAdded === 'number' ? Math.floor(record.linesAdded) : 0,
|
|
@@ -313,7 +326,9 @@ export class ReviewTranscriptBuilder {
|
|
|
313
326
|
const type = typeof entry.type === 'string' ? entry.type : 'decision';
|
|
314
327
|
this.timeline.push({
|
|
315
328
|
timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : isoNow(),
|
|
316
|
-
round: typeof entry.round === 'number'
|
|
329
|
+
round: typeof entry.round === 'number' && Number.isFinite(entry.round)
|
|
330
|
+
? Math.floor(entry.round)
|
|
331
|
+
: this.round,
|
|
317
332
|
type,
|
|
318
333
|
data: entry.data && typeof entry.data === 'object'
|
|
319
334
|
? entry.data
|
package/lib/client.js
CHANGED
|
@@ -1283,7 +1283,10 @@ function buildRuntimeStatusGuide() {
|
|
|
1283
1283
|
...RUNTIME_ARTIFACTS.map((a) => `- ${a.key}\uFF08${a.label}\uFF09\uFF1A${a.hint}`),
|
|
1284
1284
|
"",
|
|
1285
1285
|
"\u67E5\u770B\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_status\uFF08\u6C47\u603B\uFF09\u6216 iterate_history\uFF08\u660E\u7EC6\uFF09\u3002",
|
|
1286
|
-
"\u6E05\u7406\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_prune\uFF08\u9ED8\u8BA4 dry-run\uFF0C\u53EA\u62A5\u544A\u4E0D\u5220\u9664\uFF0C\u663E\u5F0F dryRun:false \u624D\u771F\u6B63\u6E05\u7406\uFF09\u3002"
|
|
1286
|
+
"\u6E05\u7406\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_prune\uFF08\u9ED8\u8BA4 dry-run\uFF0C\u53EA\u62A5\u544A\u4E0D\u5220\u9664\uFF0C\u663E\u5F0F dryRun:false \u624D\u771F\u6B63\u6E05\u7406\uFF09\u3002",
|
|
1287
|
+
'\u91CD\u7F6E\u72B6\u6001\uFF1A\u4E2D\u65AD\u540E\u91CD\u65B0\u5F00\u59CB\u524D\uFF0C\u53EF\u5206\u522B\u8BA9\u6A21\u578B\u8C03\u7528 iterate_checkpoint(operation:"clear")\u3001',
|
|
1288
|
+
' iterate_quality_gate(operation:"clear")\u3001iterate_defense_events(operation:"clear")',
|
|
1289
|
+
" \u6E05\u9664\u9648\u65E7\u65AD\u70B9\u3001\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\u4E0E\u9632\u5FA1\u4E8B\u4EF6\u6D41\u3002"
|
|
1287
1290
|
];
|
|
1288
1291
|
return lines.join("\n");
|
|
1289
1292
|
}
|
|
@@ -3095,6 +3098,11 @@ ${JSON.stringify({
|
|
|
3095
3098
|
mode: String(cp.mode || "normal"),
|
|
3096
3099
|
maxRounds: typeof cp.maxRounds === "number" ? cp.maxRounds : null
|
|
3097
3100
|
}, null, 2)}
|
|
3101
|
+
\`\`\``;
|
|
3102
|
+
const clearText = `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF1A
|
|
3103
|
+
|
|
3104
|
+
\`\`\`json
|
|
3105
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3098
3106
|
\`\`\``;
|
|
3099
3107
|
const item = (label, value) => React.createElement("span", { className: "iterate-obs-chip" }, `${label} ${String(value ?? "?")}`);
|
|
3100
3108
|
return React.createElement(
|
|
@@ -3118,13 +3126,24 @@ ${JSON.stringify({
|
|
|
3118
3126
|
item("fixed", cp.fixedCount),
|
|
3119
3127
|
item("resume", cp.resumeCount)
|
|
3120
3128
|
),
|
|
3121
|
-
React.createElement(
|
|
3122
|
-
|
|
3123
|
-
"
|
|
3124
|
-
"
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3129
|
+
React.createElement(
|
|
3130
|
+
"div",
|
|
3131
|
+
{ className: "iterate-obs-bar", style: { marginBottom: 4 } },
|
|
3132
|
+
React.createElement("button", {
|
|
3133
|
+
className: "iterate-btn",
|
|
3134
|
+
"data-primary": "",
|
|
3135
|
+
"data-copied": copiedKey === "cp-resume" ? "" : void 0,
|
|
3136
|
+
onClick: () => copyInstruction("cp-resume", resumeText),
|
|
3137
|
+
title: "\u590D\u5236 iterate_checkpoint resume \u6307\u4EE4\u6587\u672C\uFF08\u52A0\u8F7D\u65AD\u70B9\u5E76\u8BA1\u6570\u4E00\u6B21\u6062\u590D\uFF09"
|
|
3138
|
+
}, copiedKey === "cp-resume" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6062\u590D\u6307\u4EE4"),
|
|
3139
|
+
React.createElement("button", {
|
|
3140
|
+
className: "iterate-btn",
|
|
3141
|
+
"data-danger": "",
|
|
3142
|
+
"data-copied": copiedKey === "cp-clear" ? "" : void 0,
|
|
3143
|
+
onClick: () => copyInstruction("cp-clear", clearText),
|
|
3144
|
+
title: "\u590D\u5236 iterate_checkpoint clear \u6307\u4EE4\u6587\u672C\uFF08\u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF0C\u4F9B\u91CD\u65B0\u5F00\u59CB\uFF09"
|
|
3145
|
+
}, copiedKey === "cp-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u65AD\u70B9")
|
|
3146
|
+
)
|
|
3128
3147
|
)
|
|
3129
3148
|
);
|
|
3130
3149
|
};
|
|
@@ -3293,6 +3312,11 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
|
|
|
3293
3312
|
};
|
|
3294
3313
|
const renderQualityGate = () => {
|
|
3295
3314
|
const gateInstruction = "\u8BF7\u8C03\u7528 `iterate_quality_gate` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001";
|
|
3315
|
+
const gateClearInstruction = `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u6E05\u9664\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\uFF1A
|
|
3316
|
+
|
|
3317
|
+
\`\`\`json
|
|
3318
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3319
|
+
\`\`\``;
|
|
3296
3320
|
const gate = qualityGate;
|
|
3297
3321
|
const dims = gate && gate.dimensions ? gate.dimensions : [];
|
|
3298
3322
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0);
|
|
@@ -3309,7 +3333,14 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
|
|
|
3309
3333
|
"data-copied": copiedKey === "qgate" ? "" : void 0,
|
|
3310
3334
|
onClick: () => copyInstruction("qgate", gateInstruction),
|
|
3311
3335
|
title: "\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4"
|
|
3312
|
-
}, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981")
|
|
3336
|
+
}, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981"),
|
|
3337
|
+
React.createElement("button", {
|
|
3338
|
+
className: "iterate-btn",
|
|
3339
|
+
"data-danger": "",
|
|
3340
|
+
"data-copied": copiedKey === "qgate-clear" ? "" : void 0,
|
|
3341
|
+
onClick: () => copyInstruction("qgate-clear", gateClearInstruction),
|
|
3342
|
+
title: "\u590D\u5236 iterate_quality_gate clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u7684 FAIL \u8BC1\u4E66\uFF09"
|
|
3343
|
+
}, copiedKey === "qgate-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u95E8\u7981")
|
|
3313
3344
|
);
|
|
3314
3345
|
if (!hasGate) {
|
|
3315
3346
|
return React.createElement(
|
|
@@ -3524,7 +3555,18 @@ ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
|
|
|
3524
3555
|
"data-copied": copiedKey === "defense-counts" ? "" : void 0,
|
|
3525
3556
|
onClick: () => copyInstruction("defense-counts", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u67E5\u8BE2\u4E8B\u4EF6\u7EDF\u8BA1"),
|
|
3526
3557
|
title: "\u590D\u5236\u7EDF\u8BA1\u6307\u4EE4"
|
|
3527
|
-
}, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1")
|
|
3558
|
+
}, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1"),
|
|
3559
|
+
React.createElement("button", {
|
|
3560
|
+
className: "iterate-btn",
|
|
3561
|
+
"data-danger": "",
|
|
3562
|
+
"data-copied": copiedKey === "defense-clear" ? "" : void 0,
|
|
3563
|
+
onClick: () => copyInstruction("defense-clear", `\u8BF7\u8C03\u7528 \`iterate_defense_events\` \u6E05\u9664\u9632\u5FA1\u4E8B\u4EF6\u6D41\uFF1A
|
|
3564
|
+
|
|
3565
|
+
\`\`\`json
|
|
3566
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3567
|
+
\`\`\``),
|
|
3568
|
+
title: "\u590D\u5236 iterate_defense_events clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u4E8B\u4EF6\u6D41\uFF09"
|
|
3569
|
+
}, copiedKey === "defense-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u4E8B\u4EF6")
|
|
3528
3570
|
);
|
|
3529
3571
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
3530
3572
|
return React.createElement(
|
package/lib/parse.js
CHANGED
|
@@ -1816,6 +1816,9 @@ export function buildRuntimeStatusGuide() {
|
|
|
1816
1816
|
'',
|
|
1817
1817
|
'查看状态:让模型调用 iterate_status(汇总)或 iterate_history(明细)。',
|
|
1818
1818
|
'清理状态:让模型调用 iterate_prune(默认 dry-run,只报告不删除,显式 dryRun:false 才真正清理)。',
|
|
1819
|
+
'重置状态:中断后重新开始前,可分别让模型调用 iterate_checkpoint(operation:"clear")、',
|
|
1820
|
+
' iterate_quality_gate(operation:"clear")、iterate_defense_events(operation:"clear")',
|
|
1821
|
+
' 清除陈旧断点、质量门禁证书与防御事件流。',
|
|
1819
1822
|
]
|
|
1820
1823
|
return lines.join('\n')
|
|
1821
1824
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
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",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"0.1.3-alpha.1": "compatible",
|
|
37
37
|
"0.1.5-alpha.1": "compatible",
|
|
38
38
|
"0.1.5-alpha.2": "compatible",
|
|
39
|
-
"0.1.5-rc.1": "compatible"
|
|
39
|
+
"0.1.5-rc.1": "compatible",
|
|
40
|
+
"0.1.5-rc.2": "compatible"
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
"client": {
|
|
@@ -74,13 +75,13 @@
|
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
77
|
"@deepseek-ai/cordis": "4.0.2",
|
|
77
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
78
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
78
|
+
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-util-values": "0.1.5-rc.2",
|
|
79
80
|
"js-yaml": "4.3.1"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
82
|
-
"@deepseek-ai/dsh-jobs": "^0.1.
|
|
83
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
83
|
+
"@deepseek-ai/dsh-jobs": "^0.1.5-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|
|
84
85
|
"@types/js-yaml": "4.0.9",
|
|
85
86
|
"@types/node": "22.15.0",
|
|
86
87
|
"@types/react": "19.2.2",
|
package/src/client/index.ts
CHANGED
|
@@ -2172,6 +2172,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2172
2172
|
mode: String(cp.mode || 'normal'),
|
|
2173
2173
|
maxRounds: typeof cp.maxRounds === 'number' ? cp.maxRounds : null,
|
|
2174
2174
|
}, null, 2)}\n\`\`\``
|
|
2175
|
+
// Clearing a stale checkpoint after an interrupted run — iterate_checkpoint
|
|
2176
|
+
// exposes `clear`, but before this button it was only reachable by typing
|
|
2177
|
+
// the command manually (an interrupted run otherwise shows a permanent
|
|
2178
|
+
// "可恢复" chip with no companion reset action).
|
|
2179
|
+
const clearText = `请调用 \`iterate_checkpoint\` 清除当前断点:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``
|
|
2175
2180
|
const item = (label: string, value: unknown) =>
|
|
2176
2181
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2177
2182
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
@@ -2187,11 +2192,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2187
2192
|
item('fixed', cp.fixedCount),
|
|
2188
2193
|
item('resume', cp.resumeCount),
|
|
2189
2194
|
),
|
|
2190
|
-
React.createElement('
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 4 } },
|
|
2196
|
+
React.createElement('button', {
|
|
2197
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
2198
|
+
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
2199
|
+
title: '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)',
|
|
2200
|
+
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
2201
|
+
React.createElement('button', {
|
|
2202
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'cp-clear' ? '' : undefined,
|
|
2203
|
+
onClick: () => copyInstruction('cp-clear', clearText),
|
|
2204
|
+
title: '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)',
|
|
2205
|
+
}, copiedKey === 'cp-clear' ? '已复制' : '清除断点'),
|
|
2206
|
+
),
|
|
2195
2207
|
),
|
|
2196
2208
|
)
|
|
2197
2209
|
}
|
|
@@ -2338,6 +2350,9 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2338
2350
|
// falls back to copy-able query instructions when no snapshot exists yet.
|
|
2339
2351
|
const renderQualityGate = () => {
|
|
2340
2352
|
const gateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
|
|
2353
|
+
// Reset a stale FAIL certificate before a fresh iteration — the tool's
|
|
2354
|
+
// `clear` operation existed since v3.5.1 but had no client entry point.
|
|
2355
|
+
const gateClearInstruction = `请调用 \`iterate_quality_gate\` 清除当前质量门禁证书:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``
|
|
2341
2356
|
const gate = qualityGate
|
|
2342
2357
|
const dims = gate && gate.dimensions ? gate.dimensions : []
|
|
2343
2358
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
|
|
@@ -2352,6 +2367,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2352
2367
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2353
2368
|
title: '复制 iterate_quality_gate 查询指令',
|
|
2354
2369
|
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2370
|
+
React.createElement('button', {
|
|
2371
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2372
|
+
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2373
|
+
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2374
|
+
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
2355
2375
|
)
|
|
2356
2376
|
|
|
2357
2377
|
if (!hasGate) {
|
|
@@ -2538,6 +2558,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2538
2558
|
onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
|
|
2539
2559
|
title: '复制统计指令',
|
|
2540
2560
|
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2561
|
+
React.createElement('button', {
|
|
2562
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'defense-clear' ? '' : undefined,
|
|
2563
|
+
onClick: () => copyInstruction('defense-clear', `请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``),
|
|
2564
|
+
title: '复制 iterate_defense_events clear 指令文本(重置陈旧事件流)',
|
|
2565
|
+
}, copiedKey === 'defense-clear' ? '已复制' : '清除事件'),
|
|
2541
2566
|
)
|
|
2542
2567
|
|
|
2543
2568
|
if (allEvents.length === 0 && totalCount === 0) {
|