iterate-plugin 2.7.1 → 2.7.2
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/index.js +6 -4
- package/dist/review.js +6 -1
- package/dist/skill-prompt.js +6 -2
- package/dist/tools/decision-log.js +41 -6
- package/dist/tools/review.js +6 -0
- package/lib/client.js +68 -13
- package/lib/parse.js +137 -1
- package/package.json +1 -1
- package/src/index.ts +6 -4
- package/src/review.ts +9 -1
- package/src/skill-prompt.ts +6 -2
- package/src/tools/decision-log.ts +43 -6
- package/src/tools/review.ts +7 -0
- package/src/types.ts +3 -0
package/dist/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* - The plugin registers
|
|
5
|
+
* - The plugin registers 13 tools (config, validate, decision-log, context, review,
|
|
6
|
+
* triage, fix, diff, rollback, checkpoint, status, history, prune)
|
|
6
7
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
7
8
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
8
9
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
9
|
-
* - Subagents use the
|
|
10
|
+
* - Subagents use the 13 tools to do real work (read config, run validation, log decisions,
|
|
11
|
+
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune)
|
|
10
12
|
*
|
|
11
13
|
* Tool invocation model:
|
|
12
14
|
* - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
*
|
|
17
19
|
* Key files:
|
|
18
20
|
* - src/index.ts — Plugin entry: register tools + inject skill prompt
|
|
19
|
-
* - src/tools/ —
|
|
21
|
+
* - src/tools/ — 13 tool implementations + meta-review/review engines
|
|
20
22
|
* - src/config-loader.ts — YAML config loading
|
|
21
23
|
* - src/types.ts — Shared types
|
|
22
24
|
*/
|
|
@@ -34,7 +36,7 @@ import { ITERATE_SKILL_PROMPT } from "./skill-prompt.js";
|
|
|
34
36
|
export const name = 'iterate-plugin';
|
|
35
37
|
export const inject = ['tools', 'systemPrompt'];
|
|
36
38
|
export function apply(ctx) {
|
|
37
|
-
// 1. Register the
|
|
39
|
+
// 1. Register the 13 tools
|
|
38
40
|
registerConfigTool(ctx);
|
|
39
41
|
registerValidateTool(ctx);
|
|
40
42
|
registerDecisionLogTool(ctx);
|
package/dist/review.js
CHANGED
|
@@ -208,6 +208,11 @@ export function buildReviewReport(input) {
|
|
|
208
208
|
const lastRound = filteredRounds.length > 0 ? filteredRounds[filteredRounds.length - 1].round : 0;
|
|
209
209
|
const lastRoundCount = lastRound > 0 ? (findingsByRound[lastRound - 1] ?? 0) : 0;
|
|
210
210
|
const converged = filteredRounds.length > 0 && lastRoundCount === 0;
|
|
211
|
+
// Attach the normal-mode fix count to the summary (dry-run leaves it absent).
|
|
212
|
+
const computed = summarize(sorted);
|
|
213
|
+
if (input.mode === 'normal' && typeof input.fixedCount === 'number' && Number.isInteger(input.fixedCount)) {
|
|
214
|
+
computed.fixedCount = input.fixedCount;
|
|
215
|
+
}
|
|
211
216
|
return {
|
|
212
217
|
mode: input.mode,
|
|
213
218
|
goal: input.goal,
|
|
@@ -225,7 +230,7 @@ export function buildReviewReport(input) {
|
|
|
225
230
|
? 'converged'
|
|
226
231
|
: 'max_rounds_reached',
|
|
227
232
|
},
|
|
228
|
-
summary:
|
|
233
|
+
summary: computed,
|
|
229
234
|
};
|
|
230
235
|
}
|
|
231
236
|
/**
|
package/dist/skill-prompt.js
CHANGED
|
@@ -13,13 +13,15 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
13
13
|
- \`iterate_validate\` — run a whitelisted validation command
|
|
14
14
|
- \`iterate_decision_log\` — append to the decision log, or read entries back for review
|
|
15
15
|
- \`iterate_context\` — read SKILL.md / ITERATE.md project context
|
|
16
|
-
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan; \`aggregate\` dedupes/merges findings and computes convergence. Purely computational.
|
|
16
|
+
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan; \`aggregate\` dedupes/merges findings 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.
|
|
17
17
|
- \`iterate_triage\` — manage "known_intentional" entries in the config (list / apply, with dedupe + backup + rollback)
|
|
18
18
|
- \`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\`
|
|
19
19
|
- \`iterate_diff\` — show the accumulated diff for a fixed file (vs its original backup) or a per-file summary of all fixes
|
|
20
20
|
- \`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
|
|
21
21
|
- \`iterate_checkpoint\` — save / load / clear an iteration checkpoint (\`.iterate/checkpoint.json\`) so a long run can resume where it left off
|
|
22
22
|
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence
|
|
23
|
+
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
24
|
+
- \`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.
|
|
23
25
|
|
|
24
26
|
### When to use
|
|
25
27
|
When the user asks to review or iterate on the project (e.g. "review this project", "iterate on error handling", "check the codebase for issues", "dry-run review", "反复审查"), run an iterate **workflow** by calling the \`workflow\` tool.
|
|
@@ -175,8 +177,10 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
175
177
|
rounds.push(thisRound)
|
|
176
178
|
|
|
177
179
|
// Deterministic dedupe / known_intentional filter / severity sort for this round.
|
|
180
|
+
// \`fixedCount\` is threaded into the report summary so the client dashboard can
|
|
181
|
+
// show a running "fixes applied" metric for normal mode.
|
|
178
182
|
const agg = await agent(
|
|
179
|
-
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
183
|
+
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + ', fixedCount:' + fixedCount + '}) and return the report JSON.',
|
|
180
184
|
{ label: 'review:aggregate:r' + r }
|
|
181
185
|
)
|
|
182
186
|
const findings = (agg && agg.report && agg.report.findings) ? agg.report.findings : thisRound.findings
|
|
@@ -4,6 +4,34 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
4
4
|
import { resolveProjectRoot } from "../config-loader.js";
|
|
5
5
|
const LOG_DIR = '.iterate';
|
|
6
6
|
const LOG_FILE = 'decision-log.jsonl';
|
|
7
|
+
/** All valid DecisionLogEntry `type` values (must stay in sync with Types). */
|
|
8
|
+
const VALID_ENTRY_TYPES = new Set([
|
|
9
|
+
'round_start',
|
|
10
|
+
'review_result',
|
|
11
|
+
'atomic_fix',
|
|
12
|
+
'architectural_fix',
|
|
13
|
+
'revert',
|
|
14
|
+
'round_failed',
|
|
15
|
+
'validation',
|
|
16
|
+
'decision',
|
|
17
|
+
'report',
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Validate a candidate (type, round, data) triple for an append operation.
|
|
21
|
+
* Returns an error string on failure, or null when the entry is well-formed.
|
|
22
|
+
*/
|
|
23
|
+
function validateEntryInput(type, round, data) {
|
|
24
|
+
if (typeof type !== 'string' || !VALID_ENTRY_TYPES.has(type)) {
|
|
25
|
+
return `type must be one of: ${[...VALID_ENTRY_TYPES].join(', ')}.`;
|
|
26
|
+
}
|
|
27
|
+
if (typeof round !== 'number' || !Number.isInteger(round) || round < 1) {
|
|
28
|
+
return 'round must be a positive integer.';
|
|
29
|
+
}
|
|
30
|
+
if (data !== undefined && data !== null && typeof data !== 'object') {
|
|
31
|
+
return 'data must be an object (or omitted).';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
7
35
|
/**
|
|
8
36
|
* Resolve the log file path, creating the directory if needed.
|
|
9
37
|
*/
|
|
@@ -73,13 +101,14 @@ export function registerDecisionLogTool(ctx) {
|
|
|
73
101
|
type: {
|
|
74
102
|
type: 'string',
|
|
75
103
|
description: 'Entry type (required for append): round_start, review_result, atomic_fix, ' +
|
|
76
|
-
'architectural_fix, revert, validation, decision, report.',
|
|
104
|
+
'architectural_fix, revert, round_failed, validation, decision, report.',
|
|
77
105
|
enum: [
|
|
78
106
|
'round_start',
|
|
79
107
|
'review_result',
|
|
80
108
|
'atomic_fix',
|
|
81
109
|
'architectural_fix',
|
|
82
110
|
'revert',
|
|
111
|
+
'round_failed',
|
|
83
112
|
'validation',
|
|
84
113
|
'decision',
|
|
85
114
|
'report',
|
|
@@ -132,17 +161,23 @@ export function registerDecisionLogTool(ctx) {
|
|
|
132
161
|
};
|
|
133
162
|
}
|
|
134
163
|
if (args.operation === 'append') {
|
|
135
|
-
|
|
164
|
+
const invalid = validateEntryInput(args.type, args.round, args.data);
|
|
165
|
+
if (invalid !== null) {
|
|
136
166
|
return {
|
|
137
167
|
operation: 'append',
|
|
138
|
-
error:
|
|
168
|
+
error: invalid,
|
|
139
169
|
};
|
|
140
170
|
}
|
|
171
|
+
const type = args.type;
|
|
172
|
+
const round = args.round;
|
|
173
|
+
const data = args.data === undefined || args.data === null
|
|
174
|
+
? {}
|
|
175
|
+
: args.data;
|
|
141
176
|
const entry = {
|
|
142
177
|
timestamp: new Date().toISOString(),
|
|
143
|
-
round
|
|
144
|
-
type
|
|
145
|
-
data
|
|
178
|
+
round,
|
|
179
|
+
type,
|
|
180
|
+
data,
|
|
146
181
|
};
|
|
147
182
|
const result = appendDecisionEntry(projectRoot, entry);
|
|
148
183
|
return {
|
package/dist/tools/review.js
CHANGED
|
@@ -62,6 +62,11 @@ export function registerReviewTool(ctx) {
|
|
|
62
62
|
description: 'For `meta-review`: the ReviewReport JSON (as returned by `aggregate`) to audit for ' +
|
|
63
63
|
'internal consistency and produce the final review report.',
|
|
64
64
|
},
|
|
65
|
+
fixedCount: {
|
|
66
|
+
type: 'integer',
|
|
67
|
+
description: 'For `aggregate` (normal mode only): number of atomic fixes applied so far. ' +
|
|
68
|
+
'Surfaces a running "fixes applied" metric on the report summary.',
|
|
69
|
+
},
|
|
65
70
|
path: {
|
|
66
71
|
type: 'string',
|
|
67
72
|
description: 'Project root directory (default: current working directory).',
|
|
@@ -128,6 +133,7 @@ export function registerReviewTool(ctx) {
|
|
|
128
133
|
maxReviewRounds,
|
|
129
134
|
rounds,
|
|
130
135
|
knownIntentional: args.knownIntentional,
|
|
136
|
+
fixedCount: typeof args.fixedCount === 'number' ? args.fixedCount : undefined,
|
|
131
137
|
});
|
|
132
138
|
return { operation: 'aggregate', mode, report: report };
|
|
133
139
|
}
|
package/lib/client.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
import {
|
|
25
25
|
findReportInObject,
|
|
26
26
|
scanSessionForReport,
|
|
27
|
+
scanSessionForRunSummary,
|
|
28
|
+
extractVerdict,
|
|
27
29
|
normalizeReport,
|
|
28
30
|
computeConvergenceProgress,
|
|
29
31
|
getCurrentRound,
|
|
@@ -199,6 +201,15 @@ const ITERATE_CSS = `
|
|
|
199
201
|
.iterate-chip[data-ok] { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); }
|
|
200
202
|
.iterate-batch-check { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; }
|
|
201
203
|
.iterate-batch-check input { margin: 0; cursor: pointer; }
|
|
204
|
+
|
|
205
|
+
/* Meta-review verdict banner (dry-run closing result) */
|
|
206
|
+
.iterate-verdict { margin: 10px 0; padding: 10px 14px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12px; color: var(--dsw-alias-label-primary); }
|
|
207
|
+
.iterate-verdict-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-weight: 600; white-space: nowrap; }
|
|
208
|
+
.iterate-verdict-tag[data-ok] { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent); }
|
|
209
|
+
.iterate-verdict-tag[data-warn] { color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent); }
|
|
210
|
+
.iterate-verdict-detail { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; color: var(--dsw-alias-label-secondary); }
|
|
211
|
+
.iterate-verdict-item { white-space: nowrap; }
|
|
212
|
+
.iterate-verdict-item b { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
202
213
|
`
|
|
203
214
|
|
|
204
215
|
// ─── Small helpers ───────────────────────────────────────────────────────────
|
|
@@ -384,15 +395,18 @@ function ConvergenceDashboard(props) {
|
|
|
384
395
|
),
|
|
385
396
|
)
|
|
386
397
|
|
|
387
|
-
// Fix-count badge: show
|
|
398
|
+
// Fix-count badge: show a running "fixes applied" metric when the report
|
|
399
|
+
// carries a number (normal mode only — threaded through `fixedCount`).
|
|
388
400
|
const mode = report.mode
|
|
389
401
|
const summary = report.summary
|
|
390
402
|
const isNormal = mode === 'normal'
|
|
391
403
|
const fixCount = isNormal && summary && typeof summary.fixedCount === 'number' ? summary.fixedCount : null
|
|
392
404
|
const fixBadge = fixCount !== null
|
|
393
|
-
? React.createElement('span', {
|
|
394
|
-
|
|
395
|
-
|
|
405
|
+
? React.createElement('span', {
|
|
406
|
+
className: 'iterate-metric',
|
|
407
|
+
key: 'fixes',
|
|
408
|
+
title: '本轮已应用的原子修复数(正常模式)',
|
|
409
|
+
}, `${String(fixCount)} fixes`)
|
|
396
410
|
: null
|
|
397
411
|
|
|
398
412
|
return React.createElement(
|
|
@@ -712,6 +726,33 @@ function TriagePanel(props) {
|
|
|
712
726
|
)
|
|
713
727
|
}
|
|
714
728
|
|
|
729
|
+
/** Meta-review verdict banner: surfaces the closing dry-run audit result. */
|
|
730
|
+
function VerdictBanner(props) {
|
|
731
|
+
const verdict = props.verdict
|
|
732
|
+
if (!verdict) return null
|
|
733
|
+
const ok = verdict.verdict === 'approved'
|
|
734
|
+
const item = (num, unit) =>
|
|
735
|
+
React.createElement('span', { className: 'iterate-verdict-item' },
|
|
736
|
+
React.createElement('b', {}, String(num)), ` ${unit}`)
|
|
737
|
+
const phrase = (text) =>
|
|
738
|
+
React.createElement('span', { className: 'iterate-verdict-item' }, text)
|
|
739
|
+
return React.createElement(
|
|
740
|
+
'div',
|
|
741
|
+
{ 'data-iterate-root': '', 'data-iterate': 'verdict', className: 'iterate-verdict' },
|
|
742
|
+
React.createElement('span', { className: 'iterate-verdict-tag', 'data-ok': ok ? '' : undefined, 'data-warn': ok ? undefined : '' },
|
|
743
|
+
ok ? '报告已批准' : '报告需修订'),
|
|
744
|
+
React.createElement('span', { className: 'iterate-verdict-detail' },
|
|
745
|
+
item(verdict.totalFindings, '项发现'),
|
|
746
|
+
item(verdict.totalRounds, '轮'),
|
|
747
|
+
item(verdict.checksRun, '项审查'),
|
|
748
|
+
ok
|
|
749
|
+
? phrase('报告通过全部一致性检查')
|
|
750
|
+
: item(verdict.reportIssues, '处报告缺陷'),
|
|
751
|
+
phrase(verdict.converged ? '已收敛' : '未收敛'),
|
|
752
|
+
),
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
|
|
715
756
|
/** Turn-tail chain entry: triage when findings exist, stats card otherwise. */
|
|
716
757
|
function TurnTailEntry(props) {
|
|
717
758
|
const candidates = []
|
|
@@ -720,19 +761,32 @@ function TurnTailEntry(props) {
|
|
|
720
761
|
if (props && props.data) candidates.push(props.data)
|
|
721
762
|
candidates.push(props)
|
|
722
763
|
|
|
764
|
+
// Meta-review verdict (dry-run closing result), found before the report.
|
|
765
|
+
let verdict = null
|
|
766
|
+
for (const c of candidates) {
|
|
767
|
+
const run = scanSessionForRunSummary(c)
|
|
768
|
+
if (run) { verdict = extractVerdict(run); break }
|
|
769
|
+
}
|
|
770
|
+
|
|
723
771
|
let report = null
|
|
724
772
|
for (const c of candidates) {
|
|
725
773
|
const raw = findReportInObject(c, undefined, 24) || scanSessionForReport(c)
|
|
726
774
|
if (raw) { report = normalizeReport(raw); break }
|
|
727
775
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
776
|
+
|
|
777
|
+
const blocks = []
|
|
778
|
+
if (verdict) blocks.push(React.createElement(VerdictBanner, { key: 'verdict', verdict }))
|
|
779
|
+
if (report) {
|
|
780
|
+
// Render through React.createElement so each panel is a real component with
|
|
781
|
+
// its own hook identity (calling them as functions would violate the Rules
|
|
782
|
+
// of Hooks and crash when the findings/empty branch flips between renders).
|
|
783
|
+
const panel = !report.findings || report.findings.length === 0
|
|
784
|
+
? React.createElement(StatsCard, { report })
|
|
785
|
+
: React.createElement(TriagePanel, { report })
|
|
786
|
+
blocks.push(React.createElement('div', { key: 'report' }, panel))
|
|
734
787
|
}
|
|
735
|
-
|
|
788
|
+
if (blocks.length === 0) return null
|
|
789
|
+
return React.createElement('div', { 'data-iterate-root': '', className: 'iterate-turn-tail-root' }, ...blocks)
|
|
736
790
|
}
|
|
737
791
|
|
|
738
792
|
/** Progress capsule: briefly surfaces round-completion (incl. convergence). */
|
|
@@ -850,8 +904,9 @@ function SettingsPanel() {
|
|
|
850
904
|
*/
|
|
851
905
|
function selectTurnTail(owner) {
|
|
852
906
|
if (!owner) return null
|
|
853
|
-
|
|
854
|
-
|
|
907
|
+
if (findReportInObject(owner.turn, undefined, 24) || scanSessionForReport(owner.turn)) return { matched: true }
|
|
908
|
+
if (scanSessionForRunSummary(owner.turn)) return { matched: true }
|
|
909
|
+
return null
|
|
855
910
|
}
|
|
856
911
|
|
|
857
912
|
/**
|
package/lib/parse.js
CHANGED
|
@@ -160,6 +160,139 @@ export function scanSessionForReport(session) {
|
|
|
160
160
|
return null
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// ─── Run-summary / meta-review verdict detection ─────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check whether `obj` is an iterate dry-run run-summary object (the structured
|
|
167
|
+
* object returned by the workflow at the end of a dry-run). It wraps the
|
|
168
|
+
* ReviewReport and carries the meta-review verdict:
|
|
169
|
+
* { mode, goal, rounds, converged, ..., report, metaReview, finalReport }
|
|
170
|
+
* The discriminator is `finalReport.verdict`, which only the meta-review
|
|
171
|
+
* closing step produces ("approved" | "needs_revision"). This shape is distinct
|
|
172
|
+
* from a ReviewReport (which has `convergence`/`findings`/`rounds`), so it never
|
|
173
|
+
* collides with `isReviewReport`.
|
|
174
|
+
*
|
|
175
|
+
* @param {unknown} obj
|
|
176
|
+
* @returns {obj is Record<string, unknown>}
|
|
177
|
+
*/
|
|
178
|
+
export function isRunSummary(obj) {
|
|
179
|
+
if (!obj || typeof obj !== 'object') return false
|
|
180
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
181
|
+
const final = o.finalReport
|
|
182
|
+
return !!final &&
|
|
183
|
+
typeof final === 'object' &&
|
|
184
|
+
(final.verdict === 'approved' || final.verdict === 'needs_revision')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Deep-scan an object tree for the first iterate run-summary (same traversal
|
|
189
|
+
* semantics as `findReportInObject`, with circular-reference + depth guards).
|
|
190
|
+
*
|
|
191
|
+
* @param {unknown} obj
|
|
192
|
+
* @param {Set<unknown>} [seen]
|
|
193
|
+
* @param {number} [maxDepth=20]
|
|
194
|
+
* @returns {Record<string, unknown> | null}
|
|
195
|
+
*/
|
|
196
|
+
export function findRunSummaryInObject(obj, seen, maxDepth = 20) {
|
|
197
|
+
if (maxDepth <= 0) return null
|
|
198
|
+
if (!obj || typeof obj !== 'object') return null
|
|
199
|
+
|
|
200
|
+
const s = seen || new Set()
|
|
201
|
+
if (s.has(obj)) return null
|
|
202
|
+
s.add(obj)
|
|
203
|
+
|
|
204
|
+
if (isRunSummary(obj)) return /** @type {Record<string, unknown>} */ (obj)
|
|
205
|
+
|
|
206
|
+
if (Array.isArray(obj)) {
|
|
207
|
+
for (const item of obj) {
|
|
208
|
+
const found = findRunSummaryInObject(item, s, maxDepth - 1)
|
|
209
|
+
if (found) return found
|
|
210
|
+
}
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const o = /** @type {Record<string, unknown>} */ (obj)
|
|
215
|
+
for (const key of Object.keys(o)) {
|
|
216
|
+
const val = o[key]
|
|
217
|
+
if (val && typeof val === 'object') {
|
|
218
|
+
const found = findRunSummaryInObject(val, s, maxDepth - 1)
|
|
219
|
+
if (found) return found
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Scan a session snapshot (or any object) for the latest iterate dry-run
|
|
228
|
+
* run-summary that exposes a meta-review verdict. Prefers the most recent.
|
|
229
|
+
*
|
|
230
|
+
* @param {unknown} session
|
|
231
|
+
* @returns {Record<string, unknown> | null}
|
|
232
|
+
*/
|
|
233
|
+
export function scanSessionForRunSummary(session) {
|
|
234
|
+
if (!session || typeof session !== 'object') return null
|
|
235
|
+
|
|
236
|
+
const direct = findRunSummaryInObject(session)
|
|
237
|
+
if (direct) return direct
|
|
238
|
+
|
|
239
|
+
const s = /** @type {Record<string, unknown>} */ (session)
|
|
240
|
+
|
|
241
|
+
// Common pattern: session.toolCalls[].result contains a run summary.
|
|
242
|
+
if (Array.isArray(s.toolCalls)) {
|
|
243
|
+
const calls = /** @type {Array<Record<string, unknown>>} */ (s.toolCalls)
|
|
244
|
+
for (let i = calls.length - 1; i >= 0; i--) {
|
|
245
|
+
const call = calls[i]
|
|
246
|
+
if (!call) continue
|
|
247
|
+
if (call.tool === 'workflow' || String(call.tool ?? '').endsWith('workflow')) {
|
|
248
|
+
const found = findRunSummaryInObject(call.result, undefined, 24)
|
|
249
|
+
if (found) return found
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Common pattern: assistant message content holding the workflow return.
|
|
255
|
+
if (Array.isArray(s.messages)) {
|
|
256
|
+
const msgs = /** @type {Array<Record<string, unknown>>} */ (s.messages)
|
|
257
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
258
|
+
const msg = msgs[i]
|
|
259
|
+
if (!msg) continue
|
|
260
|
+
const found = findRunSummaryInObject(msg.content)
|
|
261
|
+
if (found) return found
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract a compact, UI-friendly verdict from a run-summary object.
|
|
270
|
+
* Returns null when the object is not a valid run-summary.
|
|
271
|
+
*
|
|
272
|
+
* @param {Record<string, unknown> | null | undefined} runSummary
|
|
273
|
+
* @returns {{ verdict: 'approved' | 'needs_revision', reportIssues: number, checksRun: number, converged: boolean, totalRounds: number, totalFindings: number } | null}
|
|
274
|
+
*/
|
|
275
|
+
export function extractVerdict(runSummary) {
|
|
276
|
+
if (!isRunSummary(runSummary)) return null
|
|
277
|
+
const o = /** @type {Record<string, unknown>} */ (runSummary)
|
|
278
|
+
const final = /** @type {Record<string, unknown>} */ (o.finalReport)
|
|
279
|
+
const meta = final.metaReview && typeof final.metaReview === 'object'
|
|
280
|
+
? /** @type {Record<string, unknown>} */ (final.metaReview)
|
|
281
|
+
: {}
|
|
282
|
+
const issues = Array.isArray(meta.issues) ? meta.issues : []
|
|
283
|
+
// `totalRounds` may be a bare number (dry-run returns `rounds`) or a count.
|
|
284
|
+
const roundsVal = o.rounds
|
|
285
|
+
const totalRounds = typeof roundsVal === 'number' ? roundsVal : (Array.isArray(roundsVal) ? roundsVal.length : 0)
|
|
286
|
+
return {
|
|
287
|
+
verdict: final.verdict === 'needs_revision' ? 'needs_revision' : 'approved',
|
|
288
|
+
reportIssues: issues.length,
|
|
289
|
+
checksRun: typeof meta.checksRun === 'number' ? meta.checksRun : 0,
|
|
290
|
+
converged: o.converged === true,
|
|
291
|
+
totalRounds,
|
|
292
|
+
totalFindings: typeof o.totalFindings === 'number' ? o.totalFindings : 0,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
163
296
|
// ─── Normalization ───────────────────────────────────────────────────────────
|
|
164
297
|
|
|
165
298
|
/**
|
|
@@ -193,7 +326,9 @@ export function normalizeReport(report) {
|
|
|
193
326
|
}
|
|
194
327
|
|
|
195
328
|
// Compute summary if missing. Always build a NEW object so the input's
|
|
196
|
-
// summary (or any other field) is never mutated.
|
|
329
|
+
// summary (or any other field) is never mutated. `fixedCount` (normal mode
|
|
330
|
+
// only) is carried through so the dashboard fix-count metric survives
|
|
331
|
+
// normalization.
|
|
197
332
|
let summary = report.summary
|
|
198
333
|
if (!summary || typeof summary !== 'object') {
|
|
199
334
|
summary = computeSummaryFromFindings(findings)
|
|
@@ -209,6 +344,7 @@ export function normalizeReport(report) {
|
|
|
209
344
|
byDimension: s.byDimension && typeof s.byDimension === 'object'
|
|
210
345
|
? s.byDimension
|
|
211
346
|
: computed.byDimension,
|
|
347
|
+
...(typeof s.fixedCount === 'number' ? { fixedCount: s.fixedCount } : {}),
|
|
212
348
|
}
|
|
213
349
|
}
|
|
214
350
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
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/index.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* iterate-plugin — dsh plugin for the iterate autonomous closed-loop workflow
|
|
3
3
|
*
|
|
4
4
|
* Architecture:
|
|
5
|
-
* - The plugin registers
|
|
5
|
+
* - The plugin registers 13 tools (config, validate, decision-log, context, review,
|
|
6
|
+
* triage, fix, diff, rollback, checkpoint, status, history, prune)
|
|
6
7
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
7
8
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
8
9
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
9
|
-
* - Subagents use the
|
|
10
|
+
* - Subagents use the 13 tools to do real work (read config, run validation, log decisions,
|
|
11
|
+
* review, triage, apply/rollback/fixing, checkpoint, status, history, prune)
|
|
10
12
|
*
|
|
11
13
|
* Tool invocation model:
|
|
12
14
|
* - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
|
|
@@ -16,7 +18,7 @@
|
|
|
16
18
|
*
|
|
17
19
|
* Key files:
|
|
18
20
|
* - src/index.ts — Plugin entry: register tools + inject skill prompt
|
|
19
|
-
* - src/tools/ —
|
|
21
|
+
* - src/tools/ — 13 tool implementations + meta-review/review engines
|
|
20
22
|
* - src/config-loader.ts — YAML config loading
|
|
21
23
|
* - src/types.ts — Shared types
|
|
22
24
|
*/
|
|
@@ -38,7 +40,7 @@ export const name = 'iterate-plugin'
|
|
|
38
40
|
export const inject = ['tools', 'systemPrompt']
|
|
39
41
|
|
|
40
42
|
export function apply(ctx: Context): void {
|
|
41
|
-
// 1. Register the
|
|
43
|
+
// 1. Register the 13 tools
|
|
42
44
|
registerConfigTool(ctx)
|
|
43
45
|
registerValidateTool(ctx)
|
|
44
46
|
registerDecisionLogTool(ctx)
|
package/src/review.ts
CHANGED
|
@@ -218,6 +218,8 @@ export function buildReviewReport(input: {
|
|
|
218
218
|
maxReviewRounds: number
|
|
219
219
|
rounds: ReviewRound[]
|
|
220
220
|
knownIntentional?: KnownIntentional[]
|
|
221
|
+
/** Number of atomic fixes applied so far. Normal mode only; omitted in dry-run. */
|
|
222
|
+
fixedCount?: number
|
|
221
223
|
}): ReviewReport {
|
|
222
224
|
// 1. Filter known-intentional per round (before cross-round dedupe).
|
|
223
225
|
const filteredRounds = input.rounds.map((r) => ({
|
|
@@ -246,6 +248,12 @@ export function buildReviewReport(input: {
|
|
|
246
248
|
lastRound > 0 ? (findingsByRound[lastRound - 1] ?? 0) : 0
|
|
247
249
|
const converged = filteredRounds.length > 0 && lastRoundCount === 0
|
|
248
250
|
|
|
251
|
+
// Attach the normal-mode fix count to the summary (dry-run leaves it absent).
|
|
252
|
+
const computed = summarize(sorted)
|
|
253
|
+
if (input.mode === 'normal' && typeof input.fixedCount === 'number' && Number.isInteger(input.fixedCount)) {
|
|
254
|
+
computed.fixedCount = input.fixedCount
|
|
255
|
+
}
|
|
256
|
+
|
|
249
257
|
return {
|
|
250
258
|
mode: input.mode,
|
|
251
259
|
goal: input.goal,
|
|
@@ -264,7 +272,7 @@ export function buildReviewReport(input: {
|
|
|
264
272
|
? 'converged'
|
|
265
273
|
: 'max_rounds_reached',
|
|
266
274
|
},
|
|
267
|
-
summary:
|
|
275
|
+
summary: computed,
|
|
268
276
|
}
|
|
269
277
|
}
|
|
270
278
|
|
package/src/skill-prompt.ts
CHANGED
|
@@ -14,13 +14,15 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
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
16
|
- \`iterate_context\` — read SKILL.md / ITERATE.md project context
|
|
17
|
-
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan; \`aggregate\` dedupes/merges findings and computes convergence. Purely computational.
|
|
17
|
+
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan; \`aggregate\` dedupes/merges findings 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
23
|
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence
|
|
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
|
+
- \`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.
|
|
24
26
|
|
|
25
27
|
### When to use
|
|
26
28
|
When the user asks to review or iterate on the project (e.g. "review this project", "iterate on error handling", "check the codebase for issues", "dry-run review", "反复审查"), run an iterate **workflow** by calling the \`workflow\` tool.
|
|
@@ -176,8 +178,10 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
176
178
|
rounds.push(thisRound)
|
|
177
179
|
|
|
178
180
|
// Deterministic dedupe / known_intentional filter / severity sort for this round.
|
|
181
|
+
// \`fixedCount\` is threaded into the report summary so the client dashboard can
|
|
182
|
+
// show a running "fixes applied" metric for normal mode.
|
|
179
183
|
const agg = await agent(
|
|
180
|
-
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
184
|
+
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + ', fixedCount:' + fixedCount + '}) and return the report JSON.',
|
|
181
185
|
{ label: 'review:aggregate:r' + r }
|
|
182
186
|
)
|
|
183
187
|
const findings = (agg && agg.report && agg.report.findings) ? agg.report.findings : thisRound.findings
|
|
@@ -8,6 +8,36 @@ import type { DecisionLogEntry } from '../types.ts'
|
|
|
8
8
|
const LOG_DIR = '.iterate'
|
|
9
9
|
const LOG_FILE = 'decision-log.jsonl'
|
|
10
10
|
|
|
11
|
+
/** All valid DecisionLogEntry `type` values (must stay in sync with Types). */
|
|
12
|
+
const VALID_ENTRY_TYPES = new Set<DecisionLogEntry['type']>([
|
|
13
|
+
'round_start',
|
|
14
|
+
'review_result',
|
|
15
|
+
'atomic_fix',
|
|
16
|
+
'architectural_fix',
|
|
17
|
+
'revert',
|
|
18
|
+
'round_failed',
|
|
19
|
+
'validation',
|
|
20
|
+
'decision',
|
|
21
|
+
'report',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a candidate (type, round, data) triple for an append operation.
|
|
26
|
+
* Returns an error string on failure, or null when the entry is well-formed.
|
|
27
|
+
*/
|
|
28
|
+
function validateEntryInput(type: unknown, round: unknown, data: unknown): string | null {
|
|
29
|
+
if (typeof type !== 'string' || !VALID_ENTRY_TYPES.has(type as DecisionLogEntry['type'])) {
|
|
30
|
+
return `type must be one of: ${[...VALID_ENTRY_TYPES].join(', ')}.`
|
|
31
|
+
}
|
|
32
|
+
if (typeof round !== 'number' || !Number.isInteger(round) || round < 1) {
|
|
33
|
+
return 'round must be a positive integer.'
|
|
34
|
+
}
|
|
35
|
+
if (data !== undefined && data !== null && typeof data !== 'object') {
|
|
36
|
+
return 'data must be an object (or omitted).'
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
/**
|
|
12
42
|
* Resolve the log file path, creating the directory if needed.
|
|
13
43
|
*/
|
|
@@ -81,13 +111,14 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
81
111
|
type: 'string',
|
|
82
112
|
description:
|
|
83
113
|
'Entry type (required for append): round_start, review_result, atomic_fix, ' +
|
|
84
|
-
'architectural_fix, revert, validation, decision, report.',
|
|
114
|
+
'architectural_fix, revert, round_failed, validation, decision, report.',
|
|
85
115
|
enum: [
|
|
86
116
|
'round_start',
|
|
87
117
|
'review_result',
|
|
88
118
|
'atomic_fix',
|
|
89
119
|
'architectural_fix',
|
|
90
120
|
'revert',
|
|
121
|
+
'round_failed',
|
|
91
122
|
'validation',
|
|
92
123
|
'decision',
|
|
93
124
|
'report',
|
|
@@ -144,18 +175,24 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
if (args.operation === 'append') {
|
|
147
|
-
|
|
178
|
+
const invalid = validateEntryInput(args.type, args.round, args.data)
|
|
179
|
+
if (invalid !== null) {
|
|
148
180
|
return {
|
|
149
181
|
operation: 'append',
|
|
150
|
-
error:
|
|
182
|
+
error: invalid,
|
|
151
183
|
}
|
|
152
184
|
}
|
|
185
|
+
const type = args.type as DecisionLogEntry['type']
|
|
186
|
+
const round = args.round as number
|
|
187
|
+
const data = args.data === undefined || args.data === null
|
|
188
|
+
? {}
|
|
189
|
+
: args.data as Record<string, unknown>
|
|
153
190
|
|
|
154
191
|
const entry: DecisionLogEntry = {
|
|
155
192
|
timestamp: new Date().toISOString(),
|
|
156
|
-
round
|
|
157
|
-
type
|
|
158
|
-
data
|
|
193
|
+
round,
|
|
194
|
+
type,
|
|
195
|
+
data,
|
|
159
196
|
}
|
|
160
197
|
|
|
161
198
|
const result = appendDecisionEntry(projectRoot, entry)
|
package/src/tools/review.ts
CHANGED
|
@@ -72,6 +72,12 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
72
72
|
'For `meta-review`: the ReviewReport JSON (as returned by `aggregate`) to audit for ' +
|
|
73
73
|
'internal consistency and produce the final review report.',
|
|
74
74
|
},
|
|
75
|
+
fixedCount: {
|
|
76
|
+
type: 'integer',
|
|
77
|
+
description:
|
|
78
|
+
'For `aggregate` (normal mode only): number of atomic fixes applied so far. ' +
|
|
79
|
+
'Surfaces a running "fixes applied" metric on the report summary.',
|
|
80
|
+
},
|
|
75
81
|
path: {
|
|
76
82
|
type: 'string',
|
|
77
83
|
description: 'Project root directory (default: current working directory).',
|
|
@@ -144,6 +150,7 @@ export function registerReviewTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
144
150
|
maxReviewRounds,
|
|
145
151
|
rounds,
|
|
146
152
|
knownIntentional: args.knownIntentional as KnownIntentional[] | undefined,
|
|
153
|
+
fixedCount: typeof args.fixedCount === 'number' ? args.fixedCount : undefined,
|
|
147
154
|
})
|
|
148
155
|
return { operation: 'aggregate', mode, report: report as unknown as JsonValue }
|
|
149
156
|
}
|
package/src/types.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface DecisionLogEntry {
|
|
|
34
34
|
| 'validation'
|
|
35
35
|
| 'decision'
|
|
36
36
|
| 'report'
|
|
37
|
+
| 'round_failed'
|
|
37
38
|
data: Record<string, unknown>
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -82,6 +83,8 @@ export interface ReviewReport {
|
|
|
82
83
|
medium: number
|
|
83
84
|
low: number
|
|
84
85
|
byDimension: Record<string, number>
|
|
86
|
+
/** Number of atomic fixes applied so far (normal mode only; absent in dry-run). */
|
|
87
|
+
fixedCount?: number
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|