@sebastianandreasson/pi-autonomous-agents 0.1.0 → 0.2.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,6 +83,14 @@ 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
+
85
94
  ## Generic Contracts
86
95
 
87
96
  - `taskFile`: usually `TODOS.md`
package/SETUP.md CHANGED
@@ -50,6 +50,11 @@ If the repo uses another package manager already, use the repo-native equivalent
50
50
  - `visualCaptureCommand`: only if this repo has a real screenshot capture flow
51
51
  - `models` / `piModel` / `visualReviewModel` / `roleModels`: configure the models actually available in this environment
52
52
 
53
+ Important:
54
+
55
+ - Do not assume a local provider’s served model id matches a GGUF filename or a guessed name.
56
+ - 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`.
57
+
53
58
  3. Create role instruction files.
54
59
 
55
60
  - Copy `node_modules/@sebastianandreasson/pi-autonomous-agents/templates/DEVELOPER.md` to `pi/DEVELOPER.md`.
@@ -136,6 +141,21 @@ Example shape:
136
141
  }
137
142
  ```
138
143
 
144
+ If the repo uses a custom OpenAI-compatible local provider, validate it directly:
145
+
146
+ 1. Verify the endpoint is reachable.
147
+ 2. Query `<baseUrl>/models`.
148
+ 3. Use the exact returned model id.
149
+ 4. Do not assume the served id equals a GGUF filename on disk.
150
+
151
+ If the repo overrides `PI_CODING_AGENT_DIR`:
152
+
153
+ - do not point it at an empty directory
154
+ - ensure that PI home is already bootstrapped
155
+ - ensure `models.json` exists there before running the harness
156
+
157
+ If `PI_CODING_AGENT_DIR` is set to a repo-local PI home and `models.json` is missing, setup is incomplete.
158
+
139
159
  9. Validate the setup.
140
160
 
141
161
  Run at least:
@@ -152,6 +172,13 @@ PI_CONFIG_FILE=pi.config.json PI_TRANSPORT=mock PI_TEST_CMD= pi-harness once
152
172
 
153
173
  If setup validation fails, fix the config rather than leaving a half-configured repo.
154
174
 
175
+ The harness should fail fast if:
176
+
177
+ - PI cannot list models
178
+ - a configured PI role model does not exist
179
+ - a configured provider endpoint is unreachable
180
+ - a configured provider does not serve the configured model id
181
+
155
182
  ## Agent Rules
156
183
 
157
184
  - Reuse existing repo conventions where possible.
@@ -169,3 +196,13 @@ When setup is complete, report:
169
196
  - whether visual review was enabled
170
197
  - which roles were mapped to which models
171
198
  - whether validation was run successfully
199
+
200
+ ## Resetting Harness State
201
+
202
+ If the user wants to start over from a clean slate later, use:
203
+
204
+ ```bash
205
+ PI_CONFIG_FILE=pi.config.json pi-harness clear-history
206
+ ```
207
+
208
+ This should remove harness-generated runtime/history state only, not project source files.
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.2.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",
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"
21
21
  },
22
22
  "files": [
23
23
  "src",
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
+ })
@@ -0,0 +1,90 @@
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.piRuntimeDir,
24
+ config.visualFeedbackFile,
25
+ config.testerFeedbackFile,
26
+ config.testerFeedbackHistoryDir,
27
+ config.visualReviewHistoryDir,
28
+ config.visualCaptureDir,
29
+ ].map((value) => String(value ?? '').trim()).filter(Boolean))
30
+ }
31
+
32
+ async function pathExists(targetPath) {
33
+ try {
34
+ await fs.access(targetPath)
35
+ return true
36
+ } catch {
37
+ return false
38
+ }
39
+ }
40
+
41
+ function validateHistoryTargets(config, targets) {
42
+ const invalidTargets = targets.filter((targetPath) => {
43
+ if (!path.isAbsolute(targetPath)) {
44
+ return true
45
+ }
46
+ if (targetPath === config.cwd || targetPath === path.parse(targetPath).root) {
47
+ return true
48
+ }
49
+ return !isWithinCwd(config.cwd, targetPath)
50
+ })
51
+
52
+ if (invalidTargets.length > 0) {
53
+ throw new Error(
54
+ `Refusing to clear history outside the repo root. Invalid targets: ${invalidTargets.join(', ')}`
55
+ )
56
+ }
57
+ }
58
+
59
+ export async function clearHarnessHistory(config) {
60
+ const targets = collectHistoryTargets(config)
61
+ validateHistoryTargets(config, targets)
62
+
63
+ const existingTargets = []
64
+ for (const targetPath of targets) {
65
+ if (await pathExists(targetPath)) {
66
+ existingTargets.push(targetPath)
67
+ }
68
+ }
69
+
70
+ for (const targetPath of [...existingTargets].sort((left, right) => right.length - left.length)) {
71
+ await fs.rm(targetPath, { recursive: true, force: true })
72
+ }
73
+
74
+ const remainingTargets = []
75
+ for (const targetPath of targets) {
76
+ if (await pathExists(targetPath)) {
77
+ remainingTargets.push(targetPath)
78
+ }
79
+ }
80
+
81
+ if (remainingTargets.length > 0) {
82
+ throw new Error(`Failed to clear harness history for: ${remainingTargets.join(', ')}`)
83
+ }
84
+
85
+ return {
86
+ targets,
87
+ clearedTargets: existingTargets,
88
+ remainingTargets,
89
+ }
90
+ }
@@ -0,0 +1,222 @@
1
+ import fs from 'node:fs/promises'
2
+ import { execFileSync } from 'node:child_process'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+
6
+ import { resolveRoleModelName } from './pi-config.mjs'
7
+
8
+ function uniqueNonEmpty(values) {
9
+ return [...new Set(values.map((value) => String(value ?? '').trim()).filter(Boolean))]
10
+ }
11
+
12
+ function formatAvailableModels(models) {
13
+ const uniqueModels = uniqueNonEmpty(models)
14
+ if (uniqueModels.length === 0) {
15
+ return '(none)'
16
+ }
17
+ if (uniqueModels.length <= 20) {
18
+ return uniqueModels.join(', ')
19
+ }
20
+ return `${uniqueModels.slice(0, 20).join(', ')}, ...`
21
+ }
22
+
23
+ export function parsePiListModelsOutput(output) {
24
+ const text = String(output ?? '').trim()
25
+ if (text === '') {
26
+ return []
27
+ }
28
+
29
+ try {
30
+ const parsed = JSON.parse(text)
31
+ return extractModelIdsFromProviderResponse(parsed)
32
+ } catch {
33
+ // fall through
34
+ }
35
+
36
+ const ids = []
37
+ for (const rawLine of text.split('\n')) {
38
+ const line = rawLine.trim()
39
+ if (
40
+ line === ''
41
+ || /^available models:?$/i.test(line)
42
+ || /^models:?$/i.test(line)
43
+ || /^id\s+/i.test(line)
44
+ || /^name\s+/i.test(line)
45
+ ) {
46
+ continue
47
+ }
48
+
49
+ const stripped = line.replace(/^[-*]\s+/, '').trim()
50
+ const firstToken = stripped.split(/\s+/)[0]?.trim() ?? ''
51
+ if (firstToken !== '') {
52
+ ids.push(firstToken)
53
+ }
54
+ }
55
+
56
+ return uniqueNonEmpty(ids)
57
+ }
58
+
59
+ export function extractModelIdsFromProviderResponse(payload) {
60
+ if (Array.isArray(payload)) {
61
+ return uniqueNonEmpty(payload.map((entry) => {
62
+ if (typeof entry === 'string') {
63
+ return entry
64
+ }
65
+ if (entry && typeof entry === 'object') {
66
+ return entry.id ?? entry.name ?? entry.model ?? ''
67
+ }
68
+ return ''
69
+ }))
70
+ }
71
+
72
+ if (!payload || typeof payload !== 'object') {
73
+ return []
74
+ }
75
+
76
+ if (Array.isArray(payload.data)) {
77
+ return extractModelIdsFromProviderResponse(payload.data)
78
+ }
79
+
80
+ if (Array.isArray(payload.models)) {
81
+ return extractModelIdsFromProviderResponse(payload.models)
82
+ }
83
+
84
+ return uniqueNonEmpty([
85
+ payload.id ?? '',
86
+ payload.name ?? '',
87
+ payload.model ?? '',
88
+ ])
89
+ }
90
+
91
+ async function ensurePiHomeModelsConfig() {
92
+ const piHome = process.env.PI_CODING_AGENT_DIR
93
+ if (!piHome) {
94
+ return
95
+ }
96
+
97
+ const resolvedPiHome = path.resolve(piHome)
98
+ const modelsFile = path.join(resolvedPiHome, 'models.json')
99
+ try {
100
+ await fs.access(modelsFile)
101
+ } catch {
102
+ throw new Error(
103
+ `PI_CODING_AGENT_DIR points at "${resolvedPiHome}", but "${modelsFile}" is missing. Either remove PI_CODING_AGENT_DIR or bootstrap that PI home before running the harness.`
104
+ )
105
+ }
106
+ }
107
+
108
+ function listPiModels(config) {
109
+ try {
110
+ const output = execFileSync(config.piCli, ['--list-models'], {
111
+ cwd: config.cwd,
112
+ env: process.env,
113
+ encoding: 'utf8',
114
+ stdio: ['ignore', 'pipe', 'pipe'],
115
+ })
116
+ return parsePiListModelsOutput(output)
117
+ } catch (error) {
118
+ const stdout = error?.stdout ? String(error.stdout).trim() : ''
119
+ const stderr = error?.stderr ? String(error.stderr).trim() : ''
120
+ const details = [stdout, stderr].filter(Boolean).join('\n')
121
+ throw new Error(
122
+ details === ''
123
+ ? `Failed to list PI models via "${config.piCli} --list-models".`
124
+ : `Failed to list PI models via "${config.piCli} --list-models".\n${details}`
125
+ )
126
+ }
127
+ }
128
+
129
+ function getConfiguredTextModels(config) {
130
+ return uniqueNonEmpty([
131
+ resolveRoleModelName(config, 'developer'),
132
+ resolveRoleModelName(config, 'developerRetry'),
133
+ resolveRoleModelName(config, 'developerFix'),
134
+ resolveRoleModelName(config, 'tester'),
135
+ resolveRoleModelName(config, 'testerCommit'),
136
+ ])
137
+ }
138
+
139
+ async function listProviderModels(modelName, modelProfile) {
140
+ const baseUrl = String(modelProfile?.baseUrl ?? '').replace(/\/$/, '')
141
+ if (baseUrl === '') {
142
+ return []
143
+ }
144
+
145
+ let response
146
+ try {
147
+ response = await fetch(`${baseUrl}/models`, {
148
+ method: 'GET',
149
+ headers: {
150
+ ...(modelProfile?.apiKey ? { authorization: `Bearer ${modelProfile.apiKey}` } : {}),
151
+ },
152
+ signal: AbortSignal.timeout(10_000),
153
+ })
154
+ } catch (error) {
155
+ throw new Error(
156
+ `Configured provider for model "${modelName}" is unreachable at ${baseUrl}/models: ${error instanceof Error ? error.message : String(error)}`
157
+ )
158
+ }
159
+
160
+ if (!response.ok) {
161
+ const errorText = (await response.text()).trim()
162
+ throw new Error(
163
+ `Configured provider for model "${modelName}" returned ${response.status} from ${baseUrl}/models.${errorText !== '' ? ` ${errorText}` : ''}`
164
+ )
165
+ }
166
+
167
+ const payload = await response.json()
168
+ return extractModelIdsFromProviderResponse(payload)
169
+ }
170
+
171
+ async function validateProviderModels(config) {
172
+ const configuredModels = uniqueNonEmpty([
173
+ ...getConfiguredTextModels(config),
174
+ config.visualReviewEnabled ? resolveRoleModelName(config, 'visualReview') : '',
175
+ ])
176
+
177
+ for (const modelName of configuredModels) {
178
+ const modelProfile = config.modelProfiles?.[modelName] ?? null
179
+ if (!modelProfile?.baseUrl) {
180
+ continue
181
+ }
182
+
183
+ const availableModels = await listProviderModels(modelName, modelProfile)
184
+ if (availableModels.length === 0) {
185
+ throw new Error(
186
+ `Configured provider for model "${modelName}" at ${String(modelProfile.baseUrl).replace(/\/$/, '')}/models returned no models.`
187
+ )
188
+ }
189
+
190
+ if (!availableModels.includes(modelName)) {
191
+ throw new Error(
192
+ `Configured model "${modelName}" not found at provider ${String(modelProfile.baseUrl).replace(/\/$/, '')}/models. Available models: ${formatAvailableModels(availableModels)}`
193
+ )
194
+ }
195
+ }
196
+ }
197
+
198
+ export async function runStartupPreflight(config) {
199
+ if (config.transport === 'mock') {
200
+ return
201
+ }
202
+
203
+ await ensurePiHomeModelsConfig()
204
+
205
+ const availablePiModels = listPiModels(config)
206
+ if (availablePiModels.length === 0) {
207
+ throw new Error(
208
+ `PI reported no models via "${config.piCli} --list-models". Ensure your PI home and model registry are configured before running the harness.`
209
+ )
210
+ }
211
+
212
+ const configuredTextModels = getConfiguredTextModels(config)
213
+ for (const modelName of configuredTextModels) {
214
+ if (!availablePiModels.includes(modelName)) {
215
+ throw new Error(
216
+ `Configured PI model "${modelName}" is not available from "${config.piCli} --list-models". Available models: ${formatAvailableModels(availablePiModels)}`
217
+ )
218
+ }
219
+ }
220
+
221
+ await validateProviderModels(config)
222
+ }
@@ -1,6 +1,15 @@
1
1
  import path from 'node:path'
2
2
 
3
- function shortName(filePath) {
3
+ function displayPath(config, filePath) {
4
+ const relativePath = path.relative(config.cwd, filePath)
5
+ if (
6
+ relativePath !== ''
7
+ && !relativePath.startsWith('..')
8
+ && !path.isAbsolute(relativePath)
9
+ ) {
10
+ return relativePath.split(path.sep).join('/')
11
+ }
12
+
4
13
  return path.basename(filePath)
5
14
  }
6
15
 
@@ -46,8 +55,8 @@ function staleEditRecoveryRules() {
46
55
  }
47
56
 
48
57
  export function buildMainPrompt(config, options = {}) {
49
- const taskFile = shortName(config.taskFile)
50
- const instructionsFile = shortName(config.developerInstructionsFile)
58
+ const taskFile = displayPath(config, config.taskFile)
59
+ const instructionsFile = displayPath(config, config.developerInstructionsFile)
51
60
  const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
52
61
  const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
53
62
 
@@ -85,8 +94,8 @@ Before stopping:
85
94
  }
86
95
 
87
96
  export function buildFixPrompt(config, recentVerificationOutput, options = {}) {
88
- const taskFile = shortName(config.taskFile)
89
- const instructionsFile = shortName(config.developerInstructionsFile)
97
+ const taskFile = displayPath(config, config.taskFile)
98
+ const instructionsFile = displayPath(config, config.developerInstructionsFile)
90
99
  const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
91
100
  const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
92
101
 
@@ -120,7 +129,7 @@ Before stopping:
120
129
  }
121
130
 
122
131
  export function buildSteeringPrompt(config, reason, options = {}) {
123
- const taskFile = shortName(config.taskFile)
132
+ const taskFile = displayPath(config, config.taskFile)
124
133
  const visualFeedbackSection = formatVisualFeedback(options.visualFeedback)
125
134
  const testerFeedbackSection = formatTesterFeedback(options.testerFeedback)
126
135
 
@@ -149,8 +158,8 @@ export function buildTesterPrompt(config, {
149
158
  visualFeedback = '',
150
159
  testerFeedback = '',
151
160
  }) {
152
- const taskFile = shortName(config.taskFile)
153
- const instructionsFile = shortName(config.testerInstructionsFile)
161
+ const taskFile = displayPath(config, config.taskFile)
162
+ const instructionsFile = displayPath(config, config.testerInstructionsFile)
154
163
  const visualFeedbackSection = formatVisualFeedback(visualFeedback)
155
164
  const testerFeedbackSection = formatTesterFeedback(testerFeedback)
156
165
  const changedFilesSection = changedFiles.length > 0
@@ -229,8 +238,8 @@ export function buildCommitPrompt(config, {
229
238
  visualFeedback = '',
230
239
  testerFeedback = '',
231
240
  }) {
232
- const taskFile = shortName(config.taskFile)
233
- const instructionsFile = shortName(config.testerInstructionsFile)
241
+ const taskFile = displayPath(config, config.taskFile)
242
+ const instructionsFile = displayPath(config, config.testerInstructionsFile)
234
243
  const visualFeedbackSection = formatVisualFeedback(visualFeedback)
235
244
  const testerFeedbackSection = formatTesterFeedback(testerFeedback)
236
245
  const changedFilesSection = changedFiles.length > 0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import process from 'node:process'
4
+ import path from 'node:path'
4
5
  import { loadConfig, resolveRoleModelName, resolveRoleModel } from './pi-config.mjs'
5
6
  import {
6
7
  buildCommitPrompt,
@@ -40,6 +41,7 @@ import {
40
41
  deriveWorkflowStatus,
41
42
  shouldPersistLatestTesterFeedback,
42
43
  } from './pi-flow.mjs'
44
+ import { runStartupPreflight } from './pi-preflight.mjs'
43
45
 
44
46
  let stopRequested = false
45
47
 
@@ -64,6 +66,7 @@ function printTerminalSummary(config, summary) {
64
66
 
65
67
  const lines = [
66
68
  `[PI supervisor] iteration=${summary.iteration} phase="${summary.phase}"`,
69
+ `[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)}`,
67
70
  `[PI supervisor] transport=${config.transport} developer_model=${summary.developerModel || resolveRoleModelName(config, 'developer') || '(PI default)'} tester_model=${summary.testerModel || resolveRoleModelName(config, 'tester') || '(PI default)'}`,
68
71
  `[PI supervisor] developer=${summary.developerStatus} tester=${summary.testerStatus} verification=${summary.verificationStatus}`,
69
72
  ]
@@ -87,6 +90,19 @@ function printTerminalSummary(config, summary) {
87
90
  process.stderr.write(`${lines.join('\n')}\n`)
88
91
  }
89
92
 
93
+ function toDisplayPath(config, filePath) {
94
+ const relativePath = path.relative(config.cwd, filePath)
95
+ if (
96
+ relativePath !== ''
97
+ && !relativePath.startsWith('..')
98
+ && !path.isAbsolute(relativePath)
99
+ ) {
100
+ return relativePath.split(path.sep).join('/')
101
+ }
102
+
103
+ return filePath
104
+ }
105
+
90
106
  function parseTesterVerdict(output) {
91
107
  const raw = String(output ?? '')
92
108
  const match = raw.match(/VERDICT:\s*(PASS|FAIL|BLOCKED)\s*$/im)
@@ -798,6 +814,9 @@ async function runIteration({ config, state, iteration }) {
798
814
  notes: 'No unchecked tasks remain in TODOS.md.',
799
815
  sessionId: state.sessionId || '',
800
816
  outputPath: config.lastAgentOutputFile,
817
+ taskFile: toDisplayPath(config, config.taskFile),
818
+ developerInstructionsFile: toDisplayPath(config, config.developerInstructionsFile),
819
+ testerInstructionsFile: toDisplayPath(config, config.testerInstructionsFile),
801
820
  developerModel: developerModelName,
802
821
  testerModel: testerModelName,
803
822
  visualModel: visualModelName,
@@ -820,7 +839,7 @@ async function runIteration({ config, state, iteration }) {
820
839
 
821
840
  await appendLog(
822
841
  config.logFile,
823
- `Starting iteration ${iteration} in phase "${phase}" with transport "${config.transport}"`
842
+ `Starting iteration ${iteration} in phase "${phase}" with transport "${config.transport}" task=${toDisplayPath(config, config.taskFile)} developer_instructions=${toDisplayPath(config, config.developerInstructionsFile)} tester_instructions=${toDisplayPath(config, config.testerInstructionsFile)} developer_model=${developerModelName || '(PI default)'} tester_model=${testerModelName || '(PI default)'}`
824
843
  )
825
844
 
826
845
  const mainInvocation = await runMainTurnWithRetries({
@@ -1108,6 +1127,9 @@ async function runIteration({ config, state, iteration }) {
1108
1127
  notes: noteParts.join(' | '),
1109
1128
  sessionId,
1110
1129
  outputPath: config.lastAgentOutputFile,
1130
+ taskFile: toDisplayPath(config, config.taskFile),
1131
+ developerInstructionsFile: toDisplayPath(config, config.developerInstructionsFile),
1132
+ testerInstructionsFile: toDisplayPath(config, config.testerInstructionsFile),
1111
1133
  developerModel: developerModelName,
1112
1134
  testerModel: testerModelName,
1113
1135
  visualModel: visualModelName,
@@ -1123,6 +1145,7 @@ async function main() {
1123
1145
  await ensureFileExists(config.developerInstructionsFile, 'developer instructions file')
1124
1146
  await ensureFileExists(config.testerInstructionsFile, 'tester instructions file')
1125
1147
  await ensureTelemetryFiles(config)
1148
+ await runStartupPreflight(config)
1126
1149
 
1127
1150
  let state = await readState(config.stateFile)
1128
1151
  let completedIterations = 0