iterate-plugin 2.8.4 → 2.9.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/dist/skill-prompt.js +64 -26
- package/dist/tools/checkpoint.js +16 -1
- package/dist/tools/context.js +121 -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 +64 -26
- package/src/tools/checkpoint.ts +19 -1
- package/src/tools/context.ts +138 -2
- package/src/tools/decision-log.ts +3 -1
- package/src/types.ts +7 -0
package/src/tools/context.ts
CHANGED
|
@@ -7,6 +7,100 @@ 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
|
+
/** A validated, normalized image-attachment entry carried into review context. */
|
|
14
|
+
export interface NormalizedAttachment {
|
|
15
|
+
name?: string
|
|
16
|
+
mediaType?: string
|
|
17
|
+
width?: number
|
|
18
|
+
height?: number
|
|
19
|
+
note?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validation result for a raw attachment entry. */
|
|
23
|
+
export type AttachmentValidationResult =
|
|
24
|
+
| { ok: true; value: NormalizedAttachment }
|
|
25
|
+
| { ok: false; error: string }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate and normalize one raw image-attachment entry passed by the
|
|
29
|
+
* orchestrator. The top-level model observes user-attached images in its own
|
|
30
|
+
* context (as image blocks) and relays their metadata here so reviewers get the
|
|
31
|
+
* same visual evidence. Pure — exported for unit tests.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeAttachment(raw: unknown): AttachmentValidationResult {
|
|
34
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
35
|
+
return { ok: false, error: 'attachment must be an object' }
|
|
36
|
+
}
|
|
37
|
+
const entry = raw as Record<string, unknown>
|
|
38
|
+
const out: NormalizedAttachment = {}
|
|
39
|
+
if (entry.name !== undefined) {
|
|
40
|
+
if (typeof entry.name !== 'string' || entry.name.length > 256) {
|
|
41
|
+
return { ok: false, error: 'attachment.name must be a string (≤ 256 chars)' }
|
|
42
|
+
}
|
|
43
|
+
out.name = entry.name
|
|
44
|
+
}
|
|
45
|
+
if (entry.mediaType !== undefined) {
|
|
46
|
+
if (typeof entry.mediaType !== 'string' || !/^image\/(png|jpeg|webp|gif)$/.test(entry.mediaType)) {
|
|
47
|
+
return { ok: false, error: 'attachment.mediaType must be image/png, image/jpeg, image/webp, or image/gif' }
|
|
48
|
+
}
|
|
49
|
+
out.mediaType = entry.mediaType
|
|
50
|
+
}
|
|
51
|
+
for (const dim of ['width', 'height'] as const) {
|
|
52
|
+
if (entry[dim] !== undefined) {
|
|
53
|
+
if (typeof entry[dim] !== 'number' || !Number.isInteger(entry[dim]) || entry[dim] < 0 || entry[dim] > 16384) {
|
|
54
|
+
return { ok: false, error: `attachment.${dim} must be an integer in [0, 16384]` }
|
|
55
|
+
}
|
|
56
|
+
out[dim] = entry[dim]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (entry.note !== undefined) {
|
|
60
|
+
if (typeof entry.note !== 'string' || entry.note.length > 1000) {
|
|
61
|
+
return { ok: false, error: 'attachment.note must be a string (≤ 1000 chars)' }
|
|
62
|
+
}
|
|
63
|
+
out.note = entry.note
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, value: out }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate a whole attachments array, dropping invalid entries.
|
|
70
|
+
* Returns the normalized list plus the reasons for any dropped entries.
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeAttachments(raw: unknown): {
|
|
73
|
+
attachments: NormalizedAttachment[]
|
|
74
|
+
errors: string[]
|
|
75
|
+
} {
|
|
76
|
+
const attachments: NormalizedAttachment[] = []
|
|
77
|
+
const errors: string[] = []
|
|
78
|
+
if (raw === undefined || raw === null) return { attachments, errors }
|
|
79
|
+
if (!Array.isArray(raw)) return { attachments, errors: ['attachments must be an array'] }
|
|
80
|
+
for (let i = 0; i < raw.length; i++) {
|
|
81
|
+
if (attachments.length >= MAX_ATTACHMENTS) {
|
|
82
|
+
errors.push(`attachments capped at ${MAX_ATTACHMENTS}; entry ${i} dropped`)
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
const result = normalizeAttachment(raw[i])
|
|
86
|
+
if (result.ok) attachments.push(result.value)
|
|
87
|
+
else errors.push(`attachments[${i}]: ${result.error}`)
|
|
88
|
+
}
|
|
89
|
+
return { attachments, errors }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a normalized attachment as a compact text block for the model.
|
|
94
|
+
*/
|
|
95
|
+
export function renderAttachment(a: NormalizedAttachment, index: number): string {
|
|
96
|
+
const bits: string[] = [`[${index + 1}]`]
|
|
97
|
+
if (a.name) bits.push(a.name)
|
|
98
|
+
if (a.mediaType) bits.push(a.mediaType)
|
|
99
|
+
if (typeof a.width === 'number' && typeof a.height === 'number') bits.push(`${a.width}x${a.height}`)
|
|
100
|
+
if (a.note) bits.push(a.note)
|
|
101
|
+
return bits.join(' · ')
|
|
102
|
+
}
|
|
103
|
+
|
|
10
104
|
/**
|
|
11
105
|
* The directory this source file lives in (…/src/tools). The plugin's own
|
|
12
106
|
* package root is one level up (…/src), and the skill root is typically a few
|
|
@@ -86,7 +180,9 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
86
180
|
'SKILL.md contains the original iterate skill instructions; it is searched in ' +
|
|
87
181
|
'the skill directory (auto-detected), the project root, or an explicit `skillDir`. ' +
|
|
88
182
|
'ITERATE.md contains the project-specific knowledge base and onboarding information. ' +
|
|
89
|
-
'
|
|
183
|
+
'Also relays user-attached image metadata (e.g. UI screenshots, error dialogs) into ' +
|
|
184
|
+
'the review context so reviewers can treat them as visual evidence. ' +
|
|
185
|
+
'Use this to understand the skill workflow, project context, and any attached visuals.',
|
|
90
186
|
|
|
91
187
|
parameters: {
|
|
92
188
|
files: {
|
|
@@ -105,6 +201,25 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
105
201
|
'Custom directory to search for SKILL.md (highest priority). ' +
|
|
106
202
|
'When omitted, SKILL.md is auto-detected from the skill directory, then the project root.',
|
|
107
203
|
},
|
|
204
|
+
attachments: {
|
|
205
|
+
type: 'array',
|
|
206
|
+
items: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
additionalProperties: false,
|
|
209
|
+
properties: {
|
|
210
|
+
name: { type: 'string', description: 'Optional display name of the attached image.' },
|
|
211
|
+
mediaType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], description: 'Optional media type of the image.' },
|
|
212
|
+
width: { type: 'integer', description: 'Optional intrinsic width in pixels.' },
|
|
213
|
+
height: { type: 'integer', description: 'Optional intrinsic height in pixels.' },
|
|
214
|
+
note: { type: 'string', description: 'Optional short description of what the image shows and why it matters for this review.' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
description:
|
|
218
|
+
'Optional: image attachments observed in the session (e.g. UI screenshots, error ' +
|
|
219
|
+
'dialogs, design references) relayed into the review context. The top-level model ' +
|
|
220
|
+
'sees these images natively and passes their metadata here so reviewers get the same ' +
|
|
221
|
+
'visual evidence. Up to 8 entries; invalid entries are dropped and reported.',
|
|
222
|
+
},
|
|
108
223
|
},
|
|
109
224
|
|
|
110
225
|
output: {
|
|
@@ -118,15 +233,23 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
118
233
|
skillSource: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
119
234
|
error: { type: 'string' },
|
|
120
235
|
searched: { type: 'array', items: { type: 'string' } },
|
|
236
|
+
attachments: { type: 'array', items: { type: 'string' }, description: 'Normalized attached-image descriptions relayed to reviewers.' },
|
|
237
|
+
attachmentErrors: { type: 'array', items: { type: 'string' }, description: 'Reasons for any attachment entries that were dropped.' },
|
|
121
238
|
},
|
|
122
239
|
},
|
|
123
240
|
render: (_args, value) => {
|
|
124
241
|
const parts: string[] = []
|
|
125
242
|
if (value.skill) parts.push(`--- SKILL.md (${value.skillSource ?? '?source?'}) ---\n${value.skill}`)
|
|
126
243
|
if (value.project) parts.push(`--- ITERATE.md ---\n${value.project}`)
|
|
127
|
-
if (
|
|
244
|
+
if (Array.isArray(value.attachments) && value.attachments.length > 0) {
|
|
245
|
+
parts.push(`--- User-attached images (${value.attachments.length}) ---\n${value.attachments.join('\n')}`)
|
|
246
|
+
}
|
|
247
|
+
if (!value.skill && !value.project && !(Array.isArray(value.attachments) && value.attachments.length > 0)) {
|
|
128
248
|
parts.push('No files found. Searched: ' + (value.searched?.join(', ') ?? 'none'))
|
|
129
249
|
}
|
|
250
|
+
if (Array.isArray(value.attachmentErrors) && value.attachmentErrors.length > 0) {
|
|
251
|
+
parts.push('Attachment warnings: ' + value.attachmentErrors.join('; '))
|
|
252
|
+
}
|
|
130
253
|
return [{ type: 'text', text: parts.join('\n\n') }]
|
|
131
254
|
},
|
|
132
255
|
},
|
|
@@ -148,8 +271,21 @@ export function registerContextTool(ctx: { tools: { register: (def: ReturnType<t
|
|
|
148
271
|
project?: string | null
|
|
149
272
|
skillSource?: string | null
|
|
150
273
|
searched: string[]
|
|
274
|
+
attachments?: string[]
|
|
275
|
+
attachmentErrors?: string[]
|
|
151
276
|
} = { found: true, searched: [] }
|
|
152
277
|
|
|
278
|
+
// Relay user-attached image metadata into the review context. The
|
|
279
|
+
// orchestrator observes attached images in the session and passes their
|
|
280
|
+
// metadata here; invalid entries are dropped with a reported reason.
|
|
281
|
+
const attachments = normalizeAttachments(args.attachments)
|
|
282
|
+
if (attachments.attachments.length > 0) {
|
|
283
|
+
result.attachments = attachments.attachments.map(renderAttachment)
|
|
284
|
+
}
|
|
285
|
+
if (attachments.errors.length > 0) {
|
|
286
|
+
result.attachmentErrors = attachments.errors
|
|
287
|
+
}
|
|
288
|
+
|
|
153
289
|
if (requested.includes('skill') || requested.includes('skill.md')) {
|
|
154
290
|
// Candidate dirs in priority order: custom path → auto-detected skill
|
|
155
291
|
// 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
|
}
|