iterate-plugin 2.3.7 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +852 -0
- package/lib/parse.js +790 -0
- package/package.json +11 -4
- package/src/config-write.ts +181 -0
- package/src/index.ts +13 -4
- package/src/paths.ts +38 -0
- package/src/review.ts +13 -6
- package/src/skill-prompt.ts +117 -24
- package/src/tools/checkpoint.ts +285 -0
- package/src/tools/config.ts +64 -6
- package/src/tools/decision-log.ts +14 -4
- package/src/tools/fix.ts +565 -0
- package/src/tools/triage.ts +370 -0
- package/src/types.ts +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
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",
|
|
@@ -26,10 +26,17 @@
|
|
|
26
26
|
"dsh": {
|
|
27
27
|
"bundle": {
|
|
28
28
|
"patch": "./cordis.patch.yml"
|
|
29
|
+
},
|
|
30
|
+
"client": {
|
|
31
|
+
"platform": "web",
|
|
32
|
+
"inject": [
|
|
33
|
+
"@deepseek-ai/dsh-client-connection"
|
|
34
|
+
]
|
|
29
35
|
}
|
|
30
36
|
},
|
|
31
37
|
"files": [
|
|
32
38
|
"src",
|
|
39
|
+
"lib",
|
|
33
40
|
"cordis.patch.yml",
|
|
34
41
|
"README.md",
|
|
35
42
|
"LICENSE"
|
|
@@ -38,7 +45,8 @@
|
|
|
38
45
|
".": {
|
|
39
46
|
"types": "./src/index.ts",
|
|
40
47
|
"default": "./src/index.ts"
|
|
41
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"./client": "./lib/client.js"
|
|
42
50
|
},
|
|
43
51
|
"scripts": {
|
|
44
52
|
"typecheck": "tsc --noEmit",
|
|
@@ -48,11 +56,10 @@
|
|
|
48
56
|
"dependencies": {
|
|
49
57
|
"@deepseek-ai/cordis": "4.0.1",
|
|
50
58
|
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
51
|
-
"@deepseek-ai/dsh-workflow": "0.1.0-rc.6",
|
|
52
|
-
"@deepseek-ai/schemastery": "3.18.1",
|
|
53
59
|
"js-yaml": "4.3.1"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
62
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
56
63
|
"@types/js-yaml": "4.0.9",
|
|
57
64
|
"@types/node": "22.15.0",
|
|
58
65
|
"tsx": "4.20.3",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/config-write.ts — shared helpers for safely WRITING iterate.config.yaml.
|
|
3
|
+
*
|
|
4
|
+
* Used by the `iterate_config` write operation. Provides:
|
|
5
|
+
* - validateConfigUpdates : validate a caller-supplied partial update
|
|
6
|
+
* - applyConfigUpdates : merge a partial update into the current config
|
|
7
|
+
* - writeConfigFile : backup + write + rollback on failure
|
|
8
|
+
*
|
|
9
|
+
* The security posture mirrors the triage tool: never overwrite a malformed
|
|
10
|
+
* config, always back up before writing, roll back on failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import yaml from 'js-yaml'
|
|
16
|
+
|
|
17
|
+
/** Config file name (must match config-loader). */
|
|
18
|
+
export const CONFIG_FILE = 'iterate.config.yaml'
|
|
19
|
+
|
|
20
|
+
/** Backup suffix helper (filesystem-safe timestamp). */
|
|
21
|
+
export function configBackupSuffix(now = new Date()): string {
|
|
22
|
+
return now.toISOString().replace(/[:.]/g, '-')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate a partial config update.
|
|
27
|
+
* Returns an array of error strings (empty when the update is valid).
|
|
28
|
+
*/
|
|
29
|
+
export function validateConfigUpdates(updates: Record<string, unknown>): string[] {
|
|
30
|
+
const errors: string[] = []
|
|
31
|
+
if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
|
|
32
|
+
return ['updates must be a JSON object']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ('goal' in updates && typeof updates.goal !== 'string') {
|
|
36
|
+
errors.push('updates.goal must be a string')
|
|
37
|
+
}
|
|
38
|
+
if ('language' in updates && updates.language !== 'zh' && updates.language !== 'en') {
|
|
39
|
+
errors.push('updates.language must be "zh" or "en"')
|
|
40
|
+
}
|
|
41
|
+
if ('dimensions' in updates) {
|
|
42
|
+
if (!Array.isArray(updates.dimensions) || updates.dimensions.some((d) => typeof d !== 'string' || d.trim().length === 0)) {
|
|
43
|
+
errors.push('updates.dimensions must be an array of non-empty strings')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ('max_rounds' in updates) {
|
|
47
|
+
if (typeof updates.max_rounds !== 'number' || !Number.isInteger(updates.max_rounds) || updates.max_rounds < 1) {
|
|
48
|
+
errors.push('updates.max_rounds must be a positive integer')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if ('review' in updates) {
|
|
52
|
+
const r = updates.review as Record<string, unknown> | undefined
|
|
53
|
+
if (!r || typeof r !== 'object') {
|
|
54
|
+
errors.push('updates.review must be an object')
|
|
55
|
+
} else if (r.scope !== undefined && r.scope !== 'full' && r.scope !== 'changed-only') {
|
|
56
|
+
errors.push('updates.review.scope must be "full" or "changed-only"')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if ('atomic' in updates) {
|
|
60
|
+
const a = updates.atomic as Record<string, unknown> | undefined
|
|
61
|
+
if (!a || typeof a !== 'object') {
|
|
62
|
+
errors.push('updates.atomic must be an object')
|
|
63
|
+
} else {
|
|
64
|
+
if (a.max_lines !== undefined && (typeof a.max_lines !== 'number' || !Number.isInteger(a.max_lines) || a.max_lines < 1)) {
|
|
65
|
+
errors.push('updates.atomic.max_lines must be a positive integer')
|
|
66
|
+
}
|
|
67
|
+
if (a.max_adjacent_methods !== undefined && (typeof a.max_adjacent_methods !== 'number' || a.max_adjacent_methods < 0)) {
|
|
68
|
+
errors.push('updates.atomic.max_adjacent_methods must be a non-negative number')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if ('git' in updates) {
|
|
73
|
+
const g = updates.git as Record<string, unknown> | undefined
|
|
74
|
+
if (!g || typeof g !== 'object') {
|
|
75
|
+
errors.push('updates.git must be an object')
|
|
76
|
+
} else {
|
|
77
|
+
if (g.target_branch !== undefined && typeof g.target_branch !== 'string') {
|
|
78
|
+
errors.push('updates.git.target_branch must be a string')
|
|
79
|
+
}
|
|
80
|
+
for (const boolKey of ['use_worktree', 'push_per_round', 'auto_merge'] as const) {
|
|
81
|
+
if (g[boolKey] !== undefined && typeof g[boolKey] !== 'boolean') {
|
|
82
|
+
errors.push(`updates.git.${boolKey} must be a boolean`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if ('validation' in updates) {
|
|
88
|
+
const v = updates.validation as Record<string, unknown> | undefined
|
|
89
|
+
if (!v || typeof v !== 'object') {
|
|
90
|
+
errors.push('updates.validation must be an object')
|
|
91
|
+
} else if ('commands' in v && v.commands !== undefined && typeof v.commands !== 'object') {
|
|
92
|
+
errors.push('updates.validation.commands must be an object of command arrays')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ('personalization' in updates && (!updates.personalization || typeof updates.personalization !== 'object')) {
|
|
96
|
+
errors.push('updates.personalization must be an object')
|
|
97
|
+
}
|
|
98
|
+
if ('onboarding' in updates && (!updates.onboarding || typeof updates.onboarding !== 'object')) {
|
|
99
|
+
errors.push('updates.onboarding must be an object')
|
|
100
|
+
}
|
|
101
|
+
return errors
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Recursively merge `updates` over `base` (arrays replaced wholesale). */
|
|
105
|
+
export function applyConfigUpdates(
|
|
106
|
+
base: Record<string, unknown>,
|
|
107
|
+
updates: Record<string, unknown>,
|
|
108
|
+
): Record<string, unknown> {
|
|
109
|
+
const out: Record<string, unknown> = { ...base }
|
|
110
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
111
|
+
if (value === undefined) continue
|
|
112
|
+
const baseValue = out[key]
|
|
113
|
+
if (
|
|
114
|
+
baseValue &&
|
|
115
|
+
typeof baseValue === 'object' &&
|
|
116
|
+
!Array.isArray(baseValue) &&
|
|
117
|
+
value &&
|
|
118
|
+
typeof value === 'object' &&
|
|
119
|
+
!Array.isArray(value)
|
|
120
|
+
) {
|
|
121
|
+
out[key] = applyConfigUpdates(baseValue as Record<string, unknown>, value as Record<string, unknown>)
|
|
122
|
+
} else {
|
|
123
|
+
out[key] = value
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Read the raw config object from disk (empty object when missing).
|
|
131
|
+
* Throws when the file exists but cannot be parsed as a YAML mapping
|
|
132
|
+
* (never overwrite a malformed config).
|
|
133
|
+
*/
|
|
134
|
+
export function readRawConfig(configPath: string): Record<string, unknown> {
|
|
135
|
+
if (!existsSync(configPath)) return {}
|
|
136
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
137
|
+
let parsed: unknown
|
|
138
|
+
try {
|
|
139
|
+
parsed = yaml.load(content)
|
|
140
|
+
} catch {
|
|
141
|
+
throw new Error('existing iterate.config.yaml is not a valid YAML mapping')
|
|
142
|
+
}
|
|
143
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
144
|
+
throw new Error('existing iterate.config.yaml is not a valid YAML mapping')
|
|
145
|
+
}
|
|
146
|
+
return parsed as Record<string, unknown>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Write a config object to disk with backup + rollback.
|
|
151
|
+
* Returns `{ ok: true, backupPath }` or `{ ok: false, error }`.
|
|
152
|
+
*/
|
|
153
|
+
export function writeConfigFile(
|
|
154
|
+
projectRoot: string,
|
|
155
|
+
config: Record<string, unknown>,
|
|
156
|
+
): { ok: true; backupPath: string | null } | { ok: false; error: string } {
|
|
157
|
+
const configPath = join(projectRoot, CONFIG_FILE)
|
|
158
|
+
const hadFile = existsSync(configPath)
|
|
159
|
+
const backupPath = hadFile ? `${configPath}.bak-${configBackupSuffix()}` : null
|
|
160
|
+
|
|
161
|
+
if (backupPath) {
|
|
162
|
+
try {
|
|
163
|
+
copyFileSync(configPath, backupPath)
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { ok: false, error: `failed to create backup: ${String(err)}` }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(configPath, yaml.dump(config, { noRefs: true }), 'utf-8')
|
|
171
|
+
} catch (err) {
|
|
172
|
+
try {
|
|
173
|
+
if (backupPath) copyFileSync(backupPath, configPath)
|
|
174
|
+
} catch {
|
|
175
|
+
// Rollback failure is reported, never swallowed silently.
|
|
176
|
+
}
|
|
177
|
+
return { ok: false, error: `failed to write config: ${String(err)}` }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { ok: true, backupPath }
|
|
181
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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 6 tools (config, validate, decision-log, context, review, triage)
|
|
6
6
|
* - The plugin injects a system prompt section teaching the iterate workflow pattern
|
|
7
7
|
* - The model (prompted by the skill) writes a workflow script using dsh's `workflow` tool
|
|
8
8
|
* - The workflow script uses `agent()` / `parallel()` / `phase()` / `log()` to orchestrate
|
|
9
|
-
* - Subagents use the
|
|
9
|
+
* - Subagents use the 6 tools to do real work (read config, run validation, log decisions, review, triage)
|
|
10
10
|
*
|
|
11
11
|
* Tool invocation model:
|
|
12
12
|
* - Workflow script CANNOT call tools directly (sandboxed vm, no Node API)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Key files:
|
|
18
18
|
* - src/index.ts — Plugin entry: register tools + inject skill prompt
|
|
19
|
-
* - src/tools/ —
|
|
19
|
+
* - src/tools/ — 6 tool implementations + meta-review/review engines
|
|
20
20
|
* - src/config-loader.ts — YAML config loading
|
|
21
21
|
* - src/types.ts — Shared types
|
|
22
22
|
*/
|
|
@@ -27,18 +27,27 @@ import { registerValidateTool } from './tools/validate.ts'
|
|
|
27
27
|
import { registerDecisionLogTool } from './tools/decision-log.ts'
|
|
28
28
|
import { registerContextTool } from './tools/context.ts'
|
|
29
29
|
import { registerReviewTool } from './tools/review.ts'
|
|
30
|
+
import { registerTriageTool } from './tools/triage.ts'
|
|
31
|
+
import { registerFixTool, registerDiffTool, registerRollbackTool } from './tools/fix.ts'
|
|
32
|
+
import { registerCheckpointTool, registerStatusTool } from './tools/checkpoint.ts'
|
|
30
33
|
import { ITERATE_SKILL_PROMPT } from './skill-prompt.ts'
|
|
31
34
|
|
|
32
35
|
export const name = 'iterate-plugin'
|
|
33
36
|
export const inject = ['tools', 'systemPrompt']
|
|
34
37
|
|
|
35
38
|
export function apply(ctx: Context): void {
|
|
36
|
-
// 1. Register the
|
|
39
|
+
// 1. Register the 11 tools
|
|
37
40
|
registerConfigTool(ctx)
|
|
38
41
|
registerValidateTool(ctx)
|
|
39
42
|
registerDecisionLogTool(ctx)
|
|
40
43
|
registerContextTool(ctx)
|
|
41
44
|
registerReviewTool(ctx)
|
|
45
|
+
registerTriageTool(ctx)
|
|
46
|
+
registerFixTool(ctx)
|
|
47
|
+
registerDiffTool(ctx)
|
|
48
|
+
registerRollbackTool(ctx)
|
|
49
|
+
registerCheckpointTool(ctx)
|
|
50
|
+
registerStatusTool(ctx)
|
|
42
51
|
|
|
43
52
|
// 2. Inject the iterate skill prompt as a system prompt section
|
|
44
53
|
// This teaches the model how to write iterate workflow scripts using the tools.
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared filesystem layout for the iterate plugin's runtime state.
|
|
3
|
+
*
|
|
4
|
+
* All runtime artifacts live under `<projectRoot>/.iterate/`:
|
|
5
|
+
* .iterate/decision-log.jsonl — append-only decision log
|
|
6
|
+
* .iterate/fixes/ — fix system: backups + fix registry
|
|
7
|
+
* .iterate/checkpoint.json — iteration checkpoint (resume support)
|
|
8
|
+
*
|
|
9
|
+
* Kept separate from config-loader so every tool points at the same dirs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/** Runtime state root for a project (e.g. `<projectRoot>/.iterate`). */
|
|
15
|
+
export function iterateDir(projectRoot: string): string {
|
|
16
|
+
return join(projectRoot, '.iterate')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Fix-system directory (backups + registry). */
|
|
20
|
+
export function fixesDir(projectRoot: string): string {
|
|
21
|
+
return join(iterateDir(projectRoot), 'fixes')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Fix-registry file (JSON). */
|
|
25
|
+
export function fixRegistryPath(projectRoot: string): string {
|
|
26
|
+
return join(fixesDir(projectRoot), 'registry.json')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Fix-backup file for one fix id + timestamp. */
|
|
30
|
+
export function fixBackupPath(projectRoot: string, id: string, timestamp: string): string {
|
|
31
|
+
const safe = id.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
32
|
+
return join(fixesDir(projectRoot), `${safe}_${timestamp.replace(/[:.]/g, '-')}.bak`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Iteration checkpoint file (JSON). */
|
|
36
|
+
export function checkpointPath(projectRoot: string): string {
|
|
37
|
+
return join(iterateDir(projectRoot), 'checkpoint.json')
|
|
38
|
+
}
|
package/src/review.ts
CHANGED
|
@@ -344,21 +344,28 @@ export function buildReviewPlan(input: {
|
|
|
344
344
|
maxReviewRounds: number
|
|
345
345
|
knownIntentional: KnownIntentional[]
|
|
346
346
|
} {
|
|
347
|
+
// Defensive reads: a malformed config (e.g. `dimensions` as a non-array, or
|
|
348
|
+
// `review`/`atomic` missing) must degrade to sane defaults instead of
|
|
349
|
+
// throwing an uncaught TypeError inside the tool's `execute`.
|
|
347
350
|
const language = input.config.language === 'zh' ? 'Chinese (中文)' : 'English'
|
|
351
|
+
const goal = input.config.goal ?? ''
|
|
352
|
+
const scope = input.config.review?.scope ?? 'full'
|
|
353
|
+
const dimensions = Array.isArray(input.config.dimensions) ? input.config.dimensions : []
|
|
354
|
+
const maxLines = input.config.atomic?.max_lines ?? 20
|
|
348
355
|
return {
|
|
349
356
|
mode: input.mode,
|
|
350
|
-
goal
|
|
351
|
-
scope
|
|
352
|
-
dimensions:
|
|
357
|
+
goal,
|
|
358
|
+
scope,
|
|
359
|
+
dimensions: dimensions.map((d) => ({
|
|
353
360
|
id: d,
|
|
354
361
|
reviewerPrompt: reviewerTaskPrompt({
|
|
355
362
|
dimension: d,
|
|
356
|
-
goal
|
|
357
|
-
scope
|
|
363
|
+
goal,
|
|
364
|
+
scope,
|
|
358
365
|
mode: input.mode,
|
|
359
366
|
alreadyKnown: [],
|
|
360
367
|
outputLanguage: language,
|
|
361
|
-
maxLines
|
|
368
|
+
maxLines,
|
|
362
369
|
}),
|
|
363
370
|
findingsSchema: findingsSchema(),
|
|
364
371
|
})),
|
package/src/skill-prompt.ts
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This teaches the model how to write a correct `workflow` script that
|
|
5
5
|
* performs the iterate autonomous closed-loop (or dry-run pure review),
|
|
6
|
-
* using the
|
|
6
|
+
* using the registered tools via subagents.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const ITERATE_SKILL_PROMPT = `
|
|
10
10
|
## Iterate Workflow (autonomous code iteration)
|
|
11
11
|
|
|
12
12
|
You have the iterate plugin installed, which registers these tools:
|
|
13
|
-
- \`iterate_config\` — read iterate.config.yaml (dimensions, validation commands, personalization)
|
|
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
|
-
- \`iterate_decision_log\` — append to the decision log
|
|
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
17
|
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan; \`aggregate\` dedupes/merges findings and computes convergence. Purely computational.
|
|
18
|
+
- \`iterate_triage\` — manage "known_intentional" entries in the config (list / apply, with dedupe + backup + rollback)
|
|
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
|
+
- \`iterate_diff\` — show the accumulated diff for a fixed file (vs its original backup) or a per-file summary of all fixes
|
|
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
|
+
- \`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
|
|
18
24
|
|
|
19
25
|
### When to use
|
|
20
26
|
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.
|
|
@@ -121,11 +127,20 @@ Key rules for dry-run:
|
|
|
121
127
|
- Only a single \`report\` entry may be appended to the decision log; nothing else is written.
|
|
122
128
|
|
|
123
129
|
### Normal-mode workflow (autonomous closed loop)
|
|
124
|
-
Set \`args.mode = "normal"\`. Loop: plan → parallel review ×N →
|
|
130
|
+
Set \`args.mode = "normal"\`. Loop: resume → plan → parallel review ×N → atomic fixes via \`iterate_fix\` → validate → rollback on failure → checkpoint → loop → auto-stop when zero findings remain.
|
|
125
131
|
Canonical script — reproduce this structure exactly (adjust dims via the plan):
|
|
126
132
|
|
|
127
133
|
\`\`\`js
|
|
128
134
|
// args = { mode: "normal", maxRounds? }
|
|
135
|
+
phase('resume')
|
|
136
|
+
// If a previous run was interrupted, resume from its checkpoint instead of restarting.
|
|
137
|
+
const ckRes = await agent(
|
|
138
|
+
'Call iterate_checkpoint({ operation: "load" }) and return the checkpoint JSON.',
|
|
139
|
+
{ label: 'checkpoint:load' }
|
|
140
|
+
)
|
|
141
|
+
const checkpoint = (ckRes && ckRes.checkpoint) ? ckRes.checkpoint : null
|
|
142
|
+
const startRound = (checkpoint && typeof checkpoint.round === 'number') ? checkpoint.round + 1 : 1
|
|
143
|
+
|
|
129
144
|
phase('plan')
|
|
130
145
|
const configRes = await agent(
|
|
131
146
|
'Call iterate_config({ validate: true }) and return the config JSON.',
|
|
@@ -144,12 +159,14 @@ const dims = plan.dimensions.map(d => d.id)
|
|
|
144
159
|
const maxRounds = plan.maxReviewRounds
|
|
145
160
|
const rounds = [] // findings per review round (each on the then-current code state)
|
|
146
161
|
const architectural = [] // findings deliberately left unfixed (reported at the end)
|
|
147
|
-
let fixedCount = 0
|
|
162
|
+
let fixedCount = (checkpoint && typeof checkpoint.fixedCount === 'number') ? checkpoint.fixedCount : 0
|
|
148
163
|
let converged = false
|
|
164
|
+
let abortedByValidation = false
|
|
165
|
+
let failedCommands = []
|
|
149
166
|
|
|
150
167
|
phase('loop')
|
|
151
|
-
for (let r =
|
|
152
|
-
log('round ' + r + ' of ' + maxRounds + ' — review current state, fix atomics, validate')
|
|
168
|
+
for (let r = startRound; r <= maxRounds; r++) {
|
|
169
|
+
log('round ' + r + ' of ' + maxRounds + ' — review current state, fix atomics via iterate_fix, validate')
|
|
153
170
|
const raw = await parallel(dims.map(dim => () => agent(
|
|
154
171
|
'Review dimension "' + dim + '" on the CURRENT code state (previous atomic findings are fixed). ' +
|
|
155
172
|
'Do NOT re-report already-known architectural findings: ' + JSON.stringify(architectural) + '\\nReturn the findings JSON object.',
|
|
@@ -167,18 +184,34 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
167
184
|
const atomic = findings.filter(f => f.is_atomic === true)
|
|
168
185
|
const remaining = findings.filter(f => f.is_atomic !== true)
|
|
169
186
|
|
|
187
|
+
const roundFixIds = []
|
|
170
188
|
if (atomic.length > 0) {
|
|
171
|
-
// Group atomic fixes by file.
|
|
172
|
-
//
|
|
173
|
-
//
|
|
189
|
+
// Group atomic fixes by file. One fixer agent handles a whole file serially —
|
|
190
|
+
// calling iterate_fix per finding (the ONLY sanctioned writer), then
|
|
191
|
+
// iterate_diff to verify — so the same file is never edited concurrently;
|
|
192
|
+
// different files still run in parallel.
|
|
174
193
|
const byFile = {}
|
|
175
194
|
atomic.forEach(f => { (byFile[f.file] = byFile[f.file] || []).push(f) })
|
|
176
|
-
await parallel(Object.keys(byFile).map(file => () => agent(
|
|
177
|
-
'Apply
|
|
178
|
-
'
|
|
179
|
-
{
|
|
195
|
+
const fixRes = await parallel(Object.keys(byFile).map(file => () => agent(
|
|
196
|
+
'Apply the fixes for ' + file + ' using iterate_fix. For EACH finding in this list, ' +
|
|
197
|
+
'read the current file, compute the edited full content (change <= ' + atomicMaxLines + ' lines), and call ' +
|
|
198
|
+
'iterate_fix({ file: "' + file + '", content: <full new file content>, finding: <that finding>, round: ' + r + ' }). ' +
|
|
199
|
+
'Apply the findings IN ORDER. After all fixes, call iterate_diff({ file: "' + file + '" }) to verify the accumulated diff. ' +
|
|
200
|
+
'Findings: ' + JSON.stringify(byFile[file]) + '. Return the array of {id, ok, error} per iterate_fix call.',
|
|
201
|
+
{ label: 'fix:' + file, phase: 'fix', schema: {
|
|
202
|
+
type: 'object', additionalProperties: false,
|
|
203
|
+
properties: {
|
|
204
|
+
fixes: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
205
|
+
},
|
|
206
|
+
required: ['fixes'] } }
|
|
180
207
|
)))
|
|
181
|
-
|
|
208
|
+
for (const res of fixRes) {
|
|
209
|
+
if (res && Array.isArray(res.fixes)) {
|
|
210
|
+
for (const fx of res.fixes) {
|
|
211
|
+
if (fx && fx.ok === true) { fixedCount += 1; roundFixIds.push(fx.id) }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
182
215
|
}
|
|
183
216
|
|
|
184
217
|
// Cross-round dedupe of architectural findings before accumulating.
|
|
@@ -188,16 +221,51 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
188
221
|
if (seenKeys.indexOf(key) < 0) { architectural.push(f); seenKeys.push(key) }
|
|
189
222
|
}
|
|
190
223
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{
|
|
224
|
+
// Validate every configured command; on ANY failure roll back this round's fixes.
|
|
225
|
+
const valRes = await agent(
|
|
226
|
+
'Read iterate.config.yaml validation.commands, then call iterate_validate({ command: <cmd> }) for EACH configured command ' +
|
|
227
|
+
'(one tool call per command). Return all results as {command, exitCode} entries.',
|
|
228
|
+
{ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
229
|
+
type: 'object', additionalProperties: false,
|
|
230
|
+
properties: {
|
|
231
|
+
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' } }, required: ['command', 'exitCode'] } }
|
|
232
|
+
},
|
|
233
|
+
required: ['results'] } }
|
|
194
234
|
)
|
|
235
|
+
failedCommands = (valRes && Array.isArray(valRes.results)) ? valRes.results.filter(v => v.exitCode !== 0).map(v => v.command) : []
|
|
236
|
+
if (failedCommands.length > 0) {
|
|
237
|
+
log('round ' + r + ' validation FAILED on: ' + failedCommands.join(', ') + ' — rolling back this round')
|
|
238
|
+
abortedByValidation = true
|
|
239
|
+
if (roundFixIds.length > 0) {
|
|
240
|
+
await agent(
|
|
241
|
+
'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}.',
|
|
242
|
+
{ label: 'rollback:r' + r, phase: 'rollback', schema: {
|
|
243
|
+
type: 'object', additionalProperties: false,
|
|
244
|
+
properties: {
|
|
245
|
+
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
246
|
+
},
|
|
247
|
+
required: ['results'] } }
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
await agent(
|
|
251
|
+
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{failedCommands:' + JSON.stringify(failedCommands) + ', rolledBack:' + roundFixIds.length + '}})',
|
|
252
|
+
{ label: 'log:failed:r' + r }
|
|
253
|
+
)
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
|
|
195
257
|
await agent(
|
|
196
258
|
'Call iterate_decision_log({operation:"append", type:"review_result", round:' + r +
|
|
197
259
|
', data:{atomic:' + atomic.length + ', architectural:' + remaining.length + ', fixedSoFar:' + fixedCount + '}})',
|
|
198
260
|
{ label: 'log:r' + r }
|
|
199
261
|
)
|
|
200
262
|
|
|
263
|
+
// Persist progress so an interrupted run can resume from the next round.
|
|
264
|
+
await agent(
|
|
265
|
+
'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.',
|
|
266
|
+
{ label: 'checkpoint:save:r' + r }
|
|
267
|
+
)
|
|
268
|
+
|
|
201
269
|
if (atomic.length === 0 && remaining.length === 0) {
|
|
202
270
|
log('round ' + r + ' found nothing to fix — converged')
|
|
203
271
|
converged = true
|
|
@@ -211,25 +279,50 @@ await agent(
|
|
|
211
279
|
', data:{mode:"normal", fixed:' + fixedCount + ', architectural:' + architectural.length + '}})',
|
|
212
280
|
{ label: 'report:log' }
|
|
213
281
|
)
|
|
282
|
+
const statusRes = await agent(
|
|
283
|
+
'Call iterate_status() and return the status JSON.',
|
|
284
|
+
{ label: 'status:final' }
|
|
285
|
+
)
|
|
286
|
+
const status = (statusRes && statusRes.ok) ? statusRes : null
|
|
287
|
+
if (!abortedByValidation) {
|
|
288
|
+
// Iteration finished cleanly → clear the checkpoint so the next run starts fresh.
|
|
289
|
+
await agent(
|
|
290
|
+
'Call iterate_checkpoint({ operation: "clear" }) and return {ok, existed}.',
|
|
291
|
+
{ label: 'checkpoint:clear' }
|
|
292
|
+
)
|
|
293
|
+
}
|
|
214
294
|
return {
|
|
215
295
|
mode: 'normal',
|
|
216
296
|
goal: plan.goal,
|
|
217
297
|
roundsExecuted: rounds.length,
|
|
218
298
|
maxRounds: maxRounds,
|
|
219
299
|
converged: converged,
|
|
300
|
+
abortedByValidation: abortedByValidation,
|
|
301
|
+
failedCommands: failedCommands,
|
|
220
302
|
findingsFixed: fixedCount,
|
|
221
303
|
remainingArchitecturalCount: architectural.length,
|
|
222
|
-
remainingArchitectural: architectural
|
|
304
|
+
remainingArchitectural: architectural,
|
|
305
|
+
status: status ? {
|
|
306
|
+
currentRound: status.currentRound,
|
|
307
|
+
totalRounds: status.totalRounds,
|
|
308
|
+
fixedCount: status.fixedCount,
|
|
309
|
+
architecturalCount: status.architecturalCount,
|
|
310
|
+
findingsCount: status.findingsCount,
|
|
311
|
+
hasCheckpoint: status.hasCheckpoint
|
|
312
|
+
} : null
|
|
223
313
|
}
|
|
224
314
|
\`\`\`
|
|
225
315
|
|
|
226
316
|
Key rules for normal mode:
|
|
227
|
-
- Fixers are the ONLY agents allowed to write files
|
|
317
|
+
- Fixers are the ONLY agents allowed to write files, and they must go through \`iterate_fix\` — never edit files directly. That is what gives every change a backup, a diff, and a rollback path. Reviewers read only. Architectural findings are reported, never auto-fixed.
|
|
228
318
|
- Aggregate the current round deterministically (\`report.findings\`) before fixing, so fixes act on deduped/filtered/sorted findings.
|
|
229
|
-
- Apply atomic fixes **per file**: one fixer agent handles all findings for a given file serially
|
|
230
|
-
-
|
|
319
|
+
- Apply atomic fixes **per file**: one fixer agent handles all findings for a given file serially (so the same file is never edited concurrently); different files are fixed in parallel.
|
|
320
|
+
- **Resume**: load the checkpoint first; if a previous run left one, continue from \`checkpoint.round + 1\` (its \`fixedCount\` and deduped \`findings\` are carried forward).
|
|
321
|
+
- **Validate after every round** of fixes; on ANY validation failure, roll back the round's fixes via \`iterate_rollback\` and stop (the checkpoint is left in place so the run can be resumed).
|
|
322
|
+
- **Checkpoint after every round**; clear it only when the iteration completes cleanly.
|
|
231
323
|
- Stop when a round produces nothing to fix (converged) or maxReviewRounds is reached.
|
|
232
|
-
- Every round and the final report go to the append-only decision log.
|
|
324
|
+
- Every round, every rollback, and the final report go to the append-only decision log.
|
|
325
|
+
- Close with \`iterate_status\` metrics and surface the convergence indicators (fixed count, remaining architectural count, abort reason) in the final summary.
|
|
233
326
|
|
|
234
327
|
### Finding schema (for reviewer agents)
|
|
235
328
|
{ "dimension": string, "file": string (relative path), "line": number (optional),
|
|
@@ -242,4 +335,4 @@ Atomic = is_atomic true (single file, single function, ≤ config.atomic.max_lin
|
|
|
242
335
|
Always pass \`meta: { name: "iterate", description: "Autonomous iterate loop" }\`.
|
|
243
336
|
|
|
244
337
|
Always end with a clear summary: total findings, count by severity, fixes applied (normal) or convergence stats (dry-run), and remaining architectural findings.
|
|
245
|
-
`
|
|
338
|
+
`
|