@sebastianandreasson/pi-autonomous-agents 0.3.0 → 0.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/README.md +9 -2
- package/SETUP.md +3 -0
- package/docs/PI_SUPERVISOR.md +4 -2
- package/package.json +1 -1
- package/src/index.mjs +1 -0
- package/src/pi-client.mjs +1 -1
- package/src/pi-config.mjs +4 -1
- package/src/pi-prompts.mjs +47 -0
- package/src/pi-repo.mjs +246 -12
- package/src/pi-report.mjs +11 -0
- package/src/pi-rpc-adapter.mjs +48 -4
- package/src/pi-supervisor.mjs +209 -24
- package/src/pi-telemetry.mjs +26 -1
- package/templates/DEVELOPER.md +3 -0
- package/templates/TESTER.md +7 -4
- package/templates/pi.config.example.json +2 -0
package/src/pi-rpc-adapter.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getHeartbeatDecision,
|
|
11
11
|
resolveHeartbeatConfig,
|
|
12
12
|
} from './pi-heartbeat.mjs'
|
|
13
|
+
import { signalProcessTree } from './pi-repo.mjs'
|
|
13
14
|
|
|
14
15
|
function createJsonlReader(stream, onLine) {
|
|
15
16
|
const rl = createInterface({ input: stream })
|
|
@@ -54,6 +55,44 @@ function extractToolTarget(toolName, args) {
|
|
|
54
55
|
return ''
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function extractShellCommand(args) {
|
|
59
|
+
if (!args || typeof args !== 'object') {
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof args.command === 'string') {
|
|
64
|
+
return args.command
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof args.cmd === 'string') {
|
|
68
|
+
return args.cmd
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isLargeShellRead(command) {
|
|
75
|
+
const text = String(command ?? '').trim()
|
|
76
|
+
if (text === '') {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (/^\s*cat\s+\S+/.test(text)) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sedMatch = text.match(/sed\s+-n\s+['"]?(\d+)\s*,\s*(\d+)p['"]?/)
|
|
85
|
+
if (sedMatch) {
|
|
86
|
+
const start = Number.parseInt(sedMatch[1], 10)
|
|
87
|
+
const end = Number.parseInt(sedMatch[2], 10)
|
|
88
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
89
|
+
return (end - start) >= 120
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
57
96
|
function extractAssistantText(message) {
|
|
58
97
|
if (!message || message.role !== 'assistant' || !Array.isArray(message.content)) {
|
|
59
98
|
return ''
|
|
@@ -113,6 +152,7 @@ async function run() {
|
|
|
113
152
|
const child = spawn(cli, args, {
|
|
114
153
|
cwd: request.cwd,
|
|
115
154
|
env: process.env,
|
|
155
|
+
detached: process.platform !== 'win32',
|
|
116
156
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
157
|
})
|
|
118
158
|
|
|
@@ -201,10 +241,10 @@ async function run() {
|
|
|
201
241
|
closeAssistantLine()
|
|
202
242
|
writeLive(`[PI guard] ${formatHeartbeatTimeoutMessage(decision)} Aborting current turn (pid=${child.pid ?? 'unknown'}).\n`)
|
|
203
243
|
void send({ type: 'abort' }).catch(() => {})
|
|
204
|
-
child.
|
|
244
|
+
signalProcessTree(child.pid, 'SIGTERM')
|
|
205
245
|
setTimeout(() => {
|
|
206
246
|
if (child.exitCode === null) {
|
|
207
|
-
child.
|
|
247
|
+
signalProcessTree(child.pid, 'SIGKILL')
|
|
208
248
|
}
|
|
209
249
|
}, 1000)
|
|
210
250
|
}
|
|
@@ -295,6 +335,7 @@ async function run() {
|
|
|
295
335
|
activeToolName = String(data.toolName ?? '')
|
|
296
336
|
activeToolStartedAt = Date.now()
|
|
297
337
|
const target = extractToolTarget(data.toolName, data.args)
|
|
338
|
+
const shellCommand = data.toolName === 'bash' ? extractShellCommand(data.args) : ''
|
|
298
339
|
if (signature === lastToolSignature) {
|
|
299
340
|
repeatedToolCount += 1
|
|
300
341
|
} else {
|
|
@@ -325,6 +366,9 @@ async function run() {
|
|
|
325
366
|
}
|
|
326
367
|
|
|
327
368
|
writeLive(`[PI tool:start] ${data.toolName}${suffix}\n`)
|
|
369
|
+
if (data.toolName === 'bash' && isLargeShellRead(shellCommand)) {
|
|
370
|
+
writeLive('[PI warning] large bash file read detected; prefer read or a smaller exact window to avoid truncated context.\n')
|
|
371
|
+
}
|
|
328
372
|
}
|
|
329
373
|
|
|
330
374
|
if (data.type === 'tool_execution_end') {
|
|
@@ -536,10 +580,10 @@ async function run() {
|
|
|
536
580
|
}
|
|
537
581
|
pending.clear()
|
|
538
582
|
|
|
539
|
-
child.
|
|
583
|
+
signalProcessTree(child.pid, 'SIGTERM')
|
|
540
584
|
await new Promise((resolve) => {
|
|
541
585
|
const timeout = setTimeout(() => {
|
|
542
|
-
child.
|
|
586
|
+
signalProcessTree(child.pid, 'SIGKILL')
|
|
543
587
|
resolve()
|
|
544
588
|
}, 1000)
|
|
545
589
|
|
package/src/pi-supervisor.mjs
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
} from './pi-prompts.mjs'
|
|
13
13
|
import { appendTelemetry, ensureTelemetryFiles } from './pi-telemetry.mjs'
|
|
14
14
|
import {
|
|
15
|
+
acquireRunLock,
|
|
15
16
|
appendLog,
|
|
17
|
+
collectLargeFileWarnings,
|
|
16
18
|
commitStagedFiles,
|
|
19
|
+
createRunId,
|
|
17
20
|
didRepoChange,
|
|
18
21
|
ensureFileExists,
|
|
19
22
|
ensureRepo,
|
|
@@ -24,10 +27,12 @@ import {
|
|
|
24
27
|
readOptionalTextFile,
|
|
25
28
|
readSessionId,
|
|
26
29
|
readState,
|
|
30
|
+
releaseRunLock,
|
|
27
31
|
runVerification,
|
|
28
32
|
runShellCommand,
|
|
29
33
|
stageFiles,
|
|
30
34
|
unstageFiles,
|
|
35
|
+
updateRunLock,
|
|
31
36
|
runVisualCapture,
|
|
32
37
|
timestamp,
|
|
33
38
|
writeChangedFiles,
|
|
@@ -65,7 +70,7 @@ function printTerminalSummary(config, summary) {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
const lines = [
|
|
68
|
-
`[PI supervisor] iteration=${summary.iteration} phase="${summary.phase}"`,
|
|
73
|
+
`[PI supervisor] run_id=${summary.runId || config.runId || ''} iteration=${summary.iteration} phase="${summary.phase}"`,
|
|
69
74
|
`[PI supervisor] task=${summary.taskFile || toDisplayPath(config, config.taskFile)} developer_instructions=${summary.developerInstructionsFile || toDisplayPath(config, config.developerInstructionsFile)} tester_instructions=${summary.testerInstructionsFile || toDisplayPath(config, config.testerInstructionsFile)}`,
|
|
70
75
|
`[PI supervisor] transport=${config.transport} developer_model=${summary.developerModel || resolveRoleModelName(config, 'developer') || '(PI default)'} tester_model=${summary.testerModel || resolveRoleModelName(config, 'tester') || '(PI default)'}`,
|
|
71
76
|
`[PI supervisor] developer=${summary.developerStatus} tester=${summary.testerStatus} verification=${summary.verificationStatus}`,
|
|
@@ -79,6 +84,10 @@ function printTerminalSummary(config, summary) {
|
|
|
79
84
|
lines.push(`[PI supervisor] notes=${summary.notes}`)
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
if (Array.isArray(summary.largeFileWarnings) && summary.largeFileWarnings.length > 0) {
|
|
88
|
+
lines.push(`[PI supervisor] large_file_warnings=${formatLargeFileWarningsInline(summary.largeFileWarnings)}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
if (summary.terminalReason) {
|
|
83
92
|
lines.push(`[PI supervisor] terminal_reason=${summary.terminalReason}`)
|
|
84
93
|
}
|
|
@@ -147,9 +156,13 @@ function formatIterationSummary(summary) {
|
|
|
147
156
|
|
|
148
157
|
async function writeIterationSummary(config, summary) {
|
|
149
158
|
await writeTextFile(config.lastIterationSummaryFile, formatIterationSummary(summary))
|
|
159
|
+
if (config.runLastIterationSummaryFile && config.runLastIterationSummaryFile !== config.lastIterationSummaryFile) {
|
|
160
|
+
await writeTextFile(config.runLastIterationSummaryFile, formatIterationSummary(summary))
|
|
161
|
+
}
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
function createIterationSummary({
|
|
165
|
+
runId,
|
|
153
166
|
iteration,
|
|
154
167
|
phase,
|
|
155
168
|
task,
|
|
@@ -162,12 +175,14 @@ function createIterationSummary({
|
|
|
162
175
|
gitFinalizeStatus,
|
|
163
176
|
visualStatus,
|
|
164
177
|
terminalReason,
|
|
178
|
+
largeFileWarnings,
|
|
165
179
|
sessionId,
|
|
166
180
|
developerModel,
|
|
167
181
|
testerModel,
|
|
168
182
|
visualModel,
|
|
169
183
|
}) {
|
|
170
184
|
return {
|
|
185
|
+
runId,
|
|
171
186
|
iteration,
|
|
172
187
|
phase,
|
|
173
188
|
task,
|
|
@@ -180,6 +195,7 @@ function createIterationSummary({
|
|
|
180
195
|
gitFinalizeStatus,
|
|
181
196
|
visualStatus,
|
|
182
197
|
terminalReason,
|
|
198
|
+
largeFileWarnings,
|
|
183
199
|
sessionId,
|
|
184
200
|
developerModel,
|
|
185
201
|
testerModel,
|
|
@@ -187,10 +203,63 @@ function createIterationSummary({
|
|
|
187
203
|
}
|
|
188
204
|
}
|
|
189
205
|
|
|
206
|
+
async function persistStateSnapshot(config, state) {
|
|
207
|
+
await writeState(config.stateFile, state)
|
|
208
|
+
if (config.runStateFile && config.runStateFile !== config.stateFile) {
|
|
209
|
+
await writeState(config.runStateFile, state)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function updateRunOwnership(config, fields = {}) {
|
|
214
|
+
if (!config.activeRunFile || !config.runId) {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await updateRunLock(config.activeRunFile, {
|
|
219
|
+
runId: config.runId,
|
|
220
|
+
pid: process.pid,
|
|
221
|
+
heartbeatAt: timestamp(),
|
|
222
|
+
...fields,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
190
226
|
function didInvocationCreateCommit(invocation) {
|
|
191
227
|
return invocation?.beforeSnapshot?.head !== invocation?.afterSnapshot?.head
|
|
192
228
|
}
|
|
193
229
|
|
|
230
|
+
function mergeLargeFileWarnings(existing, incoming) {
|
|
231
|
+
const merged = new Map()
|
|
232
|
+
for (const warning of [...(existing || []), ...(incoming || [])]) {
|
|
233
|
+
if (!warning?.file) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
const key = `${warning.kind}:${warning.file}`
|
|
237
|
+
const current = merged.get(key)
|
|
238
|
+
if (!current || Number(warning.lineCount) > Number(current.lineCount)) {
|
|
239
|
+
merged.set(key, warning)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return [...merged.values()].sort((left, right) => right.lineCount - left.lineCount)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findLargeFileWarnings(config, files) {
|
|
246
|
+
return collectLargeFileWarnings(config.cwd, files, {
|
|
247
|
+
largeFileWarningLines: config.largeFileWarningLines,
|
|
248
|
+
largeSpecWarningLines: config.largeSpecWarningLines,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatLargeFileWarningsInline(warnings) {
|
|
253
|
+
const list = Array.isArray(warnings) ? warnings : []
|
|
254
|
+
if (list.length === 0) {
|
|
255
|
+
return ''
|
|
256
|
+
}
|
|
257
|
+
return list
|
|
258
|
+
.slice(0, 3)
|
|
259
|
+
.map((warning) => `${warning.file}(${warning.lineCount}${warning.kind === 'large_spec' ? ',spec' : ''})`)
|
|
260
|
+
.join(', ')
|
|
261
|
+
}
|
|
262
|
+
|
|
194
263
|
function clampPromptLines(text, maxLines) {
|
|
195
264
|
const normalized = String(text ?? '').trim()
|
|
196
265
|
if (normalized === '') {
|
|
@@ -232,6 +301,7 @@ function isInfrastructureVerificationFailure(output) {
|
|
|
232
301
|
async function recordEvent(config, event) {
|
|
233
302
|
await appendTelemetry(config, {
|
|
234
303
|
timestamp: timestamp(),
|
|
304
|
+
runId: config.runId || '',
|
|
235
305
|
...event,
|
|
236
306
|
})
|
|
237
307
|
}
|
|
@@ -644,6 +714,7 @@ async function runMainTurnWithRetries({ config, iteration, phase, sessionId, ses
|
|
|
644
714
|
prompt = buildSteeringPrompt(config, reason, {
|
|
645
715
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
646
716
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
717
|
+
largeFileWarnings: findLargeFileWarnings(config, listChangedFiles(config.cwd)),
|
|
647
718
|
})
|
|
648
719
|
|
|
649
720
|
if (shouldRetryForTimeout || shouldRetryForNoChange) {
|
|
@@ -656,12 +727,14 @@ async function runMainTurnWithRetries({ config, iteration, phase, sessionId, ses
|
|
|
656
727
|
}
|
|
657
728
|
|
|
658
729
|
async function runFixTurn({ config, iteration, phase, sessionId, sessionFile, testerOutput }) {
|
|
730
|
+
const largeFileWarnings = findLargeFileWarnings(config, listChangedFiles(config.cwd))
|
|
659
731
|
const fixPrompt = buildFixPrompt(
|
|
660
732
|
config,
|
|
661
733
|
clampPromptLines(testerOutput, Number(config.maxVerificationExcerptLines) || 40),
|
|
662
734
|
{
|
|
663
735
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
664
736
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
737
|
+
largeFileWarnings,
|
|
665
738
|
}
|
|
666
739
|
)
|
|
667
740
|
return await runAgentInvocation({
|
|
@@ -762,6 +835,7 @@ async function runTesterTurn({
|
|
|
762
835
|
developerNotes,
|
|
763
836
|
reason,
|
|
764
837
|
}) {
|
|
838
|
+
const largeFileWarnings = findLargeFileWarnings(config, changedFiles)
|
|
765
839
|
const prompt = buildTesterPrompt(config, {
|
|
766
840
|
phase,
|
|
767
841
|
task,
|
|
@@ -770,6 +844,7 @@ async function runTesterTurn({
|
|
|
770
844
|
reason,
|
|
771
845
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
772
846
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
847
|
+
largeFileWarnings,
|
|
773
848
|
})
|
|
774
849
|
|
|
775
850
|
const invocation = await runAgentInvocation({
|
|
@@ -835,6 +910,7 @@ async function runTesterCommitTurn({
|
|
|
835
910
|
developerNotes,
|
|
836
911
|
reason,
|
|
837
912
|
}) {
|
|
913
|
+
const largeFileWarnings = findLargeFileWarnings(config, changedFiles)
|
|
838
914
|
const prompt = buildCommitPrompt(config, {
|
|
839
915
|
phase,
|
|
840
916
|
task,
|
|
@@ -843,6 +919,7 @@ async function runTesterCommitTurn({
|
|
|
843
919
|
reason,
|
|
844
920
|
visualFeedback: await readLatestVisualFeedback(config),
|
|
845
921
|
testerFeedback: await readLatestTesterFeedback(config),
|
|
922
|
+
largeFileWarnings,
|
|
846
923
|
})
|
|
847
924
|
|
|
848
925
|
const invocation = await runAgentInvocation({
|
|
@@ -1029,6 +1106,13 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1029
1106
|
const iterationStartSnapshot = getRepoSnapshot(config.cwd)
|
|
1030
1107
|
const taskInfo = findFirstUncheckedTaskInfo(config.taskFile)
|
|
1031
1108
|
if (!taskInfo.hasUncheckedTasks) {
|
|
1109
|
+
await updateRunOwnership(config, {
|
|
1110
|
+
status: 'idle',
|
|
1111
|
+
iteration,
|
|
1112
|
+
phase: taskInfo.phase || 'complete',
|
|
1113
|
+
task: '',
|
|
1114
|
+
lastCompletedIteration: iteration,
|
|
1115
|
+
})
|
|
1032
1116
|
await appendLog(config.logFile, 'No unchecked tasks remain in TODOS.md')
|
|
1033
1117
|
return {
|
|
1034
1118
|
stateUpdate: {
|
|
@@ -1039,9 +1123,12 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1039
1123
|
lastPhase: taskInfo.phase,
|
|
1040
1124
|
lastStatus: 'complete',
|
|
1041
1125
|
lastVerificationStatus: 'not_needed',
|
|
1126
|
+
runId: config.runId || '',
|
|
1127
|
+
inProgress: null,
|
|
1042
1128
|
lastRunAt: timestamp(),
|
|
1043
1129
|
},
|
|
1044
1130
|
summary: {
|
|
1131
|
+
runId: config.runId || '',
|
|
1045
1132
|
iteration,
|
|
1046
1133
|
phase: taskInfo.phase || 'complete',
|
|
1047
1134
|
task: '',
|
|
@@ -1054,6 +1141,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1054
1141
|
gitFinalizeStatus: 'not_run',
|
|
1055
1142
|
visualStatus: 'not_run',
|
|
1056
1143
|
terminalReason: 'all_tasks_complete',
|
|
1144
|
+
largeFileWarnings: [],
|
|
1057
1145
|
notes: 'No unchecked tasks remain in TODOS.md.',
|
|
1058
1146
|
sessionId: state.sessionId || '',
|
|
1059
1147
|
outputPath: config.lastAgentOutputFile,
|
|
@@ -1070,6 +1158,26 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1070
1158
|
|
|
1071
1159
|
const phase = taskInfo.phase || 'unknown'
|
|
1072
1160
|
const task = taskInfo.task || 'unknown'
|
|
1161
|
+
const inProgressState = {
|
|
1162
|
+
...state,
|
|
1163
|
+
runId: config.runId || '',
|
|
1164
|
+
inProgress: {
|
|
1165
|
+
runId: config.runId || '',
|
|
1166
|
+
status: 'in_progress',
|
|
1167
|
+
iteration,
|
|
1168
|
+
phase,
|
|
1169
|
+
task,
|
|
1170
|
+
startedAt: timestamp(),
|
|
1171
|
+
transport: config.transport,
|
|
1172
|
+
},
|
|
1173
|
+
}
|
|
1174
|
+
await persistStateSnapshot(config, inProgressState)
|
|
1175
|
+
await updateRunOwnership(config, {
|
|
1176
|
+
status: 'iteration_in_progress',
|
|
1177
|
+
iteration,
|
|
1178
|
+
phase,
|
|
1179
|
+
task,
|
|
1180
|
+
})
|
|
1073
1181
|
const canResumePriorSession = (
|
|
1074
1182
|
state.lastTransport === config.transport
|
|
1075
1183
|
&& state.lastPiModel === developerModelName
|
|
@@ -1103,6 +1211,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1103
1211
|
let commitPlanFound = false
|
|
1104
1212
|
let gitFinalizeStatus = 'not_run'
|
|
1105
1213
|
let terminalReason = mainInvocation.result.terminalReason || ''
|
|
1214
|
+
let largeFileWarnings = findLargeFileWarnings(config, mainInvocation.changedFiles)
|
|
1106
1215
|
const noteParts = [`developer: ${mainInvocation.result.notes}`]
|
|
1107
1216
|
|
|
1108
1217
|
if (mainInvocation.result.status === 'success' && config.transport === 'mock') {
|
|
@@ -1157,6 +1266,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1157
1266
|
testerVerdict = testerInvocation.testerVerdict
|
|
1158
1267
|
commitPlanFound = testerInvocation.commitPlanFound === true
|
|
1159
1268
|
terminalReason = testerInvocation.result.terminalReason || terminalReason
|
|
1269
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1160
1270
|
noteParts.push(`tester: ${testerInvocation.result.notes}`)
|
|
1161
1271
|
await writeTesterFeedback(config, {
|
|
1162
1272
|
iteration,
|
|
@@ -1184,6 +1294,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1184
1294
|
testerVerdict = testerCommitInvocation.testerVerdict
|
|
1185
1295
|
commitPlanFound = testerCommitInvocation.commitPlanFound === true
|
|
1186
1296
|
terminalReason = testerCommitInvocation.result.terminalReason || terminalReason
|
|
1297
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1187
1298
|
noteParts.push(`tester_commit: ${testerCommitInvocation.result.notes}`)
|
|
1188
1299
|
await writeTesterFeedback(config, {
|
|
1189
1300
|
iteration,
|
|
@@ -1241,6 +1352,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1241
1352
|
sessionFile = fixInvocation.result.sessionFile || sessionFile
|
|
1242
1353
|
developerStatus = fixInvocation.result.status
|
|
1243
1354
|
terminalReason = fixInvocation.result.terminalReason || 'developer_fix_incomplete'
|
|
1355
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1244
1356
|
noteParts.push(`developer_fix: ${fixInvocation.result.notes}`)
|
|
1245
1357
|
|
|
1246
1358
|
if (fixInvocation.result.status === 'success') {
|
|
@@ -1258,6 +1370,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1258
1370
|
testerVerdict = testerRecheck.testerVerdict
|
|
1259
1371
|
commitPlanFound = testerRecheck.commitPlanFound === true
|
|
1260
1372
|
terminalReason = testerRecheck.result.terminalReason || terminalReason
|
|
1373
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1261
1374
|
noteParts.push(`tester_recheck: ${testerRecheck.result.notes}`)
|
|
1262
1375
|
await writeTesterFeedback(config, {
|
|
1263
1376
|
iteration,
|
|
@@ -1285,6 +1398,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1285
1398
|
testerVerdict = testerCommitInvocation.testerVerdict
|
|
1286
1399
|
commitPlanFound = testerCommitInvocation.commitPlanFound === true
|
|
1287
1400
|
terminalReason = testerCommitInvocation.result.terminalReason || terminalReason
|
|
1401
|
+
largeFileWarnings = mergeLargeFileWarnings(largeFileWarnings, findLargeFileWarnings(config, listChangedFiles(config.cwd)))
|
|
1288
1402
|
noteParts.push(`tester_commit: ${testerCommitInvocation.result.notes}`)
|
|
1289
1403
|
await writeTesterFeedback(config, {
|
|
1290
1404
|
iteration,
|
|
@@ -1432,15 +1546,27 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1432
1546
|
lastRunAt: timestamp(),
|
|
1433
1547
|
successfulIterations,
|
|
1434
1548
|
lastVisualStatus: visualStatus,
|
|
1549
|
+
runId: config.runId || '',
|
|
1550
|
+
inProgress: null,
|
|
1435
1551
|
}
|
|
1436
1552
|
|
|
1553
|
+
await updateRunOwnership(config, {
|
|
1554
|
+
status: 'idle',
|
|
1555
|
+
iteration,
|
|
1556
|
+
phase,
|
|
1557
|
+
task,
|
|
1558
|
+
lastCompletedIteration: iteration,
|
|
1559
|
+
lastStatus: finalStatus,
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1437
1562
|
await appendLog(
|
|
1438
1563
|
config.logFile,
|
|
1439
|
-
`Finished iteration ${iteration} with status=${finalStatus} verification=${finalVerificationStatus} tester_verdict=${testerVerdict} commit_plan_found=${commitPlanFound} terminal_reason=${terminalReason}`
|
|
1564
|
+
`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)}` : ''}`
|
|
1440
1565
|
)
|
|
1441
1566
|
|
|
1442
1567
|
const iterationEndSnapshot = getRepoSnapshot(config.cwd)
|
|
1443
1568
|
const iterationSummary = createIterationSummary({
|
|
1569
|
+
runId: config.runId || '',
|
|
1444
1570
|
iteration,
|
|
1445
1571
|
phase,
|
|
1446
1572
|
task,
|
|
@@ -1453,6 +1579,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1453
1579
|
gitFinalizeStatus,
|
|
1454
1580
|
visualStatus,
|
|
1455
1581
|
terminalReason,
|
|
1582
|
+
largeFileWarnings,
|
|
1456
1583
|
sessionId,
|
|
1457
1584
|
developerModel: developerModelName,
|
|
1458
1585
|
testerModel: testerModelName,
|
|
@@ -1486,12 +1613,14 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1486
1613
|
testerVerdict,
|
|
1487
1614
|
commitPlanFound,
|
|
1488
1615
|
terminalReason,
|
|
1616
|
+
riskWarnings: formatLargeFileWarningsInline(largeFileWarnings),
|
|
1489
1617
|
notes: noteParts.join(' | '),
|
|
1490
1618
|
})
|
|
1491
1619
|
|
|
1492
1620
|
return {
|
|
1493
1621
|
stateUpdate: nextState,
|
|
1494
1622
|
summary: {
|
|
1623
|
+
runId: config.runId || '',
|
|
1495
1624
|
iteration,
|
|
1496
1625
|
phase,
|
|
1497
1626
|
task,
|
|
@@ -1504,6 +1633,7 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1504
1633
|
gitFinalizeStatus,
|
|
1505
1634
|
visualStatus,
|
|
1506
1635
|
terminalReason,
|
|
1636
|
+
largeFileWarnings,
|
|
1507
1637
|
notes: noteParts.join(' | '),
|
|
1508
1638
|
sessionId,
|
|
1509
1639
|
outputPath: config.lastAgentOutputFile,
|
|
@@ -1521,40 +1651,95 @@ async function runIteration({ config, state, iteration }) {
|
|
|
1521
1651
|
|
|
1522
1652
|
async function main() {
|
|
1523
1653
|
const config = loadConfig(process.argv[2] ?? 'once')
|
|
1654
|
+
const runId = createRunId()
|
|
1655
|
+
const runStartedAt = timestamp()
|
|
1656
|
+
const runDir = path.join(config.piRuntimeDir, 'runs', runId)
|
|
1657
|
+
config.runId = runId
|
|
1658
|
+
config.runStartedAt = runStartedAt
|
|
1659
|
+
config.runRuntimeDir = runDir
|
|
1660
|
+
config.runLogFile = path.join(runDir, 'pi.log')
|
|
1661
|
+
config.runTelemetryJsonl = path.join(runDir, 'pi_telemetry.jsonl')
|
|
1662
|
+
config.runTelemetryCsv = path.join(runDir, 'pi_telemetry.csv')
|
|
1663
|
+
config.runStateFile = path.join(runDir, 'state.json')
|
|
1664
|
+
config.runLastIterationSummaryFile = path.join(runDir, 'last-iteration.json')
|
|
1665
|
+
|
|
1524
1666
|
ensureRepo(config.cwd)
|
|
1525
1667
|
await ensureFileExists(config.taskFile, 'task file')
|
|
1526
1668
|
await ensureFileExists(config.developerInstructionsFile, 'developer instructions file')
|
|
1527
1669
|
await ensureFileExists(config.testerInstructionsFile, 'tester instructions file')
|
|
1528
|
-
await
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1670
|
+
const lockResult = await acquireRunLock(config.activeRunFile, {
|
|
1671
|
+
runId,
|
|
1672
|
+
pid: process.pid,
|
|
1673
|
+
startedAt: runStartedAt,
|
|
1674
|
+
heartbeatAt: runStartedAt,
|
|
1675
|
+
status: 'starting',
|
|
1676
|
+
iteration: 0,
|
|
1677
|
+
phase: '',
|
|
1678
|
+
task: '',
|
|
1679
|
+
mode: config.mode,
|
|
1680
|
+
configFile: config.configFile,
|
|
1681
|
+
cwd: config.cwd,
|
|
1682
|
+
})
|
|
1683
|
+
try {
|
|
1684
|
+
process.env.PI_RUN_ID = runId
|
|
1685
|
+
process.env.PI_RUN_LOG_FILE = config.runLogFile
|
|
1686
|
+
await ensureTelemetryFiles(config)
|
|
1687
|
+
await appendLog(config.logFile, `Run started pid=${process.pid} mode=${config.mode}`)
|
|
1688
|
+
if (lockResult.staleLock) {
|
|
1689
|
+
await appendLog(
|
|
1690
|
+
config.logFile,
|
|
1691
|
+
`Recovered stale run lock from runId=${String(lockResult.staleLock.runId ?? '')} pid=${String(lockResult.staleLock.pid ?? '')} startedAt=${String(lockResult.staleLock.startedAt ?? '')}`
|
|
1692
|
+
)
|
|
1545
1693
|
}
|
|
1694
|
+
await runStartupPreflight(config)
|
|
1546
1695
|
|
|
1547
|
-
await
|
|
1548
|
-
|
|
1696
|
+
let state = await readState(config.stateFile)
|
|
1697
|
+
if (state?.inProgress?.status === 'in_progress') {
|
|
1698
|
+
await appendLog(
|
|
1699
|
+
config.logFile,
|
|
1700
|
+
`Recovering unfinished iteration=${state.inProgress.iteration} phase="${state.inProgress.phase || ''}" task="${state.inProgress.task || ''}" from runId=${String(state.inProgress.runId || state.runId || '')}`
|
|
1701
|
+
)
|
|
1702
|
+
}
|
|
1703
|
+
let completedIterations = 0
|
|
1704
|
+
|
|
1705
|
+
while (!stopRequested) {
|
|
1706
|
+
const iteration = state?.inProgress?.status === 'in_progress'
|
|
1707
|
+
? Number(state.inProgress.iteration) || (state.iteration + 1)
|
|
1708
|
+
: state.iteration + 1
|
|
1709
|
+
await updateRunOwnership(config, {
|
|
1710
|
+
status: 'starting_iteration',
|
|
1711
|
+
iteration,
|
|
1712
|
+
})
|
|
1713
|
+
const result = await runIteration({ config, state, iteration })
|
|
1714
|
+
await writeIterationSummary(config, result.iterationSummary ?? result.summary)
|
|
1715
|
+
state = result.stateUpdate
|
|
1716
|
+
await persistStateSnapshot(config, state)
|
|
1717
|
+
printTerminalSummary(config, result.summary)
|
|
1718
|
+
completedIterations += 1
|
|
1719
|
+
|
|
1720
|
+
if (result.shouldStop || config.mode !== 'run' || completedIterations >= config.maxIterations) {
|
|
1721
|
+
break
|
|
1722
|
+
}
|
|
1549
1723
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1724
|
+
await sleep(config.sleepBetweenSeconds)
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
if (stopRequested) {
|
|
1728
|
+
await appendLog(config.logFile, 'Stop requested by signal')
|
|
1729
|
+
}
|
|
1730
|
+
} finally {
|
|
1731
|
+
await updateRunOwnership(config, {
|
|
1732
|
+
status: stopRequested ? 'stopped' : 'finished',
|
|
1733
|
+
heartbeatAt: timestamp(),
|
|
1734
|
+
})
|
|
1735
|
+
await releaseRunLock(config.activeRunFile, runId)
|
|
1736
|
+
delete process.env.PI_RUN_ID
|
|
1737
|
+
delete process.env.PI_RUN_LOG_FILE
|
|
1552
1738
|
}
|
|
1553
1739
|
}
|
|
1554
1740
|
|
|
1555
1741
|
main().catch(async (error) => {
|
|
1556
1742
|
const config = loadConfig(process.argv[2] ?? 'once')
|
|
1557
|
-
await ensureTelemetryFiles(config)
|
|
1558
1743
|
await appendLog(config.logFile, `Supervisor error: ${error instanceof Error ? error.stack ?? error.message : String(error)}`)
|
|
1559
1744
|
console.error(error instanceof Error ? error.message : String(error))
|
|
1560
1745
|
process.exitCode = 1
|
package/src/pi-telemetry.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
|
|
3
|
-
const CSV_HEADER = 'timestamp,iteration,phase,kind,status,transport,session_id,timed_out,exit_code,duration_seconds,commit_before,commit_after,repo_changed,changed_files_count,verification_status,retry_count,role,model,tool_calls,tool_errors,message_updates,stop_reason,loop_detected,loop_signature,tester_verdict,commit_plan_found,terminal_reason,notes\n'
|
|
4
|
+
const CSV_HEADER = 'timestamp,run_id,iteration,phase,kind,status,transport,session_id,timed_out,exit_code,duration_seconds,commit_before,commit_after,repo_changed,changed_files_count,verification_status,retry_count,role,model,tool_calls,tool_errors,message_updates,stop_reason,loop_detected,loop_signature,tester_verdict,commit_plan_found,terminal_reason,risk_warnings,notes\n'
|
|
4
5
|
|
|
5
6
|
function csvEscape(value) {
|
|
6
7
|
const text = String(value ?? '')
|
|
@@ -14,22 +15,42 @@ export async function ensureTelemetryFiles(config) {
|
|
|
14
15
|
await fs.writeFile(config.lastPromptFile, '', 'utf8')
|
|
15
16
|
await fs.writeFile(config.lastIterationSummaryFile, '', 'utf8')
|
|
16
17
|
|
|
18
|
+
await fs.mkdir(path.dirname(config.logFile), { recursive: true })
|
|
19
|
+
await fs.mkdir(path.dirname(config.telemetryJsonl), { recursive: true })
|
|
20
|
+
await fs.mkdir(path.dirname(config.telemetryCsv), { recursive: true })
|
|
17
21
|
await fs.appendFile(config.logFile, '', 'utf8')
|
|
18
22
|
await fs.appendFile(config.telemetryJsonl, '', 'utf8')
|
|
23
|
+
if (config.runTelemetryJsonl && config.runTelemetryJsonl !== config.telemetryJsonl) {
|
|
24
|
+
await fs.mkdir(path.dirname(config.runTelemetryJsonl), { recursive: true })
|
|
25
|
+
await fs.appendFile(config.runTelemetryJsonl, '', 'utf8')
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
try {
|
|
21
29
|
await fs.access(config.telemetryCsv)
|
|
22
30
|
} catch {
|
|
23
31
|
await fs.writeFile(config.telemetryCsv, CSV_HEADER, 'utf8')
|
|
24
32
|
}
|
|
33
|
+
|
|
34
|
+
if (config.runTelemetryCsv && config.runTelemetryCsv !== config.telemetryCsv) {
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(config.runTelemetryCsv)
|
|
37
|
+
} catch {
|
|
38
|
+
await fs.mkdir(path.dirname(config.runTelemetryCsv), { recursive: true })
|
|
39
|
+
await fs.writeFile(config.runTelemetryCsv, CSV_HEADER, 'utf8')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
export async function appendTelemetry(config, event) {
|
|
28
45
|
const jsonLine = `${JSON.stringify(event)}\n`
|
|
29
46
|
await fs.appendFile(config.telemetryJsonl, jsonLine, 'utf8')
|
|
47
|
+
if (config.runTelemetryJsonl && config.runTelemetryJsonl !== config.telemetryJsonl) {
|
|
48
|
+
await fs.appendFile(config.runTelemetryJsonl, jsonLine, 'utf8')
|
|
49
|
+
}
|
|
30
50
|
|
|
31
51
|
const csvRow = [
|
|
32
52
|
event.timestamp,
|
|
53
|
+
event.runId,
|
|
33
54
|
event.iteration,
|
|
34
55
|
event.phase,
|
|
35
56
|
event.kind,
|
|
@@ -56,10 +77,14 @@ export async function appendTelemetry(config, event) {
|
|
|
56
77
|
event.testerVerdict,
|
|
57
78
|
event.commitPlanFound,
|
|
58
79
|
event.terminalReason,
|
|
80
|
+
event.riskWarnings,
|
|
59
81
|
event.notes,
|
|
60
82
|
].map(csvEscape).join(',')
|
|
61
83
|
|
|
62
84
|
await fs.appendFile(config.telemetryCsv, `${csvRow}\n`, 'utf8')
|
|
85
|
+
if (config.runTelemetryCsv && config.runTelemetryCsv !== config.telemetryCsv) {
|
|
86
|
+
await fs.appendFile(config.runTelemetryCsv, `${csvRow}\n`, 'utf8')
|
|
87
|
+
}
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
export async function readTelemetry(config) {
|
package/templates/DEVELOPER.md
CHANGED
|
@@ -20,6 +20,9 @@ Rules:
|
|
|
20
20
|
- Use the configured smoke verification path as the fast inner-loop gate. Do not replace it with a long full-flow Playwright spec unless the task explicitly requires it.
|
|
21
21
|
- If a long Playwright happy-path spec changes, validate with smoke plus one narrow targeted spec or deterministic state hook, not the entire full-flow run.
|
|
22
22
|
- Reserve long full-flow Playwright specs for an explicit nightly or post-run lane, not the developer turn.
|
|
23
|
+
- Use `read` for source inspection. Use shell only for `git`, tests, and narrow diagnostics.
|
|
24
|
+
- If a snippet seems incomplete, reread a smaller exact window instead of another huge overlapping shell range.
|
|
25
|
+
- Do not build edits from large `sed`/`grep` output or from memory after partial shell reads.
|
|
23
26
|
- Trust tool output over your own guesses.
|
|
24
27
|
- Do not repeatedly reread or rewrite the same file when one focused fix will do.
|
|
25
28
|
- After one failed edit attempt, reread the file before retrying.
|