iterate-plugin 3.5.5 → 3.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +60 -21
- package/dist/tools/checkpoint.js +11 -4
- package/dist/tools/history.js +1 -1
- package/dist/tools/prune.js +22 -5
- 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 +11 -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 +60 -21
- package/src/tools/checkpoint.ts +12 -4
- package/src/tools/history.ts +1 -1
- package/src/tools/prune.ts +21 -5
- package/src/tools/transcript.ts +16 -5
- package/src/transcript.ts +85 -2
- package/src/types.ts +9 -4
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.6",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,10 @@
|
|
|
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"
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
"client": {
|
|
@@ -76,15 +79,15 @@
|
|
|
76
79
|
"test:validate": "tsx --test test/validate.test.ts"
|
|
77
80
|
},
|
|
78
81
|
"dependencies": {
|
|
79
|
-
"@deepseek-ai/cordis": "4.0.
|
|
80
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
81
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
82
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
83
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-util-values": "0.1.7-rc.1",
|
|
82
85
|
"js-yaml": "4.3.2"
|
|
83
86
|
},
|
|
84
87
|
"devDependencies": {
|
|
85
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
86
|
-
"@deepseek-ai/dsh-jobs": "
|
|
87
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
88
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-jobs": "0.1.7-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.1",
|
|
88
91
|
"@types/js-yaml": "4.0.9",
|
|
89
92
|
"@types/node": "22.15.0",
|
|
90
93
|
"@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) {
|
package/src/review.ts
CHANGED
|
@@ -162,13 +162,17 @@ export function aggregateRounds(
|
|
|
162
162
|
// Clamp the allocation bound so a hostile round number cannot OOM the tool.
|
|
163
163
|
const effectiveMax = Math.min(maxRound, Math.max(1, roundCap * 2))
|
|
164
164
|
|
|
165
|
-
const findingsByRound: number[] =
|
|
166
|
-
for (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
const findingsByRound: number[] = new Array(effectiveMax).fill(0)
|
|
166
|
+
for (const key of firstRoundByKey.keys()) {
|
|
167
|
+
const firstRound = firstRoundByKey.get(key) ?? 1
|
|
168
|
+
// Fold rounds BEYOND the allocation bound into the highest slot instead of
|
|
169
|
+
// dropping them: a finding first seen in an over-cap round was previously
|
|
170
|
+
// invisible to `findingsByRound`, so a run whose final rounds exceeded the
|
|
171
|
+
// cap looked "converged" even though a real round found new issues. Folding
|
|
172
|
+
// keeps the counts truthful (the sum still equals totalFindings) while the
|
|
173
|
+
// memory bound stays fixed.
|
|
174
|
+
const idx = Math.min(firstRound, effectiveMax) - 1
|
|
175
|
+
findingsByRound[idx] = (findingsByRound[idx] || 0) + 1
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
return { findings: dedupeFindings(merged), findingsByRound, firstRoundByKey }
|
|
@@ -282,8 +286,13 @@ export function buildReviewReport(input: {
|
|
|
282
286
|
for (const r of filteredRounds) {
|
|
283
287
|
if (typeof r.round === 'number' && r.round > lastRound) lastRound = r.round
|
|
284
288
|
}
|
|
289
|
+
// `lastRound` can exceed the array length when over-cap rounds were FOLDED
|
|
290
|
+
// into the last slot (see aggregateRounds). Read the same clamped index
|
|
291
|
+
// computeConvergence uses, so a real over-cap round is never mis-read as 0
|
|
292
|
+
// new findings (which would falsely report "converged").
|
|
293
|
+
const idx = Math.min(lastRound, findingsByRound.length) - 1
|
|
285
294
|
const lastRoundCount =
|
|
286
|
-
lastRound > 0 ? (findingsByRound[
|
|
295
|
+
lastRound > 0 && idx >= 0 ? (findingsByRound[idx] ?? 0) : 0
|
|
287
296
|
const converged = filteredRounds.length > 0 && lastRoundCount === 0
|
|
288
297
|
|
|
289
298
|
// Attach the normal-mode fix count to the summary (dry-run leaves it absent).
|
|
@@ -630,6 +639,12 @@ export function reviewerTaskPrompt(input: {
|
|
|
630
639
|
* every dimension reviewer inspects/considers them before judging.
|
|
631
640
|
*/
|
|
632
641
|
attachments?: ReviewAttachment[]
|
|
642
|
+
/**
|
|
643
|
+
* Configured reasoning effort ('low' | 'medium' | 'high') to apply to this
|
|
644
|
+
* review pass. Injected as a directive so the harness can honor it when
|
|
645
|
+
* running each reviewer subagent. Omitted → follow the provider default.
|
|
646
|
+
*/
|
|
647
|
+
effort?: 'low' | 'medium' | 'high'
|
|
633
648
|
}): string {
|
|
634
649
|
const parts: string[] = []
|
|
635
650
|
parts.push(
|
|
@@ -637,6 +652,9 @@ export function reviewerTaskPrompt(input: {
|
|
|
637
652
|
`Goal: ${input.goal}`,
|
|
638
653
|
`Scope: ${input.scope === 'full' ? 'entire codebase' : 'changed files only'}.`,
|
|
639
654
|
)
|
|
655
|
+
if (input.effort) {
|
|
656
|
+
parts.push(`Reasoning effort for this review pass: ${input.effort}.`)
|
|
657
|
+
}
|
|
640
658
|
if (input.focus) {
|
|
641
659
|
parts.push(`FOCUS: ${input.focus}`)
|
|
642
660
|
}
|
|
@@ -744,6 +762,12 @@ export function buildReviewPlan(input: {
|
|
|
744
762
|
fallbackToFull: boolean
|
|
745
763
|
/** The attachments threaded into every reviewer prompt (empty when none). */
|
|
746
764
|
attachments: ReviewAttachment[]
|
|
765
|
+
/**
|
|
766
|
+
* Configured reasoning effort carried on the plan for the orchestrator to
|
|
767
|
+
* honor (null when unset → provider default). Also injected as a directive
|
|
768
|
+
* into every dimension's reviewer prompt.
|
|
769
|
+
*/
|
|
770
|
+
reasoningEffort: 'low' | 'medium' | 'high' | null
|
|
747
771
|
} {
|
|
748
772
|
// Defensive reads: a malformed config (e.g. `dimensions` as a non-array, or
|
|
749
773
|
// `review`/`atomic` missing) must degrade to sane defaults instead of
|
|
@@ -751,6 +775,12 @@ export function buildReviewPlan(input: {
|
|
|
751
775
|
const language = input.config.language === 'zh' ? 'Chinese (中文)' : 'English'
|
|
752
776
|
const goal = input.config.goal ?? ''
|
|
753
777
|
const configuredScope = input.config.review?.scope ?? 'full'
|
|
778
|
+
const reasoningEffort =
|
|
779
|
+
input.config.reasoning_effort === 'low' ||
|
|
780
|
+
input.config.reasoning_effort === 'medium' ||
|
|
781
|
+
input.config.reasoning_effort === 'high'
|
|
782
|
+
? input.config.reasoning_effort
|
|
783
|
+
: null
|
|
754
784
|
const dimensions = Array.isArray(input.config.dimensions) ? input.config.dimensions : []
|
|
755
785
|
const maxLines = input.config.atomic?.max_lines ?? 20
|
|
756
786
|
const changedFiles = Array.isArray(input.changedFiles) ? input.changedFiles : []
|
|
@@ -816,6 +846,7 @@ export function buildReviewPlan(input: {
|
|
|
816
846
|
scopeFiles: batch,
|
|
817
847
|
focus: focusMap.get(d),
|
|
818
848
|
attachments,
|
|
849
|
+
effort: reasoningEffort ?? undefined,
|
|
819
850
|
}),
|
|
820
851
|
findingsSchema: findingsSchema(),
|
|
821
852
|
})
|
|
@@ -832,5 +863,6 @@ export function buildReviewPlan(input: {
|
|
|
832
863
|
changedFiles: effectiveChangedOnly ? changedFiles : [],
|
|
833
864
|
fallbackToFull,
|
|
834
865
|
attachments,
|
|
866
|
+
reasoningEffort,
|
|
835
867
|
}
|
|
836
868
|
}
|