ypi 0.5.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to ypi are documented here.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## [0.5.1] - 2026-03-23
7
+
8
+ ### Fixed
9
+ - **macOS mktemp compatibility**: BSD `mktemp` does not allow characters after the `XXXXXX` template suffix — moved `XXXXXX` to end of all templates and use `${TMPDIR:-/tmp}` for portable temp directory resolution
10
+ - **Bash 3.2 unbound variable crash**: empty array expansion under `set -u` fails on macOS default bash — build argv incrementally with length checks in `ypi` launcher
11
+
6
12
  ## [0.5.0] - 2026-02-15
7
13
 
8
14
  ### Added
package/README.md CHANGED
@@ -173,13 +173,32 @@ jj git push # Push to GitHub
173
173
  ### Testing
174
174
 
175
175
  ```bash
176
- make test-fast # 54 tests, no LLM calls, seconds
177
- make test-e2e # Real LLM calls, costs ~$0.05
178
- make test # Both
176
+ make test-fast # unit + guardrails
177
+ make test-extensions # extension compatibility with installed pi
178
+ make pre-push-checks # shared local/CI gate (recommended before push)
179
+ make test-e2e # real LLM calls, costs money
180
+ make test # all of the above
181
+ ```
182
+
183
+ Install hooks once per clone to run checks automatically on git push:
184
+ ```bash
185
+ make install-hooks
186
+ ```
187
+
188
+ Release/update helper:
189
+ ```bash
190
+ make release-preflight # same checks + upstream dry-run in one command
191
+ make land # deterministic-ish landing helper
179
192
  ```
180
193
 
181
194
  **Before any change to `rlm_query`:** run `make test-fast`. After: run it again. `rlm_query` is a live dependency of the agent's own execution — breaking it breaks the agent.
182
195
 
196
+ CI helper commands:
197
+ ```bash
198
+ make ci-status N=15 # recent workflow runs
199
+ make ci-last-failure # dump latest failing workflow log
200
+ ```
201
+
183
202
 
184
203
  ### History
185
204
 
package/SYSTEM_PROMPT.md CHANGED
@@ -19,13 +19,15 @@ If a `$CONTEXT` file is set, it contains data relevant to your task. Treat it li
19
19
  **Core pattern: size up → search → delegate → combine**
20
20
  1. **Size up the problem** – How big is it? Can you do it directly, or does it need decomposition? For files: `wc -l` / `wc -c`. For code tasks: how many files, how complex?
21
21
  2. **Search & explore** – `grep`, `find`, `ls`, `head` — orient yourself before diving in.
22
- 3. **Delegate** – use `rlm_query` to hand sub‑tasks to child agents. Two patterns:
22
+ 3. **Delegate** – use `rlm_query` to hand sub‑tasks to child agents. Three patterns:
23
23
  ```bash
24
- # Pipe data as the child's context
24
+ # Pipe data as the child's context (synchronous — blocks until done)
25
25
  sed -n '100,200p' bigfile.txt | rlm_query "Summarize this section"
26
-
27
- # Child inherits your environment (files, cwd, $CONTEXT)
26
+ # Child inherits your environment (synchronous)
28
27
  rlm_query "Refactor the error handling in src/api.py"
28
+ # ASYNC — returns immediately, child runs in background (PREFERRED for parallel work)
29
+ rlm_query --async "Write tests for the auth module"
30
+ # Returns: {"job_id": "...", "output": "/tmp/...", "sentinel": "/tmp/...done", "pid": 12345}
29
31
  ```
30
32
  4. **Combine** – aggregate results, deduplicate, resolve conflicts, produce the final output.
31
33
  5. **Do it directly when it's small** – don't delegate what you can do in one step.
@@ -44,11 +46,11 @@ cat src/config.py
44
46
  ```bash
45
47
  # Find all files that need updating
46
48
  grep -rl "old_api_call" src/
47
-
48
- # Delegate each file to a sub-agent (each gets its own jj workspace)
49
+ # Delegate each file to a sub-agent using --async (non-blocking)
49
50
  for f in $(grep -rl "old_api_call" src/); do
50
- rlm_query "In $f, replace all old_api_call() with new_api_call(). Update the imports too."
51
- done
51
+ rlm_query --async "In $f, replace all old_api_call() with new_api_call(). Update the imports. Then jj commit -m 'refactor: $f'"
52
+ done
53
+ # Children run in parallel, each in its own jj workspace. Check sentinels for completion.
52
54
  ```
53
55
 
54
56
  **Example 3 – Large file analysis, chunk and search**
@@ -61,17 +63,27 @@ grep -n "ERROR\|FATAL" data/logs.txt
61
63
  sed -n '480,600p' data/logs.txt | rlm_query "What caused this error? Suggest a fix."
62
64
  ```
63
65
 
64
- **Example 4 – Parallel sub-tasks with different goals**
66
+ **Example 4 – Parallel sub-tasks with --async (PREFERRED)**
65
67
  ```bash
66
- # Break a complex task into independent pieces
67
- SUMMARY=$(rlm_query "Read README.md and summarize what this project does in one paragraph.")
68
- ISSUES=$(rlm_query "Run the test suite and report any failures.")
69
- DEPS=$(rlm_query "Check for outdated dependencies in package.json.")
70
-
71
- # Combine into a report
72
- echo "Summary: $SUMMARY"
73
- echo "Test issues: $ISSUES"
74
- echo "Dependency status: $DEPS"
68
+ # Break a complex task into independent pieces — all run in parallel
69
+ JOB1=$(rlm_query --async "Read README.md and summarize what this project does in one paragraph.")
70
+ JOB2=$(rlm_query --async "Run the test suite and report any failures.")
71
+ JOB3=$(rlm_query --async "Check for outdated dependencies in package.json.")
72
+
73
+ # Each returns immediately with {"job_id", "output", "sentinel", "pid"}
74
+ # Check completion non-blockingly:
75
+ for JOB in "$JOB1" "$JOB2" "$JOB3"; do
76
+ SENTINEL=$(echo "$JOB" | python3 -c "import sys,json; print(json.load(sys.stdin)['sentinel'])")
77
+ OUTPUT=$(echo "$JOB" | python3 -c "import sys,json; print(json.load(sys.stdin)['output'])")
78
+ [ -f "$SENTINEL" ] && echo "Done: $(cat $OUTPUT)" || echo "Still running..."
79
+ done
80
+ ```
81
+
82
+ **Example 5 – Sequential sub-tasks (when order matters)**
83
+ ```bash
84
+ # Use synchronous rlm_query ONLY when each step depends on the previous
85
+ SUMMARY=$(rlm_query "Read README.md and summarize what this project does.")
86
+ ISSUES=$(rlm_query "Given this summary: $SUMMARY — what are the main risks?")
75
87
  ```
76
88
 
77
89
  **Example 5 – Iterative chunking over a huge file**
@@ -97,6 +109,7 @@ done
97
109
  - **Write files directly** with `write` or standard Bash redirection; do **not** merely describe the change.
98
110
  - When you need to create or modify multiple files, perform each action explicitly (e.g., `echo >> file`, `sed -i`, `cat > newfile`).
99
111
  - Any sub‑agents you spawn via `rlm_query` inherit their own jj workspaces, so their edits are also isolated.
112
+ - **Always commit before exiting** — if you're in a jj workspace, run `jj commit -m 'description'` before you finish. Uncommitted work is **lost** when the workspace is forgotten on exit.
100
113
 
101
114
  ## SECTION 4 – Guardrails & Cost Awareness
102
115
  - **RLM_TIMEOUT** – if set, respect the remaining wall‑clock budget; avoid long‑running loops.
@@ -115,8 +128,16 @@ done
115
128
  rlm_sessions grep <pattern> # search across sessions
116
129
  ```
117
130
  Available for debugging and reviewing what other agents in the tree have done.
131
+ - **`rlm_cleanup`** – clean up stale temp files and jj workspaces from previous rlm_query runs:
132
+ ```bash
133
+ rlm_cleanup # dry-run: show what would be cleaned
134
+ rlm_cleanup --force # actually delete stale files and workspace dirs
135
+ rlm_cleanup --age 60 # override age threshold (default: 120 min)
136
+ ```
137
+ Run this when the machine feels slow or /tmp is filling up. The reaper in `rlm_query` runs automatically at depth 0, but this lets you trigger it manually or with a different age threshold.
118
138
  - **Depth awareness** – at deeper `RLM_DEPTH` levels, prefer **direct actions** (e.g., file edits, single‑pass searches) over spawning many sub‑agents.
119
139
  - Always **clean up temporary files** and respect `trap` handlers defined by the infrastructure.
140
+ - **NEVER run `rlm_query` in a foreground for-loop** — this blocks the parent's conversation for the entire duration. Use `rlm_query --async` for parallel work. Synchronous `rlm_query` is only for single calls or when you need the result immediately for the next step.
120
141
 
121
142
  ## SECTION 5 – Rules
122
143
  1. **Search before reading** – `grep`, `wc -l`, `head` before `cat` or unbounded `read`. Never ingest a file you haven’t sized up. If it’s over 50 lines, search for what you need instead of reading it all.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ypi",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "ypi — a recursive coding agent. Pi that can call itself via rlm_query.",
5
5
  "license": "MIT",
6
6
  "author": "Raymond Weitekamp",
package/rlm_query CHANGED
@@ -9,6 +9,8 @@
9
9
  # echo "some text" | rlm_query "What is the main topic?"
10
10
  # sed -n '100,200p' "$CONTEXT" | rlm_query "Summarize this section"
11
11
  # rlm_query --fork "Continue working on this refactor"
12
+ # rlm_query --async "Analyze the codebase" # returns immediately with job JSON
13
+ # echo "data" | rlm_query --async --notify $$ "Summarize this"
12
14
  #
13
15
  # If stdin has data (piped), that becomes the child's context.
14
16
  # Otherwise, the child inherits the parent's $CONTEXT file.
@@ -16,6 +18,9 @@
16
18
  # Flags:
17
19
  # --fork Fork parent session into child (carries conversation history)
18
20
  # Default: fresh session per child (only data context, no history)
21
+ # --async Spawn child in background, return immediately with job JSON
22
+ # Output goes to a temp file; sentinel touched when done
23
+ # --notify PID With --async: write result to target PID's peer inbox when done
19
24
  #
20
25
  # Environment:
21
26
  # RLM_DEPTH — current recursion depth (default: 0)
@@ -50,14 +55,18 @@ rlm_error() { echo "✗ $1" >&2; [ -n "${2:-}" ] && echo " Why: $2" >&2; [ -n "
50
55
  # Parse flags
51
56
  # ----------------------------------------------------------------------
52
57
  FORK=false
58
+ ASYNC=false
59
+ NOTIFY_PID=""
53
60
  while [[ "${1:-}" == --* ]]; do
54
61
  case "$1" in
55
62
  --fork) FORK=true; shift ;;
63
+ --async) ASYNC=true; shift ;;
64
+ --notify) NOTIFY_PID="$2"; shift 2 ;;
56
65
  *) break ;;
57
66
  esac
58
67
  done
59
68
 
60
- PROMPT="${1:?Usage: rlm_query [--fork] \"your prompt here\"}"
69
+ PROMPT="${1:?Usage: rlm_query [--fork] [--async] [--notify PID] \"your prompt here\"}"
61
70
 
62
71
  # ----------------------------------------------------------------------
63
72
  # Depth guard — refuse to go beyond max depth
@@ -72,6 +81,19 @@ if [ "$NEXT_DEPTH" -gt "$MAX_DEPTH" ]; then
72
81
  exit 1
73
82
  fi
74
83
 
84
+ # ----------------------------------------------------------------------
85
+ # Stale temp reaper — runs once per root invocation (depth 0 only)
86
+ # Cleans up leaked files from crashed/killed processes (SIGKILL skips traps)
87
+ # ----------------------------------------------------------------------
88
+ if [ "$DEPTH" -eq 0 ]; then
89
+ (
90
+ # Delete rlm temp files older than 2 hours whose parent process is gone
91
+ find /tmp -maxdepth 1 -name 'rlm_*' -mmin +120 -delete 2>/dev/null
92
+ # Remove stale jj workspace directories older than 2 hours
93
+ find /tmp -maxdepth 1 -name 'rlm_ws_*' -type d -mmin +120 -exec rm -rf {} + 2>/dev/null
94
+ ) &
95
+ fi
96
+
75
97
  PROVIDER="${RLM_PROVIDER:-}"
76
98
  MODEL="${RLM_MODEL:-}"
77
99
  SYSTEM_PROMPT_FILE="${RLM_SYSTEM_PROMPT:-}"
@@ -117,7 +139,7 @@ fi
117
139
 
118
140
  # Initialize cost file if budget is set but no file exists yet
119
141
  if [ -n "${RLM_BUDGET:-}" ] && [ -z "${RLM_COST_FILE:-}" ]; then
120
- export RLM_COST_FILE=$(mktemp /tmp/rlm_cost_XXXXXX.jsonl)
142
+ export RLM_COST_FILE=$(mktemp "${TMPDIR:-/tmp}/rlm_cost.XXXXXX")
121
143
  fi
122
144
 
123
145
  # ----------------------------------------------------------------------
@@ -149,12 +171,12 @@ fi
149
171
  # ----------------------------------------------------------------------
150
172
  # Temporary child context file
151
173
  # ----------------------------------------------------------------------
152
- CHILD_CONTEXT=$(mktemp /tmp/rlm_ctx_d${NEXT_DEPTH}_XXXXXX.txt)
174
+ CHILD_CONTEXT=$(mktemp "${TMPDIR:-/tmp}/rlm_ctx_d${NEXT_DEPTH}.XXXXXX")
153
175
  COMBINED_PROMPT=""
154
176
 
155
177
  # Write prompt to a file for symbolic access — agents can grep/sed the original
156
178
  # question instead of relying on in-context token copying.
157
- PROMPT_FILE=$(mktemp /tmp/rlm_prompt_d${NEXT_DEPTH}_XXXXXX.txt)
179
+ PROMPT_FILE=$(mktemp "${TMPDIR:-/tmp}/rlm_prompt_d${NEXT_DEPTH}.XXXXXX")
158
180
  echo "$PROMPT" > "$PROMPT_FILE"
159
181
 
160
182
  # ----------------------------------------------------------------------
@@ -166,7 +188,7 @@ if [ "${RLM_JJ:-1}" != "0" ] \
166
188
  && command -v jj &>/dev/null \
167
189
  && jj root &>/dev/null 2>&1; then
168
190
  JJ_WS_NAME="rlm-d${NEXT_DEPTH}-$$"
169
- JJ_WORKSPACE=$(mktemp -d /tmp/rlm_ws_d${NEXT_DEPTH}_XXXXXX)
191
+ JJ_WORKSPACE=$(mktemp -d "${TMPDIR:-/tmp}/rlm_ws_d${NEXT_DEPTH}.XXXXXX")
170
192
  if ! jj workspace add --name "$JJ_WS_NAME" "$JJ_WORKSPACE" &>/dev/null; then
171
193
  JJ_WORKSPACE=""
172
194
  JJ_WS_NAME=""
@@ -174,6 +196,8 @@ if [ "${RLM_JJ:-1}" != "0" ] \
174
196
  fi
175
197
 
176
198
  # Cleanup: remove temp context + forget jj workspace (updated in run section below)
199
+ # In async mode, the child needs these files — skip cleanup (child cleans up after itself)
200
+ if [ "$ASYNC" != true ]; then
177
201
  trap '{
178
202
  rm -f "$CHILD_CONTEXT" "$PROMPT_FILE"
179
203
  rm -f "${COMBINED_PROMPT:-}"
@@ -181,20 +205,28 @@ trap '{
181
205
  jj workspace forget "$JJ_WS_NAME" 2>/dev/null || true
182
206
  fi
183
207
  }' EXIT
208
+ fi
184
209
  trap 'rlm_error "Interrupted" "Received signal" "Re-run the command"; exit 130' INT TERM
185
210
 
186
211
  # ----------------------------------------------------------------------
187
212
  # Detect piped stdin
188
213
  # ----------------------------------------------------------------------
189
214
  HAS_STDIN=false
190
- if [ -p /dev/stdin ]; then
215
+ if [ -n "${RLM_STDIN:-}" ]; then
191
216
  HAS_STDIN=true
192
- elif [ -n "${RLM_STDIN:-}" ]; then
217
+ elif [ -p /dev/stdin ]; then
193
218
  HAS_STDIN=true
194
219
  fi
195
220
 
196
221
  if [ "$HAS_STDIN" = true ]; then
197
222
  cat > "$CHILD_CONTEXT"
223
+
224
+ # Some CI runners expose stdin as an empty pipe even when nothing was piped
225
+ # to rlm_query. In that case, preserve inherited CONTEXT unless stdin was
226
+ # explicitly signaled via RLM_STDIN.
227
+ if [ ! -s "$CHILD_CONTEXT" ] && [ -z "${RLM_STDIN:-}" ] && [ -n "${CONTEXT:-}" ] && [ -f "${CONTEXT:-}" ]; then
228
+ cp "$CONTEXT" "$CHILD_CONTEXT"
229
+ fi
198
230
  else
199
231
  if [ -n "${CONTEXT:-}" ] && [ -f "${CONTEXT:-}" ]; then
200
232
  cp "$CONTEXT" "$CHILD_CONTEXT"
@@ -265,7 +297,7 @@ fi
265
297
  # Build combined system prompt with rlm_query source embedded
266
298
  COMBINED_PROMPT=""
267
299
  if [ -n "$SYSTEM_PROMPT_FILE" ] && [ -f "$SYSTEM_PROMPT_FILE" ]; then
268
- COMBINED_PROMPT=$(mktemp /tmp/rlm_system_prompt_XXXXXX.md)
300
+ COMBINED_PROMPT=$(mktemp "${TMPDIR:-/tmp}/rlm_system_prompt.XXXXXX")
269
301
  cat "$SYSTEM_PROMPT_FILE" > "$COMBINED_PROMPT"
270
302
  SELF_SOURCE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/rlm_query"
271
303
  if [ -f "$SELF_SOURCE" ]; then
@@ -301,11 +333,63 @@ if [ -n "$JJ_WORKSPACE" ]; then
301
333
  cd "$JJ_WORKSPACE"
302
334
  fi
303
335
 
336
+ # ----------------------------------------------------------------------
337
+ # Async mode — spawn child in background and return immediately
338
+ # ----------------------------------------------------------------------
339
+ if [ "$ASYNC" = true ]; then
340
+ ASYNC_ID="rlm_async_${RLM_TRACE_ID}_c${RLM_CALL_COUNT}_$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')"
341
+ ASYNC_OUTPUT="/tmp/${ASYNC_ID}.txt"
342
+ ASYNC_SENTINEL="/tmp/${ASYNC_ID}.done"
343
+
344
+ # Build the full command
345
+ CHILD_CMD=(pi "${CMD_ARGS[@]}" "$PROMPT")
346
+ if [ -n "$TIMEOUT_CMD" ]; then
347
+ CHILD_CMD=($TIMEOUT_CMD "${CHILD_CMD[@]}")
348
+ fi
349
+
350
+ # Spawn in background: run child, capture output, touch sentinel, optionally notify
351
+ (
352
+ "${CHILD_CMD[@]}" > "$ASYNC_OUTPUT" 2>&1
353
+ touch "$ASYNC_SENTINEL"
354
+
355
+ # If --notify PID was given, write to that peer's inbox
356
+ if [ -n "$NOTIFY_PID" ]; then
357
+ INBOX_DIR=$(find /tmp/pi_peer_* -maxdepth 0 -type d 2>/dev/null | while read d; do
358
+ if [ -f "$d/meta.json" ] && grep -q "\"pid\":$NOTIFY_PID" "$d/meta.json" 2>/dev/null; then
359
+ echo "$d"; break
360
+ fi
361
+ done)
362
+ if [ -n "$INBOX_DIR" ] && [ -d "$INBOX_DIR" ]; then
363
+ RESULT=$(cat "$ASYNC_OUTPUT" | tail -c 50000)
364
+ MSG=$(cat <<MSGJSON
365
+ {"from_pid":$$,"from_project":"rlm_query","message":"[rlm_query --async result]\n\n$RESULT","timestamp":"$(date -Iseconds)","id":"async_${ASYNC_ID}"}
366
+ MSGJSON
367
+ )
368
+ echo "$MSG" >> "$INBOX_DIR/inbox.jsonl"
369
+ fi
370
+ fi
371
+
372
+ # Clean up temp files the parent skipped
373
+ rm -f "$CHILD_CONTEXT" "$PROMPT_FILE" "${COMBINED_PROMPT:-}"
374
+ if [ -n "$JJ_WS_NAME" ]; then
375
+ jj workspace forget "$JJ_WS_NAME" 2>/dev/null || true
376
+ fi
377
+ ) &
378
+ ASYNC_PID=$!
379
+ disown $ASYNC_PID 2>/dev/null || true
380
+
381
+ # Return job metadata immediately
382
+ cat <<EOF
383
+ {"job_id": "$ASYNC_ID", "output": "$ASYNC_OUTPUT", "sentinel": "$ASYNC_SENTINEL", "pid": $ASYNC_PID}
384
+ EOF
385
+ exit 0
386
+ fi
387
+
304
388
  # ----------------------------------------------------------------------
305
389
  # Run child Pi — JSON mode (default) or plain text
306
390
  # JSON mode streams text to stdout and captures cost via fd 3.
307
391
  # ----------------------------------------------------------------------
308
- COST_OUT=$(mktemp /tmp/rlm_cost_out_XXXXXX.json)
392
+ COST_OUT=$(mktemp "${TMPDIR:-/tmp}/rlm_cost_out.XXXXXX")
309
393
  trap '{
310
394
  rm -f "$CHILD_CONTEXT" "$PROMPT_FILE"
311
395
  rm -f "${COMBINED_PROMPT:-}"
package/ypi CHANGED
@@ -66,7 +66,7 @@ mkdir -p "$RLM_SESSION_DIR"
66
66
 
67
67
  # Build combined system prompt: SYSTEM_PROMPT.md + rlm_query source
68
68
  # This way the agent sees the full implementation, not just usage docs.
69
- COMBINED_PROMPT=$(mktemp /tmp/ypi_system_prompt_XXXXXX.md)
69
+ COMBINED_PROMPT=$(mktemp "${TMPDIR:-/tmp}/ypi_system_prompt.XXXXXX")
70
70
  trap 'rm -f "$COMBINED_PROMPT"' EXIT
71
71
 
72
72
  cat "$SCRIPT_DIR/SYSTEM_PROMPT.md" > "$COMBINED_PROMPT"
@@ -94,6 +94,7 @@ fi
94
94
  # We append to the combined prompt file rather than passing through,
95
95
  # since pi already gets --system-prompt from us.
96
96
  PASS_ARGS=()
97
+ QUIET="${YPI_QUIET:-0}"
97
98
  while [[ $# -gt 0 ]]; do
98
99
  case "$1" in
99
100
  --append-system-prompt)
@@ -101,9 +102,13 @@ while [[ $# -gt 0 ]]; do
101
102
  printf '\n%s\n' "$1" >> "$COMBINED_PROMPT"
102
103
  shift
103
104
  ;;
105
+ --quiet|-q)
106
+ QUIET=1
107
+ shift
108
+ ;;
104
109
  --system-prompt)
105
110
  # User overriding ypi's system prompt entirely
106
- echo "⚠️ Overriding ypi's system prompt. Did you mean --append-system-prompt?" >&2
111
+ [ "$QUIET" != "1" ] && echo "⚠️ Overriding ypi's system prompt. Did you mean --append-system-prompt?" >&2
107
112
  shift
108
113
  cat "$1" > "$COMBINED_PROMPT" 2>/dev/null || echo "$1" > "$COMBINED_PROMPT"
109
114
  shift
@@ -116,4 +121,7 @@ while [[ $# -gt 0 ]]; do
116
121
  done
117
122
  # Launch Pi with the combined system prompt, passing all args through
118
123
  # User's own extensions (hashline, etc.) are discovered automatically by Pi.
119
- exec pi --system-prompt "$COMBINED_PROMPT" "${YPI_EXT_ARGS[@]}" "${PASS_ARGS[@]}"
124
+ PI_ARGV=(pi --system-prompt "$COMBINED_PROMPT")
125
+ [ ${#YPI_EXT_ARGS[@]} -gt 0 ] && PI_ARGV+=("${YPI_EXT_ARGS[@]}")
126
+ [ ${#PASS_ARGS[@]} -gt 0 ] && PI_ARGV+=("${PASS_ARGS[@]}")
127
+ exec "${PI_ARGV[@]}"