iterate-plugin 2.8.4 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/skill-prompt.js +68 -27
- package/dist/tools/checkpoint.js +16 -1
- package/dist/tools/context.js +123 -2
- package/dist/tools/decision-log.js +3 -1
- package/lib/client.js +113 -0
- package/lib/parse.js +118 -0
- package/package.json +1 -1
- package/src/client/index.ts +27 -0
- package/src/skill-prompt.ts +68 -27
- package/src/tools/checkpoint.ts +19 -1
- package/src/tools/context.ts +141 -2
- package/src/tools/decision-log.ts +3 -1
- package/src/types.ts +7 -0
package/src/tools/checkpoint.ts
CHANGED
|
@@ -42,6 +42,7 @@ export function validateCheckpoint(input: {
|
|
|
42
42
|
maxRounds: unknown
|
|
43
43
|
fixedCount: unknown
|
|
44
44
|
architecturalCount: unknown
|
|
45
|
+
resumeCount?: unknown
|
|
45
46
|
}): string | null {
|
|
46
47
|
if (input.mode !== 'dry-run' && input.mode !== 'normal') {
|
|
47
48
|
return 'mode must be "dry-run" or "normal"'
|
|
@@ -58,6 +59,12 @@ export function validateCheckpoint(input: {
|
|
|
58
59
|
if (typeof input.architecturalCount !== 'number' || !Number.isInteger(input.architecturalCount) || input.architecturalCount < 0) {
|
|
59
60
|
return 'architecturalCount must be a non-negative integer'
|
|
60
61
|
}
|
|
62
|
+
if (
|
|
63
|
+
input.resumeCount !== undefined &&
|
|
64
|
+
(typeof input.resumeCount !== 'number' || !Number.isInteger(input.resumeCount) || input.resumeCount < 0)
|
|
65
|
+
) {
|
|
66
|
+
return 'resumeCount must be a non-negative integer'
|
|
67
|
+
}
|
|
61
68
|
return null
|
|
62
69
|
}
|
|
63
70
|
|
|
@@ -103,6 +110,10 @@ export function computeStatus(input: {
|
|
|
103
110
|
findingsCount: checkpoint?.findings.length ?? 0,
|
|
104
111
|
totalDecisionLogEntries: entries.length,
|
|
105
112
|
hasCheckpoint: checkpoint !== null,
|
|
113
|
+
// A checkpoint left on disk means the previous run was interrupted before
|
|
114
|
+
// it could clear it — this is the durable "interruption" signal.
|
|
115
|
+
interrupted: checkpoint !== null,
|
|
116
|
+
resumeCount: checkpoint?.resumeCount ?? 0,
|
|
106
117
|
checkpoint,
|
|
107
118
|
lastUpdated,
|
|
108
119
|
}
|
|
@@ -133,6 +144,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
133
144
|
maxRounds: { type: 'integer', description: 'Required for save: total round cap.' },
|
|
134
145
|
fixedCount: { type: 'integer', description: 'Required for save: number of fixes applied so far.' },
|
|
135
146
|
architecturalCount: { type: 'integer', description: 'Required for save: architectural findings left unfixed.' },
|
|
147
|
+
resumeCount: { type: 'integer', description: 'Optional for save: how many times this checkpoint has already been resumed after an interruption (default 0).' },
|
|
136
148
|
findings: { type: 'json', description: 'Optional for save: the current deduped findings to resume from.' },
|
|
137
149
|
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
138
150
|
},
|
|
@@ -181,6 +193,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
181
193
|
maxRounds: args.maxRounds,
|
|
182
194
|
fixedCount: args.fixedCount,
|
|
183
195
|
architecturalCount: args.architecturalCount,
|
|
196
|
+
resumeCount: args.resumeCount,
|
|
184
197
|
})
|
|
185
198
|
if (invalid) return { operation: 'save', ok: false, error: invalid }
|
|
186
199
|
const checkpoint: IterationCheckpoint = {
|
|
@@ -189,6 +202,7 @@ export function registerCheckpointTool(ctx: { tools: { register: (def: ReturnTyp
|
|
|
189
202
|
maxRounds: args.maxRounds as number,
|
|
190
203
|
fixedCount: args.fixedCount as number,
|
|
191
204
|
architecturalCount: args.architecturalCount as number,
|
|
205
|
+
resumeCount: (typeof args.resumeCount === 'number' ? args.resumeCount : 0),
|
|
192
206
|
findings: (Array.isArray(args.findings) ? args.findings : []) as unknown as IterationCheckpoint['findings'],
|
|
193
207
|
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
194
208
|
updatedAt: new Date().toISOString(),
|
|
@@ -239,6 +253,8 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
239
253
|
findingsCount: { type: 'integer' },
|
|
240
254
|
totalDecisionLogEntries: { type: 'integer' },
|
|
241
255
|
hasCheckpoint: { type: 'boolean' },
|
|
256
|
+
interrupted: { type: 'boolean', description: 'True when a checkpoint exists, meaning the previous run was interrupted before finishing.' },
|
|
257
|
+
resumeCount: { type: 'integer', description: 'How many times the current checkpoint has already been resumed.' },
|
|
242
258
|
lastUpdated: { type: 'string' },
|
|
243
259
|
error: { type: 'string' },
|
|
244
260
|
},
|
|
@@ -251,7 +267,7 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
251
267
|
`Fixed: ${value.fixedCount} · Architectural remaining: ${value.architecturalCount}`,
|
|
252
268
|
`Findings in checkpoint: ${value.findingsCount}`,
|
|
253
269
|
`Decision-log entries: ${value.totalDecisionLogEntries}`,
|
|
254
|
-
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}`,
|
|
270
|
+
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}${value.interrupted ? ' (interrupted — resumable)' : ''}${value.resumeCount ? ` · resumed ${value.resumeCount}x` : ''}`,
|
|
255
271
|
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
256
272
|
]
|
|
257
273
|
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }]
|
|
@@ -277,6 +293,8 @@ export function registerStatusTool(ctx: { tools: { register: (def: ReturnType<ty
|
|
|
277
293
|
findingsCount: status.findingsCount,
|
|
278
294
|
totalDecisionLogEntries: status.totalDecisionLogEntries,
|
|
279
295
|
hasCheckpoint: status.hasCheckpoint,
|
|
296
|
+
interrupted: status.interrupted,
|
|
297
|
+
resumeCount: status.resumeCount,
|
|
280
298
|
lastUpdated: status.lastUpdated ?? undefined,
|
|
281
299
|
}
|
|
282
300
|
},
|
package/src/tools/context.ts
CHANGED
|
@@ -7,6 +7,103 @@ import { resolveProjectRoot } from '../config-loader.ts'
|
|
|
7
7
|
/** How many ancestor directories we walk up looking for a SKILL.md. */
|
|
8
8
|
const MAX_SKILL_DIR_LOOKUP_DEPTH = 12
|
|
9
9
|
|
|
10
|
+
/** Maximum number of image attachments relayed into the context in one call. */
|
|
11
|
+
const MAX_ATTACHMENTS = 8
|
|
12
|
+
|
|
13
|
+
/** Maximum intrinsic width/height (px) accepted for an attached image. */
|
|
14
|
+
const MAX_ATTACHMENT_DIMENSION = 16384
|
|
15
|
+
|
|
16
|
+
/** A validated, normalized image-attachment entry carried into review context. */
|
|
17
|
+
export interface NormalizedAttachment {
|
|
18
|
+
name?: string
|
|
19
|
+
mediaType?: string
|
|
20
|
+
width?: number
|
|
21
|
+
height?: number
|
|
22
|
+
note?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Validation result for a raw attachment entry. */
|
|
26
|
+
export type AttachmentValidationResult =
|
|
27
|
+
| { ok: true; value: NormalizedAttachment }
|
|
28
|
+
| { ok: false; error: string }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate and normalize one raw image-attachment entry passed by the
|
|
32
|
+
* orchestrator. The top-level model observes user-attached images in its own
|
|
33
|
+
* context (as image blocks) and relays their metadata here so reviewers get the
|
|
34
|
+
* same visual evidence. Pure — exported for unit tests.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeAttachment(raw: unknown): AttachmentValidationResult {
|
|
37
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
38
|
+
return { ok: false, error: 'attachment must be an object' }
|
|
39
|
+
}
|
|
40
|
+
const entry = raw as Record<string, unknown>
|
|
41
|
+
const out: NormalizedAttachment = {}
|
|
42
|
+
if (entry.name !== undefined) {
|
|
43
|
+
if (typeof entry.name !== 'string' || entry.name.length > 256) {
|
|
44
|
+
return { ok: false, error: 'attachment.name must be a string (≤ 256 chars)' }
|
|
45
|
+
}
|
|
46
|
+
out.name = entry.name
|
|
47
|
+
}
|
|
48
|
+
if (entry.mediaType !== undefined) {
|
|
49
|
+
if (typeof entry.mediaType !== 'string' || !/^image\/(png|jpeg|webp|gif)$/.test(entry.mediaType)) {
|
|
50
|
+
return { ok: false, error: 'attachment.mediaType must be image/png, image/jpeg, image/webp, or image/gif' }
|
|
51
|
+
}
|
|
52
|
+
out.mediaType = entry.mediaType
|
|
53
|
+
}
|
|
54
|
+
for (const dim of ['width', 'height'] as const) {
|
|
55
|
+
if (entry[dim] !== undefined) {
|
|
56
|
+
if (typeof entry[dim] !== 'number' || !Number.isInteger(entry[dim]) || entry[dim] < 0 || entry[dim] > MAX_ATTACHMENT_DIMENSION) {
|
|
57
|
+
return { ok: false, error: `attachment.${dim} must be an integer in [0, ${MAX_ATTACHMENT_DIMENSION}]` }
|
|
58
|
+
}
|
|
59
|
+
out[dim] = entry[dim]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (entry.note !== undefined) {
|
|
63
|
+
if (typeof entry.note !== 'string' || entry.note.length > 1000) {
|
|
64
|
+
return { ok: false, error: 'attachment.note must be a string (≤ 1000 chars)' }
|
|
65
|
+
}
|
|
66
|
+
out.note = entry.note
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, value: out }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate a whole attachments array, dropping invalid entries.
|
|
73
|
+
* Returns the normalized list plus the reasons for any dropped entries.
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeAttachments(raw: unknown): {
|
|
76
|
+
attachments: NormalizedAttachment[]
|
|
77
|
+
errors: string[]
|
|
78
|
+
} {
|
|
79
|
+
const attachments: NormalizedAttachment[] = []
|
|
80
|
+
const errors: string[] = []
|
|
81
|
+
if (raw === undefined || raw === null) return { attachments, errors }
|
|
82
|
+
if (!Array.isArray(raw)) return { attachments, errors: ['attachments must be an array'] }
|
|
83
|
+
for (let i = 0; i < raw.length; i++) {
|
|
84
|
+
if (attachments.length >= MAX_ATTACHMENTS) {
|
|
85
|
+
errors.push(`attachments capped at ${MAX_ATTACHMENTS}; entry ${i} dropped`)
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
const result = normalizeAttachment(raw[i])
|
|
89
|
+
if (result.ok) attachments.push(result.value)
|
|
90
|
+
else errors.push(`attachments[${i}]: ${result.error}`)
|
|
91
|
+
}
|
|
92
|
+
return { attachments, errors }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render a normalized attachment as a compact text block for the model.
|
|
97
|
+
*/
|
|
98
|
+
export function renderAttachment(a: NormalizedAttachment, index: number): string {
|
|
99
|
+
const bits: string[] = [`[${index + 1}]`]
|
|
100
|
+
if (a.name) bits.push(a.name)
|
|
101
|
+
if (a.mediaType) bits.push(a.mediaType)
|
|
102
|
+
if (typeof a.width === 'number' && typeof a.height === 'number') bits.push(`${a.width}x${a.height}`)
|
|
103
|
+
if (a.note) bits.push(a.note)
|
|
104
|
+
return bits.join(' · ')
|
|
105
|
+
}
|
|
106
|
+
|
|
10
107
|
/**
|
|
11
108
|
* The directory this source file lives in (…/src/tools). The plugin's own
|
|
12
109
|
* package root is one level up (…/src), and the skill root is typically a few
|
|
@@ -86,7 +183,9 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
86
183
|
'SKILL.md contains the original iterate skill instructions; it is searched in ' +
|
|
87
184
|
'the skill directory (auto-detected), the project root, or an explicit `skillDir`. ' +
|
|
88
185
|
'ITERATE.md contains the project-specific knowledge base and onboarding information. ' +
|
|
89
|
-
'
|
|
186
|
+
'Also relays user-attached image metadata (e.g. UI screenshots, error dialogs) into ' +
|
|
187
|
+
'the review context so reviewers can treat them as visual evidence. ' +
|
|
188
|
+
'Use this to understand the skill workflow, project context, and any attached visuals.',
|
|
90
189
|
|
|
91
190
|
parameters: {
|
|
92
191
|
files: {
|
|
@@ -105,6 +204,25 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
105
204
|
'Custom directory to search for SKILL.md (highest priority). ' +
|
|
106
205
|
'When omitted, SKILL.md is auto-detected from the skill directory, then the project root.',
|
|
107
206
|
},
|
|
207
|
+
attachments: {
|
|
208
|
+
type: 'array',
|
|
209
|
+
items: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
properties: {
|
|
213
|
+
name: { type: 'string', description: 'Optional display name of the attached image.' },
|
|
214
|
+
mediaType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], description: 'Optional media type of the image.' },
|
|
215
|
+
width: { type: 'integer', description: 'Optional intrinsic width in pixels.' },
|
|
216
|
+
height: { type: 'integer', description: 'Optional intrinsic height in pixels.' },
|
|
217
|
+
note: { type: 'string', description: 'Optional short description of what the image shows and why it matters for this review.' },
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
description:
|
|
221
|
+
'Optional: image attachments observed in the session (e.g. UI screenshots, error ' +
|
|
222
|
+
'dialogs, design references) relayed into the review context. The top-level model ' +
|
|
223
|
+
'sees these images natively and passes their metadata here so reviewers get the same ' +
|
|
224
|
+
'visual evidence. Up to 8 entries; invalid entries are dropped and reported.',
|
|
225
|
+
},
|
|
108
226
|
},
|
|
109
227
|
|
|
110
228
|
output: {
|
|
@@ -118,15 +236,23 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
118
236
|
skillSource: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
119
237
|
error: { type: 'string' },
|
|
120
238
|
searched: { type: 'array', items: { type: 'string' } },
|
|
239
|
+
attachments: { type: 'array', items: { type: 'string' }, description: 'Normalized attached-image descriptions relayed to reviewers.' },
|
|
240
|
+
attachmentErrors: { type: 'array', items: { type: 'string' }, description: 'Reasons for any attachment entries that were dropped.' },
|
|
121
241
|
},
|
|
122
242
|
},
|
|
123
243
|
render: (_args, value) => {
|
|
124
244
|
const parts: string[] = []
|
|
125
245
|
if (value.skill) parts.push(`--- SKILL.md (${value.skillSource ?? '?source?'}) ---\n${value.skill}`)
|
|
126
246
|
if (value.project) parts.push(`--- ITERATE.md ---\n${value.project}`)
|
|
127
|
-
if (
|
|
247
|
+
if (Array.isArray(value.attachments) && value.attachments.length > 0) {
|
|
248
|
+
parts.push(`--- User-attached images (${value.attachments.length}) ---\n${value.attachments.join('\n')}`)
|
|
249
|
+
}
|
|
250
|
+
if (!value.skill && !value.project && !(Array.isArray(value.attachments) && value.attachments.length > 0)) {
|
|
128
251
|
parts.push('No files found. Searched: ' + (value.searched?.join(', ') ?? 'none'))
|
|
129
252
|
}
|
|
253
|
+
if (Array.isArray(value.attachmentErrors) && value.attachmentErrors.length > 0) {
|
|
254
|
+
parts.push('Attachment warnings: ' + value.attachmentErrors.join('; '))
|
|
255
|
+
}
|
|
130
256
|
return [{ type: 'text', text: parts.join('\n\n') }]
|
|
131
257
|
},
|
|
132
258
|
},
|
|
@@ -148,8 +274,21 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
148
274
|
project?: string | null
|
|
149
275
|
skillSource?: string | null
|
|
150
276
|
searched: string[]
|
|
277
|
+
attachments?: string[]
|
|
278
|
+
attachmentErrors?: string[]
|
|
151
279
|
} = { found: true, searched: [] }
|
|
152
280
|
|
|
281
|
+
// Relay user-attached image metadata into the review context. The
|
|
282
|
+
// orchestrator observes attached images in the session and passes their
|
|
283
|
+
// metadata here; invalid entries are dropped with a reported reason.
|
|
284
|
+
const attachments = normalizeAttachments(args.attachments)
|
|
285
|
+
if (attachments.attachments.length > 0) {
|
|
286
|
+
result.attachments = attachments.attachments.map(renderAttachment)
|
|
287
|
+
}
|
|
288
|
+
if (attachments.errors.length > 0) {
|
|
289
|
+
result.attachmentErrors = attachments.errors
|
|
290
|
+
}
|
|
291
|
+
|
|
153
292
|
if (requested.includes('skill') || requested.includes('skill.md')) {
|
|
154
293
|
// Candidate dirs in priority order: custom path → auto-detected skill
|
|
155
294
|
// root → project root. This is how "skill 目录、项目根、自定义路径"
|
|
@@ -19,6 +19,7 @@ const VALID_ENTRY_TYPES = new Set<DecisionLogEntry['type']>([
|
|
|
19
19
|
'validation',
|
|
20
20
|
'decision',
|
|
21
21
|
'report',
|
|
22
|
+
'resume',
|
|
22
23
|
])
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -111,7 +112,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
111
112
|
type: 'string',
|
|
112
113
|
description:
|
|
113
114
|
'Entry type (required for append): round_start, review_result, atomic_fix, ' +
|
|
114
|
-
'architectural_fix, revert, round_failed, validation, decision, report.',
|
|
115
|
+
'architectural_fix, revert, round_failed, validation, decision, report, resume.',
|
|
115
116
|
enum: [
|
|
116
117
|
'round_start',
|
|
117
118
|
'review_result',
|
|
@@ -122,6 +123,7 @@ export function registerDecisionLogTool(ctx: { tools: { register: (def: ReturnTy
|
|
|
122
123
|
'validation',
|
|
123
124
|
'decision',
|
|
124
125
|
'report',
|
|
126
|
+
'resume',
|
|
125
127
|
],
|
|
126
128
|
},
|
|
127
129
|
round: {
|
package/src/types.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface DecisionLogEntry {
|
|
|
40
40
|
| 'decision'
|
|
41
41
|
| 'report'
|
|
42
42
|
| 'round_failed'
|
|
43
|
+
| 'resume'
|
|
43
44
|
data: Record<string, unknown>
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -151,6 +152,8 @@ export interface IterationCheckpoint {
|
|
|
151
152
|
maxRounds: number
|
|
152
153
|
fixedCount: number
|
|
153
154
|
architecturalCount: number
|
|
155
|
+
/** How many times this checkpoint has been resumed after an interruption/abort. */
|
|
156
|
+
resumeCount: number
|
|
154
157
|
findings: ReviewFinding[]
|
|
155
158
|
startedAt: string
|
|
156
159
|
updatedAt: string
|
|
@@ -167,6 +170,10 @@ export interface IterationStatus {
|
|
|
167
170
|
findingsCount: number
|
|
168
171
|
totalDecisionLogEntries: number
|
|
169
172
|
hasCheckpoint: boolean
|
|
173
|
+
/** True when a checkpoint is present — i.e. the previous run was interrupted before finishing. */
|
|
174
|
+
interrupted: boolean
|
|
175
|
+
/** How many times the current checkpoint has already been resumed. */
|
|
176
|
+
resumeCount: number
|
|
170
177
|
checkpoint: IterationCheckpoint | null
|
|
171
178
|
lastUpdated: string | null
|
|
172
179
|
}
|