@freibergergarcia/phone-a-friend 2.3.0 → 2.6.2

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,7 +1,7 @@
1
1
  {
2
2
  "name": "phone-a-friend",
3
3
  "description": "CLI relay that lets AI coding agents collaborate by sending prompts and repository context to backend agents.",
4
- "version": "2.3.0",
4
+ "version": "2.6.2",
5
5
  "author": {
6
6
  "name": "Bruno Freiberger"
7
7
  }
package/README.md CHANGED
@@ -11,7 +11,6 @@
11
11
  [![CI](https://github.com/freibergergarcia/phone-a-friend/actions/workflows/ci.yml/badge.svg)](https://github.com/freibergergarcia/phone-a-friend/actions/workflows/ci.yml)
12
12
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
13
13
  ![Node.js 22.13+](https://img.shields.io/badge/node-%E2%89%A522.13-green)
14
- [![Website](https://img.shields.io/badge/website-phone--a--friend-blue)](https://freibergergarcia.github.io/phone-a-friend/)
15
14
 
16
15
  </div>
17
16
 
@@ -136,7 +135,7 @@ phone-a-friend --to codex --prompt "List files that need refactoring" \
136
135
  --schema '{"type":"object","properties":{"files":{"type":"array","items":{"type":"string"}}},"required":["files"],"additionalProperties":false}'
137
136
  ```
138
137
 
139
- Claude and Codex enforce the schema natively. Gemini, Ollama, and OpenCode use prompt injection (best-effort).
138
+ Claude, Codex, and Ollama enforce the schema through their native structured-output surfaces. Gemini and OpenCode CLI use prompt injection (best-effort), with PaF validating built-in verdict envelopes before returning them.
140
139
 
141
140
  ### Sessions
142
141
 
@@ -201,6 +200,40 @@ phone-a-friend config edit # Open in $EDITOR
201
200
 
202
201
  `doctor` reports CLI backends, local backends (Ollama), host integration status (Claude / OpenCode plugin install state), and a summary count. The OpenCode CLI is treated as optional: if you only use Claude Code and don't have OpenCode installed, doctor will not flag that as a degraded state.
203
202
 
203
+ ### Update notifications
204
+
205
+ phone-a-friend checks the npm registry for newer stable releases at most once
206
+ every 24 hours and prints a one-time stderr banner the next time it runs in an
207
+ interactive terminal. The current invocation is never slowed down: the registry
208
+ fetch happens in the background, with results applied on the next run.
209
+
210
+ Sample banner:
211
+
212
+ ```
213
+ ↑ phone-a-friend X.Y.Z available (current: A.B.C)
214
+ Run: npm install -g @freibergergarcia/phone-a-friend@latest
215
+ ```
216
+
217
+ The banner is suppressed automatically when:
218
+ - stdout or stderr is not a TTY (piped or redirected output)
219
+ - `CI` is set, or `TERM=dumb`
220
+ - the command uses `--quiet`, `--schema`, `--verdict-json`, or any subcommand-level `--json` flag
221
+ - the same version was already shown within the last 7 days
222
+
223
+ To disable update checks entirely:
224
+
225
+ ```bash
226
+ # One-off
227
+ PHONE_A_FRIEND_UPDATE_CHECK=false phone-a-friend ...
228
+
229
+ # Permanent
230
+ phone-a-friend config set defaults.update_check false
231
+ ```
232
+
233
+ The cache lives at `~/.config/phone-a-friend/update-check.json` (or under
234
+ `$XDG_CONFIG_HOME` if set). Run `phone-a-friend doctor` to inspect the current
235
+ state.
236
+
204
237
  ## Backends
205
238
 
206
239
  | Backend | Type | Streaming |
@@ -326,6 +359,14 @@ npm test # Run tests (vitest)
326
359
  npm run typecheck # Type check (tsc --noEmit)
327
360
  ```
328
361
 
362
+ ## Privacy
363
+
364
+ Phone a Friend does not collect, transmit, or store any data on servers operated by this project. There is no telemetry and no analytics.
365
+
366
+ Prompts and repository context are passed only to backends you have installed and authenticated yourself: the Claude, Codex, Gemini, and OpenCode CLIs, or a local Ollama instance. Each backend is governed by its own provider's privacy policy and terms.
367
+
368
+ Local state (config, sessions, jobs, agentic transcripts, and the web dashboard event log) is written only to `~/.config/phone-a-friend/` on your machine. The web dashboard is served on `localhost` and is not exposed to the network.
369
+
329
370
  ## License
330
371
 
331
372
  Apache-2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
@@ -62,12 +62,12 @@ When `RELAY_MODE = direct`, call backend CLIs directly instead of using the
62
62
 
63
63
  | Backend | Direct command |
64
64
  |---------|---------------|
65
- | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "<combined-prompt>" < /dev/null` |
66
- | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<combined-prompt>"` |
67
- | **Ollama** | `curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{"model":"<model>","messages":[{"role":"user","content":"<combined-prompt>"}],"stream":false}' \| jq -r '.message.content'` |
65
+ | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "$(cat "$PROMPT_FILE")" < /dev/null` |
66
+ | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$PROMPT_FILE")"` |
67
+ | **Ollama** | `PROMPT_JSON="$(jq -Rs . < "$PROMPT_FILE")"; curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d "{\"model\":\"<model>\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}],\"stream\":false}" \| jq -r '.message.content'` |
68
68
 
69
- In direct mode, combine the relay prompt into a single string using this
70
- template:
69
+ In direct mode, build `PROMPT_FILE` from the relay prompt using this
70
+ template and the quoted-heredoc rule:
71
71
 
72
72
  ```
73
73
  You are helping another coding agent by reviewing or advising on work in a local repository.
@@ -123,6 +123,7 @@ If `--backend` value is not `codex`, `gemini`, or `ollama`: report error and sto
123
123
 
124
124
  Set:
125
125
  - TOPIC = parsed topic string
126
+ - TOPIC_SAFE = TOPIC (untrusted text; never splice it into an inline shell command)
126
127
  - MAX_ROUNDS = parsed rounds (default 3, clamped [1, 6])
127
128
  - BACKEND = parsed backend (default `codex`)
128
129
  - ROUND = 1
@@ -179,43 +180,48 @@ Display to user:
179
180
  Claude Code, the OpenCode model name in OpenCode). Pick one that the user
180
181
  will recognize.
181
182
 
182
- Then relay to backend:
183
+ Then relay to backend. First build `PROMPT_FILE` so untrusted text such as
184
+ TOPIC and QUESTION is passed as data, not spliced into an inline shell
185
+ command:
186
+
187
+ ```bash
188
+ PROMPT_FILE="$(mktemp)"
189
+ trap 'rm -f "$PROMPT_FILE" "${REPROMPT_FILE:-}"' EXIT
190
+ {
191
+ printf '%s\n' 'You are playing The Curiosity Engine — a structured Q&A rally with another agent.'
192
+ printf 'Topic: %s\n' "$TOPIC_SAFE"
193
+ printf 'Round: 1 of %s\n\n' "$MAX_ROUNDS"
194
+ printf '%s\n' "The orchestrating agent's question for you:"
195
+ printf '%s\n\n' "$QUESTION"
196
+ cat <<'PAF_CURIOSITY_PROMPT_EOF'
197
+ You MUST respond in EXACTLY this format — no exceptions, no extra text:
198
+
199
+ ANSWER: <your answer to the orchestrator's question, 2-4 sentences>
200
+ QUESTION: <a new question for the orchestrator on the same topic, that you are genuinely curious about>
201
+
202
+ Do not add any text before ANSWER: or after the QUESTION line.
203
+ PAF_CURIOSITY_PROMPT_EOF
204
+ } > "$PROMPT_FILE"
205
+ ```
183
206
 
184
207
  **Binary mode** (`RELAY_MODE = binary`):
185
208
  ```bash
186
- phone-a-friend --to <BACKEND> --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF [--model <model>] --prompt "<relay-prompt>"
209
+ phone-a-friend --to <BACKEND> --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF [--model <model>] --prompt "$(cat "$PROMPT_FILE")"
187
210
  ```
188
211
 
189
212
  **Direct mode** (`RELAY_MODE = direct`):
190
213
  ```bash
191
214
  # Codex:
192
- codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "<relay-prompt>" < /dev/null
215
+ codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "$(cat "$PROMPT_FILE")" < /dev/null
193
216
  # Gemini (always include -m):
194
- gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<relay-prompt>"
217
+ gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$PROMPT_FILE")"
195
218
  # Ollama (use OLLAMA_SELECTED_MODEL from Step 2):
219
+ PROMPT_JSON="$(jq -Rs . < "$PROMPT_FILE")"
196
220
  curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" \
197
- -d '{"model":"<OLLAMA_SELECTED_MODEL>","messages":[{"role":"user","content":"<relay-prompt>"}],"stream":false}' \
221
+ -d "{\"model\":\"<OLLAMA_SELECTED_MODEL>\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}],\"stream\":false}" \
198
222
  | jq -r '.message.content'
199
223
  ```
200
224
 
201
- Where `<relay-prompt>` is:
202
-
203
- ```
204
- You are playing The Curiosity Engine — a structured Q&A rally with another agent.
205
- Topic: <TOPIC>
206
- Round: 1 of <MAX_ROUNDS>
207
-
208
- The orchestrating agent's question for you:
209
- <QUESTION>
210
-
211
- You MUST respond in EXACTLY this format — no exceptions, no extra text:
212
-
213
- ANSWER: <your answer to the orchestrator's question, 2-4 sentences>
214
- QUESTION: <a new question for the orchestrator on the same topic, that you are genuinely curious about>
215
-
216
- Do not add any text before ANSWER: or after the QUESTION line.
217
- ```
218
-
219
225
  ## Step 4 — Parse Backend Response
220
226
 
221
227
  If the relay call (binary or direct) produces no output, empty stdout, or a
@@ -237,35 +243,39 @@ After each relay call, parse the response for `ANSWER:` and `QUESTION:` fields.
237
243
 
238
244
  Send one correction relay if `ANSWER:` or `QUESTION:` is missing:
239
245
 
246
+ First create `REPROMPT_FILE` with a quoted heredoc:
247
+
248
+ ```bash
249
+ REPROMPT_FILE="$(mktemp)"
250
+ cat > "$REPROMPT_FILE" <<'PAF_CURIOSITY_REPROMPT_EOF'
251
+ Your previous response did not follow the required format.
252
+ You MUST respond with EXACTLY this structure:
253
+
254
+ ANSWER: <your answer>
255
+ QUESTION: <your question for the orchestrator>
256
+
257
+ No other text. Try again.
258
+ PAF_CURIOSITY_REPROMPT_EOF
259
+ ```
260
+
240
261
  **Binary mode** (`RELAY_MODE = binary`):
241
262
  ```bash
242
- phone-a-friend --to <BACKEND> --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF [--model <model>] --prompt "<re-prompt>"
263
+ phone-a-friend --to <BACKEND> --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF [--model <model>] --prompt "$(cat "$REPROMPT_FILE")"
243
264
  ```
244
265
 
245
266
  **Direct mode** (`RELAY_MODE = direct`):
246
267
  ```bash
247
268
  # Codex:
248
- codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "<re-prompt>" < /dev/null
269
+ codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "$(cat "$REPROMPT_FILE")" < /dev/null
249
270
  # Gemini:
250
- gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<re-prompt>"
271
+ gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$REPROMPT_FILE")"
251
272
  # Ollama:
273
+ REPROMPT_JSON="$(jq -Rs . < "$REPROMPT_FILE")"
252
274
  curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" \
253
- -d '{"model":"<OLLAMA_SELECTED_MODEL>","messages":[{"role":"user","content":"<re-prompt>"}],"stream":false}' \
275
+ -d "{\"model\":\"<OLLAMA_SELECTED_MODEL>\",\"messages\":[{\"role\":\"user\",\"content\":${REPROMPT_JSON}}],\"stream\":false}" \
254
276
  | jq -r '.message.content'
255
277
  ```
256
278
 
257
- Where `<re-prompt>` is:
258
-
259
- ```
260
- Your previous response did not follow the required format.
261
- You MUST respond with EXACTLY this structure:
262
-
263
- ANSWER: <your answer>
264
- QUESTION: <your question for the orchestrator>
265
-
266
- No other text. Try again.
267
- ```
268
-
269
279
  Parse again. If still missing `QUESTION:` → end game early. Display:
270
280
  ```
271
281
  ⚠️ <BACKEND> broke the chain on round <N> (missing QUESTION: after re-prompt).
@@ -363,7 +373,7 @@ capability, or debugging requires a pin.
363
373
  with cache path, expiry, and bypass instructions; no auto-substitution):
364
374
 
365
375
  ```bash
366
- phone-a-friend --to gemini --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF --prompt "<relay-prompt>"
376
+ phone-a-friend --to gemini --repo "$PWD" --sandbox read-only --fast $PAF_NO_DIFF --prompt "$(cat "$PROMPT_FILE")"
367
377
  ```
368
378
 
369
379
  To bypass the cache: `PHONE_A_FRIEND_GEMINI_DEAD_CACHE=false`. Or delete the
@@ -371,7 +381,7 @@ cache file to clear it.
371
381
 
372
382
  **Direct mode** (no PaF wrapper — orchestrator handles retry):
373
383
  ```bash
374
- gemini --sandbox --yolo --include-directories "$PWD" --output-format text --prompt "<relay-prompt>"
384
+ gemini --sandbox --yolo --include-directories "$PWD" --output-format text --prompt "$(cat "$PROMPT_FILE")"
375
385
  ```
376
386
 
377
387
  In direct mode, on capacity/transient errors (429, 500, 503), retry with a
@@ -35,6 +35,9 @@ into the current conversation.
35
35
  subcommands (e.g. `phone-a-friend phone-a-team`).
36
36
  - `--backend` is a `/phone-a-team` skill argument, not a PaF CLI flag. Do
37
37
  not pass `--backend` to `phone-a-friend`.
38
+ - When materializing relay commands, write dynamic prompt/context text into
39
+ temp files using single-quoted heredocs. Do not splice user text, prior
40
+ model output, or conversation context into double-quoted shell arguments.
38
41
  - Do NOT dump repo files or git output into `--context-file` or
39
42
  `--context-text`. Repo-aware backends read files via `--repo "$PWD"`
40
43
  using their own tools. See "Context hygiene" below.
@@ -61,11 +64,11 @@ When `RELAY_MODE = direct`, call backend CLIs directly instead of using the
61
64
 
62
65
  | Backend | Direct command |
63
66
  |---------|---------------|
64
- | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "<combined-prompt>" < /dev/null` |
65
- | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<combined-prompt>"` |
67
+ | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "$(cat "$PROMPT_FILE")" < /dev/null` |
68
+ | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$PROMPT_FILE")"` |
66
69
 
67
- In direct mode, combine prompt + context into a single string using this
68
- template:
70
+ In direct mode, build `PROMPT_FILE` from prompt + context using this
71
+ template and the quoted-heredoc rule:
69
72
 
70
73
  ```
71
74
  You are helping another coding agent by reviewing or advising on work in a local repository.
@@ -168,12 +171,29 @@ I'm working on this task and got the above response. Please review it and return
168
171
 
169
172
  **Binary mode** (`RELAY_MODE = binary`):
170
173
  ```bash
171
- phone-a-friend --to codex --repo "$PWD" --prompt "<relay-prompt>" --context-text "<context-payload>" $PAF_NO_DIFF [--fast] [--session <id>]
174
+ RELAY_BIN="$(command -v phone-a-friend)"
175
+ PROMPT_FILE="$(mktemp)"
176
+ CONTEXT_FILE="$(mktemp)"
177
+ trap 'rm -f "$PROMPT_FILE" "$CONTEXT_FILE"' EXIT
178
+
179
+ cat > "$PROMPT_FILE" <<'PAF_PROMPT_EOF'
180
+ <relay-prompt>
181
+ PAF_PROMPT_EOF
182
+
183
+ cat > "$CONTEXT_FILE" <<'PAF_CONTEXT_EOF'
184
+ <context-payload>
185
+ PAF_CONTEXT_EOF
186
+
187
+ "$RELAY_BIN" --to codex --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast] [--session <id>]
172
188
  # For gemini, omit --model by default (let auto-routing pick); see "Gemini model selection" below.
173
189
  # Do NOT pass --session to gemini — it will error (see "Session continuity" below):
174
- phone-a-friend --to gemini --repo "$PWD" --prompt "<relay-prompt>" --context-text "<context-payload>" $PAF_NO_DIFF [--fast]
190
+ "$RELAY_BIN" --to gemini --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" --context-file "$CONTEXT_FILE" $PAF_NO_DIFF [--fast]
175
191
  ```
176
192
 
193
+ Use delimiter names that do not appear in the payload. The quoted heredoc
194
+ marker (`<<'PAF_PROMPT_EOF'`) is intentional: it makes shell treat the
195
+ body as data, not executable text.
196
+
177
197
  `$PAF_NO_DIFF` comes from the probe in "Diff suppression" above. It
178
198
  resolves to `--no-include-diff` on new binaries and an empty string on
179
199
  stale binaries (with `PHONE_A_FRIEND_INCLUDE_DIFF=false` exported as
@@ -185,14 +205,14 @@ I'm working on this task and got the above response. Please review it and return
185
205
  **Direct mode** (`RELAY_MODE = direct`):
186
206
  ```bash
187
207
  # Codex:
188
- codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "<combined-prompt>" < /dev/null
208
+ codex exec -C "$PWD" --skip-git-repo-check --sandbox read-only "$(cat "$PROMPT_FILE")" < /dev/null
189
209
  # Gemini (omit -m for auto-routing; pin only when reproducibility/capability is needed):
190
- gemini --sandbox --yolo --include-directories "$PWD" --output-format text --prompt "<combined-prompt>"
210
+ gemini --sandbox --yolo --include-directories "$PWD" --output-format text --prompt "$(cat "$PROMPT_FILE")"
191
211
  ```
192
212
 
193
- In direct mode, build `<combined-prompt>` using the template from the
194
- "Direct call reference" section, substituting `<relay-prompt>` and
195
- `<context-payload>` into the template.
213
+ In direct mode, build `PROMPT_FILE` from the template in the "Direct call
214
+ reference" section using the same quoted-heredoc rule, substituting
215
+ `<relay-prompt>` and `<context-payload>` into the file body.
196
216
 
197
217
  Note: `--fast`, `--session`, and `--no-include-diff` are PaF CLI flags
198
218
  only available in binary mode. Do not append them to direct-mode
@@ -37,19 +37,20 @@ When `RELAY_MODE = direct`, call backend CLIs directly instead of using the
37
37
 
38
38
  | Backend | Direct command |
39
39
  |---------|---------------|
40
- | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox <mode> "<combined-prompt>" < /dev/null` |
41
- | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<combined-prompt>"` |
42
- | **Ollama** | `curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{"model":"<model>","messages":[{"role":"user","content":"<combined-prompt>"}],"stream":false}' \| jq -r '.message.content'` |
40
+ | **Codex** | `codex exec -C "$PWD" --skip-git-repo-check --sandbox <mode> "$(cat "$PROMPT_FILE")" < /dev/null` |
41
+ | **Gemini** | `gemini --sandbox --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$PROMPT_FILE")"` |
42
+ | **Ollama** | `PROMPT_JSON="$(jq -Rs . < "$PROMPT_FILE")"; curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d "{\"model\":\"<model>\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}],\"stream\":false}" \| jq -r '.message.content'` |
43
43
 
44
44
  Sandbox mapping for direct mode:
45
45
  - **Codex**: pass the mode string directly (`--sandbox read-only` or
46
46
  `--sandbox workspace-write`)
47
47
  - **Gemini**: `--sandbox` flag is boolean. Present = sandboxed (read-only).
48
- For workspace-write, omit `--sandbox`.
48
+ Use `--sandbox` for both read-only and workspace-write; omit it only for
49
+ `danger-full-access`.
49
50
  - **Ollama**: no sandbox support. All context must be in the prompt.
50
51
 
51
- In direct mode, combine prompt + context + diff into a single string using
52
- this template:
52
+ In direct mode, build `PROMPT_FILE` from prompt + context + diff using this
53
+ template and the quoted-heredoc rule:
53
54
 
54
55
  ```
55
56
  You are helping another coding agent by reviewing or advising on work in a local repository.
@@ -113,9 +114,13 @@ matrix and skip rules).
113
114
  Extract a model name from the task arguments.
114
115
 
115
116
  **Explicit flag (highest priority, all backends):**
116
- - If `$ARGUMENTS` contains `--model <name>`: set `MODEL_OVERRIDE = <name>`.
117
- Remove the `--model <name>` pair from TASK_DESCRIPTION.
118
- - This applies to all backends (codex, gemini, ollama, both).
117
+ - If `$ARGUMENTS` contains `--model <name>`: validate `<name>` against
118
+ `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`.
119
+ - If invalid (spaces, quotes, backticks, shell metacharacters, or a leading
120
+ punctuation character): abort and ask the user for a safe model name.
121
+ - If valid: set `MODEL_OVERRIDE = <name>` and remove the `--model <name>`
122
+ pair from TASK_DESCRIPTION.
123
+ - This applies to all backends (codex, gemini, ollama, both, all).
119
124
 
120
125
  **Natural language extraction (Ollama only, lower priority):**
121
126
  - Only attempt NL extraction when BACKEND is exactly `ollama` and no
@@ -129,6 +134,9 @@ Extract a model name from the task arguments.
129
134
  TASK_DESCRIPTION.
130
135
  - If the candidate appears inside quotes, backticks, or code blocks, do
131
136
  NOT extract (it's an example or reference, not a meta-instruction).
137
+ - Validate extracted names with `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`. If
138
+ the extracted candidate fails validation, abort and ask the user for a
139
+ safe model name.
132
140
 
133
141
  **Examples:**
134
142
  - "review this code, use deepseek" (backend=ollama) → extract "deepseek" ✓
@@ -227,13 +235,14 @@ server has nothing to run — proceeding would always fail.
227
235
  If models are available, select using this precedence:
228
236
  1. If `MODEL_OVERRIDE` is set (from `--model` flag or NL extraction in
229
237
  Step 1): set `OLLAMA_SELECTED_MODEL = MODEL_OVERRIDE`. Check if it exists
230
- in `OLLAMA_AVAILABLE_MODELS`. If not found, **warn** (e.g., "Model 'foo'
231
- not found in local models: [bar, baz]. Proceeding anyway — it may be a
232
- tag variant.") but proceed.
238
+ in `OLLAMA_AVAILABLE_MODELS`. If not found, **abort** and ask the user
239
+ to choose one of the discovered local models.
233
240
  2. If no override and `RELAY_MODE = binary`: check config by running
234
241
  `phone-a-friend config get backends.ollama.model`. If a value is
235
- returned, set `OLLAMA_SELECTED_MODEL` to that value. Validate against
236
- `OLLAMA_AVAILABLE_MODELS` warn if not found but proceed.
242
+ returned, validate it against `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`,
243
+ then set `OLLAMA_SELECTED_MODEL` to that value. Validate against
244
+ `OLLAMA_AVAILABLE_MODELS` — if not found, abort and ask the user to
245
+ choose one of the discovered local models.
237
246
  If `RELAY_MODE = direct`: skip this step (the binary is not available to
238
247
  query config). Fall through to option 3.
239
248
  3. If neither override nor config: set `OLLAMA_SELECTED_MODEL` to the first
@@ -342,6 +351,12 @@ command:
342
351
 
343
352
  3. **Each teammate's prompt** must use this template:
344
353
 
354
+ Shell safety rule: every dynamic prompt/context payload must be written
355
+ to a temp file with a single-quoted heredoc before invoking Bash. Do not
356
+ splice user text, prior model output, or conversation context into
357
+ double-quoted shell arguments. Model names still go through the safe
358
+ model-name validation from Step 1.
359
+
345
360
  **Binary mode** (`RELAY_MODE = binary`):
346
361
  ```
347
362
  You are a relay worker. Your ONLY job: run the command below via Bash,
@@ -350,8 +365,20 @@ command:
350
365
 
351
366
  Run this now:
352
367
 
353
- phone-a-friend --to <backend> --repo "$PWD" --prompt "<prompt>" \
354
- [--context-text "<context>"] $PAF_NO_DIFF \
368
+ PROMPT_FILE="$(mktemp)"
369
+ CONTEXT_FILE="$(mktemp)"
370
+ trap 'rm -f "$PROMPT_FILE" "$CONTEXT_FILE"' EXIT
371
+
372
+ cat > "$PROMPT_FILE" <<'PAF_TEAM_PROMPT_EOF'
373
+ <prompt>
374
+ PAF_TEAM_PROMPT_EOF
375
+
376
+ cat > "$CONTEXT_FILE" <<'PAF_TEAM_CONTEXT_EOF'
377
+ <context>
378
+ PAF_TEAM_CONTEXT_EOF
379
+
380
+ phone-a-friend --to <backend> --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" \
381
+ [--context-file "$CONTEXT_FILE"] $PAF_NO_DIFF \
355
382
  [--sandbox <mode>] [--model <model>] --fast [--session <SESSION_ID>]
356
383
 
357
384
  Note: for `--to claude`, `--fast` has no effect.
@@ -439,6 +466,61 @@ is identical — only the execution mechanism changes.
439
466
  Execute a do-review-decide loop. Maximum MAX_ROUNDS rounds. Stop early if
440
467
  converged.
441
468
 
469
+ ### Convergence trace
470
+
471
+ Before the loop starts, initialize an in-memory `CONVERGENCE_TRACE` array.
472
+ This trace is local to the current command run; it is not analytics, tracking,
473
+ or persisted product telemetry.
474
+
475
+ After the REVIEW phase of each round, append a verdict envelope (the exact
476
+ same shape used by `phone-a-friend --verdict-json`). The array index is the
477
+ round number minus one, so do not add a separate `round` property to the
478
+ envelope:
479
+
480
+ ```
481
+ CONVERGENCE_TRACE = [] # index = round - 1
482
+
483
+ # Each entry is a verdict envelope:
484
+ {
485
+ "schema_version": 1,
486
+ "verdict": "ship" | "iterate" | "abstain",
487
+ "summary": "<one-sentence synthesis>",
488
+ "findings": [
489
+ { "severity": "blocker" | "important" | "nit",
490
+ "title": "<headline>",
491
+ "rationale": "<why it matters>",
492
+ "location": "<file or file:line> or null" }
493
+ ]
494
+ }
495
+ ```
496
+
497
+ The verdict is **derived from severities**: any `blocker` or `important`
498
+ finding => `iterate`; empty findings or only `nit` findings => `ship`;
499
+ `abstain` only when the reviewer cannot make a confident call AND findings
500
+ is empty. This matches `parseVerdict()` in PaF's `src/verdict.ts`. Do not
501
+ contradict the rule (e.g. do not record verdict=ship while listing a
502
+ blocker — that is a malformed envelope).
503
+
504
+ Two ways to source the verdict envelope per round:
505
+
506
+ 1. **Lead-judged (default)**: the lead orchestrator runs the rubric in
507
+ Phase 2 REVIEW and emits the envelope based on its own judgment. No
508
+ extra relay call. Cheap, fits text-output rounds, suitable for most
509
+ tasks.
510
+ 2. **Backend-judged (optional, file-change rounds)**: when the round
511
+ produced file changes that exist as a git diff, the lead MAY also
512
+ run `phone-a-friend --to <backend> --review --verdict-json` for an
513
+ independent third-party verdict. If used, merge it conservatively with
514
+ the lead-judged envelope: any blocker or important finding from either
515
+ source makes the trace verdict `iterate`, and a backend `ship` verdict
516
+ MUST NOT erase blocker or important findings the lead already found.
517
+ Prefer the backend envelope only when it is stricter than the lead, or
518
+ when the lead abstained and the backend produced a concrete verdict.
519
+ Cap at one verdict-json relay call per round.
520
+
521
+ Both sources produce the same envelope shape, so CONVERGENCE_TRACE is uniform
522
+ either way.
523
+
442
524
  ### Timing Expectations
443
525
 
444
526
  Different backends have different response times:
@@ -487,7 +569,19 @@ Delegate the task to the backend via the relay. The lead's job is to
487
569
 
488
570
  **Binary mode** (`RELAY_MODE = binary`):
489
571
  ```bash
490
- phone-a-friend --to <backend> --repo "$PWD" --prompt "<prompt>" [--context-text "<context>"] $PAF_NO_DIFF [--sandbox <mode>] [--model <model>] --fast [--session <SESSION_ID>]
572
+ PROMPT_FILE="$(mktemp)"
573
+ CONTEXT_FILE="$(mktemp)"
574
+ trap 'rm -f "$PROMPT_FILE" "$CONTEXT_FILE"' EXIT
575
+
576
+ cat > "$PROMPT_FILE" <<'PAF_TEAM_PROMPT_EOF'
577
+ <prompt>
578
+ PAF_TEAM_PROMPT_EOF
579
+
580
+ cat > "$CONTEXT_FILE" <<'PAF_TEAM_CONTEXT_EOF'
581
+ <context>
582
+ PAF_TEAM_CONTEXT_EOF
583
+
584
+ phone-a-friend --to <backend> --repo "$PWD" --prompt "$(cat "$PROMPT_FILE")" [--context-file "$CONTEXT_FILE"] $PAF_NO_DIFF [--sandbox <mode>] [--model <model>] --fast [--session <SESSION_ID>]
491
585
  ```
492
586
 
493
587
  Diff inclusion: `$PAF_NO_DIFF` is set by the probe in "Diff inclusion
@@ -509,21 +603,23 @@ Delegate the task to the backend via the relay. The lead's job is to
509
603
  **Direct mode** (`RELAY_MODE = direct`):
510
604
  ```bash
511
605
  # Codex:
512
- codex exec -C "$PWD" --skip-git-repo-check --sandbox <mode> "<combined-prompt>" < /dev/null
513
- # Gemini (omit --sandbox for workspace-write):
514
- gemini [--sandbox] --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "<combined-prompt>"
606
+ codex exec -C "$PWD" --skip-git-repo-check --sandbox <mode> "$(cat "$PROMPT_FILE")" < /dev/null
607
+ # Gemini (`--sandbox` for read-only/workspace-write; omit only for danger-full-access):
608
+ gemini [--sandbox] --yolo --include-directories "$PWD" --output-format text -m <model> --prompt "$(cat "$PROMPT_FILE")"
515
609
  # Ollama:
610
+ PROMPT_JSON="$(jq -Rs . < "$PROMPT_FILE")"
516
611
  curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" \
517
- -d '{"model":"<OLLAMA_SELECTED_MODEL>","messages":[{"role":"user","content":"<combined-prompt>"}],"stream":false}' \
612
+ -d "{\"model\":\"<OLLAMA_SELECTED_MODEL>\",\"messages\":[{\"role\":\"user\",\"content\":${PROMPT_JSON}}],\"stream\":false}" \
518
613
  | jq -r '.message.content'
519
614
  ```
520
615
 
521
616
  Note: `--fast` and `--session` are not available in direct mode. Direct
522
617
  mode relay calls are always stateless (each round starts fresh).
523
618
 
524
- In direct mode, build `<combined-prompt>` using the template from the
525
- "Direct call reference" section. If `--include-diff` is used, run
526
- `git diff HEAD` and append the output to the template's "Git Diff" section.
619
+ In direct mode, build `PROMPT_FILE` using the template from the "Direct call
620
+ reference" section and the quoted-heredoc rule. If `--include-diff` is used,
621
+ run `git diff HEAD` and append the output to the template's "Git Diff"
622
+ section inside that file.
527
623
 
528
624
  For gemini, omit `--model` by default and let auto-routing pick (see "Gemini model selection" section).
529
625
  For ollama, always include `--model` / model field using `OLLAMA_SELECTED_MODEL` from preflight.
@@ -562,16 +658,47 @@ round 1" on tasks that deserve iteration.
562
658
  the better output, note the disagreement and rationale for selection.
563
659
  - If one backend fails → continue with the successful one, note the failure.
564
660
 
661
+ #### Phase 2.5: Convergence trace snapshot
662
+
663
+ After REVIEW, append the verdict envelope for this round to
664
+ `CONVERGENCE_TRACE` (see "Convergence trace" above). Display a one-line
665
+ summary to the user before moving to DECIDE:
666
+
667
+ ```
668
+ Round N: verdict=<ship|iterate|abstain> | catches: B blocker, I important, X nits
669
+ ```
670
+
671
+ (omit zero-count categories: `Round 2: verdict=iterate | catches: 1 important, 2 nits`).
672
+
673
+ **Diminishing-returns warning** (only when comparing round N to round N-1,
674
+ both with verdict=iterate): if round N has equal-or-more findings than
675
+ round N-1 AND blocker+important counts did not decrease, surface a single
676
+ stderr-style line BEFORE the DECIDE phase:
677
+
678
+ ```
679
+ Round N may not be making progress: same/more catches than round N-1, no severity decrease. Consider stopping.
680
+ ```
681
+
682
+ This is a hint, not a hard stop. The lead may still continue if there is a
683
+ reason (e.g. the round addressed a blocker but introduced an important
684
+ finding). When continuing past the warning, briefly note the rationale in
685
+ the next-round feedback.
686
+
565
687
  #### Phase 3: DECIDE
566
688
 
567
- Based on the review:
689
+ Based on CONVERGENCE_TRACE[round-1].verdict and the review:
568
690
 
569
- - **Converged** (all rubric items pass): Stop the loop. Execute Step 8
691
+ - **Converged** (verdict = `ship`): Stop the loop. Execute Step 8
570
692
  (Cleanup), then Step 9 (Final Synthesis). Do not iterate further — no
571
693
  iterating for its own sake.
572
- - **Issues found** (one or more rubric items fail): Formulate specific,
573
- actionable feedback. Start the next round with this feedback incorporated
574
- into the prompt.
694
+ - **Issues found** (verdict = `iterate`): Formulate specific, actionable
695
+ feedback derived from the round's findings (use the `title` and
696
+ `rationale` of each blocker/important entry). Start the next round
697
+ with this feedback incorporated into the prompt.
698
+ - **Inconclusive** (verdict = `abstain`): The reviewer could not make
699
+ a confident call. Surface what's missing (in `summary`) and either
700
+ request that information from the user OR run one more round with a
701
+ more focused prompt. Do not declare convergence on `abstain`.
575
702
  - **Backend error** (timeout, crash, unexpected failure): Note the failure.
576
703
  If another backend is available, try it. If no backend produced a
577
704
  successful result this round: if a previous round had a usable result,
@@ -753,6 +880,18 @@ a clear synthesis to the user. Include ALL of the following:
753
880
  convergence.
754
881
  5. **Sandbox note**: If `--sandbox workspace-write` was used at any point,
755
882
  note it here.
883
+ 6. **Convergence retrospective** (from `CONVERGENCE_TRACE`):
884
+ - Find the first round whose verdict is `ship`. Call that one-based round
885
+ number `K` (`CONVERGENCE_TRACE` array index + 1).
886
+ - If `K` exists and `K < MAX_ROUNDS`, append:
887
+ `Hint: this run reached "ship" at round K. Try --max-rounds K next time for a similar task.`
888
+ - If `K` does not exist (no ship-verdict in any round), append:
889
+ `Hint: no round reached "ship" — task may need decomposition, more context, or out-of-band work before re-running.`
890
+ - If a diminishing-returns warning fired during the run, mention it
891
+ here too: `Round X did not show progress over round X-1; if you see
892
+ this pattern again, consider --max-rounds (X-1).`
893
+ - Skip the retrospective when only a single round ran (insufficient
894
+ data to make any recommendation).
756
895
 
757
896
  Format the synthesis clearly. The user should understand at a glance what
758
897
  happened and whether the result is complete.
@@ -861,10 +1000,10 @@ The following precedence determines `OLLAMA_SELECTED_MODEL` during preflight:
861
1000
 
862
1001
  1. **`MODEL_OVERRIDE`** (from `--model` flag or NL extraction in Step 1) —
863
1002
  highest priority. Validate against `OLLAMA_AVAILABLE_MODELS`. If not
864
- found, warn but proceed.
1003
+ found, abort and ask the user to choose one of the discovered models.
865
1004
  2. **Config `backends.ollama.model`** — set via TUI model picker or
866
- `phone-a-friend config set`. Validate against available models, warn if
867
- not found.
1005
+ `phone-a-friend config set`. Validate against the safe model-name pattern
1006
+ and available models; abort if invalid or unavailable.
868
1007
  3. **First model from `/api/tags`** — fallback auto-selection.
869
1008
 
870
1009
  - **Do NOT maintain a model priority list** for Ollama. Unlike Gemini, Ollama