iterate-plugin 3.5.5 → 3.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-loader.js +22 -2
- package/dist/config-write.js +28 -0
- package/dist/evidence.js +5 -1
- package/dist/meta-review.js +5 -1
- package/dist/review.js +27 -9
- package/dist/skill-prompt.js +86 -27
- package/dist/tools/checkpoint.js +11 -4
- package/dist/tools/decision-log.js +11 -0
- package/dist/tools/history.js +1 -1
- package/dist/tools/prune.js +29 -6
- package/dist/tools/transcript.js +17 -6
- package/dist/transcript.js +78 -2
- package/lib/client.js +63 -31
- package/lib/parse.js +28 -0
- package/package.json +12 -8
- package/src/client/index.ts +45 -24
- package/src/config-loader.ts +20 -2
- package/src/config-write.ts +32 -0
- package/src/evidence.ts +5 -1
- package/src/meta-review.ts +5 -1
- package/src/review.ts +40 -8
- package/src/skill-prompt.ts +86 -27
- package/src/tools/checkpoint.ts +12 -4
- package/src/tools/decision-log.ts +12 -0
- package/src/tools/history.ts +1 -1
- package/src/tools/prune.ts +28 -6
- package/src/tools/transcript.ts +16 -5
- package/src/transcript.ts +85 -2
- package/src/types.ts +9 -4
package/dist/tools/transcript.js
CHANGED
|
@@ -148,7 +148,7 @@ export function registerTranscriptTool(ctx) {
|
|
|
148
148
|
description: 'For `capture`: array of applied fixes [{id, file, round, summary, linesAdded, linesRemoved, success}].',
|
|
149
149
|
},
|
|
150
150
|
refReadFiles: { type: 'json', description: 'For `capture`: flat array of all read files across rounds (optional).' },
|
|
151
|
-
stoppedReason: { type: 'string', description: 'For `capture`: why the run ended — "converged" | "max_rounds_reached" | "aborted_by_validation". When omitted, derived from the convergence trend (trailing 0 = converged, otherwise = max_rounds_reached once a round ran).' },
|
|
151
|
+
stoppedReason: { type: 'string', description: 'For `capture`: why the run ended — "converged" | "max_rounds_reached" | "aborted_by_validation" | "aborted_by_config" | "schema_invalid" | "no_usable_reviewer_output" | "inconclusive". When omitted, derived from the convergence trend (trailing 0 = converged, otherwise = max_rounds_reached once a round ran).' },
|
|
152
152
|
text: { type: 'string', description: 'For `nudge`: steering text to set (or null to clear).' },
|
|
153
153
|
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
154
154
|
},
|
|
@@ -344,13 +344,24 @@ function rehydrateBuilder(manifest, approval) {
|
|
|
344
344
|
maxRounds: manifest.maxRounds,
|
|
345
345
|
});
|
|
346
346
|
for (const r of Array.isArray(manifest.rounds) ? manifest.rounds : []) {
|
|
347
|
+
// A malformed/null round row must be skipped, never a throw (the caller
|
|
348
|
+
// wraps rehydrate in a fallback, but preserving sibling rows is better).
|
|
349
|
+
if (!r || typeof r !== 'object')
|
|
350
|
+
continue;
|
|
347
351
|
builder.roundStart(r.round, manifest.maxRounds);
|
|
348
352
|
for (const t of Array.isArray(r.threads) ? r.threads : []) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
// restoreThread (not reviewerStart + reviewerMessage(join)) preserves the
|
|
354
|
+
// message ARRAY boundaries and the thread's own dimension: rehydration
|
|
355
|
+
// previously collapsed each thread's messages into one string and, once a
|
|
356
|
+
// round hit the per-round thread cap, silently DROPPED the extra threads
|
|
357
|
+
// / mis-merged their findings into the previous dimension's thread.
|
|
358
|
+
builder.restoreThread(r.round, {
|
|
359
|
+
dimension: t.dimension,
|
|
360
|
+
attempt: t.attempt,
|
|
361
|
+
messages: t.messages,
|
|
362
|
+
readFiles: t.readFiles,
|
|
363
|
+
findings: t.findings,
|
|
364
|
+
});
|
|
354
365
|
}
|
|
355
366
|
}
|
|
356
367
|
for (let idx = 0; idx < (manifest.convergence ?? []).length; idx += 1) {
|
package/dist/transcript.js
CHANGED
|
@@ -82,7 +82,9 @@ function normalizeFinding(input) {
|
|
|
82
82
|
const severity = sev === 'critical' || sev === 'high' || sev === 'medium' || sev === 'low'
|
|
83
83
|
? sev
|
|
84
84
|
: 'low';
|
|
85
|
-
|
|
85
|
+
// Only a non-negative integer is a real line anchor; a fractional/malformed
|
|
86
|
+
// value (e.g. 3.7 from a bypassed schema) must not leak into the manifest.
|
|
87
|
+
const line = typeof f.line === 'number' && Number.isInteger(f.line) && f.line >= 0 ? f.line : 0;
|
|
86
88
|
return {
|
|
87
89
|
dimension,
|
|
88
90
|
file,
|
|
@@ -219,7 +221,9 @@ export class ReviewTranscriptBuilder {
|
|
|
219
221
|
const att = typeof attempt === 'number' && Number.isFinite(attempt) ? Math.floor(attempt) : 1;
|
|
220
222
|
this.roundStart(this.round);
|
|
221
223
|
const live = this.rounds[this.round - 1];
|
|
222
|
-
if (live
|
|
224
|
+
if (!live)
|
|
225
|
+
return;
|
|
226
|
+
if (this.threadCount(live) < MAX_THREADS_PER_ROUND) {
|
|
223
227
|
live.threads.push({
|
|
224
228
|
dimension: dim || 'review',
|
|
225
229
|
attempt: att > 0 ? att : 1,
|
|
@@ -228,6 +232,78 @@ export class ReviewTranscriptBuilder {
|
|
|
228
232
|
findings: [],
|
|
229
233
|
});
|
|
230
234
|
}
|
|
235
|
+
else {
|
|
236
|
+
// Thread cap reached: route the extra dimension/retry into ONE shared
|
|
237
|
+
// "other" overflow thread instead of silently dropping it. A dropped
|
|
238
|
+
// reviewerStart used to leave `currentThread()` pointing at the PREVIOUS
|
|
239
|
+
// dimension's thread, so its findings were mis-attributed to that
|
|
240
|
+
// dimension. The overflow thread stays bounded (at most ONE extra thread
|
|
241
|
+
// per round) while keeping over-cap findings attributable to "other".
|
|
242
|
+
let overflow = live.threads.find((t) => t.dimension === 'other');
|
|
243
|
+
if (!overflow) {
|
|
244
|
+
overflow = {
|
|
245
|
+
dimension: 'other',
|
|
246
|
+
attempt: att > 0 ? att : 1,
|
|
247
|
+
messages: [],
|
|
248
|
+
readFiles: [],
|
|
249
|
+
findings: [],
|
|
250
|
+
};
|
|
251
|
+
live.threads.push(overflow);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.touch();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Restore ONE persisted thread during rehydration, preserving the message
|
|
258
|
+
* ARRAY boundaries (a rehydrated manifest is re-serialized on the next nudge
|
|
259
|
+
* edit, so collapsed messages would silently rewrite the evidence) and the
|
|
260
|
+
* thread's own dimension (never merging it into a previous dimension's
|
|
261
|
+
* thread). Bypasses the live per-round cap — the manifest already enforced
|
|
262
|
+
* it at capture time — but every restored field is hard-bounded so a hostile
|
|
263
|
+
* hand-edited manifest can never blow up memory.
|
|
264
|
+
*/
|
|
265
|
+
restoreThread(round, thread) {
|
|
266
|
+
const r = typeof round === 'number' && Number.isFinite(round)
|
|
267
|
+
? Math.min(Math.floor(round), MAX_ROUNDS)
|
|
268
|
+
: 1;
|
|
269
|
+
this.roundStart(r);
|
|
270
|
+
const live = this.rounds[this.round - 1];
|
|
271
|
+
if (!live)
|
|
272
|
+
return;
|
|
273
|
+
if (!thread || typeof thread !== 'object')
|
|
274
|
+
return;
|
|
275
|
+
// Hostile-manifest guard: keep the live hard bound (with headroom for the
|
|
276
|
+
// single overflow thread) and newest-first truncate beyond it.
|
|
277
|
+
if (live.threads.length >= MAX_THREADS_PER_ROUND * 2 + 1)
|
|
278
|
+
return;
|
|
279
|
+
const dim = typeof thread.dimension === 'string' && thread.dimension.trim()
|
|
280
|
+
? thread.dimension.trim()
|
|
281
|
+
: 'review';
|
|
282
|
+
const att = typeof thread.attempt === 'number' && Number.isFinite(thread.attempt)
|
|
283
|
+
? Math.floor(thread.attempt)
|
|
284
|
+
: 1;
|
|
285
|
+
const t = {
|
|
286
|
+
dimension: dim,
|
|
287
|
+
attempt: att > 0 ? att : 1,
|
|
288
|
+
messages: [],
|
|
289
|
+
readFiles: [],
|
|
290
|
+
findings: [],
|
|
291
|
+
};
|
|
292
|
+
const messages = Array.isArray(thread.messages) ? thread.messages : [];
|
|
293
|
+
for (const m of messages.slice(0, MAX_MESSAGES_PER_THREAD)) {
|
|
294
|
+
if (typeof m === 'string' && m.trim())
|
|
295
|
+
t.messages.push(m);
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(thread.readFiles)) {
|
|
298
|
+
t.readFiles.push(...dedupePaths(thread.readFiles));
|
|
299
|
+
}
|
|
300
|
+
if (Array.isArray(thread.findings)) {
|
|
301
|
+
for (const f of thread.findings) {
|
|
302
|
+
if (normalizeFinding(f))
|
|
303
|
+
pushThreadFinding(t, f);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
live.threads.push(t);
|
|
231
307
|
this.touch();
|
|
232
308
|
}
|
|
233
309
|
/** Append narration (assistant text) to the current reviewer thread. */
|
package/lib/client.js
CHANGED
|
@@ -1211,6 +1211,26 @@ function trendMax(points) {
|
|
|
1211
1211
|
}
|
|
1212
1212
|
return max;
|
|
1213
1213
|
}
|
|
1214
|
+
function stoppedReasonLabel(reason) {
|
|
1215
|
+
switch (reason) {
|
|
1216
|
+
case "converged":
|
|
1217
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u5DF2\u6536\u655B\uFF08\u65E0\u65B0\u53D1\u73B0\uFF09";
|
|
1218
|
+
case "max_rounds_reached":
|
|
1219
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u8FBE\u5230\u8F6E\u6570\u4E0A\u9650";
|
|
1220
|
+
case "aborted_by_validation":
|
|
1221
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA\u505C\u6B62";
|
|
1222
|
+
case "aborted_by_config":
|
|
1223
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u547D\u4EE4\u4E0D\u5728\u767D\u540D\u5355\uFF08\u914D\u7F6E\u9700\u4FEE\u590D\uFF09";
|
|
1224
|
+
case "inconclusive":
|
|
1225
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u8F6E\u6B21\u7ED3\u8BBA\u4E0D\u660E\uFF08\u5BA1\u67E5\u8F93\u51FA\u65E0\u6548\uFF09";
|
|
1226
|
+
case "schema_invalid":
|
|
1227
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u5BA1\u67E5\u8F93\u51FA\u8FDE\u7EED schema \u6821\u9A8C\u5931\u8D25";
|
|
1228
|
+
case "no_usable_reviewer_output":
|
|
1229
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u65E0\u53EF\u7528\u5BA1\u67E5\u8F93\u51FA";
|
|
1230
|
+
default:
|
|
1231
|
+
return reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F";
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1214
1234
|
function buildCompletionSummary(report) {
|
|
1215
1235
|
const conv = (
|
|
1216
1236
|
/** @type {Record<string, unknown>} */
|
|
@@ -1229,6 +1249,7 @@ var CONFIG_EDIT_FIELDS = [
|
|
|
1229
1249
|
{ key: "dimensions", label: "\u5BA1\u67E5\u7EF4\u5EA6", hint: '\u6570\u7EC4\uFF0C\u5982 ["correctness","security"]' },
|
|
1230
1250
|
{ key: "max_rounds", label: "\u6700\u5927\u8F6E\u6570", hint: "\u6B63\u6574\u6570" },
|
|
1231
1251
|
{ key: "review.scope", label: "\u5BA1\u67E5\u8303\u56F4", hint: '"full" \u6216 "changed-only"' },
|
|
1252
|
+
{ key: "reasoning_effort", label: "\u5BA1\u67E5\u63A8\u7406\u5F3A\u5EA6", hint: '"low" / "medium" / "high"\uFF0C\u7F3A\u7701\u8DDF\u968F\u6A21\u578B\u9ED8\u8BA4' },
|
|
1232
1253
|
{ key: "atomic.max_lines", label: "\u539F\u5B50\u4FEE\u590D\u4E0A\u9650\u884C\u6570", hint: "\u6B63\u6574\u6570" },
|
|
1233
1254
|
{ key: "git.push_per_round", label: "\u6BCF\u8F6E\u63A8\u9001", hint: "true / false" }
|
|
1234
1255
|
];
|
|
@@ -1724,20 +1745,6 @@ function severityColor(severity) {
|
|
|
1724
1745
|
function severityLabel(severity) {
|
|
1725
1746
|
return SEVERITY_LABEL[coerceSeverity(severity)];
|
|
1726
1747
|
}
|
|
1727
|
-
function stoppedReasonLabel(reason) {
|
|
1728
|
-
switch (reason) {
|
|
1729
|
-
case "converged":
|
|
1730
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u5DF2\u6536\u655B\uFF08\u65E0\u65B0\u53D1\u73B0\uFF09";
|
|
1731
|
-
case "max_rounds_reached":
|
|
1732
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u8FBE\u5230\u8F6E\u6570\u4E0A\u9650";
|
|
1733
|
-
case "aborted_by_validation":
|
|
1734
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA\u505C\u6B62";
|
|
1735
|
-
case "aborted_by_config":
|
|
1736
|
-
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u547D\u4EE4\u4E0D\u5728\u767D\u540D\u5355\uFF08\u914D\u7F6E\u9700\u4FEE\u590D\uFF09";
|
|
1737
|
-
default:
|
|
1738
|
-
return reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F";
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
1748
|
function runStatusText(live, manifest) {
|
|
1742
1749
|
if (live) return "\u8FD0\u884C\u4E2D";
|
|
1743
1750
|
const reason = manifest && typeof manifest.stoppedReason === "string" ? manifest.stoppedReason : "";
|
|
@@ -1767,6 +1774,7 @@ var slotsSvc = void 0;
|
|
|
1767
1774
|
var themeSvc = void 0;
|
|
1768
1775
|
var storage = null;
|
|
1769
1776
|
var themeDisposer = null;
|
|
1777
|
+
var themeListener = null;
|
|
1770
1778
|
var themeEnabled = true;
|
|
1771
1779
|
var roundPulseListeners = [];
|
|
1772
1780
|
function emitRoundPulse(round, converged) {
|
|
@@ -1788,6 +1796,31 @@ function clearThemeSkin() {
|
|
|
1788
1796
|
themeDisposer = null;
|
|
1789
1797
|
}
|
|
1790
1798
|
}
|
|
1799
|
+
function registerThemeListener(ctx) {
|
|
1800
|
+
if (typeof ctx.on !== "function") return;
|
|
1801
|
+
if (themeListener) {
|
|
1802
|
+
try {
|
|
1803
|
+
themeListener();
|
|
1804
|
+
} catch {
|
|
1805
|
+
}
|
|
1806
|
+
themeListener = null;
|
|
1807
|
+
}
|
|
1808
|
+
const off = ctx.on("theme/change", () => {
|
|
1809
|
+
if (themeEnabled) applyThemeSkin();
|
|
1810
|
+
});
|
|
1811
|
+
themeListener = typeof off === "function" ? off : null;
|
|
1812
|
+
if (typeof ctx.effect === "function") {
|
|
1813
|
+
ctx.effect(() => {
|
|
1814
|
+
if (themeListener) {
|
|
1815
|
+
try {
|
|
1816
|
+
themeListener();
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
themeListener = null;
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1791
1824
|
function setThemeEnabled(enabled) {
|
|
1792
1825
|
themeEnabled = enabled;
|
|
1793
1826
|
if (storage) storage.set(THEME_STORAGE_KEY, enabled ? "1" : "0");
|
|
@@ -3874,18 +3907,21 @@ function apply(ctx) {
|
|
|
3874
3907
|
themeEnabled = savedTheme === null ? true : savedTheme === "1";
|
|
3875
3908
|
if (themeEnabled) applyThemeSkin();
|
|
3876
3909
|
if (typeof document !== "undefined" && document.createElement && document.head) {
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3910
|
+
if (!document.head.querySelector('style[data-iterate-lock="1"]')) {
|
|
3911
|
+
const style = document.createElement("style");
|
|
3912
|
+
style.dataset.iterateLock = "1";
|
|
3913
|
+
style.dataset.plugin = PLUGIN_TAG;
|
|
3914
|
+
style.dataset.pluginCss = "iterate-main";
|
|
3915
|
+
style.textContent = ITERATE_CSS;
|
|
3916
|
+
document.head.appendChild(style);
|
|
3917
|
+
if (typeof ctx.effect === "function") {
|
|
3918
|
+
ctx.effect(() => {
|
|
3919
|
+
try {
|
|
3920
|
+
style.remove();
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3889
3925
|
}
|
|
3890
3926
|
}
|
|
3891
3927
|
if (slotsSvc === void 0) {
|
|
@@ -3929,11 +3965,7 @@ function apply(ctx) {
|
|
|
3929
3965
|
)
|
|
3930
3966
|
);
|
|
3931
3967
|
}
|
|
3932
|
-
|
|
3933
|
-
ctx.on("theme/change", () => {
|
|
3934
|
-
if (themeEnabled) applyThemeSkin();
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3968
|
+
registerThemeListener(ctx);
|
|
3937
3969
|
}
|
|
3938
3970
|
return module.exports; } });
|
|
3939
3971
|
|
package/lib/parse.js
CHANGED
|
@@ -1687,6 +1687,33 @@ export function trendMax(points) {
|
|
|
1687
1687
|
return max
|
|
1688
1688
|
}
|
|
1689
1689
|
|
|
1690
|
+
// ─── Stop reason badges (v3.5.5) ─────────────────────────────────────────────
|
|
1691
|
+
//
|
|
1692
|
+
// Every value the iterate loop can report as `stoppedReason` — including the
|
|
1693
|
+
// schema-retry batch (schema_invalid / no_usable_reviewer_output / inconclusive)
|
|
1694
|
+
// — maps to a concrete Chinese label. Unknown values still get a readable
|
|
1695
|
+
// default instead of a bare "已结束". Kept in parse.js so both the client
|
|
1696
|
+
// observatory badge and Node unit tests share one source of truth.
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Chinese badge label for a run's `stoppedReason`, or null/undefined-safe.
|
|
1700
|
+
*
|
|
1701
|
+
* @param {string | null | undefined} reason
|
|
1702
|
+
* @returns {string}
|
|
1703
|
+
*/
|
|
1704
|
+
export function stoppedReasonLabel(reason) {
|
|
1705
|
+
switch (reason) {
|
|
1706
|
+
case 'converged': return '已结束 · 已收敛(无新发现)'
|
|
1707
|
+
case 'max_rounds_reached': return '已结束 · 达到轮数上限'
|
|
1708
|
+
case 'aborted_by_validation': return '已结束 · 验证失败后回滚停止'
|
|
1709
|
+
case 'aborted_by_config': return '已结束 · 验证命令不在白名单(配置需修复)'
|
|
1710
|
+
case 'inconclusive': return '已结束 · 轮次结论不明(审查输出无效)'
|
|
1711
|
+
case 'schema_invalid': return '已结束 · 审查输出连续 schema 校验失败'
|
|
1712
|
+
case 'no_usable_reviewer_output': return '已结束 · 无可用审查输出'
|
|
1713
|
+
default: return reason ? `已结束 · ${reason}` : '已结束'
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1690
1717
|
// ─── Completion notification ────────────────────────────────────────────────
|
|
1691
1718
|
|
|
1692
1719
|
/**
|
|
@@ -1717,6 +1744,7 @@ export const CONFIG_EDIT_FIELDS = [
|
|
|
1717
1744
|
{ key: 'dimensions', label: '审查维度', hint: '数组,如 ["correctness","security"]' },
|
|
1718
1745
|
{ key: 'max_rounds', label: '最大轮数', hint: '正整数' },
|
|
1719
1746
|
{ key: 'review.scope', label: '审查范围', hint: '"full" 或 "changed-only"' },
|
|
1747
|
+
{ key: 'reasoning_effort', label: '审查推理强度', hint: '"low" / "medium" / "high",缺省跟随模型默认' },
|
|
1720
1748
|
{ key: 'atomic.max_lines', label: '原子修复上限行数', hint: '正整数' },
|
|
1721
1749
|
{ key: 'git.push_per_round', label: '每轮推送', hint: 'true / false' },
|
|
1722
1750
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.7",
|
|
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",
|
|
@@ -39,7 +39,11 @@
|
|
|
39
39
|
"0.1.5-rc.1": "compatible",
|
|
40
40
|
"0.1.5-rc.2": "compatible",
|
|
41
41
|
"0.1.6-alpha.1": "compatible",
|
|
42
|
-
"0.1.6-alpha.2": "compatible"
|
|
42
|
+
"0.1.6-alpha.2": "compatible",
|
|
43
|
+
"0.1.7-alpha.1": "compatible",
|
|
44
|
+
"0.1.7-alpha.2": "compatible",
|
|
45
|
+
"0.1.7-rc.1": "compatible",
|
|
46
|
+
"0.1.7-rc.2": "compatible"
|
|
43
47
|
}
|
|
44
48
|
},
|
|
45
49
|
"client": {
|
|
@@ -76,15 +80,15 @@
|
|
|
76
80
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
77
81
|
},
|
|
78
82
|
"dependencies": {
|
|
79
|
-
"@deepseek-ai/cordis": "4.0.
|
|
80
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
81
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
83
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
84
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.2",
|
|
82
86
|
"js-yaml": "4.3.2"
|
|
83
87
|
},
|
|
84
88
|
"devDependencies": {
|
|
85
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
86
|
-
"@deepseek-ai/dsh-jobs": "
|
|
87
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
89
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-jobs": "0.1.7-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
88
92
|
"@types/js-yaml": "4.0.9",
|
|
89
93
|
"@types/node": "22.15.0",
|
|
90
94
|
"@types/react": "19.2.2",
|
package/src/client/index.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
latestPhase,
|
|
82
82
|
SEVERITY_LABEL,
|
|
83
83
|
SEVERITY_COLOR,
|
|
84
|
+
stoppedReasonLabel,
|
|
84
85
|
} from '../../lib/parse.js'
|
|
85
86
|
|
|
86
87
|
// ─── Module contract ─────────────────────────────────────────────────────────
|
|
@@ -737,17 +738,6 @@ function severityLabel(severity: string | undefined): string {
|
|
|
737
738
|
return SEVERITY_LABEL[coerceSeverity(severity)]
|
|
738
739
|
}
|
|
739
740
|
|
|
740
|
-
/** Human label for WHY a finished run stopped (transcript manifest). */
|
|
741
|
-
function stoppedReasonLabel(reason: string | null | undefined): string {
|
|
742
|
-
switch (reason) {
|
|
743
|
-
case 'converged': return '已结束 · 已收敛(无新发现)'
|
|
744
|
-
case 'max_rounds_reached': return '已结束 · 达到轮数上限'
|
|
745
|
-
case 'aborted_by_validation': return '已结束 · 验证失败后回滚停止'
|
|
746
|
-
case 'aborted_by_config': return '已结束 · 验证命令不在白名单(配置需修复)'
|
|
747
|
-
default: return reason ? `已结束 · ${reason}` : '已结束'
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
741
|
/** Badge text for the run's live/finished state, annotated with the stop reason. */
|
|
752
742
|
function runStatusText(live: boolean, manifest: ObsManifest | null | undefined): string {
|
|
753
743
|
if (live) return '运行中'
|
|
@@ -785,6 +775,7 @@ let slotsSvc: SlotsService | undefined = undefined
|
|
|
785
775
|
let themeSvc: ThemeService | undefined = undefined
|
|
786
776
|
let storage: ReturnType<typeof createStorage> | null = null
|
|
787
777
|
let themeDisposer: (() => void) | null = null
|
|
778
|
+
let themeListener: (() => void) | null = null
|
|
788
779
|
let themeEnabled = true
|
|
789
780
|
const roundPulseListeners: Array<(payload: { round: number; converged: boolean }) => void> = []
|
|
790
781
|
|
|
@@ -814,6 +805,33 @@ function clearThemeSkin(): void {
|
|
|
814
805
|
}
|
|
815
806
|
}
|
|
816
807
|
|
|
808
|
+
/**
|
|
809
|
+
* Register the theme/change hook exactly once per client lifecycle.
|
|
810
|
+
* The listener is tracked so a re-`apply()` (hot reload / re-mount) never
|
|
811
|
+
* stacks a second copy — every new registration first drops the previous
|
|
812
|
+
* one, and `ctx.effect` pairs it off when the client unmounts.
|
|
813
|
+
*/
|
|
814
|
+
function registerThemeListener(ctx: ClientContext): void {
|
|
815
|
+
if (typeof ctx.on !== 'function') return
|
|
816
|
+
if (themeListener) {
|
|
817
|
+
try { themeListener() } catch { /* noop */ }
|
|
818
|
+
themeListener = null
|
|
819
|
+
}
|
|
820
|
+
const off = ctx.on('theme/change', () => {
|
|
821
|
+
if (themeEnabled) applyThemeSkin()
|
|
822
|
+
})
|
|
823
|
+
// `ctx.on` may return void (no unsubscribe support) — only track a callback.
|
|
824
|
+
themeListener = typeof off === 'function' ? off : null
|
|
825
|
+
if (typeof ctx.effect === 'function') {
|
|
826
|
+
ctx.effect(() => {
|
|
827
|
+
if (themeListener) {
|
|
828
|
+
try { themeListener() } catch { /* noop */ }
|
|
829
|
+
themeListener = null
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
817
835
|
/** Persist + apply the theme preference. */
|
|
818
836
|
function setThemeEnabled(enabled: boolean): void {
|
|
819
837
|
themeEnabled = enabled
|
|
@@ -2826,15 +2844,20 @@ export function apply(ctx: ClientContext): void {
|
|
|
2826
2844
|
themeEnabled = savedTheme === null ? true : savedTheme === '1'
|
|
2827
2845
|
if (themeEnabled) applyThemeSkin()
|
|
2828
2846
|
|
|
2829
|
-
// 3. Inject styles (independent of slots / React availability).
|
|
2847
|
+
// 3. Inject styles (independent of slots / React availability). Guarded so a
|
|
2848
|
+
// re-`apply()` never appends a duplicate <style>: the tag is keyed by a
|
|
2849
|
+
// `dataset.iterateLock` marker instead of relying on the CSS content.
|
|
2830
2850
|
if (typeof document !== 'undefined' && document.createElement && document.head) {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2851
|
+
if (!document.head.querySelector('style[data-iterate-lock="1"]')) {
|
|
2852
|
+
const style = document.createElement('style')
|
|
2853
|
+
style.dataset.iterateLock = '1'
|
|
2854
|
+
style.dataset.plugin = PLUGIN_TAG
|
|
2855
|
+
style.dataset.pluginCss = 'iterate-main'
|
|
2856
|
+
style.textContent = ITERATE_CSS
|
|
2857
|
+
document.head.appendChild(style)
|
|
2858
|
+
if (typeof ctx.effect === 'function') {
|
|
2859
|
+
ctx.effect(() => { try { style.remove() } catch { /* noop */ } })
|
|
2860
|
+
}
|
|
2838
2861
|
}
|
|
2839
2862
|
}
|
|
2840
2863
|
|
|
@@ -2889,9 +2912,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
2889
2912
|
}
|
|
2890
2913
|
|
|
2891
2914
|
// 6: keep theme toggle state consistent across theme/change events.
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
})
|
|
2896
|
-
}
|
|
2915
|
+
// Registered through registerThemeListener so repeated apply() and client
|
|
2916
|
+
// unmounts never leak a second listener.
|
|
2917
|
+
registerThemeListener(ctx)
|
|
2897
2918
|
}
|
package/src/config-loader.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { join, resolve, sep } from 'node:path'
|
|
4
4
|
import yaml from 'js-yaml'
|
|
@@ -217,7 +217,25 @@ export function resolveProjectRoot(input?: string, sessionCwd?: string): Project
|
|
|
217
217
|
if (raw.includes('\0')) {
|
|
218
218
|
return { ok: false, reason: 'Refusing project root containing NUL bytes.' }
|
|
219
219
|
}
|
|
220
|
-
const
|
|
220
|
+
const rootLexical = raw ? resolve(raw) : resolve(effectiveCwd(sessionCwd))
|
|
221
|
+
// Collapse symlinks (documented contract: "collapsing `..` and symlinks") —
|
|
222
|
+
// a path through a symlinked directory must resolve to its REAL location so
|
|
223
|
+
// two aliases of the same project share one `.iterate/` state, and tools can
|
|
224
|
+
// never read outside the resolved root via a link. Only when the path
|
|
225
|
+
// exists: a not-yet-created target (dirs created through the tools) keeps
|
|
226
|
+
// the lexical path so the caller can still write into it. The filesystem
|
|
227
|
+
// root check runs against the NORMALIZED path, so a symlink to `/` is
|
|
228
|
+
// refused too.
|
|
229
|
+
let root = rootLexical
|
|
230
|
+
try {
|
|
231
|
+
if (existsSync(rootLexical)) {
|
|
232
|
+
const real = realpathSync(rootLexical)
|
|
233
|
+
if (real) root = real
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// realpath can throw on a broken link or a permission fence — keep the
|
|
237
|
+
// lexical path (existing behavior) rather than failing the resolution.
|
|
238
|
+
}
|
|
221
239
|
if (!root || root === sep) {
|
|
222
240
|
return { ok: false, reason: 'Refusing filesystem root as project root.' }
|
|
223
241
|
}
|
package/src/config-write.ts
CHANGED
|
@@ -32,6 +32,9 @@ export const MAX_ATOMIC_MAX_LINES = 10_000
|
|
|
32
32
|
/** Upper bound on `atomic.max_adjacent_methods`. */
|
|
33
33
|
export const MAX_MAX_ADJACENT_METHODS = 200
|
|
34
34
|
|
|
35
|
+
/** Upper bound on `reviewer.scope_chunk_size` (files per reviewer task batch). */
|
|
36
|
+
export const MAX_SCOPE_CHUNK_SIZE = 1000
|
|
37
|
+
|
|
35
38
|
/** Keep at most this many timestamped config backups (older ones are removed). */
|
|
36
39
|
export const MAX_CONFIG_BACKUPS = 5
|
|
37
40
|
|
|
@@ -92,6 +95,35 @@ export function validateConfigUpdates(updates: Record<string, unknown>): string[
|
|
|
92
95
|
errors.push(`updates.max_rounds must be an integer between 1 and ${MAX_MAX_ROUNDS}`)
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
if ('reasoning_effort' in updates) {
|
|
99
|
+
if (
|
|
100
|
+
updates.reasoning_effort !== undefined &&
|
|
101
|
+
updates.reasoning_effort !== 'low' &&
|
|
102
|
+
updates.reasoning_effort !== 'medium' &&
|
|
103
|
+
updates.reasoning_effort !== 'high'
|
|
104
|
+
) {
|
|
105
|
+
errors.push('updates.reasoning_effort must be "low", "medium", or "high"')
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if ('reviewer' in updates) {
|
|
109
|
+
const rv = updates.reviewer as Record<string, unknown> | undefined
|
|
110
|
+
if (!rv || typeof rv !== 'object') {
|
|
111
|
+
errors.push('updates.reviewer must be an object')
|
|
112
|
+
} else {
|
|
113
|
+
for (const boolKey of ['output_schema_validation', 'evidence_validation', 'coverage_validation'] as const) {
|
|
114
|
+
if (rv[boolKey] !== undefined && typeof rv[boolKey] !== 'boolean') {
|
|
115
|
+
errors.push(`updates.reviewer.${boolKey} must be a boolean`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
rv.scope_chunk_size !== undefined &&
|
|
120
|
+
(typeof rv.scope_chunk_size !== 'number' || !Number.isInteger(rv.scope_chunk_size) ||
|
|
121
|
+
rv.scope_chunk_size < 1 || rv.scope_chunk_size > MAX_SCOPE_CHUNK_SIZE)
|
|
122
|
+
) {
|
|
123
|
+
errors.push(`updates.reviewer.scope_chunk_size must be an integer between 1 and ${MAX_SCOPE_CHUNK_SIZE}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
95
127
|
if ('review' in updates) {
|
|
96
128
|
const r = updates.review as Record<string, unknown> | undefined
|
|
97
129
|
if (!r || typeof r !== 'object') {
|
package/src/evidence.ts
CHANGED
|
@@ -115,7 +115,11 @@ export function verifyLineBounds(
|
|
|
115
115
|
if (line === undefined || line === null || line === WHOLE_FILE_LINE) {
|
|
116
116
|
return { inBounds: true, lineTotal }
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
// Only a positive INTEGER is a valid line anchor. A fractional line (e.g.
|
|
119
|
+
// 3.7 from a bypassed schema) or a negative value is out of bounds — a
|
|
120
|
+
// fractional line "within" the file would otherwise be accepted and poison
|
|
121
|
+
// the evidence trail with a location nothing can jump to.
|
|
122
|
+
if (line < 1 || !Number.isInteger(line)) return { inBounds: false, lineTotal }
|
|
119
123
|
return { inBounds: line <= lineTotal, lineTotal }
|
|
120
124
|
}
|
|
121
125
|
|
package/src/meta-review.ts
CHANGED
|
@@ -233,7 +233,11 @@ export function metaReviewReport(report: ReviewReport): MetaReviewResult {
|
|
|
233
233
|
: null
|
|
234
234
|
const lastRoundNew =
|
|
235
235
|
lastRecordedRound !== null && lastRecordedRound > 0
|
|
236
|
-
|
|
236
|
+
// Over-cap rounds are FOLDED by aggregateRounds into the final slot, so a
|
|
237
|
+
// reported round number may exceed the array length — read the SAME
|
|
238
|
+
// clamped index buildReviewReport/computeConvergence use, or a real
|
|
239
|
+
// over-cap round would be mis-read as 0 new (flag mismatch).
|
|
240
|
+
? Number(findingsByRound[Math.min(lastRecordedRound, findingsByRound.length) - 1] ?? 0)
|
|
237
241
|
: null
|
|
238
242
|
const expectedConverged = lastRoundNew === 0
|
|
239
243
|
if (report.convergence?.converged !== expectedConverged) {
|