iterate-plugin 2.8.4 → 2.9.1
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/skill-prompt.js +68 -27
- package/dist/tools/checkpoint.js +16 -1
- package/dist/tools/context.js +123 -2
- package/dist/tools/decision-log.js +3 -1
- package/lib/client.js +113 -0
- package/lib/parse.js +118 -0
- package/package.json +1 -1
- package/src/client/index.ts +27 -0
- package/src/skill-prompt.ts +68 -27
- package/src/tools/checkpoint.ts +19 -1
- package/src/tools/context.ts +141 -2
- package/src/tools/decision-log.ts +3 -1
- package/src/types.ts +7 -0
package/lib/client.js
CHANGED
|
@@ -43,6 +43,101 @@ var SEVERITY_COLOR = {
|
|
|
43
43
|
medium: "#eab308",
|
|
44
44
|
low: "#6b7280"
|
|
45
45
|
};
|
|
46
|
+
function scanSessionForResume(obj, seen, maxDepth = 20) {
|
|
47
|
+
if (maxDepth <= 0) return 0;
|
|
48
|
+
if (!obj || typeof obj !== "object") return 0;
|
|
49
|
+
const s = seen || /* @__PURE__ */ new Set();
|
|
50
|
+
if (s.has(obj)) return 0;
|
|
51
|
+
s.add(obj);
|
|
52
|
+
let best = 0;
|
|
53
|
+
const direct = (
|
|
54
|
+
/** @type {Record<string, unknown>} */
|
|
55
|
+
obj
|
|
56
|
+
);
|
|
57
|
+
if (direct.type === "resume") {
|
|
58
|
+
const data = (
|
|
59
|
+
/** @type {Record<string, unknown>} */
|
|
60
|
+
direct.data || {}
|
|
61
|
+
);
|
|
62
|
+
if (typeof data.resumeCount === "number" && data.resumeCount > best) {
|
|
63
|
+
best = data.resumeCount;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (direct.entry && typeof direct.entry === "object") {
|
|
67
|
+
const entry = (
|
|
68
|
+
/** @type {Record<string, unknown>} */
|
|
69
|
+
direct.entry
|
|
70
|
+
);
|
|
71
|
+
if (entry.type === "resume") {
|
|
72
|
+
const data = (
|
|
73
|
+
/** @type {Record<string, unknown>} */
|
|
74
|
+
entry.data || {}
|
|
75
|
+
);
|
|
76
|
+
if (typeof data.resumeCount === "number" && data.resumeCount > best) {
|
|
77
|
+
best = data.resumeCount;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(obj)) {
|
|
82
|
+
for (const item of obj) {
|
|
83
|
+
const found = scanSessionForResume(item, s, maxDepth - 1);
|
|
84
|
+
if (found > best) best = found;
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
88
|
+
for (const key of Object.keys(direct)) {
|
|
89
|
+
const val = direct[key];
|
|
90
|
+
if (val && typeof val === "object") {
|
|
91
|
+
const found = scanSessionForResume(val, s, maxDepth - 1);
|
|
92
|
+
if (found > best) best = found;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
function countSessionImages(session) {
|
|
98
|
+
if (!session || typeof session !== "object") return 0;
|
|
99
|
+
const ids = /* @__PURE__ */ new Set();
|
|
100
|
+
let count = 0;
|
|
101
|
+
const walk = (obj, depth) => {
|
|
102
|
+
if (depth <= 0 || !obj || typeof obj !== "object") return;
|
|
103
|
+
if (seen.has(obj)) return;
|
|
104
|
+
seen.add(obj);
|
|
105
|
+
const o = (
|
|
106
|
+
/** @type {Record<string, unknown>} */
|
|
107
|
+
obj
|
|
108
|
+
);
|
|
109
|
+
let ref = null;
|
|
110
|
+
if (o.type === "image" && o.attachment && typeof o.attachment === "object") {
|
|
111
|
+
ref = /** @type {Record<string, unknown>} */
|
|
112
|
+
o.attachment;
|
|
113
|
+
}
|
|
114
|
+
if (!ref && typeof o.mediaType === "string" && String(o.mediaType).startsWith("image/")) {
|
|
115
|
+
ref = o;
|
|
116
|
+
}
|
|
117
|
+
if (ref) {
|
|
118
|
+
const id = typeof ref.attachmentId === "string" ? ref.attachmentId : null;
|
|
119
|
+
if (id) {
|
|
120
|
+
if (!ids.has(id)) {
|
|
121
|
+
ids.add(id);
|
|
122
|
+
count += 1;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
count += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(obj)) {
|
|
129
|
+
for (const item of obj) walk(item, depth - 1);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(o)) {
|
|
133
|
+
const val = o[key];
|
|
134
|
+
if (val && typeof val === "object") walk(val, depth - 1);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
138
|
+
walk(session, 12);
|
|
139
|
+
return count;
|
|
140
|
+
}
|
|
46
141
|
function isReviewReport(obj) {
|
|
47
142
|
if (!obj || typeof obj !== "object") return false;
|
|
48
143
|
const o = (
|
|
@@ -827,6 +922,10 @@ var ITERATE_CSS = `
|
|
|
827
922
|
.iterate-pill { display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
828
923
|
.iterate-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
829
924
|
|
|
925
|
+
/* Interruption / resume + attachment chips (dashboard) */
|
|
926
|
+
.iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
927
|
+
.iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
928
|
+
|
|
830
929
|
/* Accessibility-switch toggle */
|
|
831
930
|
.iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
|
|
832
931
|
.iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
@@ -1010,6 +1109,18 @@ function ConvergenceDashboard(props) {
|
|
|
1010
1109
|
const stats = severityStats(report);
|
|
1011
1110
|
const dims = groupByDimension(report);
|
|
1012
1111
|
const trend = computeTrendMetrics(report);
|
|
1112
|
+
const resumeCount = scanSessionForResume(session);
|
|
1113
|
+
const imageCount = countSessionImages(session);
|
|
1114
|
+
const resumeChip = resumeCount > 0 ? React.createElement("span", {
|
|
1115
|
+
className: "iterate-chip-resume",
|
|
1116
|
+
key: "resume",
|
|
1117
|
+
title: "\u672C\u6B21\u8FED\u4EE3\u4ECE\u4E0A\u4E00\u6B21\u4E2D\u65AD\u7684\u65AD\u70B9\u7EE7\u7EED\u6267\u884C"
|
|
1118
|
+
}, `\u5DF2\u4E2D\u65AD\u6062\u590D \xD7${String(resumeCount)}`) : null;
|
|
1119
|
+
const imageChip = imageCount > 0 ? React.createElement("span", {
|
|
1120
|
+
className: "iterate-chip-images",
|
|
1121
|
+
key: "images",
|
|
1122
|
+
title: "\u4F1A\u8BDD\u4E2D\u68C0\u6D4B\u5230\u7528\u6237\u9644\u5E26\u7684\u56FE\u7247\uFF0C\u8BC4\u5BA1\u5C06\u4F5C\u4E3A\u89C6\u89C9\u8BC1\u636E\u53C2\u8003"
|
|
1123
|
+
}, `\u9644\u4EF6\u56FE\u7247 ${String(imageCount)}`) : null;
|
|
1013
1124
|
const dimBadges = Object.keys(dims).slice(0, 6).map(
|
|
1014
1125
|
(dim) => React.createElement(
|
|
1015
1126
|
"span",
|
|
@@ -1058,6 +1169,8 @@ function ConvergenceDashboard(props) {
|
|
|
1058
1169
|
stats.medium
|
|
1059
1170
|
),
|
|
1060
1171
|
fixBadge,
|
|
1172
|
+
resumeChip,
|
|
1173
|
+
imageChip,
|
|
1061
1174
|
React.createElement(TrendChart, { points: trend.points }),
|
|
1062
1175
|
...dimBadges
|
|
1063
1176
|
);
|
package/lib/parse.js
CHANGED
|
@@ -28,6 +28,124 @@ export const SEVERITY_COLOR = {
|
|
|
28
28
|
low: '#6b7280',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// ─── Interruption / resume + image attachment detection ──────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deep-scan an object tree for a decision-log `resume` marker.
|
|
35
|
+
* The normal-mode workflow appends a `resume` decision-log entry when it
|
|
36
|
+
* continues a previous interrupted run:
|
|
37
|
+
* { type: "resume", data: { resumedFromRound, resumeCount } }
|
|
38
|
+
* This is the durable client-side signal that a run was interrupted and
|
|
39
|
+
* recovered. Returns the highest `resumeCount` observed, or 0 when none.
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} obj
|
|
42
|
+
* @param {Set<unknown>} [seen]
|
|
43
|
+
* @param {number} [maxDepth=20]
|
|
44
|
+
* @returns {number}
|
|
45
|
+
*/
|
|
46
|
+
export function scanSessionForResume(obj, seen, maxDepth = 20) {
|
|
47
|
+
if (maxDepth <= 0) return 0
|
|
48
|
+
if (!obj || typeof obj !== 'object') return 0
|
|
49
|
+
|
|
50
|
+
const s = seen || new Set()
|
|
51
|
+
if (s.has(obj)) return 0
|
|
52
|
+
s.add(obj)
|
|
53
|
+
|
|
54
|
+
let best = 0
|
|
55
|
+
|
|
56
|
+
// Direct marker: { type: "resume", data: { resumeCount } }.
|
|
57
|
+
const direct = /** @type {Record<string, unknown>} */ (obj)
|
|
58
|
+
if (direct.type === 'resume') {
|
|
59
|
+
const data = /** @type {Record<string, unknown>} */ (direct.data || {})
|
|
60
|
+
if (typeof data.resumeCount === 'number' && data.resumeCount > best) {
|
|
61
|
+
best = data.resumeCount
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Nested entry: { entry: { type: "resume", data: { resumeCount } } }.
|
|
65
|
+
if (direct.entry && typeof direct.entry === 'object') {
|
|
66
|
+
const entry = /** @type {Record<string, unknown>} */ (direct.entry)
|
|
67
|
+
if (entry.type === 'resume') {
|
|
68
|
+
const data = /** @type {Record<string, unknown>} */ (entry.data || {})
|
|
69
|
+
if (typeof data.resumeCount === 'number' && data.resumeCount > best) {
|
|
70
|
+
best = data.resumeCount
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(obj)) {
|
|
76
|
+
for (const item of obj) {
|
|
77
|
+
const found = scanSessionForResume(item, s, maxDepth - 1)
|
|
78
|
+
if (found > best) best = found
|
|
79
|
+
}
|
|
80
|
+
return best
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const key of Object.keys(direct)) {
|
|
84
|
+
const val = direct[key]
|
|
85
|
+
if (val && typeof val === 'object') {
|
|
86
|
+
const found = scanSessionForResume(val, s, maxDepth - 1)
|
|
87
|
+
if (found > best) best = found
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return best
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Count distinct user-attached images inside a session snapshot.
|
|
96
|
+
* Matches dsh image blocks ({ type: "image", attachment: {...} }) and
|
|
97
|
+
* raw attachment references ({ mediaType, width, height, bytes }). Dedupes by
|
|
98
|
+
* `attachmentId` when present so the same image never counts twice.
|
|
99
|
+
*
|
|
100
|
+
* @param {unknown} session
|
|
101
|
+
* @returns {number}
|
|
102
|
+
*/
|
|
103
|
+
export function countSessionImages(session) {
|
|
104
|
+
if (!session || typeof session !== 'object') return 0
|
|
105
|
+
|
|
106
|
+
const ids = new Set()
|
|
107
|
+
let count = 0
|
|
108
|
+
|
|
109
|
+
/** @param {unknown} obj */
|
|
110
|
+
const walk = (obj, depth) => {
|
|
111
|
+
if (depth <= 0 || !obj || typeof obj !== 'object') return
|
|
112
|
+
if (seen.has(obj)) return
|
|
113
|
+
seen.add(obj)
|
|
114
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
115
|
+
|
|
116
|
+
// Image block: { type: "image", attachment: { ...ref } }.
|
|
117
|
+
let ref = null
|
|
118
|
+
if (o.type === 'image' && o.attachment && typeof o.attachment === 'object') {
|
|
119
|
+
ref = /** @type {Record<string, unknown>} */ (o.attachment)
|
|
120
|
+
}
|
|
121
|
+
// Raw attachment reference shape.
|
|
122
|
+
if (!ref && typeof o.mediaType === 'string' && String(o.mediaType).startsWith('image/')) {
|
|
123
|
+
ref = o
|
|
124
|
+
}
|
|
125
|
+
if (ref) {
|
|
126
|
+
const id = typeof ref.attachmentId === 'string' ? ref.attachmentId : null
|
|
127
|
+
if (id) {
|
|
128
|
+
if (!ids.has(id)) { ids.add(id); count += 1 }
|
|
129
|
+
} else {
|
|
130
|
+
count += 1
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(obj)) {
|
|
135
|
+
for (const item of obj) walk(item, depth - 1)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
for (const key of Object.keys(o)) {
|
|
139
|
+
const val = o[key]
|
|
140
|
+
if (val && typeof val === 'object') walk(val, depth - 1)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const seen = new Set()
|
|
145
|
+
walk(session, 12)
|
|
146
|
+
return count
|
|
147
|
+
}
|
|
148
|
+
|
|
31
149
|
// ─── ReviewReport detection ──────────────────────────────────────────────────
|
|
32
150
|
|
|
33
151
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus a dry-run pure-review mode with multi-round convergence and a meta-review that audits the report and emits a final review report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/client/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
keyToVerdict,
|
|
69
69
|
allVerdictKeys,
|
|
70
70
|
buildRuntimeStatusGuide,
|
|
71
|
+
scanSessionForResume,
|
|
72
|
+
countSessionImages,
|
|
71
73
|
SEVERITY_LABEL,
|
|
72
74
|
SEVERITY_COLOR,
|
|
73
75
|
} from '../../lib/parse.js'
|
|
@@ -320,6 +322,10 @@ const ITERATE_CSS = `
|
|
|
320
322
|
.iterate-pill { display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
321
323
|
.iterate-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
322
324
|
|
|
325
|
+
/* Interruption / resume + attachment chips (dashboard) */
|
|
326
|
+
.iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
327
|
+
.iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
328
|
+
|
|
323
329
|
/* Accessibility-switch toggle */
|
|
324
330
|
.iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
|
|
325
331
|
.iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
@@ -552,6 +558,25 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
552
558
|
const dims = groupByDimension(report)
|
|
553
559
|
const trend = computeTrendMetrics(report)
|
|
554
560
|
|
|
561
|
+
// Interruption / resume awareness: a decision-log `resume` entry in the
|
|
562
|
+
// session means this run continued from an interrupted checkpoint.
|
|
563
|
+
const resumeCount = scanSessionForResume(session)
|
|
564
|
+
const imageCount = countSessionImages(session)
|
|
565
|
+
const resumeChip = resumeCount > 0
|
|
566
|
+
? React.createElement('span', {
|
|
567
|
+
className: 'iterate-chip-resume',
|
|
568
|
+
key: 'resume',
|
|
569
|
+
title: '本次迭代从上一次中断的断点继续执行',
|
|
570
|
+
}, `已中断恢复 ×${String(resumeCount)}`)
|
|
571
|
+
: null
|
|
572
|
+
const imageChip = imageCount > 0
|
|
573
|
+
? React.createElement('span', {
|
|
574
|
+
className: 'iterate-chip-images',
|
|
575
|
+
key: 'images',
|
|
576
|
+
title: '会话中检测到用户附带的图片,评审将作为视觉证据参考',
|
|
577
|
+
}, `附件图片 ${String(imageCount)}`)
|
|
578
|
+
: null
|
|
579
|
+
|
|
555
580
|
const dimBadges = Object.keys(dims).slice(0, 6).map((dim) =>
|
|
556
581
|
React.createElement(
|
|
557
582
|
'span',
|
|
@@ -598,6 +623,8 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
598
623
|
stats.medium,
|
|
599
624
|
),
|
|
600
625
|
fixBadge,
|
|
626
|
+
resumeChip,
|
|
627
|
+
imageChip,
|
|
601
628
|
React.createElement(TrendChart, { points: trend.points }),
|
|
602
629
|
...dimBadges,
|
|
603
630
|
)
|
package/src/skill-prompt.ts
CHANGED
|
@@ -13,14 +13,14 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
13
13
|
- \`iterate_config\` — read iterate.config.yaml (dimensions, validation commands, personalization) or write a validated partial update (operation:"write", with automatic backup + rollback)
|
|
14
14
|
- \`iterate_validate\` — run a whitelisted validation command
|
|
15
15
|
- \`iterate_decision_log\` — append to the decision log, or read entries back for review
|
|
16
|
-
- \`iterate_context\` — read SKILL.md / ITERATE.md project context
|
|
16
|
+
- \`iterate_context\` — read SKILL.md / ITERATE.md project context; also relays user-attached image metadata (e.g. UI screenshots, error dialogs) so reviewers can treat them as visual evidence
|
|
17
17
|
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan (for \`review.scope: changed-only\`, it resolves the git-diff file set against \`git.target_branch\` and auto-falls back to \`full\` when nothing changed); \`aggregate\` dedupes/merges findings, validates every finding against the findings schema when \`reviewer.output_schema_validation\` is on (dropping invalid entries and reporting them via \`schemaValidation\`), and computes convergence; \`meta-review\` audits a built report for internal consistency (counts, buckets, sorting, convergence math) and returns a final report with an \`approved\` / \`needs_revision\` verdict. Purely computational.
|
|
18
18
|
- \`iterate_triage\` — manage "known_intentional" entries in the config (list / apply, with dedupe + backup + rollback)
|
|
19
19
|
- \`iterate_fix\` — apply ONE atomic fix: backs up the file, enforces the atomic max_lines threshold, writes the new content, and records the fix (id + diff summary) in \`.iterate/fixes/registry.json\`
|
|
20
20
|
- \`iterate_diff\` — show the accumulated diff for a fixed file (vs its original backup) or a per-file summary of all fixes
|
|
21
21
|
- \`iterate_rollback\` — revert a fix by id: restore the file from its backup, remove the fix from the registry, log a \`revert\` entry. Use when a round's validation fails
|
|
22
22
|
- \`iterate_checkpoint\` — save / load / clear an iteration checkpoint (\`.iterate/checkpoint.json\`) so a long run can resume where it left off
|
|
23
|
-
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence
|
|
23
|
+
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence, and whether the run was interrupted (a checkpoint left on disk means the previous run was interrupted and can be resumed)
|
|
24
24
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
25
25
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
26
26
|
|
|
@@ -31,13 +31,26 @@ When the user asks to review or iterate on the project (e.g. "review this projec
|
|
|
31
31
|
|
|
32
32
|
### Workflow script contract
|
|
33
33
|
Write a plain-JS script (top-level await, ends with \`return <json>\`). Available globals:
|
|
34
|
-
- \`agent(prompt, opts?): Promise<value>\` — spawn a subagent. \`opts.schema\` gives structured output (object-rooted JSON Schema: type/properties/required/additionalProperties/items/enum/const/oneOf only). Resolves \`null\` on child failure. Other opts: \`label\`, \`phase\`.
|
|
34
|
+
- \`agent(prompt, opts?): Promise<value>\` — spawn a subagent. \`opts.schema\` gives structured output (object-rooted JSON Schema: type/properties/required/additionalProperties/items/enum/const/oneOf only). Resolves \`null\` on child failure. Other opts: \`label\`, \`phase\`. Backend selection (optional): pass \`provider\` (e.g. \`"codex"\`, \`"claude"\`, \`"default"\`) to route the sub-agent to a specific provider backend, and/or \`model\` to pin a model id. When omitted, the sub-agent uses the same provider/model as the parent session.
|
|
35
35
|
- \`parallel(thunks): Promise<value[]>\` — run zero-arg async functions concurrently, await all.
|
|
36
36
|
- \`phase(title)\`, \`log(message)\` — progress narration.
|
|
37
37
|
- \`args\` — the args object passed to the workflow tool.
|
|
38
38
|
|
|
39
39
|
The script CANNOT call tools directly. Subagents are the ones who call tools.
|
|
40
40
|
|
|
41
|
+
### Sub-agent backend selection
|
|
42
|
+
Every \`agent()\` call may carry a backend hint via \`opts.provider\` / \`opts.model\`. Use it deliberately to balance cost, speed, and reliability:
|
|
43
|
+
- **Reviewers** (many, run in parallel, read-only, benefit from strict JSON): prefer a fast/cheap model when one is configured; otherwise omit the hint and inherit the session backend.
|
|
44
|
+
- **Fixers / aggregators** (few, must be reliable and follow tool results exactly): keep them on the parent's default backend unless a specific provider is known-good.
|
|
45
|
+
- **Never invent a provider/model name.** Pass a hint ONLY when the deployment actually registers that adapter (see \`ih config\` / the configured provider list). When in doubt, omit \`provider\`/\`model\` entirely — the sub-agent then runs on the same backend as the parent session, which is always a safe default.
|
|
46
|
+
- The optional \`args.subagentProvider\` / \`args.subagentModel\` allow the caller to override the whole run's sub-agent backend from the invocation; the canonical scripts below read them and spread the hint onto every spawned sub-agent (reviewers, fixers, validators, aggregators).
|
|
47
|
+
|
|
48
|
+
### User-attached image evidence
|
|
49
|
+
The user may attach images to the conversation (UI screenshots, error dialogs, design references, logs-as-pictures). When they do:
|
|
50
|
+
- You see those images natively in the session. Capture their metadata and pass it into the workflow via \`args.attachments\` — an array of objects, each with optional \`name\`, \`mediaType\`, \`width\`, \`height\`, and a \`note\` describing what the image shows and why it matters for this review.
|
|
51
|
+
- The canonical scripts below read \`args.attachments\` and relay them into every reviewer prompt so reviewers treat the attached visuals as evidence (e.g. "the screenshot in this message shows the broken layout the review should reproduce").
|
|
52
|
+
- If a reviewer needs the images relayed explicitly, it can call \`iterate_context\` with \`attachments\` to get the normalized image descriptions in its context. Never fabricate an attachment — only relay images the user actually attached.
|
|
53
|
+
|
|
41
54
|
### Dry-run mode workflow (pure review — the ONLY mode that never touches files)
|
|
42
55
|
This is iterate's read-only health-check: repeated review rounds until findings converge,
|
|
43
56
|
then produce an auditable report, then audit the report itself (meta-review) and give a
|
|
@@ -47,9 +60,15 @@ Canonical script — reproduce this structure exactly (adjust dims via the plan)
|
|
|
47
60
|
|
|
48
61
|
\`\`\`js
|
|
49
62
|
phase('plan')
|
|
63
|
+
// Optional per-run backend override for sub-agents (omit to inherit the session backend).
|
|
64
|
+
const subAgentProvider = (args && args.subagentProvider) || undefined
|
|
65
|
+
const subAgentModel = (args && args.subagentModel) || undefined
|
|
66
|
+
const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvider } : {}, subAgentModel ? { model: subAgentModel } : {})
|
|
67
|
+
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
68
|
+
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
50
69
|
const planRes = await agent(
|
|
51
70
|
'Call iterate_review({operation:"plan", mode:"dry-run"}) and return the plan JSON.',
|
|
52
|
-
{ label: 'review:plan' }
|
|
71
|
+
Object.assign({ label: 'review:plan' }, backend)
|
|
53
72
|
)
|
|
54
73
|
const plan = (planRes && planRes.plan) ? planRes.plan : null
|
|
55
74
|
if (!plan || !Array.isArray(plan.dimensions)) throw new Error('plan failed: iterate_review did not return a valid plan')
|
|
@@ -71,16 +90,18 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
71
90
|
? '\\nSTRICT JSON REQUIRED: your previous output failed schema validation. Return ONLY a JSON object {"findings":[...]} where EVERY finding has dimension, file, line (non-negative integer; 0 = whole-file), severity (critical|high|medium|low), summary, failure_scenario, suggested_fix, is_atomic (boolean).'
|
|
72
91
|
: ''
|
|
73
92
|
const raw = await parallel(dims.map(dim => () => agent(
|
|
74
|
-
'Review dimension "' + dim + '".
|
|
93
|
+
'Review dimension "' + dim + '".' +
|
|
94
|
+
(attachments.length > 0 ? ' User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
95
|
+
' Already-known findings (do NOT re-report): ' +
|
|
75
96
|
JSON.stringify(known) + nudge + '\\nReturn the findings JSON object.',
|
|
76
|
-
{ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }
|
|
97
|
+
Object.assign({ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }, backend)
|
|
77
98
|
)))
|
|
78
99
|
const thisRound = { round: r, findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])) }
|
|
79
100
|
if (rounds.length >= r) rounds[r - 1] = thisRound; else rounds.push(thisRound)
|
|
80
101
|
// Deterministic aggregate: cross-round dedupe + known_intentional filter + severity sort.
|
|
81
102
|
agg = await agent(
|
|
82
103
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
83
|
-
{ label: 'review:aggregate:r' + r }
|
|
104
|
+
Object.assign({ label: 'review:aggregate:r' + r }, backend)
|
|
84
105
|
)
|
|
85
106
|
// reviewer.output_schema_validation (default on): aggregate returns per-round
|
|
86
107
|
// schemaValidation; retry the just-finished round (≤2 times) when invalid.
|
|
@@ -104,20 +125,20 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
104
125
|
phase('report')
|
|
105
126
|
const finalAgg = await agent(
|
|
106
127
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
107
|
-
{ label: 'review:aggregate:final' }
|
|
128
|
+
Object.assign({ label: 'review:aggregate:final' }, backend)
|
|
108
129
|
)
|
|
109
130
|
const report = (finalAgg && finalAgg.report) ? finalAgg.report : null
|
|
110
131
|
if (!report || !report.convergence) throw new Error('aggregate failed: no valid report was produced')
|
|
111
132
|
await agent(
|
|
112
133
|
'Call iterate_decision_log({operation:"append", type:"report", round:' + report.convergence.totalRounds + ', data:{mode:"dry-run", totalFindings:' + report.summary.totalFindings + '}})',
|
|
113
|
-
{ label: 'review:log' }
|
|
134
|
+
Object.assign({ label: 'review:log' }, backend)
|
|
114
135
|
)
|
|
115
136
|
|
|
116
137
|
phase('meta-review')
|
|
117
138
|
// Audit the report itself for internal consistency, then produce the final report.
|
|
118
139
|
const metaRes = await agent(
|
|
119
140
|
'Call iterate_review({operation:"meta-review", report:' + JSON.stringify(report) + '}) and return the finalReport JSON.',
|
|
120
|
-
{ label: 'review:meta' }
|
|
141
|
+
Object.assign({ label: 'review:meta' }, backend)
|
|
121
142
|
)
|
|
122
143
|
const finalReport = metaRes && metaRes.finalReport ? metaRes.finalReport : null
|
|
123
144
|
const metaAudit = finalReport && finalReport.metaReview ? finalReport.metaReview : null
|
|
@@ -153,26 +174,44 @@ Set \`args.mode = "normal"\`. Loop: resume → plan → parallel review ×N →
|
|
|
153
174
|
Canonical script — reproduce this structure exactly (adjust dims via the plan):
|
|
154
175
|
|
|
155
176
|
\`\`\`js
|
|
156
|
-
// args = { mode: "normal", maxRounds? }
|
|
177
|
+
// args = { mode: "normal", maxRounds?, subagentProvider?, subagentModel?, attachments? }
|
|
178
|
+
// Optional per-run backend override for sub-agents (omit to inherit the session backend).
|
|
179
|
+
const subAgentProvider = (args && args.subagentProvider) || undefined
|
|
180
|
+
const subAgentModel = (args && args.subagentModel) || undefined
|
|
181
|
+
const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvider } : {}, subAgentModel ? { model: subAgentModel } : {})
|
|
182
|
+
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
183
|
+
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
157
184
|
phase('resume')
|
|
158
185
|
// If a previous run was interrupted, resume from its checkpoint instead of restarting.
|
|
159
186
|
const ckRes = await agent(
|
|
160
187
|
'Call iterate_checkpoint({ operation: "load" }) and return the checkpoint JSON.',
|
|
161
|
-
{ label: 'checkpoint:load' }
|
|
188
|
+
Object.assign({ label: 'checkpoint:load' }, backend)
|
|
162
189
|
)
|
|
163
190
|
const checkpoint = (ckRes && ckRes.checkpoint) ? ckRes.checkpoint : null
|
|
164
191
|
const startRound = (checkpoint && typeof checkpoint.round === 'number') ? checkpoint.round + 1 : 1
|
|
192
|
+
// Track how many times this checkpoint has already been resumed (interruption recovery).
|
|
193
|
+
const resumeCount = (checkpoint && typeof checkpoint.resumeCount === 'number') ? checkpoint.resumeCount : 0
|
|
194
|
+
// The effective count AFTER this recovery: this run counts as one more resume.
|
|
195
|
+
const effectiveResumeCount = checkpoint ? resumeCount + 1 : 0
|
|
196
|
+
if (checkpoint) {
|
|
197
|
+
// A previous run left a checkpoint — record the recovery so the decision log
|
|
198
|
+
// shows the resume, then continue where it left off.
|
|
199
|
+
await agent(
|
|
200
|
+
'Call iterate_decision_log({operation:"append", type:"resume", round:' + startRound + ', data:{resumedFromRound:' + checkpoint.round + ', resumeCount:' + effectiveResumeCount + '}})',
|
|
201
|
+
Object.assign({ label: 'log:resume' }, backend)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
165
204
|
|
|
166
205
|
phase('plan')
|
|
167
206
|
const configRes = await agent(
|
|
168
207
|
'Call iterate_config({ validate: true }) and return the config JSON.',
|
|
169
|
-
{ label: 'config:read' }
|
|
208
|
+
Object.assign({ label: 'config:read' }, backend)
|
|
170
209
|
)
|
|
171
210
|
const cfg = (configRes && configRes.config) ? configRes.config : null
|
|
172
211
|
const atomicMaxLines = (cfg && cfg.atomic && cfg.atomic.max_lines) ? cfg.atomic.max_lines : 20
|
|
173
212
|
const planRes = await agent(
|
|
174
213
|
'Call iterate_review({operation:"plan", mode:"normal", maxReviewRounds:' + (args.maxRounds || 3) + '}) and return the plan JSON.',
|
|
175
|
-
{ label: 'review:plan' }
|
|
214
|
+
Object.assign({ label: 'review:plan' }, backend)
|
|
176
215
|
)
|
|
177
216
|
const plan = (planRes && planRes.plan) ? planRes.plan : null
|
|
178
217
|
if (!plan || !Array.isArray(plan.dimensions)) throw new Error('plan failed: iterate_review did not return a valid plan')
|
|
@@ -180,7 +219,8 @@ const knownIntentional = (plan.knownIntentional || []) // config personalizati
|
|
|
180
219
|
const dims = plan.dimensions.map(d => d.id)
|
|
181
220
|
const maxRounds = plan.maxReviewRounds
|
|
182
221
|
const rounds = [] // findings per review round (each on the then-current code state)
|
|
183
|
-
|
|
222
|
+
// Restore previously-unfixed architectural findings when resuming an interrupted run.
|
|
223
|
+
const architectural = (checkpoint && Array.isArray(checkpoint.findings)) ? checkpoint.findings : [] // findings deliberately left unfixed (reported at the end)
|
|
184
224
|
let fixedCount = (checkpoint && typeof checkpoint.fixedCount === 'number') ? checkpoint.fixedCount : 0
|
|
185
225
|
let converged = false
|
|
186
226
|
let abortedByValidation = false
|
|
@@ -199,8 +239,9 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
199
239
|
: ''
|
|
200
240
|
const raw = await parallel(dims.map(dim => () => agent(
|
|
201
241
|
'Review dimension "' + dim + '" on the CURRENT code state (previous atomic findings are fixed). ' +
|
|
242
|
+
(attachments.length > 0 ? ' User-attached images are part of the evidence; use their descriptions when judging (you see the metadata/descriptions below, not the pixels): ' + JSON.stringify(attachments) + '.' : '') +
|
|
202
243
|
'Do NOT re-report already-known architectural findings: ' + JSON.stringify(architectural) + nudge + '\\nReturn the findings JSON object.',
|
|
203
|
-
{ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }
|
|
244
|
+
Object.assign({ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }, backend)
|
|
204
245
|
)))
|
|
205
246
|
const thisRound = { round: r, findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])) }
|
|
206
247
|
if (rounds.length >= r) rounds[r - 1] = thisRound; else rounds.push(thisRound)
|
|
@@ -210,7 +251,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
210
251
|
// show a running "fixes applied" metric for normal mode.
|
|
211
252
|
agg = await agent(
|
|
212
253
|
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + ', fixedCount:' + fixedCount + '}) and return the report JSON.',
|
|
213
|
-
{ label: 'review:aggregate:r' + r }
|
|
254
|
+
Object.assign({ label: 'review:aggregate:r' + r }, backend)
|
|
214
255
|
)
|
|
215
256
|
// reviewer.output_schema_validation (default on): aggregate returns per-round
|
|
216
257
|
// schemaValidation; retry the just-finished round (≤2 times) when invalid.
|
|
@@ -240,12 +281,12 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
240
281
|
'iterate_fix({ file: "' + file + '", content: <full new file content>, finding: <that finding>, round: ' + r + ' }). ' +
|
|
241
282
|
'Apply the findings IN ORDER. After all fixes, call iterate_diff({ file: "' + file + '" }) to verify the accumulated diff. ' +
|
|
242
283
|
'Findings: ' + JSON.stringify(byFile[file]) + '. Return the array of {id, ok, error} per iterate_fix call.',
|
|
243
|
-
{ label: 'fix:' + file, phase: 'fix', schema: {
|
|
284
|
+
Object.assign({ label: 'fix:' + file, phase: 'fix', schema: {
|
|
244
285
|
type: 'object', additionalProperties: false,
|
|
245
286
|
properties: {
|
|
246
287
|
fixes: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
247
288
|
},
|
|
248
|
-
required: ['fixes'] } }
|
|
289
|
+
required: ['fixes'] } }, backend)
|
|
249
290
|
)))
|
|
250
291
|
for (const res of fixRes) {
|
|
251
292
|
if (res && Array.isArray(res.fixes)) {
|
|
@@ -267,12 +308,12 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
267
308
|
const valRes = await agent(
|
|
268
309
|
'Read iterate.config.yaml validation.commands, then call iterate_validate({ command: <cmd> }) for EACH configured command ' +
|
|
269
310
|
'(one tool call per command). Return all results as {command, exitCode} entries.',
|
|
270
|
-
{ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
311
|
+
Object.assign({ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
271
312
|
type: 'object', additionalProperties: false,
|
|
272
313
|
properties: {
|
|
273
314
|
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' } }, required: ['command', 'exitCode'] } }
|
|
274
315
|
},
|
|
275
|
-
required: ['results'] } }
|
|
316
|
+
required: ['results'] } }, backend)
|
|
276
317
|
)
|
|
277
318
|
failedCommands = (valRes && Array.isArray(valRes.results)) ? valRes.results.filter(v => v.exitCode !== 0).map(v => v.command) : []
|
|
278
319
|
if (failedCommands.length > 0) {
|
|
@@ -281,17 +322,17 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
281
322
|
if (roundFixIds.length > 0) {
|
|
282
323
|
await agent(
|
|
283
324
|
'Call iterate_rollback({ id: <id> }) for EACH of these fix ids (one call per id): ' + JSON.stringify(roundFixIds) + '. Return the array of {id, ok, error}.',
|
|
284
|
-
{ label: 'rollback:r' + r, phase: 'rollback', schema: {
|
|
325
|
+
Object.assign({ label: 'rollback:r' + r, phase: 'rollback', schema: {
|
|
285
326
|
type: 'object', additionalProperties: false,
|
|
286
327
|
properties: {
|
|
287
328
|
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
288
329
|
},
|
|
289
|
-
required: ['results'] } }
|
|
330
|
+
required: ['results'] } }, backend)
|
|
290
331
|
)
|
|
291
332
|
}
|
|
292
333
|
await agent(
|
|
293
334
|
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{failedCommands:' + JSON.stringify(failedCommands) + ', rolledBack:' + roundFixIds.length + '}})',
|
|
294
|
-
{ label: 'log:failed:r' + r }
|
|
335
|
+
Object.assign({ label: 'log:failed:r' + r }, backend)
|
|
295
336
|
)
|
|
296
337
|
break
|
|
297
338
|
}
|
|
@@ -299,13 +340,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
299
340
|
await agent(
|
|
300
341
|
'Call iterate_decision_log({operation:"append", type:"review_result", round:' + r +
|
|
301
342
|
', data:{atomic:' + atomic.length + ', architectural:' + remaining.length + ', fixedSoFar:' + fixedCount + '}})',
|
|
302
|
-
{ label: 'log:r' + r }
|
|
343
|
+
Object.assign({ label: 'log:r' + r }, backend)
|
|
303
344
|
)
|
|
304
345
|
|
|
305
346
|
// Persist progress so an interrupted run can resume from the next round.
|
|
306
347
|
await agent(
|
|
307
|
-
'Call iterate_checkpoint({ operation: "save", mode: "normal", round:' + r + ', maxRounds:' + maxRounds + ', fixedCount:' + fixedCount + ', architecturalCount:' + architectural.length + ', findings:' + JSON.stringify(architectural) + ' }) and return the checkpoint JSON.',
|
|
308
|
-
{ label: 'checkpoint:save:r' + r }
|
|
348
|
+
'Call iterate_checkpoint({ operation: "save", mode: "normal", round:' + r + ', maxRounds:' + maxRounds + ', fixedCount:' + fixedCount + ', architecturalCount:' + architectural.length + ', resumeCount:' + effectiveResumeCount + ', findings:' + JSON.stringify(architectural) + ' }) and return the checkpoint JSON.',
|
|
349
|
+
Object.assign({ label: 'checkpoint:save:r' + r }, backend)
|
|
309
350
|
)
|
|
310
351
|
|
|
311
352
|
if (atomic.length === 0 && remaining.length === 0) {
|