iterate-plugin 2.4.0 → 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 +245 -28
- package/lib/parse.js +331 -0
- package/package.json +1 -1
- package/src/config-write.ts +181 -0
- package/src/index.ts +8 -1
- package/src/paths.ts +38 -0
- 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 +4 -4
- package/src/tools/fix.ts +565 -0
- package/src/types.ts +64 -0
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
|
+
`
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/tools/checkpoint.ts — iteration checkpoint + status tools.
|
|
3
|
+
*
|
|
4
|
+
* iterate_checkpoint — save / load / clear a resume checkpoint so a long
|
|
5
|
+
* iteration can continue where it left off.
|
|
6
|
+
* iterate_status — summarize the current iteration state from the
|
|
7
|
+
* decision log, fix registry, and checkpoint.
|
|
8
|
+
*
|
|
9
|
+
* Checkpoint layout: `.iterate/checkpoint.json`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
14
|
+
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
15
|
+
import { resolveProjectRoot } from '../config-loader.ts'
|
|
16
|
+
import { checkpointPath, iterateDir } from '../paths.ts'
|
|
17
|
+
import { readRegistry } from './fix.ts'
|
|
18
|
+
import { readDecisionEntries } from './decision-log.ts'
|
|
19
|
+
import type { IterationCheckpoint, IterationStatus } from '../types.ts'
|
|
20
|
+
|
|
21
|
+
// ─── Pure helpers (exported for unit tests) ─────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Read a checkpoint from disk (missing/corrupt → null). */
|
|
24
|
+
export function readCheckpoint(projectRoot: string): IterationCheckpoint | null {
|
|
25
|
+
const file = checkpointPath(projectRoot)
|
|
26
|
+
if (!existsSync(file)) return null
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8')) as IterationCheckpoint
|
|
29
|
+
if (!parsed || typeof parsed !== 'object') return null
|
|
30
|
+
if (parsed.mode !== 'dry-run' && parsed.mode !== 'normal') return null
|
|
31
|
+
if (typeof parsed.round !== 'number') return null
|
|
32
|
+
return parsed
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Validate a checkpoint payload (returns error string or null). */
|
|
39
|
+
export function validateCheckpoint(input: {
|
|
40
|
+
mode: unknown
|
|
41
|
+
round: unknown
|
|
42
|
+
maxRounds: unknown
|
|
43
|
+
fixedCount: unknown
|
|
44
|
+
architecturalCount: unknown
|
|
45
|
+
}): string | null {
|
|
46
|
+
if (input.mode !== 'dry-run' && input.mode !== 'normal') {
|
|
47
|
+
return 'mode must be "dry-run" or "normal"'
|
|
48
|
+
}
|
|
49
|
+
if (typeof input.round !== 'number' || !Number.isInteger(input.round) || input.round < 0) {
|
|
50
|
+
return 'round must be a non-negative integer'
|
|
51
|
+
}
|
|
52
|
+
if (typeof input.maxRounds !== 'number' || !Number.isInteger(input.maxRounds) || input.maxRounds < 1) {
|
|
53
|
+
return 'maxRounds must be a positive integer'
|
|
54
|
+
}
|
|
55
|
+
if (typeof input.fixedCount !== 'number' || !Number.isInteger(input.fixedCount) || input.fixedCount < 0) {
|
|
56
|
+
return 'fixedCount must be a non-negative integer'
|
|
57
|
+
}
|
|
58
|
+
if (typeof input.architecturalCount !== 'number' || !Number.isInteger(input.architecturalCount) || input.architecturalCount < 0) {
|
|
59
|
+
return 'architecturalCount must be a non-negative integer'
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute a status summary from the runtime artifacts.
|
|
66
|
+
* Pure (no I/O) — all reads are injected, so it is unit-testable.
|
|
67
|
+
*/
|
|
68
|
+
export function computeStatus(input: {
|
|
69
|
+
checkpoint: IterationCheckpoint | null
|
|
70
|
+
decisionEntries: { timestamp: string; type: string; round?: number; data?: Record<string, unknown> }[]
|
|
71
|
+
fixRegistry: { rounds: { round: number; fixedCount: number; failedCount: number }[] }
|
|
72
|
+
}): IterationStatus {
|
|
73
|
+
const checkpoint = input.checkpoint
|
|
74
|
+
const entries = input.decisionEntries
|
|
75
|
+
const registry = input.fixRegistry
|
|
76
|
+
|
|
77
|
+
const lastEntry = entries.length > 0 ? entries[entries.length - 1] : null
|
|
78
|
+
const lastUpdated = lastEntry?.timestamp ?? checkpoint?.updatedAt ?? null
|
|
79
|
+
|
|
80
|
+
// Round = checkpoint.round (explicit) or max round seen in the decision log.
|
|
81
|
+
let currentRound = checkpoint?.round ?? 0
|
|
82
|
+
if (!checkpoint) {
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
if (typeof e.round === 'number' && e.round > currentRound) currentRound = e.round
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalRounds = checkpoint?.maxRounds ?? currentRound
|
|
89
|
+
const registryFixed = registry.rounds.reduce((sum, r) => sum + r.fixedCount, 0)
|
|
90
|
+
const failedCount = registry.rounds.reduce((sum, r) => sum + r.failedCount, 0)
|
|
91
|
+
// When a checkpoint exists, its snapshot fields are authoritative for resume
|
|
92
|
+
// (fixedCount / architecturalCount / findings); otherwise derive from the
|
|
93
|
+
// live fix registry and decision log.
|
|
94
|
+
const fixedCount = checkpoint ? checkpoint.fixedCount : registryFixed
|
|
95
|
+
const architecturalCount = checkpoint?.architecturalCount ?? 0
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
mode: checkpoint?.mode ?? null,
|
|
99
|
+
currentRound,
|
|
100
|
+
totalRounds,
|
|
101
|
+
fixedCount,
|
|
102
|
+
architecturalCount,
|
|
103
|
+
findingsCount: checkpoint?.findings.length ?? 0,
|
|
104
|
+
totalDecisionLogEntries: entries.length,
|
|
105
|
+
hasCheckpoint: checkpoint !== null,
|
|
106
|
+
checkpoint,
|
|
107
|
+
lastUpdated,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── iterate_checkpoint ──────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Register the `iterate_checkpoint` tool.
|
|
115
|
+
* Saves progress so the orchestrator can resume a long iteration.
|
|
116
|
+
*/
|
|
117
|
+
export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
118
|
+
ctx.tools.register(
|
|
119
|
+
defineTool({
|
|
120
|
+
name: 'iterate_checkpoint',
|
|
121
|
+
description:
|
|
122
|
+
'Save / load / clear the iteration checkpoint. The workflow saves a checkpoint at the start of ' +
|
|
123
|
+
'each round (so a long run can resume) and clears it when the iteration completes.',
|
|
124
|
+
parameters: {
|
|
125
|
+
operation: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
required: true,
|
|
128
|
+
description: '"save" to persist the current progress, "load" to read it back, "clear" to remove it.',
|
|
129
|
+
enum: ['save', 'load', 'clear'],
|
|
130
|
+
},
|
|
131
|
+
mode: { type: 'string', description: 'Required for save: "dry-run" or "normal".' },
|
|
132
|
+
round: { type: 'integer', description: 'Required for save: current round number (0 = none started).' },
|
|
133
|
+
maxRounds: { type: 'integer', description: 'Required for save: total round cap.' },
|
|
134
|
+
fixedCount: { type: 'integer', description: 'Required for save: number of fixes applied so far.' },
|
|
135
|
+
architecturalCount: { type: 'integer', description: 'Required for save: architectural findings left unfixed.' },
|
|
136
|
+
findings: { type: 'json', description: 'Optional for save: the current deduped findings to resume from.' },
|
|
137
|
+
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
output: {
|
|
141
|
+
schema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
properties: {
|
|
145
|
+
operation: { type: 'string', required: true },
|
|
146
|
+
ok: { type: 'boolean', required: true },
|
|
147
|
+
checkpoint: { type: 'json' },
|
|
148
|
+
existed: { type: 'boolean' },
|
|
149
|
+
error: { type: 'string' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
render: (_args, value) => [
|
|
153
|
+
{ type: 'text', text: JSON.stringify(value, null, 2) },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async execute(args) {
|
|
158
|
+
const resolved = resolveProjectRoot(args.path)
|
|
159
|
+
if (!resolved.ok) return { operation: args.operation, ok: false, error: resolved.reason }
|
|
160
|
+
const projectRoot = resolved.root
|
|
161
|
+
|
|
162
|
+
if (args.operation === 'load') {
|
|
163
|
+
const checkpoint = readCheckpoint(projectRoot)
|
|
164
|
+
return { operation: 'load', ok: true, checkpoint: (checkpoint as unknown as JsonValue | null) ?? undefined }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (args.operation === 'clear') {
|
|
168
|
+
const existed = existsSync(checkpointPath(projectRoot))
|
|
169
|
+
if (existed) {
|
|
170
|
+
try { rmSync(checkpointPath(projectRoot), { force: true }) } catch (err) {
|
|
171
|
+
return { operation: 'clear', ok: false, existed, error: `failed to clear checkpoint: ${String(err)}` }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { operation: 'clear', ok: true, existed }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (args.operation === 'save') {
|
|
178
|
+
const invalid = validateCheckpoint({
|
|
179
|
+
mode: args.mode,
|
|
180
|
+
round: args.round,
|
|
181
|
+
maxRounds: args.maxRounds,
|
|
182
|
+
fixedCount: args.fixedCount,
|
|
183
|
+
architecturalCount: args.architecturalCount,
|
|
184
|
+
})
|
|
185
|
+
if (invalid) return { operation: 'save', ok: false, error: invalid }
|
|
186
|
+
const checkpoint: IterationCheckpoint = {
|
|
187
|
+
mode: args.mode as 'dry-run' | 'normal',
|
|
188
|
+
round: args.round as number,
|
|
189
|
+
maxRounds: args.maxRounds as number,
|
|
190
|
+
fixedCount: args.fixedCount as number,
|
|
191
|
+
architecturalCount: args.architecturalCount as number,
|
|
192
|
+
findings: (Array.isArray(args.findings) ? args.findings : []) as unknown as IterationCheckpoint['findings'],
|
|
193
|
+
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
194
|
+
updatedAt: new Date().toISOString(),
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
mkdirSync(iterateDir(projectRoot), { recursive: true })
|
|
198
|
+
writeFileSync(checkpointPath(projectRoot), JSON.stringify(checkpoint, null, 2), 'utf-8')
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { operation: 'save', ok: false, error: `failed to write checkpoint: ${String(err)}` }
|
|
201
|
+
}
|
|
202
|
+
return { operation: 'save', ok: true, checkpoint: checkpoint as unknown as JsonValue }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { operation: args.operation, ok: false, error: 'unknown operation. Use "save", "load", or "clear".' }
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── iterate_status ──────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Register the `iterate_status` tool.
|
|
215
|
+
* Summarizes the current iteration state (mode, round, fixed count, findings).
|
|
216
|
+
*/
|
|
217
|
+
export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
218
|
+
ctx.tools.register(
|
|
219
|
+
defineTool({
|
|
220
|
+
name: 'iterate_status',
|
|
221
|
+
description:
|
|
222
|
+
'Summarize the current iterate run: mode, current round vs total, fixes applied, architectural ' +
|
|
223
|
+
'findings remaining, decision-log size, and whether a resume checkpoint exists.',
|
|
224
|
+
parameters: {
|
|
225
|
+
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
output: {
|
|
229
|
+
schema: {
|
|
230
|
+
type: 'object',
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: {
|
|
233
|
+
ok: { type: 'boolean', required: true },
|
|
234
|
+
mode: { type: 'string' },
|
|
235
|
+
currentRound: { type: 'integer' },
|
|
236
|
+
totalRounds: { type: 'integer' },
|
|
237
|
+
fixedCount: { type: 'integer' },
|
|
238
|
+
architecturalCount: { type: 'integer' },
|
|
239
|
+
findingsCount: { type: 'integer' },
|
|
240
|
+
totalDecisionLogEntries: { type: 'integer' },
|
|
241
|
+
hasCheckpoint: { type: 'boolean' },
|
|
242
|
+
lastUpdated: { type: 'string' },
|
|
243
|
+
error: { type: 'string' },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
render: (_args, value) => {
|
|
247
|
+
if (!value.ok) return [{ type: 'text', text: `status failed: ${value.error}` }]
|
|
248
|
+
const lines = [
|
|
249
|
+
`Mode: ${value.mode ?? 'none'}`,
|
|
250
|
+
`Round: ${value.currentRound} / ${value.totalRounds}`,
|
|
251
|
+
`Fixed: ${value.fixedCount} · Architectural remaining: ${value.architecturalCount}`,
|
|
252
|
+
`Findings in checkpoint: ${value.findingsCount}`,
|
|
253
|
+
`Decision-log entries: ${value.totalDecisionLogEntries}`,
|
|
254
|
+
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}`,
|
|
255
|
+
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
256
|
+
]
|
|
257
|
+
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }]
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async execute(args) {
|
|
262
|
+
const resolved = resolveProjectRoot(args.path)
|
|
263
|
+
if (!resolved.ok) return { ok: false, error: resolved.reason }
|
|
264
|
+
const projectRoot = resolved.root
|
|
265
|
+
const status = computeStatus({
|
|
266
|
+
checkpoint: readCheckpoint(projectRoot),
|
|
267
|
+
decisionEntries: readDecisionEntries(projectRoot),
|
|
268
|
+
fixRegistry: readRegistry(projectRoot),
|
|
269
|
+
})
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
mode: status.mode ?? undefined,
|
|
273
|
+
currentRound: status.currentRound,
|
|
274
|
+
totalRounds: status.totalRounds,
|
|
275
|
+
fixedCount: status.fixedCount,
|
|
276
|
+
architecturalCount: status.architecturalCount,
|
|
277
|
+
findingsCount: status.findingsCount,
|
|
278
|
+
totalDecisionLogEntries: status.totalDecisionLogEntries,
|
|
279
|
+
hasCheckpoint: status.hasCheckpoint,
|
|
280
|
+
lastUpdated: status.lastUpdated ?? undefined,
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
)
|
|
285
|
+
}
|
package/src/tools/config.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
1
2
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
3
|
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
|
3
4
|
import { loadEffectiveConfig, validateConfig, resolveProjectRoot } from '../config-loader.ts'
|
|
5
|
+
import {
|
|
6
|
+
applyConfigUpdates,
|
|
7
|
+
readRawConfig,
|
|
8
|
+
validateConfigUpdates,
|
|
9
|
+
writeConfigFile,
|
|
10
|
+
} from '../config-write.ts'
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Register the `iterate_config` tool.
|
|
7
|
-
* Reads and returns the iterate.config.yaml configuration
|
|
14
|
+
* Reads and returns the iterate.config.yaml configuration, or writes a
|
|
15
|
+
* validated partial update back to it (with backup + rollback).
|
|
8
16
|
* Model-facing: returns JSON with the full config, a specific section, or validation errors.
|
|
9
17
|
*/
|
|
10
18
|
export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<typeof defineTool>) => void } }): void {
|
|
@@ -12,11 +20,17 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
12
20
|
defineTool({
|
|
13
21
|
name: 'iterate_config',
|
|
14
22
|
description:
|
|
15
|
-
'Read the iterate.config.yaml configuration from the project root. ' +
|
|
23
|
+
'Read or update the iterate.config.yaml configuration from the project root. ' +
|
|
16
24
|
'Returns the full parsed config, a specific section, or validation errors. ' +
|
|
17
|
-
'Use this to discover available dimensions, validation commands, git settings, and personalization rules
|
|
25
|
+
'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
|
|
26
|
+
'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).',
|
|
18
27
|
|
|
19
28
|
parameters: {
|
|
29
|
+
operation: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Default "read". "write" validates and applies a partial config update (backed up first).',
|
|
32
|
+
enum: ['read', 'write'],
|
|
33
|
+
},
|
|
20
34
|
path: {
|
|
21
35
|
type: 'string',
|
|
22
36
|
description: 'Project root directory (default: current working directory).',
|
|
@@ -30,6 +44,13 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
30
44
|
type: 'boolean',
|
|
31
45
|
description: 'If true, validate the config schema and return any missing fields.',
|
|
32
46
|
},
|
|
47
|
+
updates: {
|
|
48
|
+
type: 'json',
|
|
49
|
+
description:
|
|
50
|
+
'For operation "write": a partial config object to merge in, e.g. ' +
|
|
51
|
+
'{"goal":"...","dimensions":["correctness","security"],"max_rounds":5}. ' +
|
|
52
|
+
'Supported keys: goal, language, dimensions, max_rounds, review, atomic, git, validation, personalization, onboarding.',
|
|
53
|
+
},
|
|
33
54
|
},
|
|
34
55
|
|
|
35
56
|
output: {
|
|
@@ -44,6 +65,9 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
44
65
|
data: { type: 'json' },
|
|
45
66
|
config: { type: 'json' },
|
|
46
67
|
availableSections: { type: 'array', items: { type: 'string' } },
|
|
68
|
+
operation: { type: 'string' },
|
|
69
|
+
ok: { type: 'boolean' },
|
|
70
|
+
backupPath: { type: 'string' },
|
|
47
71
|
error: { type: 'string' },
|
|
48
72
|
},
|
|
49
73
|
},
|
|
@@ -58,9 +82,43 @@ export function registerConfigTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
58
82
|
return { found: false, error: resolved.reason }
|
|
59
83
|
}
|
|
60
84
|
const projectRoot = resolved.root
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
|
|
85
|
+
|
|
86
|
+
// ── Write operation ────────────────────────────────────────────────
|
|
87
|
+
if (args.operation === 'write') {
|
|
88
|
+
const updates = args.updates as Record<string, unknown> | undefined
|
|
89
|
+
const updateErrors = validateConfigUpdates(updates ?? {})
|
|
90
|
+
if (updateErrors.length > 0) {
|
|
91
|
+
return { operation: 'write', ok: false, found: false, errors: updateErrors }
|
|
92
|
+
}
|
|
93
|
+
let base: Record<string, unknown>
|
|
94
|
+
try {
|
|
95
|
+
base = readRawConfig(join(projectRoot, 'iterate.config.yaml'))
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { operation: 'write', ok: false, found: false, error: `failed to read config: ${String(err)}` }
|
|
98
|
+
}
|
|
99
|
+
const merged = applyConfigUpdates(base, updates ?? {})
|
|
100
|
+
const schemaErrors = validateConfig(merged)
|
|
101
|
+
if (schemaErrors.length > 0) {
|
|
102
|
+
return {
|
|
103
|
+
operation: 'write',
|
|
104
|
+
ok: false,
|
|
105
|
+
found: false,
|
|
106
|
+
errors: schemaErrors.map((e) => `missing/required field: ${e}`),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const result = writeConfigFile(projectRoot, merged)
|
|
110
|
+
if (!result.ok) return { operation: 'write', ok: false, found: false, error: result.error }
|
|
111
|
+
const { config } = loadEffectiveConfig(projectRoot)
|
|
112
|
+
return {
|
|
113
|
+
operation: 'write',
|
|
114
|
+
ok: true,
|
|
115
|
+
found: true,
|
|
116
|
+
backupPath: result.backupPath ?? undefined,
|
|
117
|
+
config: config as unknown as JsonValue,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Read operations (original behavior) ────────────────────────────
|
|
64
122
|
const { config, source } = loadEffectiveConfig(projectRoot)
|
|
65
123
|
const hasOverride = source === 'override'
|
|
66
124
|
|
|
@@ -23,7 +23,7 @@ function logPath(projectRoot: string): string {
|
|
|
23
23
|
* Append one entry to the decision log (JSONL format).
|
|
24
24
|
* Returns the entry count after appending.
|
|
25
25
|
*/
|
|
26
|
-
function
|
|
26
|
+
export function appendDecisionEntry(projectRoot: string, entry: DecisionLogEntry): { count: number; path: string } {
|
|
27
27
|
const filePath = logPath(projectRoot)
|
|
28
28
|
const line = JSON.stringify(entry) + '\n'
|
|
29
29
|
appendFileSync(filePath, line, 'utf-8')
|
|
@@ -41,7 +41,7 @@ function appendEntry(projectRoot: string, entry: DecisionLogEntry): { count: num
|
|
|
41
41
|
/**
|
|
42
42
|
* Read all entries from the decision log.
|
|
43
43
|
*/
|
|
44
|
-
function
|
|
44
|
+
export function readDecisionEntries(projectRoot: string): DecisionLogEntry[] {
|
|
45
45
|
const filePath = join(projectRoot, LOG_DIR, LOG_FILE)
|
|
46
46
|
if (!existsSync(filePath)) return []
|
|
47
47
|
try {
|
|
@@ -134,7 +134,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
134
134
|
const projectRoot = resolved.root
|
|
135
135
|
|
|
136
136
|
if (args.operation === 'read') {
|
|
137
|
-
const entries =
|
|
137
|
+
const entries = readDecisionEntries(projectRoot)
|
|
138
138
|
return {
|
|
139
139
|
operation: 'read',
|
|
140
140
|
entryCount: entries.length,
|
|
@@ -158,7 +158,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
158
158
|
data: (args.data as Record<string, unknown>) ?? {},
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const result =
|
|
161
|
+
const result = appendDecisionEntry(projectRoot, entry)
|
|
162
162
|
return {
|
|
163
163
|
operation: 'append',
|
|
164
164
|
success: true,
|