iterate-plugin 3.5.2 → 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/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 +30 -10
- 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 +1 -1
- package/src/client/index.ts +30 -5
- package/src/tools/checkpoint.ts +45 -6
- package/src/tools/config.ts +11 -2
- 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 +32 -12
- 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/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",
|
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) {
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -24,16 +24,41 @@ import type { DefenseEventType, IterationCheckpoint, IterationStatus, QualityGat
|
|
|
24
24
|
|
|
25
25
|
// ─── Pure helpers (exported for unit tests) ─────────────────────────────────
|
|
26
26
|
|
|
27
|
+
/** Max findings persisted in a checkpoint (a model-authored findings payload is
|
|
28
|
+
* bounded, mirroring MAX_FIX_CONTENT_CHARS for fix content). */
|
|
29
|
+
export const MAX_CHECKPOINT_FINDINGS = 1000
|
|
30
|
+
|
|
27
31
|
/** Read the current checkpoint from disk (missing/corrupt → null). */
|
|
28
32
|
export function readCheckpoint(projectRoot: string): IterationCheckpoint | null {
|
|
29
33
|
const file = checkpointPath(projectRoot)
|
|
30
34
|
if (!existsSync(file)) return null
|
|
31
35
|
try {
|
|
32
|
-
const parsed = JSON.parse(readFileSync(file, 'utf-8')) as IterationCheckpoint
|
|
36
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8')) as Partial<IterationCheckpoint>
|
|
33
37
|
if (!parsed || typeof parsed !== 'object') return null
|
|
34
38
|
if (parsed.mode !== 'dry-run' && parsed.mode !== 'normal') return null
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
// Strict round gate: non-numeric (incl. NaN — `NaN <= 0` is false and
|
|
40
|
+
// `NaN < 0` also false) round values must not survive into the status.
|
|
41
|
+
const round =
|
|
42
|
+
typeof parsed.round === 'number' && Number.isFinite(parsed.round)
|
|
43
|
+
? Math.floor(parsed.round)
|
|
44
|
+
: null
|
|
45
|
+
if (round === null || round < 0) return null
|
|
46
|
+
const num = (v: unknown, fallback: number): number =>
|
|
47
|
+
typeof v === 'number' && Number.isFinite(v) ? v : fallback
|
|
48
|
+
// Normalize every field so a hand-edited/corrupt checkpoint (string
|
|
49
|
+
// maxRounds, missing fixedCount, …) can never leak a non-numeric value
|
|
50
|
+
// into the status output or crash a consumer.
|
|
51
|
+
return {
|
|
52
|
+
mode: parsed.mode,
|
|
53
|
+
round,
|
|
54
|
+
maxRounds: num(parsed.maxRounds, round),
|
|
55
|
+
fixedCount: num(parsed.fixedCount, 0),
|
|
56
|
+
architecturalCount: num(parsed.architecturalCount, 0),
|
|
57
|
+
resumeCount: num(parsed.resumeCount, 0),
|
|
58
|
+
findings: Array.isArray(parsed.findings) ? (parsed.findings as IterationCheckpoint['findings']) : [],
|
|
59
|
+
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : new Date().toISOString(),
|
|
60
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
61
|
+
}
|
|
37
62
|
} catch {
|
|
38
63
|
return null
|
|
39
64
|
}
|
|
@@ -82,6 +107,17 @@ export function validateCheckpoint(input: {
|
|
|
82
107
|
) {
|
|
83
108
|
return 'resumeCount must be a non-negative integer'
|
|
84
109
|
}
|
|
110
|
+
// A round that claims to be past the configured cap is an inconsistent save —
|
|
111
|
+
// the status would render `Round X / Y` with X > Y, which is never legitimate
|
|
112
|
+
// for a resumed run (resume continues AT the checkpoint, it does not exceed
|
|
113
|
+
// the cap).
|
|
114
|
+
if (
|
|
115
|
+
typeof input.round === 'number' &&
|
|
116
|
+
typeof input.maxRounds === 'number' &&
|
|
117
|
+
input.round > input.maxRounds
|
|
118
|
+
) {
|
|
119
|
+
return 'round must not exceed maxRounds'
|
|
120
|
+
}
|
|
85
121
|
return null
|
|
86
122
|
}
|
|
87
123
|
|
|
@@ -118,8 +154,11 @@ export function computeStatus(input: {
|
|
|
118
154
|
}
|
|
119
155
|
|
|
120
156
|
const totalRounds = checkpoint?.maxRounds ?? currentRound
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
// Coerce the registry's per-round counts defensively: a hand-edited registry
|
|
158
|
+
// round missing fixedCount/failedCount would otherwise feed NaN into the
|
|
159
|
+
// status integer fields.
|
|
160
|
+
const registryFixed = registry.rounds.reduce((sum, r) => sum + (Number(r.fixedCount) || 0), 0)
|
|
161
|
+
const failedCount = registry.rounds.reduce((sum, r) => sum + (Number(r.failedCount) || 0), 0)
|
|
123
162
|
// When a checkpoint exists, its snapshot fields are authoritative for resume
|
|
124
163
|
// (fixedCount / architecturalCount / findings); otherwise derive from the
|
|
125
164
|
// live fix registry and decision log.
|
|
@@ -258,7 +297,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
258
297
|
fixedCount: args.fixedCount as number,
|
|
259
298
|
architecturalCount: args.architecturalCount as number,
|
|
260
299
|
resumeCount: (typeof args.resumeCount === 'number' ? args.resumeCount : 0),
|
|
261
|
-
findings: (Array.isArray(args.findings) ? args.findings : []) as unknown as IterationCheckpoint['findings'],
|
|
300
|
+
findings: (Array.isArray(args.findings) ? args.findings.slice(0, MAX_CHECKPOINT_FINDINGS) : []) as unknown as IterationCheckpoint['findings'],
|
|
262
301
|
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
263
302
|
updatedAt: new Date().toISOString(),
|
|
264
303
|
}
|
package/src/tools/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
4
|
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
|
4
|
-
import { loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from '../config-loader.ts'
|
|
5
|
+
import { defaultConfig, loadEffectiveConfig, validateConfig, resolveProjectRootForExec } from '../config-loader.ts'
|
|
5
6
|
import {
|
|
6
7
|
applyConfigUpdates,
|
|
7
8
|
readRawConfig,
|
|
@@ -106,7 +107,15 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
106
107
|
}
|
|
107
108
|
let base: Record<string, unknown>
|
|
108
109
|
try {
|
|
109
|
-
|
|
110
|
+
// Partial updates must be mergeable on a FRESH project (no config
|
|
111
|
+
// file yet): otherwise `{max_rounds: 5}` alone fails schema
|
|
112
|
+
// validation with a misleading "missing goal". Merge against the
|
|
113
|
+
// built-in defaults when there is no file to read, so the written
|
|
114
|
+
// config captures the effective config (defaults + update) and the
|
|
115
|
+
// documented "write a partial update" contract works out of the box.
|
|
116
|
+
base = existsSync(join(projectRoot, 'iterate.config.yaml'))
|
|
117
|
+
? readRawConfig(join(projectRoot, 'iterate.config.yaml'))
|
|
118
|
+
: (defaultConfig() as unknown as Record<string, unknown>)
|
|
110
119
|
} catch (err) {
|
|
111
120
|
return { operation: 'write', ok: false, found: false, error: `failed to read config: ${String(err)}` }
|
|
112
121
|
}
|