@sebastianandreasson/pi-autonomous-agents 0.2.0 → 0.4.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/README.md +18 -3
- package/SETUP.md +9 -0
- package/docs/PI_SUPERVISOR.md +11 -11
- package/package.json +3 -3
- package/pi.config.json +1 -0
- package/src/index.mjs +1 -0
- package/src/pi-client.mjs +37 -0
- package/src/pi-config.mjs +51 -18
- package/src/pi-history.mjs +2 -0
- package/src/pi-preflight.mjs +48 -17
- package/src/pi-prompts.mjs +339 -103
- package/src/pi-repo.mjs +65 -3
- package/src/pi-report.mjs +11 -0
- package/src/pi-rpc-adapter.mjs +73 -0
- package/src/pi-supervisor.mjs +465 -26
- package/src/pi-telemetry.mjs +15 -1
- package/templates/DEVELOPER.md +3 -0
- package/templates/TESTER.md +7 -4
- package/templates/pi.config.example.json +4 -1
package/src/pi-supervisor.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { appendTelemetry, ensureTelemetryFiles } from './pi-telemetry.mjs'
|
|
14
14
|
import {
|
|
15
15
|
appendLog,
|
|
16
|
+
collectLargeFileWarnings,
|
|
16
17
|
commitStagedFiles,
|
|
17
18
|
didRepoChange,
|
|
18
19
|
ensureFileExists,
|
|
@@ -79,6 +80,18 @@ function printTerminalSummary(config, summary) {
|
|
|
79
80
|
lines.push(`[PI supervisor] notes=${summary.notes}`)
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (Array.isArray(summary.largeFileWarnings) && summary.largeFileWarnings.length > 0) {
|
|
84
|
+
lines.push(`[PI supervisor] large_file_warnings=${formatLargeFileWarningsInline(summary.largeFileWarnings)}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (summary.terminalReason) {
|
|
88
|
+
lines.push(`[PI supervisor] terminal_reason=${summary.terminalReason}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (summary.commitPlanFound !== undefined) {
|
|
92
|
+
lines.push(`[PI supervisor] commit_plan_found=${summary.commitPlanFound}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
if (summary.sessionId) {
|
|
83
96
|
lines.push(`[PI supervisor] session=${summary.sessionId}`)
|
|
84
97
|
}
|
|
@@ -87,6 +100,14 @@ function printTerminalSummary(config, summary) {
|
|
|
87
100
|
lines.push(`[PI supervisor] last_output=${summary.outputPath}`)
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
if (config.lastPromptFile) {
|
|
104
|
+
lines.push(`[PI supervisor] last_prompt=${toDisplayPath(config, config.lastPromptFile)}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (config.lastIterationSummaryFile) {
|
|
108
|
+
lines.push(`[PI supervisor] iteration_summary=${toDisplayPath(config, config.lastIterationSummaryFile)}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
process.stderr.write(`${lines.join('\n')}\n`)
|
|
91
112
|
}
|
|
92
113
|
|
|
@@ -109,6 +130,133 @@ function parseTesterVerdict(output) {
|
|
|
109
130
|
return match?.[1]?.toUpperCase() ?? 'UNKNOWN'
|
|
110
131
|
}
|
|
111
132
|
|
|
133
|
+
function buildRetryReason(invocation) {
|
|
134
|
+
const loopSignature = String(invocation?.result?.loopSignature ?? '')
|
|
135
|
+
const notes = String(invocation?.result?.notes ?? '')
|
|
136
|
+
|
|
137
|
+
if (loopSignature.startsWith('same_path:')) {
|
|
138
|
+
const target = loopSignature.slice('same_path:'.length)
|
|
139
|
+
return `The previous turn got stuck repeatedly editing ${target}. Reread ${target} exactly once before any new edit. Switch approach. Do not attempt another exact oldText patch on ${target} unless the file changed since the failed attempt.`
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (notes.includes('loop_detected=')) {
|
|
143
|
+
return `The previous turn got stuck repeating the same tool call (${notes}). Continue from the current repo state without rereading the same file over and over.`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return 'The previous turn stalled or timed out. Continue from the current repo state.'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatIterationSummary(summary) {
|
|
150
|
+
return `${JSON.stringify(summary, null, 2)}\n`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function writeIterationSummary(config, summary) {
|
|
154
|
+
await writeTextFile(config.lastIterationSummaryFile, formatIterationSummary(summary))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createIterationSummary({
|
|
158
|
+
iteration,
|
|
159
|
+
phase,
|
|
160
|
+
task,
|
|
161
|
+
repoChanged,
|
|
162
|
+
developerStatus,
|
|
163
|
+
testerStatus,
|
|
164
|
+
testerVerdict,
|
|
165
|
+
verificationStatus,
|
|
166
|
+
commitPlanFound,
|
|
167
|
+
gitFinalizeStatus,
|
|
168
|
+
visualStatus,
|
|
169
|
+
terminalReason,
|
|
170
|
+
largeFileWarnings,
|
|
171
|
+
sessionId,
|
|
172
|
+
developerModel,
|
|
173
|
+
testerModel,
|
|
174
|
+
visualModel,
|
|
175
|
+
}) {
|
|
176
|
+
return {
|
|
177
|
+
iteration,
|
|
178
|
+
phase,
|
|
179
|
+
task,
|
|
180
|
+
repoChanged,
|
|
181
|
+
developerStatus,
|
|
182
|
+
testerStatus,
|
|
183
|
+
testerVerdict,
|
|
184
|
+
verificationStatus,
|
|
185
|
+
commitPlanFound,
|
|
186
|
+
gitFinalizeStatus,
|
|
187
|
+
visualStatus,
|
|
188
|
+
terminalReason,
|
|
189
|
+
largeFileWarnings,
|
|
190
|
+
sessionId,
|
|
191
|
+
developerModel,
|
|
192
|
+
testerModel,
|
|
193
|
+
visualModel,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function didInvocationCreateCommit(invocation) {
|
|
198
|
+
return invocation?.beforeSnapshot?.head !== invocation?.afterSnapshot?.head
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mergeLargeFileWarnings(existing, incoming) {
|
|
202
|
+
const merged = new Map()
|
|
203
|
+
for (const warning of [...(existing || []), ...(incoming || [])]) {
|
|
204
|
+
if (!warning?.file) {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const key = `${warning.kind}:${warning.file}`
|
|
208
|
+
const current = merged.get(key)
|
|
209
|
+
if (!current || Number(warning.lineCount) > Number(current.lineCount)) {
|
|
210
|
+
merged.set(key, warning)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [...merged.values()].sort((left, right) => right.lineCount - left.lineCount)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function findLargeFileWarnings(config, files) {
|
|
217
|
+
return collectLargeFileWarnings(config.cwd, files, {
|
|
218
|
+
largeFileWarningLines: config.largeFileWarningLines,
|
|
219
|
+
largeSpecWarningLines: config.largeSpecWarningLines,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatLargeFileWarningsInline(warnings) {
|
|
224
|
+
const list = Array.isArray(warnings) ? warnings : []
|
|
225
|
+
if (list.length === 0) {
|
|
226
|
+
return ''
|
|
227
|
+
}
|
|
228
|
+
return list
|
|
229
|
+
.slice(0, 3)
|
|
230
|
+
.map((warning) => `${warning.file}(${warning.lineCount}${warning.kind === 'large_spec' ? ',spec' : ''})`)
|
|
231
|
+
.join(', ')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function clampPromptLines(text, maxLines) {
|
|
235
|
+
const normalized = String(text ?? '').trim()
|
|
236
|
+
if (normalized === '') {
|
|
237
|
+
return ''
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const lines = normalized.split('\n')
|
|
241
|
+
if (!Number.isFinite(maxLines) || maxLines <= 0 || lines.length <= maxLines) {
|
|
242
|
+
return normalized
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const remaining = lines.length - maxLines
|
|
246
|
+
return `${lines.slice(0, maxLines).join('\n')}\n... (${remaining} more lines omitted)`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function compactNotePartsForPrompt(config, noteParts, fallback = '(none provided)') {
|
|
250
|
+
const items = Array.isArray(noteParts) ? noteParts.filter(Boolean) : []
|
|
251
|
+
if (items.length === 0) {
|
|
252
|
+
return fallback
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const maxItems = Math.min(6, items.length)
|
|
256
|
+
const selected = items.slice(-maxItems)
|
|
257
|
+
return clampPromptLines(selected.join('\n'), Number(config.maxPromptNotesLines) || 16)
|
|
258
|
+
}
|
|
259
|
+
|
|
112
260
|
function isInfrastructureVerificationFailure(output) {
|
|
113
261
|
const text = String(output ?? '')
|
|
114
262
|
return [
|
|
@@ -142,6 +290,16 @@ async function runAgentInvocation({
|
|
|
142
290
|
}) {
|
|
143
291
|
const beforeSnapshot = getRepoSnapshot(config.cwd)
|
|
144
292
|
const resolvedModel = resolveRoleModel(config, role)
|
|
293
|
+
const promptSnapshot = [
|
|
294
|
+
`role=${role}`,
|
|
295
|
+
`kind=${kind}`,
|
|
296
|
+
`phase=${phase}`,
|
|
297
|
+
`reason=${reason}`,
|
|
298
|
+
`model=${resolvedModel.model || '(PI default)'}`,
|
|
299
|
+
'',
|
|
300
|
+
prompt,
|
|
301
|
+
].join('\n')
|
|
302
|
+
await writeTextFile(config.lastPromptFile, `${promptSnapshot}\n`)
|
|
145
303
|
const result = await runAgentTurn({
|
|
146
304
|
config,
|
|
147
305
|
model: resolvedModel.model,
|
|
@@ -178,6 +336,17 @@ async function runAgentInvocation({
|
|
|
178
336
|
changedFilesCount: changedFiles.length,
|
|
179
337
|
verificationStatus,
|
|
180
338
|
retryCount,
|
|
339
|
+
role,
|
|
340
|
+
model: resolvedModel.model || '(PI default)',
|
|
341
|
+
toolCalls: result.toolCalls ?? 0,
|
|
342
|
+
toolErrors: result.toolErrors ?? 0,
|
|
343
|
+
messageUpdates: result.messageUpdates ?? 0,
|
|
344
|
+
stopReason: result.stopReason ?? '',
|
|
345
|
+
loopDetected: result.loopDetected === true,
|
|
346
|
+
loopSignature: result.loopSignature ?? '',
|
|
347
|
+
testerVerdict: '',
|
|
348
|
+
commitPlanFound: '',
|
|
349
|
+
terminalReason: result.terminalReason ?? '',
|
|
181
350
|
notes: `${result.notes} role=${role} model=${resolvedModel.model || '(PI default)'}`.trim(),
|
|
182
351
|
})
|
|
183
352
|
|
|
@@ -202,7 +371,7 @@ async function readLatestVisualFeedback(config) {
|
|
|
202
371
|
if (trimmed === '') {
|
|
203
372
|
return ''
|
|
204
373
|
}
|
|
205
|
-
return trimmed.
|
|
374
|
+
return clampPromptLines(trimmed, Number(config.maxVisualFeedbackLines) || 20)
|
|
206
375
|
}
|
|
207
376
|
|
|
208
377
|
async function readLatestTesterFeedback(config) {
|
|
@@ -211,7 +380,7 @@ async function readLatestTesterFeedback(config) {
|
|
|
211
380
|
if (trimmed === '') {
|
|
212
381
|
return ''
|
|
213
382
|
}
|
|
214
|
-
return trimmed.
|
|
383
|
+
return clampPromptLines(trimmed, Number(config.maxTesterFeedbackLines) || 32)
|
|
215
384
|
}
|
|
216
385
|
|
|
217
386
|
async function writeTesterFeedback(config, { iteration, phase, task, source, status, output }) {
|
|
@@ -241,17 +410,43 @@ async function writeTesterFeedback(config, { iteration, phase, task, source, sta
|
|
|
241
410
|
|
|
242
411
|
function parseCommitPlan(output) {
|
|
243
412
|
const raw = String(output ?? '')
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
?
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
413
|
+
const lines = raw.split('\n')
|
|
414
|
+
const messageLine = lines.find((line) => /^\s*(?:[-*]\s+)?COMMIT_MESSAGE:\s*(.+?)\s*$/i.test(line))
|
|
415
|
+
const message = messageLine
|
|
416
|
+
? messageLine.replace(/^\s*(?:[-*]\s+)?COMMIT_MESSAGE:\s*/i, '').trim()
|
|
417
|
+
: ''
|
|
418
|
+
|
|
419
|
+
const filesStartIndex = lines.findIndex((line) => /^\s*(?:[-*]\s+)?COMMIT_FILES:\s*$/i.test(line))
|
|
420
|
+
const files = []
|
|
421
|
+
if (filesStartIndex >= 0) {
|
|
422
|
+
for (const line of lines.slice(filesStartIndex + 1)) {
|
|
423
|
+
const trimmed = line.trim()
|
|
424
|
+
if (trimmed === '') {
|
|
425
|
+
if (files.length > 0) {
|
|
426
|
+
break
|
|
427
|
+
}
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
if (/^VERDICT:/i.test(trimmed)) {
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
if (/^(?:[-*]\s+)?[A-Z_]+:\s*/.test(trimmed)) {
|
|
434
|
+
break
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const normalized = trimmed
|
|
438
|
+
.replace(/^[-*]\s+/, '')
|
|
439
|
+
.replace(/^\d+\.\s+/, '')
|
|
440
|
+
.trim()
|
|
441
|
+
|
|
442
|
+
if (normalized !== '') {
|
|
443
|
+
files.push(normalized)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
252
447
|
|
|
253
448
|
return {
|
|
254
|
-
message
|
|
449
|
+
message,
|
|
255
450
|
files: [...new Set(files)],
|
|
256
451
|
}
|
|
257
452
|
}
|
|
@@ -275,6 +470,7 @@ async function runHarnessGitFinalize({
|
|
|
275
470
|
const commitMessage = String(commitPlan.message ?? '').trim()
|
|
276
471
|
let status = 'success'
|
|
277
472
|
let notes = ''
|
|
473
|
+
let terminalReason = 'commit_created'
|
|
278
474
|
const cleanupNewlyStagedFiles = (stagedBefore, stagedNow) => {
|
|
279
475
|
const stagedBeforeSet = new Set(stagedBefore)
|
|
280
476
|
const newlyStagedFiles = stagedNow.filter((file) => !stagedBeforeSet.has(file))
|
|
@@ -288,6 +484,7 @@ async function runHarnessGitFinalize({
|
|
|
288
484
|
if (commitMessage === '' || requestedFiles.length === 0) {
|
|
289
485
|
status = 'stalled'
|
|
290
486
|
notes = 'commit_plan_missing=true'
|
|
487
|
+
terminalReason = 'awaiting_commit_plan'
|
|
291
488
|
} else {
|
|
292
489
|
const stagedBefore = listStagedFiles(config.cwd)
|
|
293
490
|
const unrelatedStagedBefore = stagedBefore.filter((file) => !requestedFiles.includes(file))
|
|
@@ -295,11 +492,13 @@ async function runHarnessGitFinalize({
|
|
|
295
492
|
if (unrelatedStagedBefore.length > 0) {
|
|
296
493
|
status = 'blocked'
|
|
297
494
|
notes = `commit_blocked_unrelated_staged_files=${unrelatedStagedBefore.join(',')}`
|
|
495
|
+
terminalReason = 'commit_finalize_blocked_unrelated_staged'
|
|
298
496
|
} else {
|
|
299
497
|
const filesToStage = requestedFiles.filter((file) => dirtyFiles.has(file))
|
|
300
498
|
if (filesToStage.length === 0) {
|
|
301
499
|
status = 'stalled'
|
|
302
500
|
notes = 'commit_plan_no_dirty_files=true'
|
|
501
|
+
terminalReason = 'commit_plan_no_dirty_files'
|
|
303
502
|
} else {
|
|
304
503
|
try {
|
|
305
504
|
stageFiles(config.cwd, filesToStage)
|
|
@@ -310,18 +509,22 @@ async function runHarnessGitFinalize({
|
|
|
310
509
|
const cleanedFiles = cleanupNewlyStagedFiles(stagedBefore, stagedAfter)
|
|
311
510
|
status = 'blocked'
|
|
312
511
|
notes = `commit_blocked_unexpected_staged_files=${unexpectedStaged.join(',')} unstaged_cleanup=${cleanedFiles.join(',')}`.trim()
|
|
512
|
+
terminalReason = 'commit_finalize_blocked_unexpected_staged'
|
|
313
513
|
} else if (!stagedAfter.some((file) => requestedFiles.includes(file))) {
|
|
314
514
|
const cleanedFiles = cleanupNewlyStagedFiles(stagedBefore, stagedAfter)
|
|
315
515
|
status = 'stalled'
|
|
316
516
|
notes = `commit_plan_failed_to_stage=true unstaged_cleanup=${cleanedFiles.join(',')}`.trim()
|
|
517
|
+
terminalReason = 'commit_plan_failed_to_stage'
|
|
317
518
|
} else {
|
|
318
519
|
commitStagedFiles(config.cwd, commitMessage)
|
|
319
520
|
notes = `commit_created=true files=${filesToStage.join(',')} message=${commitMessage}`
|
|
521
|
+
terminalReason = 'commit_created'
|
|
320
522
|
}
|
|
321
523
|
} catch (error) {
|
|
322
524
|
const cleanedFiles = cleanupNewlyStagedFiles(stagedBefore, listStagedFiles(config.cwd))
|
|
323
525
|
status = 'failed'
|
|
324
526
|
notes = `commit_failed=${formatExecError(error)}${cleanedFiles.length > 0 ? ` unstaged_cleanup=${cleanedFiles.join(',')}` : ''}`
|
|
527
|
+
terminalReason = 'commit_finalize_failed'
|
|
325
528
|
}
|
|
326
529
|
}
|
|
327
530
|
}
|
|
@@ -329,12 +532,16 @@ async function runHarnessGitFinalize({
|
|
|
329
532
|
|
|
330
533
|
const afterSnapshot = getRepoSnapshot(config.cwd)
|
|
331
534
|
const changedFiles = listChangedFiles(config.cwd)
|
|
535
|
+
const finalStatus = status === 'success' && beforeSnapshot.head === afterSnapshot.head ? 'stalled' : status
|
|
536
|
+
if (status === 'success' && finalStatus === 'stalled') {
|
|
537
|
+
terminalReason = 'commit_not_created'
|
|
538
|
+
}
|
|
332
539
|
|
|
333
540
|
await recordEvent(config, {
|
|
334
541
|
iteration,
|
|
335
542
|
phase,
|
|
336
543
|
kind: 'git_finalize',
|
|
337
|
-
status,
|
|
544
|
+
status: finalStatus,
|
|
338
545
|
transport: 'local',
|
|
339
546
|
sessionId: '',
|
|
340
547
|
timedOut: false,
|
|
@@ -346,12 +553,24 @@ async function runHarnessGitFinalize({
|
|
|
346
553
|
changedFilesCount: changedFiles.length,
|
|
347
554
|
verificationStatus: 'not_run',
|
|
348
555
|
retryCount: 0,
|
|
556
|
+
role: '',
|
|
557
|
+
model: '',
|
|
558
|
+
toolCalls: 0,
|
|
559
|
+
toolErrors: 0,
|
|
560
|
+
messageUpdates: 0,
|
|
561
|
+
stopReason: '',
|
|
562
|
+
loopDetected: false,
|
|
563
|
+
loopSignature: '',
|
|
564
|
+
testerVerdict: '',
|
|
565
|
+
commitPlanFound: requestedFiles.length > 0,
|
|
566
|
+
terminalReason,
|
|
349
567
|
notes,
|
|
350
568
|
})
|
|
351
569
|
|
|
352
570
|
return {
|
|
353
|
-
status:
|
|
571
|
+
status: finalStatus,
|
|
354
572
|
notes,
|
|
573
|
+
terminalReason,
|
|
355
574
|
}
|
|
356
575
|
}
|
|
357
576
|
|
|
@@ -385,6 +604,17 @@ async function runVerificationStep({ config, iteration, phase, kind }) {
|
|
|
385
604
|
changedFilesCount: changedFiles.length,
|
|
386
605
|
verificationStatus: verification.status,
|
|
387
606
|
retryCount: 0,
|
|
607
|
+
role: '',
|
|
608
|
+
model: '',
|
|
609
|
+
toolCalls: 0,
|
|
610
|
+
toolErrors: 0,
|
|
611
|
+
messageUpdates: 0,
|
|
612
|
+
stopReason: '',
|
|
613
|
+
loopDetected: false,
|
|
614
|
+
loopSignature: '',
|
|
615
|
+
testerVerdict: '',
|
|
616
|
+
commitPlanFound: '',
|
|
617
|
+
terminalReason: `verification_${verification.status}`,
|
|
388
618
|
notes: verificationNotes,
|
|
389
619
|
})
|
|
390
620
|
|
|
@@ -438,6 +668,7 @@ async function runMainTurnWithRetries({ config, iteration, phase, sessionId, ses
|
|
|
438
668
|
result: {
|
|
439
669
|
...invocation.result,
|
|
440
670
|
status: 'stalled',
|
|
671
|
+
terminalReason: 'no_repo_change',
|
|
441
672
|
notes: `${invocation.result.notes} no_repo_change=true`,
|
|
442
673
|
},
|
|
443
674
|
}
|
|
@@ -448,13 +679,12 @@ async function runMainTurnWithRetries({ config, iteration, phase, sessionId, ses
|
|
|
448
679
|
}
|
|
449
680
|
|
|
450
681
|
reason = shouldRetryForTimeout
|
|
451
|
-
? invocation
|
|
452
|
-
? `The previous turn got stuck repeating the same tool call (${invocation.result.notes}). Continue from the current repo state without rereading the same file over and over.`
|
|
453
|
-
: 'The previous turn stalled or timed out. Continue from the current repo state.'
|
|
682
|
+
? buildRetryReason(invocation)
|
|
454
683
|
: 'The previous turn ended without changing the repo. Continue and complete one coherent task.'
|
|
455
684
|
prompt = buildSteeringPrompt(config, reason, {
|
|
456
685
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
457
686
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
687
|
+
largeFileWarnings: findLargeFileWarnings(config, listChangedFiles(config.cwd)),
|
|
458
688
|
})
|
|
459
689
|
|
|
460
690
|
if (shouldRetryForTimeout || shouldRetryForNoChange) {
|
|
@@ -467,12 +697,14 @@ async function runMainTurnWithRetries({ config, iteration, phase, sessionId, ses
|
|
|
467
697
|
}
|
|
468
698
|
|
|
469
699
|
async function runFixTurn({ config, iteration, phase, sessionId, sessionFile, testerOutput }) {
|
|
700
|
+
const largeFileWarnings = findLargeFileWarnings(config, listChangedFiles(config.cwd))
|
|
470
701
|
const fixPrompt = buildFixPrompt(
|
|
471
702
|
config,
|
|
472
|
-
testerOutput
|
|
703
|
+
clampPromptLines(testerOutput, Number(config.maxVerificationExcerptLines) || 40),
|
|
473
704
|
{
|
|
474
705
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
475
706
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
707
|
+
largeFileWarnings,
|
|
476
708
|
}
|
|
477
709
|
)
|
|
478
710
|
return await runAgentInvocation({
|
|
@@ -573,6 +805,7 @@ async function runTesterTurn({
|
|
|
573
805
|
developerNotes,
|
|
574
806
|
reason,
|
|
575
807
|
}) {
|
|
808
|
+
const largeFileWarnings = findLargeFileWarnings(config, changedFiles)
|
|
576
809
|
const prompt = buildTesterPrompt(config, {
|
|
577
810
|
phase,
|
|
578
811
|
task,
|
|
@@ -581,6 +814,7 @@ async function runTesterTurn({
|
|
|
581
814
|
reason,
|
|
582
815
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
583
816
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
817
|
+
largeFileWarnings,
|
|
584
818
|
})
|
|
585
819
|
|
|
586
820
|
const invocation = await runAgentInvocation({
|
|
@@ -600,22 +834,38 @@ async function runTesterTurn({
|
|
|
600
834
|
const commitPlan = parseCommitPlan(invocation.result.output)
|
|
601
835
|
const notesWithVerdict = `${invocation.result.notes} tester_verdict=${verdict} commit_plan_files=${commitPlan.files.length}`.trim()
|
|
602
836
|
let testerStatus = invocation.result.status
|
|
837
|
+
let terminalReason = invocation.result.terminalReason || ''
|
|
603
838
|
|
|
604
839
|
if (testerStatus === 'success' && verdict === 'FAIL') {
|
|
605
840
|
testerStatus = 'failed'
|
|
841
|
+
terminalReason = 'tester_verdict_fail'
|
|
606
842
|
} else if (testerStatus === 'success' && verdict === 'BLOCKED') {
|
|
607
843
|
testerStatus = 'stalled'
|
|
844
|
+
terminalReason = 'tester_verdict_blocked'
|
|
608
845
|
} else if (testerStatus === 'success' && verdict === 'UNKNOWN') {
|
|
609
846
|
testerStatus = 'stalled'
|
|
847
|
+
terminalReason = 'tester_verdict_unknown'
|
|
848
|
+
} else if (testerStatus === 'success' && config.commitMode === 'plan') {
|
|
849
|
+
terminalReason = commitPlan.message !== '' && commitPlan.files.length > 0
|
|
850
|
+
? 'tester_pass_with_commit_plan'
|
|
851
|
+
: 'awaiting_commit_plan'
|
|
852
|
+
} else if (testerStatus === 'success') {
|
|
853
|
+
terminalReason = didInvocationCreateCommit(invocation)
|
|
854
|
+
? 'tester_pass_with_agent_commit'
|
|
855
|
+
: invocation.repoChanged
|
|
856
|
+
? 'tester_left_uncommitted_changes'
|
|
857
|
+
: 'awaiting_agent_commit'
|
|
610
858
|
}
|
|
611
859
|
|
|
612
860
|
return {
|
|
613
861
|
...invocation,
|
|
614
862
|
testerVerdict: verdict,
|
|
863
|
+
commitPlanFound: commitPlan.message !== '' && commitPlan.files.length > 0,
|
|
615
864
|
commitPlan,
|
|
616
865
|
result: {
|
|
617
866
|
...invocation.result,
|
|
618
867
|
status: testerStatus,
|
|
868
|
+
terminalReason,
|
|
619
869
|
notes: notesWithVerdict,
|
|
620
870
|
},
|
|
621
871
|
}
|
|
@@ -630,6 +880,7 @@ async function runTesterCommitTurn({
|
|
|
630
880
|
developerNotes,
|
|
631
881
|
reason,
|
|
632
882
|
}) {
|
|
883
|
+
const largeFileWarnings = findLargeFileWarnings(config, changedFiles)
|
|
633
884
|
const prompt = buildCommitPrompt(config, {
|
|
634
885
|
phase,
|
|
635
886
|
task,
|
|
@@ -638,6 +889,7 @@ async function runTesterCommitTurn({
|
|
|
638
889
|
reason,
|
|
639
890
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
640
891
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
892
|
+
largeFileWarnings,
|
|
641
893
|
})
|
|
642
894
|
|
|
643
895
|
const invocation = await runAgentInvocation({
|
|
@@ -657,22 +909,30 @@ async function runTesterCommitTurn({
|
|
|
657
909
|
const commitPlan = parseCommitPlan(invocation.result.output)
|
|
658
910
|
const notesWithVerdict = `${invocation.result.notes} tester_verdict=${verdict} commit_plan_files=${commitPlan.files.length}`.trim()
|
|
659
911
|
let testerStatus = invocation.result.status
|
|
912
|
+
let terminalReason = invocation.result.terminalReason || ''
|
|
660
913
|
|
|
661
914
|
if (testerStatus === 'success' && verdict === 'BLOCKED') {
|
|
662
915
|
testerStatus = 'stalled'
|
|
916
|
+
terminalReason = 'tester_commit_blocked'
|
|
663
917
|
} else if (testerStatus === 'success' && verdict !== 'PASS') {
|
|
664
918
|
testerStatus = 'stalled'
|
|
919
|
+
terminalReason = 'tester_commit_missing_pass'
|
|
665
920
|
} else if (testerStatus === 'success' && (commitPlan.message === '' || commitPlan.files.length === 0)) {
|
|
666
921
|
testerStatus = 'stalled'
|
|
922
|
+
terminalReason = 'awaiting_commit_plan'
|
|
923
|
+
} else if (testerStatus === 'success') {
|
|
924
|
+
terminalReason = 'tester_commit_plan_ready'
|
|
667
925
|
}
|
|
668
926
|
|
|
669
927
|
return {
|
|
670
928
|
...invocation,
|
|
671
929
|
testerVerdict: verdict,
|
|
930
|
+
commitPlanFound: commitPlan.message !== '' && commitPlan.files.length > 0,
|
|
672
931
|
commitPlan,
|
|
673
932
|
result: {
|
|
674
933
|
...invocation.result,
|
|
675
934
|
status: testerStatus,
|
|
935
|
+
terminalReason,
|
|
676
936
|
notes: notesWithVerdict,
|
|
677
937
|
},
|
|
678
938
|
}
|
|
@@ -701,6 +961,17 @@ async function runVisualReview({ config, iteration, phase, task, changedFiles })
|
|
|
701
961
|
changedFilesCount: changedFiles.length,
|
|
702
962
|
verificationStatus: 'not_run',
|
|
703
963
|
retryCount: 0,
|
|
964
|
+
role: '',
|
|
965
|
+
model: '',
|
|
966
|
+
toolCalls: 0,
|
|
967
|
+
toolErrors: 0,
|
|
968
|
+
messageUpdates: 0,
|
|
969
|
+
stopReason: '',
|
|
970
|
+
loopDetected: false,
|
|
971
|
+
loopSignature: '',
|
|
972
|
+
testerVerdict: '',
|
|
973
|
+
commitPlanFound: '',
|
|
974
|
+
terminalReason: `visual_capture_${capture.status}`,
|
|
704
975
|
notes: capture.status === 'passed'
|
|
705
976
|
? `screenshots=${capture.screenshots.length} manifest=${capture.manifestPath}`
|
|
706
977
|
: capture.output.trim().split('\n').slice(-8).join(' '),
|
|
@@ -777,6 +1048,17 @@ async function runVisualReview({ config, iteration, phase, task, changedFiles })
|
|
|
777
1048
|
changedFilesCount: changedFiles.length,
|
|
778
1049
|
verificationStatus: 'not_run',
|
|
779
1050
|
retryCount: 0,
|
|
1051
|
+
role: 'visualReview',
|
|
1052
|
+
model: visualReviewModel.model || '(unset)',
|
|
1053
|
+
toolCalls: 0,
|
|
1054
|
+
toolErrors: 0,
|
|
1055
|
+
messageUpdates: 0,
|
|
1056
|
+
stopReason: '',
|
|
1057
|
+
loopDetected: false,
|
|
1058
|
+
loopSignature: '',
|
|
1059
|
+
testerVerdict: verdict,
|
|
1060
|
+
commitPlanFound: '',
|
|
1061
|
+
terminalReason: `visual_review_${status}`,
|
|
780
1062
|
notes: `verdict=${verdict} feedback=${config.visualFeedbackFile} role=visualReview model=${visualReviewModel.model || '(unset)'}`.trim(),
|
|
781
1063
|
})
|
|
782
1064
|
|
|
@@ -791,6 +1073,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
791
1073
|
const testerModelName = resolveRoleModelName(config, 'tester')
|
|
792
1074
|
const visualReviewRoleModel = resolveRoleModel(config, 'visualReview')
|
|
793
1075
|
const visualModelName = visualReviewRoleModel.model
|
|
1076
|
+
const iterationStartSnapshot = getRepoSnapshot(config.cwd)
|
|
794
1077
|
const taskInfo = findFirstUncheckedTaskInfo(config.taskFile)
|
|
795
1078
|
if (!taskInfo.hasUncheckedTasks) {
|
|
796
1079
|
await appendLog(config.logFile, 'No unchecked tasks remain in TODOS.md')
|
|
@@ -808,9 +1091,17 @@ async function runIteration({ config, state, iteration }) {
|
|
|
808
1091
|
summary: {
|
|
809
1092
|
iteration,
|
|
810
1093
|
phase: taskInfo.phase || 'complete',
|
|
1094
|
+
task: '',
|
|
1095
|
+
repoChanged: false,
|
|
811
1096
|
developerStatus: 'complete',
|
|
812
1097
|
testerStatus: 'not_needed',
|
|
1098
|
+
testerVerdict: 'NOT_RUN',
|
|
813
1099
|
verificationStatus: 'not_needed',
|
|
1100
|
+
commitPlanFound: false,
|
|
1101
|
+
gitFinalizeStatus: 'not_run',
|
|
1102
|
+
visualStatus: 'not_run',
|
|
1103
|
+
terminalReason: 'all_tasks_complete',
|
|
1104
|
+
largeFileWarnings: [],
|
|
814
1105
|
notes: 'No unchecked tasks remain in TODOS.md.',
|
|
815
1106
|
sessionId: state.sessionId || '',
|
|
816
1107
|
outputPath: config.lastAgentOutputFile,
|
|
@@ -854,13 +1145,19 @@ async function runIteration({ config, state, iteration }) {
|
|
|
854
1145
|
let sessionFile = mainInvocation.result.sessionFile || startingSessionFile
|
|
855
1146
|
let developerStatus = mainInvocation.result.status
|
|
856
1147
|
let testerStatus = 'not_run'
|
|
1148
|
+
let testerVerdict = 'NOT_RUN'
|
|
857
1149
|
let finalVerificationStatus = 'not_run'
|
|
858
1150
|
let visualStatus = 'not_run'
|
|
1151
|
+
let commitPlanFound = false
|
|
1152
|
+
let gitFinalizeStatus = 'not_run'
|
|
1153
|
+
let terminalReason = mainInvocation.result.terminalReason || ''
|
|
1154
|
+
let largeFileWarnings = findLargeFileWarnings(config, mainInvocation.changedFiles)
|
|
859
1155
|
const noteParts = [`developer: ${mainInvocation.result.notes}`]
|
|
860
1156
|
|
|
861
1157
|
if (mainInvocation.result.status === 'success' && config.transport === 'mock') {
|
|
862
1158
|
testerStatus = 'skipped'
|
|
863
1159
|
finalVerificationStatus = 'skipped'
|
|
1160
|
+
terminalReason = 'mock_completed'
|
|
864
1161
|
} else if (mainInvocation.result.status === 'success') {
|
|
865
1162
|
const developerVerification = await runDeveloperVerificationAndFix({
|
|
866
1163
|
config,
|
|
@@ -875,6 +1172,13 @@ async function runIteration({ config, state, iteration }) {
|
|
|
875
1172
|
sessionFile = developerVerification.sessionFile
|
|
876
1173
|
developerStatus = developerVerification.developerStatus
|
|
877
1174
|
finalVerificationStatus = developerVerification.verificationStatus
|
|
1175
|
+
if (developerStatus !== 'success') {
|
|
1176
|
+
terminalReason = developerStatus === 'blocked'
|
|
1177
|
+
? 'verification_infrastructure_failure'
|
|
1178
|
+
: 'developer_fix_incomplete'
|
|
1179
|
+
} else if (finalVerificationStatus !== 'passed' && finalVerificationStatus !== 'not_run') {
|
|
1180
|
+
terminalReason = `verification_${finalVerificationStatus}`
|
|
1181
|
+
}
|
|
878
1182
|
|
|
879
1183
|
if (developerVerification.feedbackSource && developerVerification.verificationOutput.trim() !== '') {
|
|
880
1184
|
await writeTesterFeedback(config, {
|
|
@@ -894,11 +1198,15 @@ async function runIteration({ config, state, iteration }) {
|
|
|
894
1198
|
phase,
|
|
895
1199
|
task,
|
|
896
1200
|
changedFiles: listChangedFiles(config.cwd),
|
|
897
|
-
developerNotes:
|
|
1201
|
+
developerNotes: compactNotePartsForPrompt(config, noteParts),
|
|
898
1202
|
reason: 'tester_review_after_basic_smoke_passed',
|
|
899
1203
|
})
|
|
900
1204
|
|
|
901
1205
|
testerStatus = testerInvocation.result.status
|
|
1206
|
+
testerVerdict = testerInvocation.testerVerdict
|
|
1207
|
+
commitPlanFound = testerInvocation.commitPlanFound === true
|
|
1208
|
+
terminalReason = testerInvocation.result.terminalReason || terminalReason
|
|
1209
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
902
1210
|
noteParts.push(`tester: ${testerInvocation.result.notes}`)
|
|
903
1211
|
await writeTesterFeedback(config, {
|
|
904
1212
|
iteration,
|
|
@@ -911,18 +1219,22 @@ async function runIteration({ config, state, iteration }) {
|
|
|
911
1219
|
|
|
912
1220
|
let commitPlan = testerInvocation.commitPlan
|
|
913
1221
|
|
|
914
|
-
if (testerStatus === 'success' && (commitPlan.message === '' || commitPlan.files.length === 0)) {
|
|
1222
|
+
if (testerStatus === 'success' && config.commitMode === 'plan' && (commitPlan.message === '' || commitPlan.files.length === 0)) {
|
|
915
1223
|
const testerCommitInvocation = await runTesterCommitTurn({
|
|
916
1224
|
config,
|
|
917
1225
|
iteration,
|
|
918
1226
|
phase,
|
|
919
1227
|
task,
|
|
920
1228
|
changedFiles: listChangedFiles(config.cwd),
|
|
921
|
-
developerNotes:
|
|
1229
|
+
developerNotes: compactNotePartsForPrompt(config, noteParts),
|
|
922
1230
|
reason: 'tester_passed_without_commit',
|
|
923
1231
|
})
|
|
924
1232
|
|
|
925
1233
|
testerStatus = testerCommitInvocation.result.status
|
|
1234
|
+
testerVerdict = testerCommitInvocation.testerVerdict
|
|
1235
|
+
commitPlanFound = testerCommitInvocation.commitPlanFound === true
|
|
1236
|
+
terminalReason = testerCommitInvocation.result.terminalReason || terminalReason
|
|
1237
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
926
1238
|
noteParts.push(`tester_commit: ${testerCommitInvocation.result.notes}`)
|
|
927
1239
|
await writeTesterFeedback(config, {
|
|
928
1240
|
iteration,
|
|
@@ -935,7 +1247,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
935
1247
|
commitPlan = testerCommitInvocation.commitPlan
|
|
936
1248
|
}
|
|
937
1249
|
|
|
938
|
-
if (testerStatus === 'success') {
|
|
1250
|
+
if (testerStatus === 'success' && config.commitMode === 'plan') {
|
|
939
1251
|
const gitFinalize = await runHarnessGitFinalize({
|
|
940
1252
|
config,
|
|
941
1253
|
iteration,
|
|
@@ -943,10 +1255,27 @@ async function runIteration({ config, state, iteration }) {
|
|
|
943
1255
|
commitPlan,
|
|
944
1256
|
})
|
|
945
1257
|
testerStatus = gitFinalize.status
|
|
1258
|
+
gitFinalizeStatus = gitFinalize.status
|
|
1259
|
+
terminalReason = gitFinalize.terminalReason || terminalReason
|
|
946
1260
|
noteParts.push(`git_finalize: ${gitFinalize.notes}`)
|
|
1261
|
+
} else if (testerStatus === 'success') {
|
|
1262
|
+
if (didInvocationCreateCommit(testerInvocation)) {
|
|
1263
|
+
gitFinalizeStatus = 'committed_by_agent'
|
|
1264
|
+
terminalReason = 'completed_phase_step'
|
|
1265
|
+
} else {
|
|
1266
|
+
testerStatus = 'stalled'
|
|
1267
|
+
gitFinalizeStatus = 'awaiting_agent_commit'
|
|
1268
|
+
terminalReason = testerInvocation.repoChanged
|
|
1269
|
+
? 'tester_left_uncommitted_changes'
|
|
1270
|
+
: 'awaiting_agent_commit'
|
|
1271
|
+
noteParts.push('git_finalize: committed_by_agent=false')
|
|
1272
|
+
}
|
|
947
1273
|
}
|
|
948
1274
|
} else {
|
|
949
1275
|
testerStatus = 'skipped'
|
|
1276
|
+
if (terminalReason === '') {
|
|
1277
|
+
terminalReason = 'tester_skipped_after_verification'
|
|
1278
|
+
}
|
|
950
1279
|
}
|
|
951
1280
|
|
|
952
1281
|
if (testerStatus === 'failed') {
|
|
@@ -956,12 +1285,14 @@ async function runIteration({ config, state, iteration }) {
|
|
|
956
1285
|
phase,
|
|
957
1286
|
sessionId,
|
|
958
1287
|
sessionFile,
|
|
959
|
-
testerOutput: noteParts
|
|
1288
|
+
testerOutput: compactNotePartsForPrompt(config, noteParts),
|
|
960
1289
|
})
|
|
961
1290
|
|
|
962
1291
|
sessionId = fixInvocation.result.sessionId || sessionId
|
|
963
1292
|
sessionFile = fixInvocation.result.sessionFile || sessionFile
|
|
964
1293
|
developerStatus = fixInvocation.result.status
|
|
1294
|
+
terminalReason = fixInvocation.result.terminalReason || 'developer_fix_incomplete'
|
|
1295
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
965
1296
|
noteParts.push(`developer_fix: ${fixInvocation.result.notes}`)
|
|
966
1297
|
|
|
967
1298
|
if (fixInvocation.result.status === 'success') {
|
|
@@ -976,6 +1307,10 @@ async function runIteration({ config, state, iteration }) {
|
|
|
976
1307
|
})
|
|
977
1308
|
|
|
978
1309
|
testerStatus = testerRecheck.result.status
|
|
1310
|
+
testerVerdict = testerRecheck.testerVerdict
|
|
1311
|
+
commitPlanFound = testerRecheck.commitPlanFound === true
|
|
1312
|
+
terminalReason = testerRecheck.result.terminalReason || terminalReason
|
|
1313
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
979
1314
|
noteParts.push(`tester_recheck: ${testerRecheck.result.notes}`)
|
|
980
1315
|
await writeTesterFeedback(config, {
|
|
981
1316
|
iteration,
|
|
@@ -988,18 +1323,22 @@ async function runIteration({ config, state, iteration }) {
|
|
|
988
1323
|
|
|
989
1324
|
let commitPlan = testerRecheck.commitPlan
|
|
990
1325
|
|
|
991
|
-
if (testerStatus === 'success' && (commitPlan.message === '' || commitPlan.files.length === 0)) {
|
|
1326
|
+
if (testerStatus === 'success' && config.commitMode === 'plan' && (commitPlan.message === '' || commitPlan.files.length === 0)) {
|
|
992
1327
|
const testerCommitInvocation = await runTesterCommitTurn({
|
|
993
1328
|
config,
|
|
994
1329
|
iteration,
|
|
995
1330
|
phase,
|
|
996
1331
|
task,
|
|
997
1332
|
changedFiles: listChangedFiles(config.cwd),
|
|
998
|
-
developerNotes:
|
|
1333
|
+
developerNotes: compactNotePartsForPrompt(config, noteParts),
|
|
999
1334
|
reason: 'tester_recheck_passed_without_commit',
|
|
1000
1335
|
})
|
|
1001
1336
|
|
|
1002
1337
|
testerStatus = testerCommitInvocation.result.status
|
|
1338
|
+
testerVerdict = testerCommitInvocation.testerVerdict
|
|
1339
|
+
commitPlanFound = testerCommitInvocation.commitPlanFound === true
|
|
1340
|
+
terminalReason = testerCommitInvocation.result.terminalReason || terminalReason
|
|
1341
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1003
1342
|
noteParts.push(`tester_commit: ${testerCommitInvocation.result.notes}`)
|
|
1004
1343
|
await writeTesterFeedback(config, {
|
|
1005
1344
|
iteration,
|
|
@@ -1012,7 +1351,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1012
1351
|
commitPlan = testerCommitInvocation.commitPlan
|
|
1013
1352
|
}
|
|
1014
1353
|
|
|
1015
|
-
if (testerStatus === 'success') {
|
|
1354
|
+
if (testerStatus === 'success' && config.commitMode === 'plan') {
|
|
1016
1355
|
const gitFinalize = await runHarnessGitFinalize({
|
|
1017
1356
|
config,
|
|
1018
1357
|
iteration,
|
|
@@ -1020,7 +1359,21 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1020
1359
|
commitPlan,
|
|
1021
1360
|
})
|
|
1022
1361
|
testerStatus = gitFinalize.status
|
|
1362
|
+
gitFinalizeStatus = gitFinalize.status
|
|
1363
|
+
terminalReason = gitFinalize.terminalReason || terminalReason
|
|
1023
1364
|
noteParts.push(`git_finalize: ${gitFinalize.notes}`)
|
|
1365
|
+
} else if (testerStatus === 'success') {
|
|
1366
|
+
if (didInvocationCreateCommit(testerRecheck)) {
|
|
1367
|
+
gitFinalizeStatus = 'committed_by_agent'
|
|
1368
|
+
terminalReason = 'completed_phase_step'
|
|
1369
|
+
} else {
|
|
1370
|
+
testerStatus = 'stalled'
|
|
1371
|
+
gitFinalizeStatus = 'awaiting_agent_commit'
|
|
1372
|
+
terminalReason = testerRecheck.repoChanged
|
|
1373
|
+
? 'tester_left_uncommitted_changes'
|
|
1374
|
+
: 'awaiting_agent_commit'
|
|
1375
|
+
noteParts.push('git_finalize: committed_by_agent=false')
|
|
1376
|
+
}
|
|
1024
1377
|
}
|
|
1025
1378
|
|
|
1026
1379
|
if (testerStatus === 'success') {
|
|
@@ -1032,12 +1385,18 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1032
1385
|
})
|
|
1033
1386
|
|
|
1034
1387
|
finalVerificationStatus = reverify.status
|
|
1388
|
+
if (finalVerificationStatus !== 'passed') {
|
|
1389
|
+
terminalReason = `verification_${finalVerificationStatus}`
|
|
1390
|
+
}
|
|
1035
1391
|
}
|
|
1036
1392
|
}
|
|
1037
1393
|
}
|
|
1038
1394
|
} else {
|
|
1039
1395
|
testerStatus = 'not_run'
|
|
1040
1396
|
finalVerificationStatus = 'not_run'
|
|
1397
|
+
if (terminalReason === '') {
|
|
1398
|
+
terminalReason = 'developer_turn_incomplete'
|
|
1399
|
+
}
|
|
1041
1400
|
}
|
|
1042
1401
|
|
|
1043
1402
|
const workflowStatus = deriveWorkflowStatus({
|
|
@@ -1070,6 +1429,9 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1070
1429
|
changedFiles: listChangedFiles(config.cwd),
|
|
1071
1430
|
})
|
|
1072
1431
|
visualStatus = visualReview.status
|
|
1432
|
+
terminalReason = visualReview.status === 'passed'
|
|
1433
|
+
? terminalReason
|
|
1434
|
+
: `visual_review_${visualReview.status}`
|
|
1073
1435
|
noteParts.push(`visual: ${visualReview.notes}`)
|
|
1074
1436
|
} else if (config.visualReviewEnabled) {
|
|
1075
1437
|
visualStatus = 'skipped'
|
|
@@ -1080,6 +1442,22 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1080
1442
|
visualStatus,
|
|
1081
1443
|
})
|
|
1082
1444
|
|
|
1445
|
+
if (finalStatus === 'success') {
|
|
1446
|
+
terminalReason = 'completed_phase_step'
|
|
1447
|
+
} else if (terminalReason === '') {
|
|
1448
|
+
terminalReason = testerStatus === 'failed'
|
|
1449
|
+
? 'tester_verdict_fail'
|
|
1450
|
+
: testerStatus === 'stalled'
|
|
1451
|
+
? 'iteration_stalled'
|
|
1452
|
+
: developerStatus === 'blocked'
|
|
1453
|
+
? 'developer_blocked'
|
|
1454
|
+
: developerStatus === 'failed'
|
|
1455
|
+
? 'developer_failed'
|
|
1456
|
+
: finalVerificationStatus !== 'not_run'
|
|
1457
|
+
? `verification_${finalVerificationStatus}`
|
|
1458
|
+
: 'workflow_incomplete'
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1083
1461
|
const successfulIterations = (
|
|
1084
1462
|
finalStatus === 'success'
|
|
1085
1463
|
? candidateSuccessfulIterations
|
|
@@ -1112,18 +1490,77 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1112
1490
|
|
|
1113
1491
|
await appendLog(
|
|
1114
1492
|
config.logFile,
|
|
1115
|
-
`Finished iteration ${iteration} with status=${finalStatus} verification=${finalVerificationStatus}`
|
|
1493
|
+
`Finished iteration ${iteration} with status=${finalStatus} verification=${finalVerificationStatus} tester_verdict=${testerVerdict} commit_plan_found=${commitPlanFound} terminal_reason=${terminalReason}${largeFileWarnings.length > 0 ? ` large_file_warnings=${formatLargeFileWarningsInline(largeFileWarnings)}` : ''}`
|
|
1116
1494
|
)
|
|
1117
1495
|
|
|
1496
|
+
const iterationEndSnapshot = getRepoSnapshot(config.cwd)
|
|
1497
|
+
const iterationSummary = createIterationSummary({
|
|
1498
|
+
iteration,
|
|
1499
|
+
phase,
|
|
1500
|
+
task,
|
|
1501
|
+
repoChanged: didRepoChange(iterationStartSnapshot, iterationEndSnapshot),
|
|
1502
|
+
developerStatus,
|
|
1503
|
+
testerStatus,
|
|
1504
|
+
testerVerdict,
|
|
1505
|
+
verificationStatus: finalVerificationStatus,
|
|
1506
|
+
commitPlanFound,
|
|
1507
|
+
gitFinalizeStatus,
|
|
1508
|
+
visualStatus,
|
|
1509
|
+
terminalReason,
|
|
1510
|
+
largeFileWarnings,
|
|
1511
|
+
sessionId,
|
|
1512
|
+
developerModel: developerModelName,
|
|
1513
|
+
testerModel: testerModelName,
|
|
1514
|
+
visualModel: visualModelName,
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
await recordEvent(config, {
|
|
1518
|
+
iteration,
|
|
1519
|
+
phase,
|
|
1520
|
+
kind: 'iteration_summary',
|
|
1521
|
+
status: finalStatus,
|
|
1522
|
+
transport: config.transport,
|
|
1523
|
+
sessionId,
|
|
1524
|
+
timedOut: false,
|
|
1525
|
+
exitCode: finalStatus === 'success' ? 0 : 1,
|
|
1526
|
+
durationSeconds: 0,
|
|
1527
|
+
commitBefore: iterationStartSnapshot.head,
|
|
1528
|
+
commitAfter: iterationEndSnapshot.head,
|
|
1529
|
+
repoChanged: iterationSummary.repoChanged,
|
|
1530
|
+
changedFilesCount: listChangedFiles(config.cwd).length,
|
|
1531
|
+
verificationStatus: finalVerificationStatus,
|
|
1532
|
+
retryCount: 0,
|
|
1533
|
+
role: '',
|
|
1534
|
+
model: '',
|
|
1535
|
+
toolCalls: 0,
|
|
1536
|
+
toolErrors: 0,
|
|
1537
|
+
messageUpdates: 0,
|
|
1538
|
+
stopReason: '',
|
|
1539
|
+
loopDetected: false,
|
|
1540
|
+
loopSignature: '',
|
|
1541
|
+
testerVerdict,
|
|
1542
|
+
commitPlanFound,
|
|
1543
|
+
terminalReason,
|
|
1544
|
+
riskWarnings: formatLargeFileWarningsInline(largeFileWarnings),
|
|
1545
|
+
notes: noteParts.join(' | '),
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1118
1548
|
return {
|
|
1119
1549
|
stateUpdate: nextState,
|
|
1120
1550
|
summary: {
|
|
1121
1551
|
iteration,
|
|
1122
1552
|
phase,
|
|
1553
|
+
task,
|
|
1554
|
+
repoChanged: iterationSummary.repoChanged,
|
|
1123
1555
|
developerStatus,
|
|
1124
1556
|
testerStatus,
|
|
1557
|
+
testerVerdict,
|
|
1125
1558
|
verificationStatus: finalVerificationStatus,
|
|
1559
|
+
commitPlanFound,
|
|
1560
|
+
gitFinalizeStatus,
|
|
1126
1561
|
visualStatus,
|
|
1562
|
+
terminalReason,
|
|
1563
|
+
largeFileWarnings,
|
|
1127
1564
|
notes: noteParts.join(' | '),
|
|
1128
1565
|
sessionId,
|
|
1129
1566
|
outputPath: config.lastAgentOutputFile,
|
|
@@ -1134,6 +1571,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1134
1571
|
testerModel: testerModelName,
|
|
1135
1572
|
visualModel: visualModelName,
|
|
1136
1573
|
},
|
|
1574
|
+
iterationSummary,
|
|
1137
1575
|
shouldStop: false,
|
|
1138
1576
|
}
|
|
1139
1577
|
}
|
|
@@ -1153,6 +1591,7 @@ async function main() {
|
|
|
1153
1591
|
while (!stopRequested) {
|
|
1154
1592
|
const iteration = state.iteration + 1
|
|
1155
1593
|
const result = await runIteration({ config, state, iteration })
|
|
1594
|
+
await writeIterationSummary(config, result.iterationSummary ?? result.summary)
|
|
1156
1595
|
state = result.stateUpdate
|
|
1157
1596
|
await writeState(config.stateFile, state)
|
|
1158
1597
|
printTerminalSummary(config, result.summary)
|