iterate-plugin 3.7.0 → 3.8.0
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 +37 -5
- package/README.zh-CN.md +23 -3
- package/dist/file-lock.js +75 -14
- package/dist/git-scope.js +22 -2
- package/dist/skill-prompt.js +2 -2
- package/dist/tools/checkpoint.js +24 -17
- package/dist/tools/config.js +42 -33
- package/dist/tools/decision-log.js +13 -93
- package/dist/tools/transcript.js +74 -44
- package/dist/tools/triage.js +52 -45
- package/dist/transcript.js +2 -2
- package/lib/client.js +34 -10
- package/lib/parse.js +29 -6
- package/package.json +31 -13
- package/src/client/index.ts +22 -4
- package/src/file-lock.ts +84 -14
- package/src/git-scope.ts +21 -2
- package/src/skill-prompt.ts +2 -2
- package/src/tools/checkpoint.ts +23 -16
- package/src/tools/config.ts +40 -31
- package/src/tools/decision-log.ts +12 -80
- package/src/tools/transcript.ts +72 -43
- package/src/tools/triage.ts +47 -40
- package/src/transcript.ts +2 -2
- package/src/types.ts +1 -1
package/dist/tools/transcript.js
CHANGED
|
@@ -23,6 +23,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
23
23
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
24
24
|
import { existsSync } from 'node:fs';
|
|
25
25
|
import { writeTextAtomicAsync } from "../atomic-fs.js";
|
|
26
|
+
import { withProjectLock } from "../file-lock.js";
|
|
26
27
|
import { dirname } from 'node:path';
|
|
27
28
|
import { loadEffectiveConfig, resolveProjectRootForExec, } from "../config-loader.js";
|
|
28
29
|
import { transcriptPath } from "../paths.js";
|
|
@@ -73,7 +74,7 @@ function normalizeCheckpoint(input) {
|
|
|
73
74
|
if (round <= 0)
|
|
74
75
|
return null;
|
|
75
76
|
return {
|
|
76
|
-
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode :
|
|
77
|
+
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : null,
|
|
77
78
|
round,
|
|
78
79
|
maxRounds: typeof c.maxRounds === 'number' && Number.isFinite(c.maxRounds) ? c.maxRounds : 0,
|
|
79
80
|
fixedCount: typeof c.fixedCount === 'number' && Number.isFinite(c.fixedCount) ? c.fixedCount : 0,
|
|
@@ -198,7 +199,23 @@ export function registerTranscriptTool(ctx) {
|
|
|
198
199
|
'the run console can show which commands ran and whether they passed.',
|
|
199
200
|
},
|
|
200
201
|
refReadFiles: { type: 'json', description: 'For `capture`: flat array of all read files across rounds (optional).' },
|
|
201
|
-
stoppedReason: {
|
|
202
|
+
stoppedReason: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
// Enum guard: the label renderer keys off this value, so a stray or
|
|
205
|
+
// prototype-chain string (e.g. "__proto__") must not enter the tool.
|
|
206
|
+
// The render path additionally uses Object.hasOwn (defense for
|
|
207
|
+
// hand-edited manifests that bypass this schema).
|
|
208
|
+
enum: [
|
|
209
|
+
'converged',
|
|
210
|
+
'max_rounds_reached',
|
|
211
|
+
'aborted_by_validation',
|
|
212
|
+
'aborted_by_config',
|
|
213
|
+
'schema_invalid',
|
|
214
|
+
'no_usable_reviewer_output',
|
|
215
|
+
'inconclusive',
|
|
216
|
+
],
|
|
217
|
+
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).'
|
|
218
|
+
},
|
|
202
219
|
// `string | null` (NOT plain `type: 'string'`): dsh validates arguments
|
|
203
220
|
// against the compiled schema BEFORE execute, and the docs — plus the
|
|
204
221
|
// client's clear button — advertise `{"operation":"nudge","text":null}`.
|
|
@@ -288,52 +305,65 @@ export function registerTranscriptTool(ctx) {
|
|
|
288
305
|
};
|
|
289
306
|
}
|
|
290
307
|
if (args.operation === 'nudge') {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
308
|
+
// Read-modify-write across processes: a concurrent nudge in a sibling
|
|
309
|
+
// process would otherwise read the same manifest and write back over
|
|
310
|
+
// each other's steering text. Serialize on the shared transcript lock
|
|
311
|
+
// (same pattern as the config/triage writers). The body still awaits,
|
|
312
|
+
// so the lock is held across the read and write; a same-process
|
|
313
|
+
// re-entry degrades to unlocked (the advisory lock is per-pid), which
|
|
314
|
+
// is the documented best-effort fallback for a low-value projection.
|
|
315
|
+
return withProjectLock(projectRoot, 'transcript', async () => {
|
|
316
|
+
let manifest = null;
|
|
317
|
+
if (existsSync(file)) {
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(await readFile(file, 'utf-8'));
|
|
320
|
+
manifest = parsed;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
manifest = null;
|
|
324
|
+
}
|
|
296
325
|
}
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
// Rehydrating a parsed-but-malformed manifest (e.g. `rounds: [null]`
|
|
327
|
+
// or a non-object) must never crash the nudge — degrade to a fresh
|
|
328
|
+
// builder so the steering text still lands. The fallback must keep
|
|
329
|
+
// the ORIGINAL run identity (mode/taskMode/goal/maxRounds) so a
|
|
330
|
+
// stray malformed field can never silently rewrite a dry-run run
|
|
331
|
+
// into a `normal` one.
|
|
332
|
+
const fallbackOpts = {
|
|
333
|
+
project: projectRoot,
|
|
334
|
+
mode: (manifest?.mode === 'dry-run' || manifest?.mode === 'normal' ? manifest.mode : null),
|
|
335
|
+
taskMode: (manifest?.taskMode === 'code' || manifest?.taskMode === 'iterate' ? manifest.taskMode : null),
|
|
336
|
+
approval,
|
|
337
|
+
goal: typeof manifest?.goal === 'string' ? manifest.goal : '',
|
|
338
|
+
maxRounds: typeof manifest?.maxRounds === 'number' ? manifest.maxRounds : 0,
|
|
339
|
+
};
|
|
340
|
+
let builder;
|
|
341
|
+
if (manifest) {
|
|
342
|
+
try {
|
|
343
|
+
builder = rehydrateBuilder(manifest, approval);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
builder = new ReviewTranscriptBuilder(fallbackOpts);
|
|
347
|
+
}
|
|
299
348
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// into a `normal` one.
|
|
307
|
-
const fallbackOpts = {
|
|
308
|
-
project: projectRoot,
|
|
309
|
-
mode: (manifest?.mode === 'dry-run' || manifest?.mode === 'normal' ? manifest.mode : null),
|
|
310
|
-
taskMode: (manifest?.taskMode === 'code' || manifest?.taskMode === 'iterate' ? manifest.taskMode : null),
|
|
311
|
-
approval,
|
|
312
|
-
goal: typeof manifest?.goal === 'string' ? manifest.goal : '',
|
|
313
|
-
maxRounds: typeof manifest?.maxRounds === 'number' ? manifest.maxRounds : 0,
|
|
314
|
-
};
|
|
315
|
-
let builder;
|
|
316
|
-
if (manifest) {
|
|
317
|
-
try {
|
|
318
|
-
builder = rehydrateBuilder(manifest, approval);
|
|
349
|
+
else {
|
|
350
|
+
// No manifest yet: the run's mode is genuinely unknown at nudge
|
|
351
|
+
// time (capture sets it). Recording `mode: 'normal'` here would
|
|
352
|
+
// rewrite a dry-run run into a `normal` one in the status/badge
|
|
353
|
+
// until capture lands — `null` honestly says "not captured yet".
|
|
354
|
+
builder = new ReviewTranscriptBuilder({ project: projectRoot, mode: null, approval });
|
|
319
355
|
}
|
|
320
|
-
|
|
321
|
-
|
|
356
|
+
builder.setNudge(typeof args.text === 'string' && args.text.trim() ? args.text : null);
|
|
357
|
+
const persisted = await persistChecked(file, builder.serialize());
|
|
358
|
+
if (!persisted.ok) {
|
|
359
|
+
return { operation: 'nudge', updated: false, error: persisted.error };
|
|
322
360
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (!persisted.ok) {
|
|
330
|
-
return { operation: 'nudge', updated: false, error: persisted.error };
|
|
331
|
-
}
|
|
332
|
-
return {
|
|
333
|
-
operation: 'nudge',
|
|
334
|
-
updated: true,
|
|
335
|
-
transcript: builder.serialize(),
|
|
336
|
-
};
|
|
361
|
+
return {
|
|
362
|
+
operation: 'nudge',
|
|
363
|
+
updated: true,
|
|
364
|
+
transcript: builder.serialize(),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
337
367
|
}
|
|
338
368
|
// capture
|
|
339
369
|
// `observatory.capture: false` opts the project out of transcript
|
package/dist/tools/triage.js
CHANGED
|
@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
|
|
|
5
5
|
import { resolveProjectRootForExec } from "../config-loader.js";
|
|
6
6
|
import { writeTextAtomic } from "../atomic-fs.js";
|
|
7
7
|
import { configBackupSuffix } from "../config-write.js";
|
|
8
|
+
import { withProjectLock } from "../file-lock.js";
|
|
8
9
|
import { WHOLE_FILE_LINE } from "../evidence.js";
|
|
9
10
|
const CONFIG_FILE = 'iterate.config.yaml';
|
|
10
11
|
/** Personalization key that holds the known-intentional list. */
|
|
@@ -227,58 +228,64 @@ function readConfigFile(configPath) {
|
|
|
227
228
|
/** Apply the triage entries: backup, merge, write, rollback on failure. */
|
|
228
229
|
function applyEntries(projectRoot, incoming) {
|
|
229
230
|
const configPath = join(projectRoot, CONFIG_FILE);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return { ok: false, error: `Failed to read config: ${String(err)}` };
|
|
237
|
-
}
|
|
238
|
-
const existing = readKnownIntentional(config);
|
|
239
|
-
const { merged, added, skipped, dropped } = mergeKnownIntentional(existing, incoming);
|
|
240
|
-
const nextConfig = buildConfigWithKnownIntentional(config, merged);
|
|
241
|
-
const hadFile = existsSync(configPath);
|
|
242
|
-
const backupPath = hadFile ? `${configPath}.bak-${backupSuffix()}` : null;
|
|
243
|
-
if (backupPath) {
|
|
231
|
+
// Serialize against `iterate_config write` (SAME lock name) so a triage apply
|
|
232
|
+
// and a config write on one project cannot interleave their read-modify-write
|
|
233
|
+
// and silently drop each other's updates. The lock spans read → merge →
|
|
234
|
+
// backup → write → backup-prune so the whole cycle is atomic.
|
|
235
|
+
return withProjectLock(projectRoot, 'config-write', () => {
|
|
236
|
+
let config;
|
|
244
237
|
try {
|
|
245
|
-
|
|
238
|
+
config = readConfigFile(configPath);
|
|
246
239
|
}
|
|
247
240
|
catch (err) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
error: `Failed to create backup: ${String(err)}`,
|
|
251
|
-
};
|
|
241
|
+
// The file exists but is malformed — refuse to overwrite user data.
|
|
242
|
+
return { ok: false, error: `Failed to read config: ${String(err)}` };
|
|
252
243
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
244
|
+
const existing = readKnownIntentional(config);
|
|
245
|
+
const { merged, added, skipped, dropped } = mergeKnownIntentional(existing, incoming);
|
|
246
|
+
const nextConfig = buildConfigWithKnownIntentional(config, merged);
|
|
247
|
+
const hadFile = existsSync(configPath);
|
|
248
|
+
const backupPath = hadFile ? `${configPath}.bak-${backupSuffix()}` : null;
|
|
249
|
+
if (backupPath) {
|
|
250
|
+
try {
|
|
251
|
+
copyFileSync(configPath, backupPath);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
error: `Failed to create backup: ${String(err)}`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const yamlText = yaml.dump(nextConfig, { noRefs: true });
|
|
263
261
|
try {
|
|
264
|
-
|
|
265
|
-
copyFileSync(backupPath, configPath);
|
|
266
|
-
else if (existsSync(configPath))
|
|
267
|
-
rmSync(configPath, { force: true });
|
|
262
|
+
writeTextAtomic(configPath, yamlText);
|
|
268
263
|
}
|
|
269
|
-
catch (
|
|
270
|
-
|
|
264
|
+
catch (err) {
|
|
265
|
+
// Rollback: restore the backup, or REMOVE the file we just created when
|
|
266
|
+
// there was no prior config — an empty file left behind would poison all
|
|
267
|
+
// future config reads (empty YAML is not a valid mapping).
|
|
268
|
+
let rollbackError = '';
|
|
269
|
+
try {
|
|
270
|
+
if (backupPath)
|
|
271
|
+
copyFileSync(backupPath, configPath);
|
|
272
|
+
else if (existsSync(configPath))
|
|
273
|
+
rmSync(configPath, { force: true });
|
|
274
|
+
}
|
|
275
|
+
catch (rbErr) {
|
|
276
|
+
rollbackError = `; rollback also failed: ${String(rbErr)}`;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: `Failed to write config: ${String(err)}${rollbackError}`,
|
|
281
|
+
};
|
|
271
282
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// project never collects an unbounded pile of config snapshots.
|
|
279
|
-
if (backupPath)
|
|
280
|
-
pruneOldConfigBackups(configPath);
|
|
281
|
-
return { ok: true, added, skipped, dropped, count: merged.length, configPath, backupPath };
|
|
283
|
+
// Success: bound the accumulation of timestamped backups so a long-lived
|
|
284
|
+
// project never collects an unbounded pile of config snapshots.
|
|
285
|
+
if (backupPath)
|
|
286
|
+
pruneOldConfigBackups(configPath);
|
|
287
|
+
return { ok: true, added, skipped, dropped, count: merged.length, configPath, backupPath };
|
|
288
|
+
});
|
|
282
289
|
}
|
|
283
290
|
/**
|
|
284
291
|
* Register the `iterate_triage` tool.
|
package/dist/transcript.js
CHANGED
|
@@ -501,7 +501,7 @@ export class ReviewTranscriptBuilder {
|
|
|
501
501
|
return;
|
|
502
502
|
}
|
|
503
503
|
this.checkpoint = {
|
|
504
|
-
mode: state.mode === 'dry-run' || state.mode === 'normal' ? state.mode :
|
|
504
|
+
mode: state.mode === 'dry-run' || state.mode === 'normal' ? state.mode : null,
|
|
505
505
|
round: typeof state.round === 'number' ? state.round : 0,
|
|
506
506
|
maxRounds: typeof state.maxRounds === 'number' ? state.maxRounds : 0,
|
|
507
507
|
fixedCount: typeof state.fixedCount === 'number' ? state.fixedCount : 0,
|
|
@@ -725,7 +725,7 @@ function normalizeCheckpointRow(input) {
|
|
|
725
725
|
if (round <= 0)
|
|
726
726
|
return null;
|
|
727
727
|
return {
|
|
728
|
-
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode :
|
|
728
|
+
mode: c.mode === 'dry-run' || c.mode === 'normal' ? c.mode : null,
|
|
729
729
|
round: clampRoundValue(c.round),
|
|
730
730
|
maxRounds: clampCountValue(c.maxRounds),
|
|
731
731
|
fixedCount: clampCountValue(c.fixedCount),
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* iterate-plugin client source-digest:
|
|
1
|
+
/* iterate-plugin client source-digest: fc3f99c3c9a74c891ce54564ac49e7a336441432dfafe0cb21775ee4afe8f411 */
|
|
2
2
|
window.__ModuleLoader__.load({ id: "iterate-plugin", factory: (require) => {
|
|
3
3
|
var module = { exports: {} };
|
|
4
4
|
var exports = module.exports;
|
|
@@ -1539,8 +1539,11 @@ function stoppedReasonLabel(reason, lang = "zh") {
|
|
|
1539
1539
|
schema_invalid: "Done \xB7 repeated schema validation failures in review output",
|
|
1540
1540
|
no_usable_reviewer_output: "Done \xB7 no usable review output"
|
|
1541
1541
|
};
|
|
1542
|
-
|
|
1543
|
-
|
|
1542
|
+
const table = lang === "en" ? en : zh;
|
|
1543
|
+
if (typeof reason === "string" && Object.hasOwn(table, reason)) return table[reason];
|
|
1544
|
+
const text = typeof reason === "string" && reason !== "" ? reason : "";
|
|
1545
|
+
if (lang === "en") return text ? `Done \xB7 ${text}` : "Done";
|
|
1546
|
+
return text ? `\u5DF2\u7ED3\u675F \xB7 ${text}` : "\u5DF2\u7ED3\u675F";
|
|
1544
1547
|
}
|
|
1545
1548
|
function buildCompletionSummary(report, lang = "zh") {
|
|
1546
1549
|
const conv = (
|
|
@@ -1626,8 +1629,9 @@ function buildConfigEditGuide() {
|
|
|
1626
1629
|
"---------------------",
|
|
1627
1630
|
"\u914D\u7F6E\u6587\u4EF6\uFF1A\u9879\u76EE\u6839\u76EE\u5F55 iterate.config.yaml\u3002",
|
|
1628
1631
|
"",
|
|
1629
|
-
"\u26A0 \u672A\u914D\u7F6E validation.commands \u65F6\u8FED\u4EE3\u4E0D\u88AB\u4FDD\u62A4\uFF1A\
|
|
1630
|
-
" \
|
|
1632
|
+
"\u26A0 \u672A\u914D\u7F6E validation.commands \u65F6\u8FED\u4EE3\u4E0D\u88AB\u4FDD\u62A4\uFF1A\u8BE5\u8F6E\u4F1A\u76F4\u63A5\u4E2D\u6B62\uFF08fail-closed\uFF09\uFF0C",
|
|
1633
|
+
" \u7A7A\u9A8C\u8BC1\u7ED3\u679C/\u9A8C\u8BC1\u672A\u6267\u884C\u7EDD\u4E0D\u5F53\u4F5C\u201C\u9A8C\u8BC1\u901A\u8FC7\u201D\uFF0C\u8FD0\u884C\u4EE5 converged_unverified /",
|
|
1634
|
+
" aborted_by_config \u6536\u5C3E\u800C\u4E0D\u662F plain converged\u3002\u7B2C\u4E00\u6B21\u542F\u52A8\u8FED\u4EE3\u524D\uFF0C",
|
|
1631
1635
|
" \u8BF7\u5148\u7ED9 validation.commands \u914D\u4E0A\u81F3\u5C11\u4E00\u6761\u771F\u5B9E\u547D\u4EE4\uFF08\u5982 npm test\uFF09\u3002",
|
|
1632
1636
|
"",
|
|
1633
1637
|
"\u53EF\u7F16\u8F91\u5B57\u6BB5\uFF1A",
|
|
@@ -1844,7 +1848,7 @@ function pickExportExtras(extra) {
|
|
|
1844
1848
|
}
|
|
1845
1849
|
return out;
|
|
1846
1850
|
}
|
|
1847
|
-
function buildRoundComparison(report, validations) {
|
|
1851
|
+
function buildRoundComparison(report, validations, fixes) {
|
|
1848
1852
|
const rounds = /* @__PURE__ */ new Map();
|
|
1849
1853
|
const rowsFor = (round) => {
|
|
1850
1854
|
const n = Number(round);
|
|
@@ -1861,7 +1865,18 @@ function buildRoundComparison(report, validations) {
|
|
|
1861
1865
|
row.findings += 1;
|
|
1862
1866
|
const sev = typeof f.severity === "string" && f.severity ? f.severity : "unknown";
|
|
1863
1867
|
row.severities[sev] = (row.severities[sev] || 0) + 1;
|
|
1864
|
-
|
|
1868
|
+
}
|
|
1869
|
+
const fixList = Array.isArray(fixes) ? fixes : [];
|
|
1870
|
+
for (const fx of fixList) {
|
|
1871
|
+
if (!fx || typeof fx !== "object") continue;
|
|
1872
|
+
if (
|
|
1873
|
+
/** @type {Record<string, unknown>} */
|
|
1874
|
+
fx.success !== true
|
|
1875
|
+
) continue;
|
|
1876
|
+
rowsFor(
|
|
1877
|
+
/** @type {Record<string, unknown>} */
|
|
1878
|
+
fx.round
|
|
1879
|
+
).fixed += 1;
|
|
1865
1880
|
}
|
|
1866
1881
|
const vals = Array.isArray(validations) ? validations : [];
|
|
1867
1882
|
for (const v of vals) {
|
|
@@ -2841,9 +2856,17 @@ function ConvergenceDashboard(props) {
|
|
|
2841
2856
|
noteSession(session);
|
|
2842
2857
|
}, [session]);
|
|
2843
2858
|
const report = latestReport(session);
|
|
2859
|
+
const pulseSeenRef = React.useRef(null);
|
|
2844
2860
|
React.useEffect(() => {
|
|
2845
2861
|
if (!report) return;
|
|
2846
2862
|
const cur = getCurrentRound(report);
|
|
2863
|
+
const key = hashReport(report) + ":" + String(cur);
|
|
2864
|
+
if (pulseSeenRef.current === null) {
|
|
2865
|
+
pulseSeenRef.current = key;
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
if (pulseSeenRef.current === key) return;
|
|
2869
|
+
pulseSeenRef.current = key;
|
|
2847
2870
|
const conv = report.convergence;
|
|
2848
2871
|
emitRoundPulse(cur, conv?.converged === true);
|
|
2849
2872
|
setPulseKey((k) => k + 1);
|
|
@@ -3146,6 +3169,7 @@ function TriagePanel(props) {
|
|
|
3146
3169
|
setVerdicts((prev) => setAllVerdicts(prev, "keep"));
|
|
3147
3170
|
setSelectAll(false);
|
|
3148
3171
|
};
|
|
3172
|
+
const rootRef = React.useRef(null);
|
|
3149
3173
|
React.useEffect(() => {
|
|
3150
3174
|
const doc = typeof document !== "undefined" ? document : null;
|
|
3151
3175
|
if (!doc) return;
|
|
@@ -3154,7 +3178,7 @@ function TriagePanel(props) {
|
|
|
3154
3178
|
const t2 = ev.target;
|
|
3155
3179
|
if (t2 && (t2.tagName === "INPUT" || t2.tagName === "TEXTAREA" || t2.tagName === "SELECT")) return;
|
|
3156
3180
|
if (t2 && typeof t2.isContentEditable === "boolean" && t2.isContentEditable) return;
|
|
3157
|
-
const rootEl =
|
|
3181
|
+
const rootEl = rootRef.current;
|
|
3158
3182
|
if (!rootEl) return;
|
|
3159
3183
|
const activeEl = doc.activeElement;
|
|
3160
3184
|
if (!activeEl) return;
|
|
@@ -3292,7 +3316,7 @@ function TriagePanel(props) {
|
|
|
3292
3316
|
});
|
|
3293
3317
|
return React.createElement(
|
|
3294
3318
|
"div",
|
|
3295
|
-
{ "data-iterate-root": "", "data-iterate": "triage", className: "iterate-triage" },
|
|
3319
|
+
{ ref: rootRef, "data-iterate-root": "", "data-iterate": "triage", className: "iterate-triage" },
|
|
3296
3320
|
React.createElement(
|
|
3297
3321
|
"div",
|
|
3298
3322
|
{ className: "iterate-triage-head" },
|
|
@@ -4196,7 +4220,7 @@ function ObservatoryPanel(props) {
|
|
|
4196
4220
|
}
|
|
4197
4221
|
const points = conv.map((n, i) => ({ round: i + 1, count: n }));
|
|
4198
4222
|
const report = latestReport(session);
|
|
4199
|
-
const comparison = buildRoundComparison(report, manifest.validations || []);
|
|
4223
|
+
const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || []);
|
|
4200
4224
|
const comparisonTable = comparison.length === 0 ? null : React.createElement(
|
|
4201
4225
|
"table",
|
|
4202
4226
|
{ className: "iterate-obs-table" },
|
package/lib/parse.js
CHANGED
|
@@ -2281,8 +2281,17 @@ export function stoppedReasonLabel(reason, lang = 'zh') {
|
|
|
2281
2281
|
schema_invalid: 'Done \u00b7 repeated schema validation failures in review output',
|
|
2282
2282
|
no_usable_reviewer_output: 'Done \u00b7 no usable review output',
|
|
2283
2283
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2284
|
+
// Object.hasOwn, NOT `zh[reason]`: a model/hand-edited `stoppedReason` of
|
|
2285
|
+
// `__proto__` / `constructor` / `toString` would otherwise resolve through
|
|
2286
|
+
// the PROTOTYPE CHAIN to Object.prototype or a function (never a string) and
|
|
2287
|
+
// crash React ("Objects are not valid as a React child") when the badge
|
|
2288
|
+
// renders it. An own-key hit is the only way to return a table value; any
|
|
2289
|
+
// other value degrades to a plain text label.
|
|
2290
|
+
const table = lang === 'en' ? en : zh
|
|
2291
|
+
if (typeof reason === 'string' && Object.hasOwn(table, reason)) return table[reason]
|
|
2292
|
+
const text = typeof reason === 'string' && reason !== '' ? reason : ''
|
|
2293
|
+
if (lang === 'en') return text ? `Done \u00b7 ${text}` : 'Done'
|
|
2294
|
+
return text ? `已结束 · ${text}` : '已结束'
|
|
2286
2295
|
}
|
|
2287
2296
|
|
|
2288
2297
|
// ─── Completion notification ────────────────────────────────────────────────
|
|
@@ -2391,8 +2400,9 @@ export function buildConfigEditGuide() {
|
|
|
2391
2400
|
'---------------------',
|
|
2392
2401
|
'配置文件:项目根目录 iterate.config.yaml。',
|
|
2393
2402
|
'',
|
|
2394
|
-
'⚠ 未配置 validation.commands
|
|
2395
|
-
'
|
|
2403
|
+
'⚠ 未配置 validation.commands 时迭代不被保护:该轮会直接中止(fail-closed),',
|
|
2404
|
+
' 空验证结果/验证未执行绝不当作“验证通过”,运行以 converged_unverified /',
|
|
2405
|
+
' aborted_by_config 收尾而不是 plain converged。第一次启动迭代前,',
|
|
2396
2406
|
' 请先给 validation.commands 配上至少一条真实命令(如 npm test)。',
|
|
2397
2407
|
'',
|
|
2398
2408
|
'可编辑字段:',
|
|
@@ -2789,14 +2799,19 @@ function pickExportExtras(extra) {
|
|
|
2789
2799
|
* One row per round: findings raised, findings fixed, severity mix and the
|
|
2790
2800
|
* validation command outcomes recorded on the manifest.
|
|
2791
2801
|
*
|
|
2802
|
+
* `fixed` counts SUCCESSFUL fixes applied in that round, taken from the
|
|
2803
|
+
* transcript's `fixes` (each `{ round, success }`) — the review `findings`
|
|
2804
|
+
* schema carries no `status`, so reading `f.status` here always yielded 0.
|
|
2805
|
+
*
|
|
2792
2806
|
* Cross-*session* comparison is intentionally out of scope — that is what the
|
|
2793
2807
|
* #4 pull instruction is for (see `buildDiskSnapshotInstruction`).
|
|
2794
2808
|
*
|
|
2795
2809
|
* @param {unknown} report
|
|
2796
2810
|
* @param {unknown} validations
|
|
2811
|
+
* @param {unknown} [fixes] transcript fixes (`{round, success}[]`) for the fixed counts
|
|
2797
2812
|
* @returns {Array<{round: number, findings: number, fixed: number, severities: Record<string, number>, validations: Array<{command: string, exitCode: number|null, allowed: boolean}>}>}
|
|
2798
2813
|
*/
|
|
2799
|
-
export function buildRoundComparison(report, validations) {
|
|
2814
|
+
export function buildRoundComparison(report, validations, fixes) {
|
|
2800
2815
|
const rounds = new Map()
|
|
2801
2816
|
const rowsFor = (round) => {
|
|
2802
2817
|
const n = Number(round)
|
|
@@ -2814,7 +2829,15 @@ export function buildRoundComparison(report, validations) {
|
|
|
2814
2829
|
row.findings += 1
|
|
2815
2830
|
const sev = typeof f.severity === 'string' && f.severity ? f.severity : 'unknown'
|
|
2816
2831
|
row.severities[sev] = (row.severities[sev] || 0) + 1
|
|
2817
|
-
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Fixed counts come from applied fixes (real data), not a finding `status`
|
|
2835
|
+
// the findings schema never produces.
|
|
2836
|
+
const fixList = Array.isArray(fixes) ? fixes : []
|
|
2837
|
+
for (const fx of fixList) {
|
|
2838
|
+
if (!fx || typeof fx !== 'object') continue
|
|
2839
|
+
if ((/** @type {Record<string, unknown>} */ (fx)).success !== true) continue
|
|
2840
|
+
rowsFor((/** @type {Record<string, unknown>} */ (fx)).round).fixed += 1
|
|
2818
2841
|
}
|
|
2819
2842
|
|
|
2820
2843
|
const vals = Array.isArray(validations) ? validations : []
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/jingzhao-l/iterate-
|
|
10
|
-
"directory": "harness/iterate-plugin"
|
|
9
|
+
"url": "https://github.com/jingzhao-l/iterate-plugin.git"
|
|
11
10
|
},
|
|
12
|
-
"homepage": "https://github.com/jingzhao-l/iterate-
|
|
11
|
+
"homepage": "https://github.com/jingzhao-l/iterate-plugin#readme",
|
|
13
12
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/jingzhao-l/iterate-
|
|
13
|
+
"url": "https://github.com/jingzhao-l/iterate-plugin/issues"
|
|
15
14
|
},
|
|
16
15
|
"keywords": [
|
|
17
16
|
"dsh-plugin",
|
|
@@ -47,7 +46,9 @@
|
|
|
47
46
|
"0.2.0-rc.1": "compatible",
|
|
48
47
|
"0.2.0-rc.2": "compatible",
|
|
49
48
|
"0.2.1-alpha.1": "compatible",
|
|
50
|
-
"0.2.1-alpha.2": "compatible"
|
|
49
|
+
"0.2.1-alpha.2": "compatible",
|
|
50
|
+
"0.2.1-alpha.3": "compatible",
|
|
51
|
+
"0.2.1-alpha.4": "compatible"
|
|
51
52
|
}
|
|
52
53
|
},
|
|
53
54
|
"client": {
|
|
@@ -84,15 +85,31 @@
|
|
|
84
85
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
85
86
|
},
|
|
86
87
|
"dependencies": {
|
|
87
|
-
"@deepseek-ai/cordis": "4.0.5-alpha.1",
|
|
88
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
|
|
89
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.2",
|
|
90
88
|
"js-yaml": "4.3.2"
|
|
91
89
|
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.2",
|
|
92
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.1 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4",
|
|
93
|
+
"@deepseek-ai/dsh-util-values": "0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2 || 0.1.7-alpha.1 || 0.1.7-alpha.2 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.1 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-alpha.4"
|
|
94
|
+
},
|
|
95
|
+
"peerDependenciesMeta": {
|
|
96
|
+
"@deepseek-ai/cordis": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"@deepseek-ai/dsh-tools": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"@deepseek-ai/dsh-util-values": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
92
106
|
"devDependencies": {
|
|
93
|
-
"@deepseek-ai/
|
|
94
|
-
"@deepseek-ai/dsh-
|
|
95
|
-
"@deepseek-ai/dsh-
|
|
107
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.2",
|
|
108
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.4",
|
|
109
|
+
"@deepseek-ai/dsh-jobs": "0.2.1-alpha.4",
|
|
110
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.4",
|
|
111
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.4",
|
|
112
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.4",
|
|
96
113
|
"@types/js-yaml": "4.0.9",
|
|
97
114
|
"@types/node": "22.15.0",
|
|
98
115
|
"@types/react": "19.2.2",
|
|
@@ -104,6 +121,7 @@
|
|
|
104
121
|
"typescript": "5.9.3"
|
|
105
122
|
},
|
|
106
123
|
"engines": {
|
|
107
|
-
"node": ">=20"
|
|
124
|
+
"node": ">=20",
|
|
125
|
+
"dsh": ">=0.1.1-rc.1"
|
|
108
126
|
}
|
|
109
127
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1054,9 +1054,22 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
1054
1054
|
}, [session])
|
|
1055
1055
|
const report = latestReport(session)
|
|
1056
1056
|
|
|
1057
|
+
// Mount-time suppression: opening a session that ALREADY has a report mounts
|
|
1058
|
+
// this component with that report, and an effect keyed on the report hash
|
|
1059
|
+
// fires immediately — popping a stale "Round X 完成" bubble for work that did
|
|
1060
|
+
// not just complete. Track the first key this instance sees and skip it; only
|
|
1061
|
+
// a subsequent round progression pulses.
|
|
1062
|
+
const pulseSeenRef = React.useRef<string | null>(null)
|
|
1057
1063
|
React.useEffect(() => {
|
|
1058
1064
|
if (!report) return
|
|
1059
1065
|
const cur = getCurrentRound(report)
|
|
1066
|
+
const key = hashReport(report) + ':' + String(cur)
|
|
1067
|
+
if (pulseSeenRef.current === null) {
|
|
1068
|
+
pulseSeenRef.current = key
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
if (pulseSeenRef.current === key) return
|
|
1072
|
+
pulseSeenRef.current = key
|
|
1060
1073
|
const conv = report.convergence
|
|
1061
1074
|
emitRoundPulse(cur, conv?.converged === true)
|
|
1062
1075
|
setPulseKey((k) => k + 1)
|
|
@@ -1406,6 +1419,11 @@ function TriagePanel(props: SlotProps) {
|
|
|
1406
1419
|
}
|
|
1407
1420
|
|
|
1408
1421
|
// ── Keyboard shortcuts (y / n / a on the selected finding, ↑/↓ to move) ──
|
|
1422
|
+
// Multi-instance scoping: every turn that has a report mounts its OWN triage
|
|
1423
|
+
// panel. Keying off `document.querySelector('[data-iterate="triage"]')` would
|
|
1424
|
+
// return only the FIRST one, so y/n/a/↑/↓ silently dead whenever focus sits
|
|
1425
|
+
// in a later panel. A per-component ref points at THIS instance's root.
|
|
1426
|
+
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
|
1409
1427
|
React.useEffect(() => {
|
|
1410
1428
|
const doc = typeof document !== 'undefined' ? document : null
|
|
1411
1429
|
if (!doc) return
|
|
@@ -1417,10 +1435,10 @@ function TriagePanel(props: SlotProps) {
|
|
|
1417
1435
|
const t = ev.target as HTMLElement | null
|
|
1418
1436
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return
|
|
1419
1437
|
if (t && typeof t.isContentEditable === 'boolean' && t.isContentEditable) return
|
|
1420
|
-
// Only act when focus is INSIDE
|
|
1438
|
+
// Only act when focus is INSIDE THIS panel. Everything else keeps
|
|
1421
1439
|
// standard behavior (↑/↓ page scroll, typing in the composer), so we do
|
|
1422
1440
|
// not hijack document-level keys just because a report is mounted.
|
|
1423
|
-
const rootEl =
|
|
1441
|
+
const rootEl = rootRef.current
|
|
1424
1442
|
if (!rootEl) return
|
|
1425
1443
|
const activeEl = doc.activeElement as HTMLElement | null
|
|
1426
1444
|
if (!activeEl) return
|
|
@@ -1584,7 +1602,7 @@ function TriagePanel(props: SlotProps) {
|
|
|
1584
1602
|
)
|
|
1585
1603
|
})
|
|
1586
1604
|
|
|
1587
|
-
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1605
|
+
return React.createElement('div', { ref: rootRef, 'data-iterate-root': '', 'data-iterate': 'triage', className: 'iterate-triage' },
|
|
1588
1606
|
React.createElement('div', { className: 'iterate-triage-head' },
|
|
1589
1607
|
React.createElement('span', { role: 'heading', 'aria-level': 3 }, tf(`Iterate · Findings 分诊 ({a0}/{a1})`, { a0: filtered.length, a1: findings.length })),
|
|
1590
1608
|
React.createElement('span', { className: 'iterate-triage-hint' }, t('y=修复 · n=跳过 · a=已知有意 · ↑/↓ 选择')),
|
|
@@ -2525,7 +2543,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2525
2543
|
// client has no filesystem access, so `iterate_history` aggregates have to
|
|
2526
2544
|
// be echoed back into the session first).
|
|
2527
2545
|
const report = latestReport(session)
|
|
2528
|
-
const comparison = buildRoundComparison(report, manifest.validations || [])
|
|
2546
|
+
const comparison = buildRoundComparison(report, manifest.validations || [], manifest.fixes || [])
|
|
2529
2547
|
const comparisonTable = comparison.length === 0
|
|
2530
2548
|
? null
|
|
2531
2549
|
: React.createElement('table', { className: 'iterate-obs-table' },
|