@sebastianandreasson/pi-autonomous-agents 0.1.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.
package/README.md CHANGED
@@ -59,6 +59,7 @@ packages/pi-harness/
59
59
  pi-harness once
60
60
  pi-harness run
61
61
  pi-harness report
62
+ pi-harness clear-history
62
63
  pi-harness visual-once
63
64
  pi-harness adapter
64
65
  pi-harness visual-review-worker
@@ -82,12 +83,25 @@ Find SETUP.md in @sebastianandreasson/pi-autonomous-agents and set everything up
82
83
 
83
84
  The package ships a top-level [SETUP.md](./SETUP.md) specifically for that workflow.
84
85
 
86
+ If you want to wipe all harness-generated state and start over cleanly in a repo, run:
87
+
88
+ ```bash
89
+ PI_CONFIG_FILE=pi.config.json pi-harness clear-history
90
+ ```
91
+
92
+ The command removes configured harness history/runtime files and verifies that no configured history paths remain afterward.
93
+
94
+ For prompt debugging, the harness also writes the exact assembled prompt for the current role to `.pi-last-prompt.txt` by default.
95
+ For flow debugging, it also writes a machine-readable `.pi-last-iteration.json` summary with the selected task, tester verdict, commit-plan state, and terminal reason.
96
+
85
97
  ## Generic Contracts
86
98
 
87
99
  - `taskFile`: usually `TODOS.md`
88
100
  - `developerInstructionsFile`: per-project developer instructions
89
101
  - `testerInstructionsFile`: per-project tester instructions
90
102
  - `roleModels`: optional per-role model overrides
103
+ - `commitMode`: `agent` by default, `plan` only for legacy harness-managed commit parsing
104
+ - `promptMode`: `compact` by default
91
105
  - `testCommand`: fast verification command
92
106
  - `visualCaptureCommand`: project-defined screenshot capture command
93
107
  - `visualFeedbackFile`: latest visual-review handoff
@@ -95,8 +109,14 @@ The package ships a top-level [SETUP.md](./SETUP.md) specifically for that workf
95
109
 
96
110
  For unattended loops, keep `testCommand` fast and bounded, such as a smoke suite. Long real-time Playwright happy-path specs belong in an explicit nightly or post-run lane, not the default developer/tester inner loop.
97
111
 
112
+ Keep TODO items extremely small and implementation-shaped when using weaker local models. Broad tasks tend to produce much longer turns, more retries, and more tester drift than narrow one-step tasks.
113
+
98
114
  The adapter heartbeat is PI-RPC-event based. Streaming shell output does not count as progress on its own, so long-running tools should rely on the tool-aware watchdog thresholds rather than terminal streaming.
99
115
 
100
- `piModel` remains the default text model, but you can override specific roles with `roleModels` such as `developer`, `developerRetry`, `developerFix`, `tester`, `testerCommit`, and `visualReview`.
116
+ `piModel` remains the default text model, but you can override specific roles with `roleModels` such as `developer`, `developerRetry`, `developerFix`, `tester`, and `visualReview`. `testerCommit` is only relevant if you opt back into `commitMode: "plan"`.
117
+
118
+ By default, successful tester passes should stage and create the commit directly in the same PI turn. The old commit-plan parsing flow is still available as `commitMode: "plan"`, but it is now a compatibility mode rather than the default.
119
+
120
+ Prompt/context handoff is compact by default. The harness now caps prior feedback excerpts, changed-file lists, verification excerpts, and prompt note handoff. If needed, tune `maxPromptChangedFiles`, `maxVisualFeedbackLines`, `maxTesterFeedbackLines`, `maxPromptNotesLines`, and `maxVerificationExcerptLines`.
101
121
 
102
122
  The harness expects screenshot capture to produce a `manifest.json` plus image files under the configured visual capture directory.
package/SETUP.md CHANGED
@@ -46,10 +46,16 @@ If the repo uses another package manager already, use the repo-native equivalent
46
46
  - `taskFile`: usually `TODOS.md`
47
47
  - `developerInstructionsFile`: `pi/DEVELOPER.md`
48
48
  - `testerInstructionsFile`: `pi/TESTER.md`
49
+ - `commitMode`: normally `agent`
49
50
  - `testCommand`: a fast bounded verification command for this repo
50
51
  - `visualCaptureCommand`: only if this repo has a real screenshot capture flow
51
52
  - `models` / `piModel` / `visualReviewModel` / `roleModels`: configure the models actually available in this environment
52
53
 
54
+ Important:
55
+
56
+ - Do not assume a local provider’s served model id matches a GGUF filename or a guessed name.
57
+ - If the repo uses custom OpenAI-compatible providers, verify the exact served ids from each provider’s `/v1/models` response before finalizing `piModel`, `visualReviewModel`, or `roleModels`.
58
+
53
59
  3. Create role instruction files.
54
60
 
55
61
  - Copy `node_modules/@sebastianandreasson/pi-autonomous-agents/templates/DEVELOPER.md` to `pi/DEVELOPER.md`.
@@ -118,6 +124,7 @@ Recommended pattern:
118
124
  - local model for `developerFix`
119
125
  - local or slightly stronger model for `tester`
120
126
  - stronger frontier model for `visualReview` only if available
127
+ - keep `commitMode` as `agent` unless the repo explicitly needs legacy harness-managed commit-plan parsing
121
128
 
122
129
  Example shape:
123
130
 
@@ -136,6 +143,21 @@ Example shape:
136
143
  }
137
144
  ```
138
145
 
146
+ If the repo uses a custom OpenAI-compatible local provider, validate it directly:
147
+
148
+ 1. Verify the endpoint is reachable.
149
+ 2. Query `<baseUrl>/models`.
150
+ 3. Use the exact returned model id.
151
+ 4. Do not assume the served id equals a GGUF filename on disk.
152
+
153
+ If the repo overrides `PI_CODING_AGENT_DIR`:
154
+
155
+ - do not point it at an empty directory
156
+ - ensure that PI home is already bootstrapped
157
+ - ensure `models.json` exists there before running the harness
158
+
159
+ If `PI_CODING_AGENT_DIR` is set to a repo-local PI home and `models.json` is missing, setup is incomplete.
160
+
139
161
  9. Validate the setup.
140
162
 
141
163
  Run at least:
@@ -152,6 +174,16 @@ PI_CONFIG_FILE=pi.config.json PI_TRANSPORT=mock PI_TEST_CMD= pi-harness once
152
174
 
153
175
  If setup validation fails, fix the config rather than leaving a half-configured repo.
154
176
 
177
+ The harness should fail fast if:
178
+
179
+ - PI cannot list models
180
+ - a configured PI role model does not exist
181
+ - a configured provider endpoint is unreachable
182
+ - a configured provider does not serve the configured model id
183
+
184
+ For prompt debugging, inspect `.pi-last-prompt.txt` after a run. It contains the exact assembled prompt that was sent for the active role.
185
+ For flow debugging, inspect `.pi-last-iteration.json` after a run. It summarizes the selected task, repo-change outcome, tester verdict, commit-plan state, and terminal reason.
186
+
155
187
  ## Agent Rules
156
188
 
157
189
  - Reuse existing repo conventions where possible.
@@ -159,6 +191,7 @@ If setup validation fails, fix the config rather than leaving a half-configured
159
191
  - Do not invent fake test commands or model endpoints.
160
192
  - Do not enable visual review unless the repo actually has a usable capture command and model config.
161
193
  - Keep changes minimal and local to harness setup.
194
+ - Prefer very small, implementation-shaped TODO items for local models. Broad tasks tend to create long turns, retries, and weak tester behavior.
162
195
 
163
196
  ## What To Report Back
164
197
 
@@ -169,3 +202,13 @@ When setup is complete, report:
169
202
  - whether visual review was enabled
170
203
  - which roles were mapped to which models
171
204
  - whether validation was run successfully
205
+
206
+ ## Resetting Harness State
207
+
208
+ If the user wants to start over from a clean slate later, use:
209
+
210
+ ```bash
211
+ PI_CONFIG_FILE=pi.config.json pi-harness clear-history
212
+ ```
213
+
214
+ This should remove harness-generated runtime/history state only, not project source files.
@@ -18,7 +18,7 @@ Each real iteration follows this sequence:
18
18
  2. A fast local verification command runs immediately after the developer round.
19
19
  3. If verification passes, `tester` reviews the change independently from a skeptical user-facing perspective.
20
20
  4. If tester or verification finds a real issue, the supervisor gives the findings back to `developer` for one focused repair pass.
21
- 5. If tester reaches `PASS`, tester provides a commit plan and the harness performs the actual git finalization.
21
+ 5. If tester reaches `PASS`, tester creates the commit directly in the same turn by default.
22
22
  6. Optionally, every `N` successful iterations, the harness runs a read-only visual review over screenshots and persists the feedback for later runs.
23
23
  7. If that visual review returns `FAIL`, `BLOCKED`, or times out, the iteration is not counted as a success and the feedback is carried into later prompts.
24
24
 
@@ -69,6 +69,7 @@ Projects typically provide their own `pi.config.json` with fields such as:
69
69
  - `models`
70
70
  - `piModel`
71
71
  - `visualReviewModel`
72
+ - `commitMode`
72
73
 
73
74
  Model entries may carry their own OpenAI-compatible endpoint settings, so the PI text loop and the multimodal visual reviewer can point at different backends without changing code.
74
75
 
@@ -83,7 +84,6 @@ Model entries may carry their own OpenAI-compatible endpoint settings, so the PI
83
84
  "developerRetry": "local/dev-model",
84
85
  "developerFix": "local/dev-model",
85
86
  "tester": "local/tester-model",
86
- "testerCommit": "local/tester-model",
87
87
  "visualReview": "cloud/vision-model"
88
88
  }
89
89
  }
@@ -162,15 +162,13 @@ Allowed response `status` values:
162
162
 
163
163
  ## Git Finalization
164
164
 
165
- The harness is designed to keep commit history structured:
165
+ The default flow keeps commit ownership with the active agent:
166
166
 
167
167
  1. `developer` should leave a clean, reviewable diff and should not commit.
168
- 2. `tester` should review functionality and, on `PASS`, provide a commit plan:
169
- - `COMMIT_MESSAGE: ...`
170
- - `COMMIT_FILES:`
171
- - `- path/to/file`
172
- 3. The harness stages only those requested files and performs the commit itself.
173
- 4. If the requested plan cannot be isolated safely, the iteration is blocked or failed instead of committing unrelated work.
168
+ 2. `tester` should review functionality and, on `PASS`, stage only the task-related files and create the commit directly.
169
+ 3. If the working tree is too messy to isolate safely, tester should return `VERDICT: BLOCKED` instead of guessing.
170
+
171
+ If a repo explicitly needs the older harness-managed commit-plan flow, set `commitMode` to `plan`. In that mode, `testerCommit` and parsed commit plans are used as a compatibility path rather than the default.
174
172
 
175
173
  ## Persistent Handoffs
176
174
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sebastianandreasson/pi-autonomous-agents",
3
3
  "private": false,
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "description": "Portable unattended PI harness for developer/tester/visual-review loops.",
7
7
  "license": "MIT",
@@ -16,8 +16,8 @@
16
16
  "pi-harness": "./src/cli.mjs"
17
17
  },
18
18
  "scripts": {
19
- "check": "node --check src/cli.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-rpc-adapter.mjs && node --check src/pi-supervisor.mjs && node --check src/pi-telemetry.mjs && node --check src/pi-visual-once.mjs && node --check src/pi-visual-review.mjs && node --check src/index.mjs && node --check test/pi-heartbeat.test.mjs && node --check test/pi-role-models.test.mjs && node --check test/pi-flow.test.mjs",
20
- "test": "node --test test/pi-heartbeat.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs"
19
+ "check": "node --check src/cli.mjs && node --check src/pi-clear-history.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-history.mjs && node --check src/pi-preflight.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-rpc-adapter.mjs && node --check src/pi-supervisor.mjs && node --check src/pi-telemetry.mjs && node --check src/pi-visual-once.mjs && node --check src/pi-visual-review.mjs && node --check src/index.mjs && node --check test/pi-heartbeat.test.mjs && node --check test/pi-role-models.test.mjs && node --check test/pi-flow.test.mjs && node --check test/pi-history.test.mjs && node --check test/pi-prompts.test.mjs && node --check test/pi-preflight.test.mjs && node --check test/pi-repo.test.mjs && node --check test/pi-telemetry.test.mjs",
20
+ "test": "node --test test/pi-heartbeat.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs test/pi-history.test.mjs test/pi-prompts.test.mjs test/pi-preflight.test.mjs test/pi-repo.test.mjs test/pi-telemetry.test.mjs"
21
21
  },
22
22
  "files": [
23
23
  "src",
package/pi.config.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "adapterCommand": "pi-harness adapter",
4
4
  "instructionsFile": "",
5
5
  "taskFile": "TODOS.md",
6
+ "commitMode": "agent",
6
7
  "streamTerminal": false,
7
8
  "loopRepeatThreshold": 12,
8
9
  "samePathRepeatThreshold": 8,
package/src/cli.mjs CHANGED
@@ -11,6 +11,7 @@ const COMMANDS = new Map([
11
11
  ['once', 'pi-supervisor.mjs'],
12
12
  ['run', 'pi-supervisor.mjs'],
13
13
  ['report', 'pi-report.mjs'],
14
+ ['clear-history', 'pi-clear-history.mjs'],
14
15
  ['visual-once', 'pi-visual-once.mjs'],
15
16
  ['adapter', 'pi-rpc-adapter.mjs'],
16
17
  ['visual-review-worker', 'pi-visual-review.mjs'],
package/src/index.mjs CHANGED
@@ -4,4 +4,10 @@ export {
4
4
  deriveWorkflowStatus,
5
5
  shouldPersistLatestTesterFeedback,
6
6
  } from './pi-flow.mjs'
7
+ export {
8
+ extractModelIdsFromProviderResponse,
9
+ parsePiListModelsOutput,
10
+ runStartupPreflight,
11
+ } from './pi-preflight.mjs'
12
+ export { clearHarnessHistory, collectHistoryTargets } from './pi-history.mjs'
7
13
  export { runAgentTurn } from './pi-client.mjs'
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { loadConfig } from './pi-config.mjs'
4
+ import { ensureRepo } from './pi-repo.mjs'
5
+ import { clearHarnessHistory } from './pi-history.mjs'
6
+
7
+ async function main() {
8
+ const config = loadConfig('once')
9
+ ensureRepo(config.cwd)
10
+
11
+ const result = await clearHarnessHistory(config)
12
+
13
+ console.log(`Cleared harness history for ${result.clearedTargets.length} existing paths.`)
14
+ if (result.clearedTargets.length > 0) {
15
+ console.log('Cleared:')
16
+ for (const targetPath of result.clearedTargets) {
17
+ console.log(`- ${targetPath}`)
18
+ }
19
+ }
20
+ console.log('Verification passed: no configured harness history paths remain.')
21
+ }
22
+
23
+ main().catch((error) => {
24
+ console.error(error instanceof Error ? error.message : String(error))
25
+ process.exitCode = 1
26
+ })
package/src/pi-client.mjs CHANGED
@@ -18,6 +18,7 @@ function formatLastAgentOutput(response) {
18
18
  `status: ${String(response.status ?? '')}`,
19
19
  `sessionId: ${String(response.sessionId ?? '')}`,
20
20
  `sessionFile: ${String(response.sessionFile ?? '')}`,
21
+ `terminalReason: ${String(response.terminalReason ?? '')}`,
21
22
  `notes: ${String(response.notes ?? '').trim()}`,
22
23
  ]
23
24
 
@@ -58,6 +59,15 @@ async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
58
59
  durationSeconds: 0,
59
60
  output,
60
61
  notes: 'Mock transport completed without repo edits.',
62
+ role: '',
63
+ model: '',
64
+ toolCalls: 0,
65
+ toolErrors: 0,
66
+ messageUpdates: 0,
67
+ stopReason: '',
68
+ loopDetected: false,
69
+ loopSignature: '',
70
+ terminalReason: 'mock_completed',
61
71
  }
62
72
  }
63
73
 
@@ -142,6 +152,15 @@ async function runAdapterTurn({ config, model, sessionId, sessionFile, prompt, i
142
152
  durationSeconds: result.durationSeconds,
143
153
  output: result.combinedOutput,
144
154
  notes: 'Adapter process exceeded the configured timeout.',
155
+ role: '',
156
+ model: model ?? config.piModel,
157
+ toolCalls: 0,
158
+ toolErrors: 0,
159
+ messageUpdates: 0,
160
+ stopReason: '',
161
+ loopDetected: false,
162
+ loopSignature: '',
163
+ terminalReason: 'agent_timeout',
145
164
  }
146
165
  }
147
166
 
@@ -157,6 +176,15 @@ async function runAdapterTurn({ config, model, sessionId, sessionFile, prompt, i
157
176
  durationSeconds: result.durationSeconds,
158
177
  output: result.combinedOutput,
159
178
  notes: truncateForNotes(result.combinedOutput) || 'Adapter exited non-zero.',
179
+ role: '',
180
+ model: model ?? config.piModel,
181
+ toolCalls: 0,
182
+ toolErrors: 0,
183
+ messageUpdates: 0,
184
+ stopReason: '',
185
+ loopDetected: false,
186
+ loopSignature: '',
187
+ terminalReason: 'adapter_failed',
160
188
  }
161
189
  }
162
190
 
@@ -179,6 +207,15 @@ async function runAdapterTurn({ config, model, sessionId, sessionFile, prompt, i
179
207
  durationSeconds: result.durationSeconds,
180
208
  output,
181
209
  notes,
210
+ role: String(response.role ?? ''),
211
+ model: String(response.model ?? model ?? config.piModel ?? ''),
212
+ toolCalls: Number.isFinite(Number(response.toolCalls)) ? Number(response.toolCalls) : 0,
213
+ toolErrors: Number.isFinite(Number(response.toolErrors)) ? Number(response.toolErrors) : 0,
214
+ messageUpdates: Number.isFinite(Number(response.messageUpdates)) ? Number(response.messageUpdates) : 0,
215
+ stopReason: String(response.stopReason ?? ''),
216
+ loopDetected: response.loopDetected === true,
217
+ loopSignature: String(response.loopSignature ?? ''),
218
+ terminalReason: String(response.terminalReason ?? ''),
182
219
  }
183
220
  }
184
221
 
package/src/pi-config.mjs CHANGED
@@ -130,6 +130,22 @@ function normalizeRoleModels(raw) {
130
130
  return normalized
131
131
  }
132
132
 
133
+ function normalizeCommitMode(raw) {
134
+ const value = normalizeString(raw, 'agent').trim().toLowerCase()
135
+ if (value === 'agent' || value === 'plan') {
136
+ return value
137
+ }
138
+ throw new Error(`Expected commitMode to be "agent" or "plan", received "${raw}"`)
139
+ }
140
+
141
+ function normalizePromptMode(raw) {
142
+ const value = normalizeString(raw, 'compact').trim().toLowerCase()
143
+ if (value === 'compact' || value === 'full') {
144
+ return value
145
+ }
146
+ throw new Error(`Expected promptMode to be "compact" or "full", received "${raw}"`)
147
+ }
148
+
133
149
  function resolveModelProfile(modelProfiles, modelName) {
134
150
  if (!modelName || typeof modelName !== 'string') {
135
151
  return null
@@ -181,12 +197,30 @@ export function loadConfig(mode = 'once') {
181
197
  const repoConfig = readRepoConfig(cwd)
182
198
  const file = repoConfig.values
183
199
  const bundledAdapterCommand = 'pi-harness adapter'
200
+ const bundledDeveloperInstructionsFile = path.join(packageRoot, 'templates', 'DEVELOPER.md')
201
+ const bundledTesterInstructionsFile = path.join(packageRoot, 'templates', 'TESTER.md')
184
202
  const modelProfiles = readObject('models', file.models, {})
185
203
  const roleModels = normalizeRoleModels(file.roleModels)
186
204
  const piModel = readString('PI_MODEL', file.piModel, '')
187
205
  const visualReviewModel = readString('PI_VISUAL_REVIEW_MODEL', file.visualReviewModel, '')
188
206
  const resolvedPiModel = resolveModelProfile(modelProfiles, piModel)
189
207
  const resolvedVisualReviewModel = resolveModelProfile(modelProfiles, visualReviewModel)
208
+ const developerInstructionsFile = resolveInstructionsFile(
209
+ cwd,
210
+ 'PI_DEVELOPER_INSTRUCTIONS_FILE',
211
+ file.developerInstructionsFile,
212
+ hasValue(file.instructionsFile)
213
+ ? String(file.instructionsFile)
214
+ : bundledDeveloperInstructionsFile
215
+ )
216
+ const testerInstructionsFile = resolveInstructionsFile(
217
+ cwd,
218
+ 'PI_TESTER_INSTRUCTIONS_FILE',
219
+ file.testerInstructionsFile,
220
+ hasValue(file.instructionsFile)
221
+ ? String(file.instructionsFile)
222
+ : bundledTesterInstructionsFile
223
+ )
190
224
 
191
225
  return {
192
226
  cwd,
@@ -196,23 +230,11 @@ export function loadConfig(mode = 'once') {
196
230
  agentName: readString('PI_AGENT_NAME', file.agentName, 'PI'),
197
231
  adapterCommand: readString('PI_ADAPTER_COMMAND', file.adapterCommand, bundledAdapterCommand),
198
232
  taskFile: resolveFromCwd(cwd, 'PI_TASK_FILE', file.taskFile, 'TODOS.md'),
199
- instructionsFile: resolveInstructionsFile(cwd, 'PI_INSTRUCTIONS_FILE', file.instructionsFile, path.join(packageRoot, 'templates', 'DEVELOPER.md')),
200
- developerInstructionsFile: resolveInstructionsFile(
201
- cwd,
202
- 'PI_DEVELOPER_INSTRUCTIONS_FILE',
203
- file.developerInstructionsFile,
204
- hasValue(file.instructionsFile)
205
- ? String(file.instructionsFile)
206
- : path.join(packageRoot, 'templates', 'DEVELOPER.md')
207
- ),
208
- testerInstructionsFile: resolveInstructionsFile(
209
- cwd,
210
- 'PI_TESTER_INSTRUCTIONS_FILE',
211
- file.testerInstructionsFile,
212
- hasValue(file.instructionsFile)
213
- ? String(file.instructionsFile)
214
- : path.join(packageRoot, 'templates', 'TESTER.md')
215
- ),
233
+ instructionsFile: resolveInstructionsFile(cwd, 'PI_INSTRUCTIONS_FILE', file.instructionsFile, bundledDeveloperInstructionsFile),
234
+ developerInstructionsFile,
235
+ testerInstructionsFile,
236
+ usingBundledDeveloperInstructions: developerInstructionsFile === bundledDeveloperInstructionsFile,
237
+ usingBundledTesterInstructions: testerInstructionsFile === bundledTesterInstructionsFile,
216
238
  logFile: resolveFromCwd(cwd, 'PI_LOG_FILE', file.logFile, 'pi.log'),
217
239
  telemetryJsonl: resolveFromCwd(cwd, 'PI_TELEMETRY_JSONL', file.telemetryJsonl, 'pi_telemetry.jsonl'),
218
240
  telemetryCsv: resolveFromCwd(cwd, 'PI_TELEMETRY_CSV', file.telemetryCsv, 'pi_telemetry.csv'),
@@ -221,12 +243,21 @@ export function loadConfig(mode = 'once') {
221
243
  lastAgentOutputFile: resolveFromCwd(cwd, 'PI_LAST_AGENT_OUTPUT_FILE', file.lastAgentOutputFile, '.pi-last-output.txt'),
222
244
  lastVerificationOutputFile: resolveFromCwd(cwd, 'PI_LAST_VERIFICATION_OUTPUT_FILE', file.lastVerificationOutputFile, '.pi-last-verification.txt'),
223
245
  changedFilesFile: resolveFromCwd(cwd, 'PI_CHANGED_FILES_FILE', file.changedFilesFile, '.pi-changed-files.txt'),
246
+ lastPromptFile: resolveFromCwd(cwd, 'PI_LAST_PROMPT_FILE', file.lastPromptFile, '.pi-last-prompt.txt'),
247
+ lastIterationSummaryFile: resolveFromCwd(cwd, 'PI_LAST_ITERATION_SUMMARY_FILE', file.lastIterationSummaryFile, '.pi-last-iteration.json'),
224
248
  piRuntimeDir: resolveFromCwd(cwd, 'PI_RUNTIME_DIR', file.piRuntimeDir, '.pi-runtime'),
225
249
  piCli: readString('PI_CLI', file.piCli, 'pi'),
226
250
  piModel,
227
251
  piModelProfile: resolvedPiModel,
228
252
  modelProfiles,
229
253
  roleModels,
254
+ commitMode: normalizeCommitMode(readString('PI_COMMIT_MODE', file.commitMode, 'agent')),
255
+ promptMode: normalizePromptMode(readString('PI_PROMPT_MODE', file.promptMode, 'compact')),
256
+ maxPromptChangedFiles: readInt('PI_MAX_PROMPT_CHANGED_FILES', file.maxPromptChangedFiles, 10),
257
+ maxVisualFeedbackLines: readInt('PI_MAX_VISUAL_FEEDBACK_LINES', file.maxVisualFeedbackLines, 20),
258
+ maxTesterFeedbackLines: readInt('PI_MAX_TESTER_FEEDBACK_LINES', file.maxTesterFeedbackLines, 32),
259
+ maxPromptNotesLines: readInt('PI_MAX_PROMPT_NOTES_LINES', file.maxPromptNotesLines, 16),
260
+ maxVerificationExcerptLines: readInt('PI_MAX_VERIFICATION_EXCERPT_LINES', file.maxVerificationExcerptLines, 40),
230
261
  piTools: readString('PI_TOOLS', file.piTools, 'read,bash,edit,write,grep,find,ls'),
231
262
  piThinking: readString('PI_THINKING', file.piThinking, ''),
232
263
  piNoExtensions: readBool('PI_NO_EXTENSIONS', file.piNoExtensions, false),
@@ -0,0 +1,92 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ function unique(values) {
5
+ return [...new Set(values)]
6
+ }
7
+
8
+ function isWithinCwd(cwd, targetPath) {
9
+ const relativePath = path.relative(cwd, targetPath)
10
+ return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
11
+ }
12
+
13
+ export function collectHistoryTargets(config) {
14
+ return unique([
15
+ config.logFile,
16
+ config.telemetryJsonl,
17
+ config.telemetryCsv,
18
+ config.stateFile,
19
+ config.sessionFile,
20
+ config.lastAgentOutputFile,
21
+ config.lastVerificationOutputFile,
22
+ config.changedFilesFile,
23
+ config.lastPromptFile,
24
+ config.lastIterationSummaryFile,
25
+ config.piRuntimeDir,
26
+ config.visualFeedbackFile,
27
+ config.testerFeedbackFile,
28
+ config.testerFeedbackHistoryDir,
29
+ config.visualReviewHistoryDir,
30
+ config.visualCaptureDir,
31
+ ].map((value) => String(value ?? '').trim()).filter(Boolean))
32
+ }
33
+
34
+ async function pathExists(targetPath) {
35
+ try {
36
+ await fs.access(targetPath)
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ function validateHistoryTargets(config, targets) {
44
+ const invalidTargets = targets.filter((targetPath) => {
45
+ if (!path.isAbsolute(targetPath)) {
46
+ return true
47
+ }
48
+ if (targetPath === config.cwd || targetPath === path.parse(targetPath).root) {
49
+ return true
50
+ }
51
+ return !isWithinCwd(config.cwd, targetPath)
52
+ })
53
+
54
+ if (invalidTargets.length > 0) {
55
+ throw new Error(
56
+ `Refusing to clear history outside the repo root. Invalid targets: ${invalidTargets.join(', ')}`
57
+ )
58
+ }
59
+ }
60
+
61
+ export async function clearHarnessHistory(config) {
62
+ const targets = collectHistoryTargets(config)
63
+ validateHistoryTargets(config, targets)
64
+
65
+ const existingTargets = []
66
+ for (const targetPath of targets) {
67
+ if (await pathExists(targetPath)) {
68
+ existingTargets.push(targetPath)
69
+ }
70
+ }
71
+
72
+ for (const targetPath of [...existingTargets].sort((left, right) => right.length - left.length)) {
73
+ await fs.rm(targetPath, { recursive: true, force: true })
74
+ }
75
+
76
+ const remainingTargets = []
77
+ for (const targetPath of targets) {
78
+ if (await pathExists(targetPath)) {
79
+ remainingTargets.push(targetPath)
80
+ }
81
+ }
82
+
83
+ if (remainingTargets.length > 0) {
84
+ throw new Error(`Failed to clear harness history for: ${remainingTargets.join(', ')}`)
85
+ }
86
+
87
+ return {
88
+ targets,
89
+ clearedTargets: existingTargets,
90
+ remainingTargets,
91
+ }
92
+ }