@sebastianandreasson/pi-autonomous-agents 0.2.0 → 0.3.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.
@@ -1,5 +1,45 @@
1
1
  import path from 'node:path'
2
2
 
3
+ function clampLines(text, maxLines) {
4
+ const normalized = String(text ?? '').trim()
5
+ if (normalized === '') {
6
+ return ''
7
+ }
8
+
9
+ const lines = normalized.split('\n')
10
+ if (!Number.isFinite(maxLines) || maxLines <= 0 || lines.length <= maxLines) {
11
+ return normalized
12
+ }
13
+
14
+ const remaining = lines.length - maxLines
15
+ return `${lines.slice(0, maxLines).join('\n')}\n... (${remaining} more lines omitted)`
16
+ }
17
+
18
+ function formatFeedbackSection(label, text, maxLines) {
19
+ const excerpt = clampLines(text, maxLines)
20
+ if (excerpt === '') {
21
+ return ''
22
+ }
23
+
24
+ return `\n${label}:\n${excerpt}\n`
25
+ }
26
+
27
+ function formatChangedFilesSection(files, maxFiles) {
28
+ const list = Array.isArray(files) ? files.filter(Boolean) : []
29
+ if (list.length === 0) {
30
+ return '- No file changes were detected from the prior turn.'
31
+ }
32
+
33
+ const limit = Number.isFinite(maxFiles) && maxFiles > 0 ? maxFiles : list.length
34
+ const visible = list.slice(0, limit)
35
+ const remaining = list.length - visible.length
36
+ const lines = visible.map((file) => `- ${file}`)
37
+ if (remaining > 0) {
38
+ lines.push(`- ... and ${remaining} more files`)
39
+ }
40
+ return lines.join('\n')
41
+ }
42
+
3
43
  function displayPath(config, filePath) {
4
44
  const relativePath = path.relative(config.cwd, filePath)
5
45
  if (
@@ -13,22 +53,25 @@ function displayPath(config, filePath) {
13
53
  return path.basename(filePath)
14
54
  }
15
55
 
16
- function formatVisualFeedback(visualFeedback) {
17
- const text = String(visualFeedback ?? '').trim()
18
- if (text === '') {
19
- return ''
20
- }
21
-
22
- return `\nLatest visual feedback from prior runs:\n${text}\n`
56
+ function formatVisualFeedback(config, visualFeedback) {
57
+ return formatFeedbackSection(
58
+ 'Latest visual feedback from prior runs',
59
+ visualFeedback,
60
+ configMaxLines(config, 'maxVisualFeedbackLines', 20),
61
+ )
23
62
  }
24
63
 
25
- function formatTesterFeedback(testerFeedback) {
26
- const text = String(testerFeedback ?? '').trim()
27
- if (text === '') {
28
- return ''
29
- }
64
+ function formatTesterFeedback(config, testerFeedback) {
65
+ return formatFeedbackSection(
66
+ 'Latest tester feedback from prior runs',
67
+ testerFeedback,
68
+ configMaxLines(config, 'maxTesterFeedbackLines', 32),
69
+ )
70
+ }
30
71
 
31
- return `\nLatest tester feedback from prior runs:\n${text}\n`
72
+ function configMaxLines(config, key, fallback) {
73
+ const value = Number(config?.[key])
74
+ return Number.isFinite(value) && value > 0 ? value : fallback
32
75
  }
33
76
 
34
77
  function indentBlock(text, prefix = '') {
@@ -54,92 +97,195 @@ function staleEditRecoveryRules() {
54
97
  ].join('\n')
55
98
  }
56
99
 
100
+ function repoInstructionsAuthorityLine(config, instructionsFile, usesBundledInstructions) {
101
+ if (usesBundledInstructions) {
102
+ return ''
103
+ }
104
+
105
+ return `Repo-local instructions in ${displayPath(config, instructionsFile)} are the primary role contract. Follow them over package defaults when they differ.\n`
106
+ }
107
+
108
+ function testerPassOwnershipRules(config) {
109
+ if (config.commitMode === 'plan') {
110
+ return {
111
+ successRule: '- If your verdict is PASS, do not run git add or git commit yourself. Provide a commit plan for the harness to execute.',
112
+ isolationRule: '- The commit plan must include only the files related to this task. If the working tree is too messy to isolate safely, use VERDICT: BLOCKED instead of guessing.',
113
+ extraRule: '- If you can produce a PASS, include the commit plan in the same response. Avoid making the harness ask for a second commit-only pass.',
114
+ successFormat: [
115
+ 'If and only if your verdict is PASS, also include exactly this commit plan block before the verdict line:',
116
+ '- COMMIT_MESSAGE: <one-line commit message>',
117
+ '- COMMIT_FILES:',
118
+ '- path/to/file-one',
119
+ '- path/to/file-two',
120
+ '',
121
+ 'Do not add commentary on the same lines as COMMIT_MESSAGE or COMMIT_FILES. Put only the message value after COMMIT_MESSAGE:, then one file path per line under COMMIT_FILES:.',
122
+ ].join('\n'),
123
+ }
124
+ }
125
+
126
+ return {
127
+ successRule: '- If your verdict is PASS, stage only the files related to this task and create the git commit yourself before the verdict line.',
128
+ isolationRule: '- If the working tree is too messy to isolate safely, use VERDICT: BLOCKED instead of guessing.',
129
+ extraRule: '- Use git status before committing, stage only the related files, and create one concise commit message in the format <type>(<scope>): <summary> when possible.',
130
+ successFormat: [
131
+ 'If and only if your verdict is PASS, include exactly this block before the verdict line after creating the commit:',
132
+ '- COMMIT_CREATED: true',
133
+ '- COMMIT_MESSAGE: <one-line commit message>',
134
+ '- COMMIT_SHA: <short-or-full-sha>',
135
+ ].join('\n'),
136
+ }
137
+ }
138
+
57
139
  export function buildMainPrompt(config, options = {}) {
58
140
  const taskFile = displayPath(config, config.taskFile)
59
141
  const instructionsFile = displayPath(config, config.developerInstructionsFile)
60
- const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
61
- const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
62
-
63
- return `Read ${taskFile} and ${instructionsFile}.
64
- ${visualFeedbackSection}
142
+ const visualFeedbackSection = formatVisualFeedback(config, options.visualFeedback)
143
+ const testerFeedbackSection = formatTesterFeedback(config, options.testerFeedback)
144
+ const authorityLine = repoInstructionsAuthorityLine(
145
+ config,
146
+ config.developerInstructionsFile,
147
+ config.usingBundledDeveloperInstructions,
148
+ )
149
+
150
+ if (!config.usingBundledDeveloperInstructions) {
151
+ return `Read ${taskFile} and ${instructionsFile}.
152
+ ${authorityLine}${visualFeedbackSection}
65
153
  ${testerFeedbackSection}
66
154
 
67
155
  Work only on the current phase.
68
156
  Select the first unchecked actionable checkbox in phase order.
69
- Complete that task, or at most 2 tightly related unchecked tasks if they are naturally done together.
157
+ Complete one coherent task, or at most 2 tightly related unchecked tasks if they are naturally done together.
70
158
 
71
- Rules:
159
+ Harness rules:
72
160
  - Start by checking git status so you know whether unrelated changes already exist.
73
161
  - Update code, config, and docs only as needed for the selected task.
74
162
  - Tick only the checkbox items that are actually completed.
75
- - Do not select "Done when" checkboxes as the active task unless the implementation items in that section are already satisfied.
76
- - If you discover missing prerequisite work, add a new unchecked checkbox under the same phase, then complete only what is necessary.
77
- - Do not skip to a later phase unless the current task is blocked.
78
163
  - If blocked, add a brief note directly under the relevant task in ${taskFile} explaining the blocker, then stop.
79
- - Do not create GitHub issue templates, project-management files, or unrelated scaffolding.
80
- - Do not edit lockfiles, generated files, or unrelated assets.
81
- - If dependencies must change, edit package.json only, then stop.
82
- - Prefer the smallest viable implementation that fully satisfies the selected checkbox.
83
- - Avoid broad refactors unless the selected task explicitly requires them.
84
- ${indentBlock(innerLoopValidationRules(config.testCommand), '\t')}
85
- - Trust tool results over your own guesses. If a read tool shows file contents, use that exact output instead of arguing with it.
86
- - Do not repeatedly rewrite the same file because you suspect a formatting issue. Read once, identify the exact mismatch, then make one focused fix.
87
- ${indentBlock(staleEditRecoveryRules(), '\t')}
88
- - Do not create the final commit during the developer pass. Leave a clean diff for the tester to validate and commit if it passes.
164
+ - Do not create the final commit during the developer pass.
165
+ ${staleEditRecoveryRules()}
89
166
 
90
167
  Before stopping:
91
168
  - Tick completed checkbox items in ${taskFile}.
92
- - Keep changes scoped to one coherent step.
93
- - Stop after finishing that step.`
169
+ - Keep changes scoped to one coherent step.
170
+ - Stop after finishing that step.`
171
+ }
172
+
173
+ return `Read ${taskFile} and ${instructionsFile}.
174
+ ${authorityLine}${visualFeedbackSection}
175
+ ${testerFeedbackSection}
176
+
177
+ Do one current-phase unchecked task.
178
+
179
+ Rules:
180
+ - Start with git status.
181
+ - Select the first unchecked actionable checkbox in phase order.
182
+ - Keep changes minimal and scoped.
183
+ - Tick only completed items.
184
+ - If blocked, note it under the task in ${taskFile} and stop.
185
+ - Do not touch lockfiles, generated files, or unrelated assets.
186
+ - Do not commit in the developer pass.
187
+ ${innerLoopValidationRules(config.testCommand)}
188
+ ${staleEditRecoveryRules()}
189
+
190
+ Before stopping:
191
+ - Tick completed checkbox items in ${taskFile}.
192
+ - Stop after one coherent step.`
94
193
  }
95
194
 
96
195
  export function buildFixPrompt(config, recentVerificationOutput, options = {}) {
97
196
  const taskFile = displayPath(config, config.taskFile)
98
197
  const instructionsFile = displayPath(config, config.developerInstructionsFile)
99
- const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
100
- const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
198
+ const visualFeedbackSection = formatVisualFeedback(config, options.visualFeedback)
199
+ const testerFeedbackSection = formatTesterFeedback(config, options.testerFeedback)
200
+ const authorityLine = repoInstructionsAuthorityLine(
201
+ config,
202
+ config.developerInstructionsFile,
203
+ config.usingBundledDeveloperInstructions,
204
+ )
205
+ const findings = clampLines(recentVerificationOutput, configMaxLines(config, 'maxVerificationExcerptLines', 40))
206
+
207
+ if (!config.usingBundledDeveloperInstructions) {
208
+ return `Read ${taskFile} and ${instructionsFile}.
209
+ ${authorityLine}${visualFeedbackSection}
210
+ ${testerFeedbackSection}
211
+
212
+ The tester step found a real problem in the current implementation. Fix only the product behavior related to the current phase and current task.
213
+
214
+ Recent tester findings:
215
+ ${findings}
216
+
217
+ Harness rules:
218
+ - Start by checking git status so you know which files are already dirty.
219
+ - Do not paper over product bugs by weakening tests.
220
+ - Keep changes minimal and focused on the failing behavior.
221
+ - Do not perform speculative cleanup or unrelated refactors in this pass.
222
+ - Do not create the final commit during the developer fix pass.
223
+ ${staleEditRecoveryRules()}
224
+
225
+ Before stopping:
226
+ - Tick any checkbox in ${taskFile} only if it is now actually complete.
227
+ - Stop after one coherent fix.`
228
+ }
101
229
 
102
230
  return `Read ${taskFile} and ${instructionsFile}.
103
- ${visualFeedbackSection}
231
+ ${authorityLine}${visualFeedbackSection}
104
232
  ${testerFeedbackSection}
105
233
 
106
234
  The tester step found a real problem in the current implementation. Fix only the product behavior related to the current phase and current task.
107
235
 
108
236
  Recent tester findings:
109
- ${recentVerificationOutput}
237
+ ${findings}
110
238
 
111
239
  Rules:
112
- - Start by checking git status so you know which files are already dirty.
113
- - Do not paper over product bugs by weakening tests.
114
- - Prefer fixing product code over rewriting tests.
115
- - Update tests only when the tester exposed a real gap in coverage or testability.
116
- - Do not create docs, issue templates, or unrelated scaffolding.
117
- - Do not edit lockfiles or other generated files.
118
- - If dependencies must change, edit package.json only, then stop.
119
- - Keep changes minimal and focused on the failing behavior.
120
- ${indentBlock(innerLoopValidationRules(config.testCommand), '\t')}
121
- - Trust tool results over your own guesses. If a read tool shows file contents, use that exact output instead of arguing with it.
122
- - Do not repeatedly rewrite the same file because you suspect a formatting issue. Read once, identify the exact mismatch, then make one focused fix.
123
- ${indentBlock(staleEditRecoveryRules(), '\t')}
124
- - Do not create the final commit during the developer fix pass. Leave the repaired diff for the tester to re-check and commit if it passes.
240
+ - Start with git status.
241
+ - Keep the fix narrow.
242
+ - Do not weaken tests to hide product bugs.
243
+ - Do not perform speculative cleanup or unrelated refactors.
244
+ - Do not create the final commit.
245
+ ${staleEditRecoveryRules()}
125
246
 
126
247
  Before stopping:
127
- - Tick any checkbox in ${taskFile} only if it is now actually complete.
128
- - Stop after one coherent fix.`
248
+ - Tick any checkbox in ${taskFile} only if it is now actually complete.
249
+ - Stop after one coherent fix.`
129
250
  }
130
251
 
131
252
  export function buildSteeringPrompt(config, reason, options = {}) {
132
253
  const taskFile = displayPath(config, config.taskFile)
133
- const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
134
- const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
254
+ const instructionsFile = displayPath(config, config.developerInstructionsFile)
255
+ const visualFeedbackSection = formatVisualFeedback(config, options.visualFeedback)
256
+ const testerFeedbackSection = formatTesterFeedback(config, options.testerFeedback)
257
+ const authorityLine = repoInstructionsAuthorityLine(
258
+ config,
259
+ config.developerInstructionsFile,
260
+ config.usingBundledDeveloperInstructions,
261
+ )
262
+
263
+ if (!config.usingBundledDeveloperInstructions) {
264
+ return `Continue from the current repo state.
265
+ Read ${taskFile} and ${instructionsFile}.
266
+ ${authorityLine}${visualFeedbackSection}
267
+ ${testerFeedbackSection}
268
+
269
+ Reason for this follow-up: ${reason}
270
+
271
+ Select the first unchecked actionable checkbox in the current phase, complete one coherent task, tick completed items, run any repo-local verification required by your role instructions, and stop.
272
+
273
+ Additional harness guardrails:
274
+ - Start by checking git status.
275
+ - Do not repeat the same tool call over and over.
276
+ - If you already read a file, use that context instead of rereading it unless something changed.
277
+ - If an edit fails once, reread the file before retrying. Do not repeat the same exact edit attempt.
278
+ - If you are stuck, make the smallest decisive next action or stop and state the blocker.`
279
+ }
135
280
 
136
281
  return `Continue from the current repo state.
137
- ${visualFeedbackSection}
282
+ Read ${taskFile} and ${instructionsFile}.
283
+ ${authorityLine}${visualFeedbackSection}
138
284
  ${testerFeedbackSection}
139
285
 
140
286
  Reason for this follow-up: ${reason}
141
287
 
142
- Read ${taskFile}, select the first unchecked actionable checkbox in the current phase, complete one coherent task, tick completed items, run verification, and stop.
288
+ Select the first unchecked actionable checkbox in the current phase, complete one coherent task, tick completed items, run verification, and stop.
143
289
 
144
290
  Additional guardrails:
145
291
  - Do not repeat the same tool call over and over.
@@ -160,18 +306,69 @@ export function buildTesterPrompt(config, {
160
306
  }) {
161
307
  const taskFile = displayPath(config, config.taskFile)
162
308
  const instructionsFile = displayPath(config, config.testerInstructionsFile)
163
- const visualFeedbackSection = formatVisualFeedback(visualFeedback)
164
- const testerFeedbackSection = formatTesterFeedback(testerFeedback)
165
- const changedFilesSection = changedFiles.length > 0
166
- ? changedFiles.map((file) => `- ${file}`).join('\n')
167
- : '- No file changes were detected from the developer turn.'
309
+ const visualFeedbackSection = formatVisualFeedback(config, visualFeedback)
310
+ const testerFeedbackSection = formatTesterFeedback(config, testerFeedback)
311
+ const changedFilesSection = formatChangedFilesSection(
312
+ changedFiles,
313
+ configMaxLines(config, 'maxPromptChangedFiles', 10),
314
+ )
315
+ const compactDeveloperNotes = clampLines(
316
+ developerNotes || '(none provided)',
317
+ configMaxLines(config, 'maxPromptNotesLines', 16),
318
+ )
168
319
  const verificationCommand = config.testCommand.trim() === '' ? '(not configured)' : config.testCommand
169
320
  const visualCaptureNote = config.visualReviewEnabled
170
- ? `\n- Maintain the screenshot capture flow used by the harness (${config.visualCaptureCommand || 'PI_VISUAL_CAPTURE_CMD'}) so current visual artifacts and manifest are produced for visual review.`
321
+ ? `\n- Keep the screenshot capture flow working so the harness still produces current visual artifacts for review.`
171
322
  : ''
323
+ const authorityLine = repoInstructionsAuthorityLine(
324
+ config,
325
+ config.testerInstructionsFile,
326
+ config.usingBundledTesterInstructions,
327
+ )
328
+ const passOwnership = testerPassOwnershipRules(config)
329
+
330
+ if (!config.usingBundledTesterInstructions) {
331
+ return `Read ${taskFile} and ${instructionsFile}.
332
+ ${authorityLine}${visualFeedbackSection}
333
+ ${testerFeedbackSection}
334
+
335
+ You are the TESTER role. You are reviewing the most recent developer work from an independent quality and functionality perspective.
336
+
337
+ Current phase: ${phase}
338
+ Current task: ${task}
339
+ Reason for this tester pass: ${reason}
340
+
341
+ Developer notes:
342
+ ${compactDeveloperNotes}
343
+
344
+ Files changed by the developer:
345
+ ${changedFilesSection}
346
+
347
+ Rules:
348
+ - Start with git status.
349
+ - Follow repo-local tester instructions for what to verify and which commands to run.
350
+ - Prefer one focused review pass.
351
+ - If blocked or inconclusive, return VERDICT: BLOCKED.
352
+ - Do not hide real bugs with brittle tests.
353
+ - ${passOwnership.successRule.slice(2)}
354
+ - ${passOwnership.isolationRule.slice(2)}
355
+ - ${passOwnership.extraRule.slice(2)}${visualCaptureNote}
356
+
357
+ Before the verdict line, include a short section in plain text with:
358
+ - Observed flow:
359
+ - Player-facing result:
360
+ - Regression check:
361
+
362
+ ${passOwnership.successFormat}
363
+
364
+ Before stopping, end your final response with exactly one verdict line:
365
+ - VERDICT: PASS
366
+ - VERDICT: FAIL
367
+ - VERDICT: BLOCKED`
368
+ }
172
369
 
173
370
  return `Read ${taskFile} and ${instructionsFile}.
174
- ${visualFeedbackSection}
371
+ ${authorityLine}${visualFeedbackSection}
175
372
  ${testerFeedbackSection}
176
373
 
177
374
  You are the TESTER role. You are reviewing the most recent developer work from an independent quality and functionality perspective.
@@ -181,47 +378,28 @@ Current task: ${task}
181
378
  Reason for this tester pass: ${reason}
182
379
 
183
380
  Developer notes:
184
- ${developerNotes || '(none provided)'}
381
+ ${compactDeveloperNotes}
185
382
 
186
383
  Files changed by the developer:
187
384
  ${changedFilesSection}
188
385
 
189
- Your responsibilities:
190
- - Inspect the implementation from a skeptical user/tester viewpoint.
191
- - Add or update verification focused on the changed behavior.
192
- - Prefer browser-driven checks and targeted tests over broad rewrites.
386
+ Rules:
387
+ - Start with git status.
193
388
  - Run the repo verification command yourself: ${verificationCommand}
194
389
  ${indentBlock(innerLoopValidationRules(verificationCommand), '\t')}
195
- - Decide whether the feature is actually functionally correct for the intended task, not just whether the code looks plausible.
196
- - For any user-facing flow, validate the actual playable path in the running app, not just the source code.
197
- - If the task touches menus, unlocks, progression, classes, routes, shops, onboarding, or gating, verify a fresh-save path so a brand-new player can still start and use the feature.
198
- ${visualCaptureNote}
199
-
200
- Rules:
201
- - Start by checking git status so you can separate this task from unrelated dirty files.
202
- - Prefer editing tests, fixtures, and minimal observability hooks.
203
- - Avoid editing product code unless a tiny testability hook is essential and does not change user-facing behavior.
204
- - If you find a real product bug or incomplete functionality, do not hide it with brittle tests.
205
- - If blocked by tooling or environment, state the blocker clearly.
206
- - Trust tool results over your own guesses. If a read tool shows file contents, use that exact output instead of arguing with it.
207
- ${indentBlock(staleEditRecoveryRules(), '\t')}
208
- - Treat "the player cannot start, continue, select, buy, unlock, or exit correctly" as a FAIL even if the code compiles.
209
- - Before PASS, identify at least one concrete player-visible success path you exercised and one thing you checked for regressions.
210
- - If your verdict is PASS and the verification command succeeded, do not run git add or git commit yourself. Instead, provide a commit plan for the harness to execute.
211
- - The commit plan must include only the files related to this task. If the working tree is too messy to isolate safely, use VERDICT: BLOCKED instead of guessing.
212
- - Use a concise commit message in the format <type>(<scope>): <summary> when possible.
213
- - Stop after one coherent tester pass.
390
+ - Prefer one focused browser-driven review pass.
391
+ - Do not hide real bugs with brittle tests.
392
+ - If blocked or inconclusive, return VERDICT: BLOCKED.
393
+ ${indentBlock(passOwnership.successRule, '\t')}
394
+ ${indentBlock(passOwnership.isolationRule, '\t')}
395
+ ${indentBlock(passOwnership.extraRule, '\t')}${visualCaptureNote}
214
396
 
215
397
  Before the verdict line, include a short section in plain text with:
216
398
  - Observed flow:
217
399
  - Player-facing result:
218
400
  - Regression check:
219
401
 
220
- If and only if your verdict is PASS, also include exactly this commit plan block before the verdict line:
221
- - COMMIT_MESSAGE: <one-line commit message>
222
- - COMMIT_FILES:
223
- - path/to/file-one
224
- - path/to/file-two
402
+ ${indentBlock(passOwnership.successFormat, '\t')}
225
403
 
226
404
  Before stopping, end your final response with exactly one verdict line:
227
405
  - VERDICT: PASS
@@ -240,14 +418,24 @@ export function buildCommitPrompt(config, {
240
418
  }) {
241
419
  const taskFile = displayPath(config, config.taskFile)
242
420
  const instructionsFile = displayPath(config, config.testerInstructionsFile)
243
- const visualFeedbackSection = formatVisualFeedback(visualFeedback)
244
- const testerFeedbackSection = formatTesterFeedback(testerFeedback)
245
- const changedFilesSection = changedFiles.length > 0
246
- ? changedFiles.map((file) => `- ${file}`).join('\n')
247
- : '- No changed files were detected. Inspect git status before deciding whether a commit is possible.'
421
+ const visualFeedbackSection = formatVisualFeedback(config, visualFeedback)
422
+ const testerFeedbackSection = formatTesterFeedback(config, testerFeedback)
423
+ const authorityLine = repoInstructionsAuthorityLine(
424
+ config,
425
+ config.testerInstructionsFile,
426
+ config.usingBundledTesterInstructions,
427
+ )
428
+ const changedFilesSection = formatChangedFilesSection(
429
+ changedFiles,
430
+ configMaxLines(config, 'maxPromptChangedFiles', 10),
431
+ )
432
+ const compactDeveloperNotes = clampLines(
433
+ developerNotes || '(none provided)',
434
+ configMaxLines(config, 'maxPromptNotesLines', 16),
435
+ )
248
436
 
249
437
  return `Read ${taskFile} and ${instructionsFile}.
250
- ${visualFeedbackSection}
438
+ ${authorityLine}${visualFeedbackSection}
251
439
  ${testerFeedbackSection}
252
440
 
253
441
  You are the TESTER role. The implementation already passed functional review, but the final commit was not created.
@@ -257,7 +445,7 @@ Current task: ${task}
257
445
  Reason for this follow-up: ${reason}
258
446
 
259
447
  Developer/tester notes:
260
- ${developerNotes || '(none provided)'}
448
+ ${compactDeveloperNotes}
261
449
 
262
450
  Files currently dirty:
263
451
  ${changedFilesSection}
@@ -265,10 +453,9 @@ ${changedFilesSection}
265
453
  Your job now is commit-plan finalization only. Do not run git commands yourself.
266
454
 
267
455
  Rules:
268
- - Start by checking git status so you can see exactly which files are dirty.
456
+ - Start with git status.
269
457
  - Do not change product code, tests, docs, or TODO items in this pass.
270
458
  - Select only the files related to this task.
271
- - Use a concise commit message in the format <type>(<scope>): <summary> when possible.
272
459
  - If the working tree is too messy to isolate safely, do not guess. End with VERDICT: BLOCKED.
273
460
 
274
461
  If you can isolate the correct commit, include exactly this block before the verdict line:
@@ -277,6 +464,8 @@ If you can isolate the correct commit, include exactly this block before the ver
277
464
  - path/to/file-one
278
465
  - path/to/file-two
279
466
 
467
+ Do not add commentary on the same lines as COMMIT_MESSAGE or COMMIT_FILES. Put only the message value after COMMIT_MESSAGE:, then one file path per line under COMMIT_FILES:.
468
+
280
469
  Before stopping, end your final response with exactly one verdict line:
281
470
  - VERDICT: PASS
282
471
  - VERDICT: BLOCKED`
package/src/pi-repo.mjs CHANGED
@@ -74,13 +74,16 @@ function normalizeStatusPath(statusPath) {
74
74
  }
75
75
 
76
76
  function parseStatusLine(line) {
77
- const trimmed = line.trim()
78
- if (trimmed === '') {
77
+ if (line.trim() === '') {
78
+ return null
79
+ }
80
+
81
+ if (line.length < 4 || line[2] !== ' ') {
79
82
  return null
80
83
  }
81
84
 
82
85
  const renamedMarker = ' -> '
83
- const pathText = trimmed.slice(3)
86
+ const pathText = line.slice(3)
84
87
  if (pathText.includes(renamedMarker)) {
85
88
  const [, nextPath] = pathText.split(renamedMarker)
86
89
  return normalizeStatusPath(nextPath)
@@ -121,6 +121,7 @@ async function run() {
121
121
  const pending = new Map()
122
122
  let requestCounter = 0
123
123
  const streamTerminal = request.streamTerminal === true
124
+ const requestedModel = typeof request.model === 'string' ? request.model : ''
124
125
  const loopRepeatThreshold = Number.isFinite(Number(request.loopRepeatThreshold))
125
126
  ? Number(request.loopRepeatThreshold)
126
127
  : 12
@@ -416,6 +417,9 @@ async function run() {
416
417
  await waitForAgentEnd()
417
418
 
418
419
  if (heartbeatTimedOut) {
420
+ const toolCalls = events.filter((event) => event.type === 'tool_execution_start').length
421
+ const toolErrors = events.filter((event) => event.type === 'tool_execution_end' && event.isError).length
422
+ const messageUpdates = events.filter((event) => event.type === 'message_update').length
419
423
  console.log(JSON.stringify({
420
424
  sessionId: request.sessionId ?? '',
421
425
  sessionFile: request.sessionFile ?? '',
@@ -438,6 +442,15 @@ async function run() {
438
442
  continueAccepted ? 'continue_accepted=true' : '',
439
443
  continueRejected ? 'continue_rejected=true' : '',
440
444
  ].join(' '),
445
+ role: '',
446
+ model: requestedModel,
447
+ toolCalls,
448
+ toolErrors,
449
+ messageUpdates,
450
+ stopReason: '',
451
+ loopDetected: false,
452
+ loopSignature: '',
453
+ terminalReason: 'heartbeat_timeout',
441
454
  }))
442
455
  return
443
456
  }
@@ -464,6 +477,15 @@ async function run() {
464
477
  : assistantError !== '' || (assistantText === '' && toolCalls === 0 && messageUpdates === 0)
465
478
  ? 'failed'
466
479
  : 'success'
480
+ const terminalReason = loopDetected
481
+ ? 'loop_detected'
482
+ : assistantError !== ''
483
+ ? 'assistant_error'
484
+ : assistantStopReason === 'length'
485
+ ? 'assistant_stop_length'
486
+ : status === 'failed'
487
+ ? 'empty_agent_turn'
488
+ : 'agent_completed'
467
489
  const notes = [
468
490
  `PI session ${state.data.sessionId}`,
469
491
  `pi_pid=${child.pid ?? 'unknown'}`,
@@ -494,6 +516,15 @@ async function run() {
494
516
  status,
495
517
  output,
496
518
  notes,
519
+ role: '',
520
+ model: requestedModel,
521
+ toolCalls,
522
+ toolErrors,
523
+ messageUpdates,
524
+ stopReason: assistantStopReason,
525
+ loopDetected,
526
+ loopSignature,
527
+ terminalReason,
497
528
  }))
498
529
  } finally {
499
530
  if (heartbeatInterval) {