agent-control-plane 0.1.16 → 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.
Files changed (47) hide show
  1. package/README.md +93 -14
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +322 -9
  5. package/package.json +1 -1
  6. package/references/architecture.md +8 -0
  7. package/references/control-plane-map.md +6 -2
  8. package/references/release-checklist.md +0 -2
  9. package/tools/bin/agent-github-update-labels +6 -1
  10. package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
  11. package/tools/bin/agent-project-catch-up-merged-prs +77 -21
  12. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
  13. package/tools/bin/agent-project-cleanup-session +84 -0
  14. package/tools/bin/agent-project-heartbeat-loop +10 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +24 -12
  16. package/tools/bin/agent-project-run-claude-session +2 -2
  17. package/tools/bin/agent-project-run-kilo-session +346 -14
  18. package/tools/bin/agent-project-run-ollama-session +658 -0
  19. package/tools/bin/agent-project-run-openclaw-session +27 -25
  20. package/tools/bin/agent-project-run-opencode-session +354 -14
  21. package/tools/bin/agent-project-run-pi-session +479 -0
  22. package/tools/bin/agent-project-worker-status +1 -1
  23. package/tools/bin/flow-config-lib.sh +116 -3
  24. package/tools/bin/flow-resident-worker-lib.sh +1 -1
  25. package/tools/bin/flow-shell-lib.sh +5 -2
  26. package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
  27. package/tools/bin/heartbeat-safe-auto.sh +105 -17
  28. package/tools/bin/install-project-launchd.sh +19 -2
  29. package/tools/bin/prepare-worktree.sh +4 -4
  30. package/tools/bin/profile-activate.sh +2 -2
  31. package/tools/bin/profile-adopt.sh +2 -2
  32. package/tools/bin/project-init.sh +1 -1
  33. package/tools/bin/project-runtimectl.sh +90 -7
  34. package/tools/bin/provider-cooldown-state.sh +14 -14
  35. package/tools/bin/render-flow-config.sh +30 -33
  36. package/tools/bin/run-codex-task.sh +53 -4
  37. package/tools/bin/scaffold-profile.sh +18 -3
  38. package/tools/bin/start-issue-worker.sh +1 -1
  39. package/tools/bin/start-pr-fix-worker.sh +30 -0
  40. package/tools/bin/start-pr-review-worker.sh +31 -0
  41. package/tools/bin/start-resident-issue-loop.sh +4 -4
  42. package/tools/bin/sync-agent-repo.sh +2 -2
  43. package/tools/bin/sync-dependency-baseline.sh +3 -3
  44. package/tools/bin/sync-shared-agent-home.sh +4 -1
  45. package/tools/templates/pr-fix-template.md +3 -7
  46. package/tools/templates/pr-merge-repair-template.md +3 -7
  47. package/tools/templates/pr-review-template.md +2 -1
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage:
7
+ agent-project-run-pi-session --mode safe|bypass --session <id> --worktree <path> --prompt-file <path> --runs-root <path> --adapter-id <id> --task-kind <kind> --task-id <id> [options]
8
+
9
+ Launch a Pi coding agent worker session inside tmux for a project adapter and
10
+ persist the standard run artifacts.
11
+
12
+ Pi supports any OpenRouter model (or other providers) via --model provider/id.
13
+
14
+ Options:
15
+ --env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
16
+ --context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
17
+ --collect-file <name> Copy sandbox artifact file into the host run dir after execution
18
+ --reconcile-command <cmd> Host-side command queued after the worker exits
19
+ --sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .pi-artifacts)
20
+ --pi-model <id> Model ID for pi (e.g. openrouter/qwen/qwen3.6-plus:free)
21
+ --pi-thinking <level> Thinking level: off, minimal, low, medium, high, xhigh (default: low)
22
+ --pi-timeout-seconds <secs> Hard timeout in seconds (default: 900)
23
+ --pi-stall-seconds <secs> Abort if no output for this long, 0 disables (default: 300)
24
+ --help Show this help
25
+ EOF
26
+ }
27
+
28
+ mode=""
29
+ session=""
30
+ worktree=""
31
+ prompt_file=""
32
+ runs_root=""
33
+ adapter_id=""
34
+ task_kind=""
35
+ task_id=""
36
+ env_prefix=""
37
+ sandbox_subdir=".pi-artifacts"
38
+ reconcile_command=""
39
+ pi_model="${ACP_PI_MODEL:-${F_LOSNING_PI_MODEL:-openrouter/qwen/qwen3.6-plus:free}}"
40
+ pi_thinking="${ACP_PI_THINKING:-${F_LOSNING_PI_THINKING:-low}}"
41
+ pi_timeout_seconds="${ACP_PI_TIMEOUT_SECONDS:-${F_LOSNING_PI_TIMEOUT_SECONDS:-900}}"
42
+ pi_stall_seconds="${ACP_PI_STALL_SECONDS:-${F_LOSNING_PI_STALL_SECONDS:-300}}"
43
+ declare -a context_items=()
44
+ declare -a collect_files=()
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case "$1" in
48
+ --mode) mode="${2:-}"; shift 2 ;;
49
+ --session) session="${2:-}"; shift 2 ;;
50
+ --worktree) worktree="${2:-}"; shift 2 ;;
51
+ --prompt-file) prompt_file="${2:-}"; shift 2 ;;
52
+ --runs-root) runs_root="${2:-}"; shift 2 ;;
53
+ --adapter-id) adapter_id="${2:-}"; shift 2 ;;
54
+ --task-kind) task_kind="${2:-}"; shift 2 ;;
55
+ --task-id) task_id="${2:-}"; shift 2 ;;
56
+ --env-prefix) env_prefix="${2:-}"; shift 2 ;;
57
+ --context) context_items+=("${2:-}"); shift 2 ;;
58
+ --collect-file) collect_files+=("${2:-}"); shift 2 ;;
59
+ --reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
60
+ --sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
61
+ --pi-model) pi_model="${2:-}"; shift 2 ;;
62
+ --pi-thinking) pi_thinking="${2:-}"; shift 2 ;;
63
+ --pi-timeout-seconds) pi_timeout_seconds="${2:-}"; shift 2 ;;
64
+ --pi-stall-seconds) pi_stall_seconds="${2:-}"; shift 2 ;;
65
+ --help|-h) usage; exit 0 ;;
66
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
67
+ esac
68
+ done
69
+
70
+ if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
71
+ usage >&2
72
+ exit 1
73
+ fi
74
+
75
+ case "$mode" in
76
+ safe|bypass) ;;
77
+ *) echo "--mode must be safe or bypass" >&2; exit 1 ;;
78
+ esac
79
+
80
+ case "$pi_thinking" in
81
+ off|minimal|low|medium|high|xhigh) ;;
82
+ *) echo "--pi-thinking must be one of: off, minimal, low, medium, high, xhigh" >&2; exit 1 ;;
83
+ esac
84
+
85
+ case "$pi_timeout_seconds" in
86
+ ''|*[!0-9]*|0) echo "--pi-timeout-seconds must be a positive integer" >&2; exit 1 ;;
87
+ esac
88
+
89
+ case "$pi_stall_seconds" in
90
+ ''|*[!0-9]*) echo "--pi-stall-seconds must be numeric" >&2; exit 1 ;;
91
+ esac
92
+
93
+ resolve_pi_bin() {
94
+ local configured_bin="${PI_BIN:-${ACP_PI_BIN:-${F_LOSNING_PI_BIN:-}}}"
95
+ if [[ -n "${configured_bin}" && -x "${configured_bin}" ]]; then
96
+ printf '%s\n' "${configured_bin}"
97
+ return 0
98
+ fi
99
+ if command -v pi >/dev/null 2>&1; then
100
+ command -v pi
101
+ return 0
102
+ fi
103
+ local -a fallback_paths=(
104
+ "/opt/homebrew/bin/pi"
105
+ "/usr/local/bin/pi"
106
+ "${HOME}/.local/bin/pi"
107
+ "${HOME}/.nvm/versions/node/$(node --version 2>/dev/null || true)/bin/pi"
108
+ )
109
+ local p
110
+ for p in "${fallback_paths[@]}"; do
111
+ if [[ -x "${p}" ]]; then
112
+ printf '%s\n' "${p}"
113
+ return 0
114
+ fi
115
+ done
116
+ return 1
117
+ }
118
+
119
+ pi_bin="$(resolve_pi_bin || true)"
120
+ if [[ -z "${pi_bin}" || ! -x "${pi_bin}" ]]; then
121
+ echo "unable to resolve a runnable pi binary — install with: npm install -g @mariozechner/pi-coding-agent" >&2
122
+ exit 1
123
+ fi
124
+
125
+ artifact_dir="${runs_root}/${session}"
126
+ output_file="${artifact_dir}/${session}.log"
127
+ inner_script="${artifact_dir}/${session}.sh"
128
+ meta_file="${artifact_dir}/run.env"
129
+ result_file="${artifact_dir}/result.env"
130
+ runner_state_file="${artifact_dir}/runner.env"
131
+ sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
132
+ retained_repo_root="${ACP_RETAINED_REPO_ROOT:-${F_LOSNING_RETAINED_REPO_ROOT:-}}"
133
+ started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
134
+
135
+ mkdir -p "$artifact_dir"
136
+ mkdir -p "$sandbox_run_dir"
137
+
138
+ if tmux has-session -t "$session" 2>/dev/null; then
139
+ echo "tmux session already exists: $session" >&2
140
+ exit 1
141
+ fi
142
+
143
+ branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
144
+
145
+ printf -v session_q '%q' "$session"
146
+ printf -v task_kind_q '%q' "$task_kind"
147
+ printf -v task_id_q '%q' "$task_id"
148
+ printf -v mode_q '%q' "$mode"
149
+ printf -v worktree_q '%q' "$worktree"
150
+ printf -v prompt_q '%q' "$prompt_file"
151
+ printf -v output_q '%q' "$output_file"
152
+ printf -v artifact_dir_q '%q' "$artifact_dir"
153
+ printf -v script_q '%q' "$inner_script"
154
+ printf -v result_q '%q' "$result_file"
155
+ printf -v meta_file_q '%q' "$meta_file"
156
+ printf -v runner_state_q '%q' "$runner_state_file"
157
+ printf -v branch_q '%q' "$branch_name"
158
+ printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
159
+ printf -v retained_repo_root_q '%q' "$retained_repo_root"
160
+ printf -v adapter_id_q '%q' "$adapter_id"
161
+ printf -v started_at_q '%q' "$started_at"
162
+ printf -v pi_bin_q '%q' "$pi_bin"
163
+ printf -v pi_model_q '%q' "$pi_model"
164
+ printf -v pi_thinking_q '%q' "$pi_thinking"
165
+ printf -v pi_timeout_q '%q' "$pi_timeout_seconds"
166
+ printf -v pi_stall_q '%q' "$pi_stall_seconds"
167
+
168
+ {
169
+ printf 'TASK_KIND=%s\n' "$task_kind_q"
170
+ printf 'TASK_ID=%s\n' "$task_id_q"
171
+ printf 'SESSION=%s\n' "$session_q"
172
+ printf 'MODE=%s\n' "$mode_q"
173
+ printf 'WORKTREE=%s\n' "$worktree_q"
174
+ printf 'PROMPT_FILE=%s\n' "$prompt_q"
175
+ printf 'OUTPUT_FILE=%s\n' "$output_q"
176
+ printf 'SCRIPT=%s\n' "$script_q"
177
+ printf 'BRANCH=%s\n' "$branch_q"
178
+ printf 'RESULT_FILE=%s\n' "$result_q"
179
+ printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
180
+ printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
181
+ printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
182
+ printf 'STARTED_AT=%s\n' "$started_at_q"
183
+ printf 'PI_BIN=%s\n' "$pi_bin_q"
184
+ printf 'PI_MODEL=%s\n' "$pi_model_q"
185
+ printf 'PI_THINKING=%s\n' "$pi_thinking_q"
186
+ printf 'PI_TIMEOUT_SECONDS=%s\n' "$pi_timeout_q"
187
+ printf 'PI_STALL_SECONDS=%s\n' "$pi_stall_q"
188
+ } >"$meta_file"
189
+
190
+ context_exports=""
191
+ if ((${#context_items[@]} > 0)); then
192
+ for item in "${context_items[@]}"; do
193
+ if [[ "$item" != *=* ]]; then
194
+ echo "--context must use KEY=VALUE syntax: $item" >&2
195
+ exit 1
196
+ fi
197
+ key="${item%%=*}"
198
+ value="${item#*=}"
199
+ if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
200
+ echo "Invalid context key: $key" >&2
201
+ exit 1
202
+ fi
203
+ printf -v value_q '%q' "$value"
204
+ printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
205
+ if [[ -n "$env_prefix" ]]; then
206
+ context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
207
+ fi
208
+ context_exports+="export ACP_${key}=${value_q}"$'\n'
209
+ if [[ "$env_prefix" != "F_LOSNING_" ]]; then
210
+ context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
211
+ fi
212
+ done
213
+ fi
214
+
215
+ runtime_exports=$(
216
+ cat <<EOF
217
+ export AGENT_PROJECT_SESSION=${session_q}
218
+ export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
219
+ export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
220
+ export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
221
+ export AGENT_PROJECT_PI_BIN=${pi_bin_q}
222
+ export AGENT_PROJECT_RETAINED_REPO_ROOT=${retained_repo_root_q}
223
+ export ACP_SESSION=${session_q}
224
+ export ACP_RUN_DIR=${sandbox_run_dir_q}
225
+ export ACP_HOST_RUN_DIR=${artifact_dir_q}
226
+ export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
227
+ export ACP_PI_BIN=${pi_bin_q}
228
+ export ACP_PI_MODEL=${pi_model_q}
229
+ export ACP_PI_THINKING=${pi_thinking_q}
230
+ export ACP_PI_TIMEOUT_SECONDS=${pi_timeout_q}
231
+ export ACP_RETAINED_REPO_ROOT=${retained_repo_root_q}
232
+ export F_LOSNING_SESSION=${session_q}
233
+ export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
234
+ export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
235
+ export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
236
+ export F_LOSNING_PI_BIN=${pi_bin_q}
237
+ export F_LOSNING_PI_MODEL=${pi_model_q}
238
+ export F_LOSNING_PI_THINKING=${pi_thinking_q}
239
+ export F_LOSNING_PI_TIMEOUT_SECONDS=${pi_timeout_q}
240
+ export F_LOSNING_RETAINED_REPO_ROOT=${retained_repo_root_q}
241
+ EOF
242
+ )
243
+
244
+ if [[ -n "$env_prefix" ]]; then
245
+ runtime_exports+=$'\n'
246
+ runtime_exports+=$(cat <<EOF
247
+ export ${env_prefix}SESSION=${session_q}
248
+ export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
249
+ export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
250
+ export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
251
+ export ${env_prefix}PI_BIN=${pi_bin_q}
252
+ export ${env_prefix}PI_MODEL=${pi_model_q}
253
+ export ${env_prefix}PI_THINKING=${pi_thinking_q}
254
+ export ${env_prefix}PI_TIMEOUT_SECONDS=${pi_timeout_q}
255
+ EOF
256
+ )
257
+ fi
258
+
259
+ collect_copy_snippet=""
260
+ if ((${#collect_files[@]} > 0)); then
261
+ for artifact_name in "${collect_files[@]}"; do
262
+ [[ -z "$artifact_name" ]] && continue
263
+ printf -v artifact_q '%q' "$artifact_name"
264
+ collect_copy_snippet+=$(
265
+ cat <<EOF
266
+ if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
267
+ cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
268
+ fi
269
+ EOF
270
+ )
271
+ collect_copy_snippet+=$'\n'
272
+ done
273
+ fi
274
+
275
+ # Always collect result.env from sandbox to artifact_dir
276
+ collect_copy_snippet+=$(
277
+ cat <<EOF
278
+ if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
279
+ cp ${sandbox_run_dir_q}/result.env ${result_q}
280
+ fi
281
+ EOF
282
+ )
283
+
284
+ reconcile_snippet=""
285
+ if [[ -n "$reconcile_command" ]]; then
286
+ printf -v delayed_reconcile_q '%q' "export ACP_EXPECTED_RUN_STARTED_AT=${started_at_q}; export F_LOSNING_EXPECTED_RUN_STARTED_AT=${started_at_q}; while tmux has-session -t ${session_q} 2>/dev/null; do sleep 1; done; sleep 2; $reconcile_command"
287
+ reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
288
+ fi
289
+
290
+ cat >"$inner_script" <<EOF
291
+ #!/usr/bin/env bash
292
+ set -euo pipefail
293
+ ${runtime_exports}
294
+ ${context_exports}cd ${worktree_q}
295
+
296
+ runner_state_file=${runner_state_q}
297
+ output_file=${output_q}
298
+ sandbox_run_dir=${sandbox_run_dir_q}
299
+ artifact_dir=${artifact_dir_q}
300
+ result_file_path=${sandbox_run_dir_q}/result.env
301
+ host_result_file=${result_q}
302
+ pi_bin=${pi_bin_q}
303
+ pi_model=${pi_model_q}
304
+ pi_thinking=${pi_thinking_q}
305
+ pi_timeout_seconds=${pi_timeout_q}
306
+ pi_stall_seconds=${pi_stall_q}
307
+ prompt_file=${prompt_q}
308
+ worktree=${worktree_q}
309
+
310
+ write_state() {
311
+ local runner_state="\${1:?runner state required}"
312
+ local last_exit_code="\${2:-}"
313
+ local failure_reason="\${3:-}"
314
+ local updated_at tmp_file
315
+
316
+ updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
317
+ tmp_file="\${runner_state_file}.tmp.\$\$"
318
+ {
319
+ printf 'RUNNER_STATE=%q\n' "\${runner_state}"
320
+ printf 'ATTEMPT=1\n'
321
+ printf 'RESUME_COUNT=0\n'
322
+ printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
323
+ printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
324
+ printf 'LAST_TRIGGER_REASON=%q\n' ''
325
+ printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
326
+ printf 'UPDATED_AT=%q\n' "\${updated_at}"
327
+ } >"\${tmp_file}"
328
+ mv "\${tmp_file}" "\${runner_state_file}"
329
+ }
330
+
331
+ write_result_fallback() {
332
+ local detail="\${1:-missing-result-contract}"
333
+ local tmp_file
334
+ tmp_file="\${result_file_path}.tmp.\$\$"
335
+ {
336
+ printf 'OUTCOME=blocked\n'
337
+ printf 'ACTION=host-comment-blocker\n'
338
+ printf 'DETAIL=%s\n' "\${detail}"
339
+ } >"\${tmp_file}"
340
+ mv "\${tmp_file}" "\${result_file_path}"
341
+ }
342
+
343
+ record_final_git_state() {
344
+ local final_head final_branch tmp_file
345
+ final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
346
+ final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
347
+ tmp_file=${meta_file_q}.tmp.final.$$
348
+ grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
349
+ {
350
+ printf 'FINAL_HEAD=%q\n' "\${final_head}"
351
+ printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
352
+ } >>"\${tmp_file}"
353
+ mv "\${tmp_file}" ${meta_file_q}
354
+ }
355
+
356
+ ${reconcile_snippet}
357
+
358
+ write_state running
359
+
360
+ mkdir -p "\${sandbox_run_dir}"
361
+
362
+ # Run pi in print mode (non-interactive, single-shot)
363
+ # --no-session: ephemeral, don't persist session state
364
+ # --thinking: reasoning depth
365
+ # @prompt_file: pass prompt as file reference
366
+ pi_exit_code=0
367
+ # macOS does not ship 'timeout'; prefer it when available, else use background watchdog
368
+ if command -v timeout >/dev/null 2>&1; then
369
+ timeout "\${pi_timeout_seconds}" \\
370
+ "\${pi_bin}" --print --no-session \\
371
+ --model "\${pi_model}" \\
372
+ --thinking "\${pi_thinking}" \\
373
+ "@\${prompt_file}" \\
374
+ 2>&1 | tee -a "\${output_file}" || pi_exit_code=\$?
375
+ elif command -v gtimeout >/dev/null 2>&1; then
376
+ gtimeout "\${pi_timeout_seconds}" \\
377
+ "\${pi_bin}" --print --no-session \\
378
+ --model "\${pi_model}" \\
379
+ --thinking "\${pi_thinking}" \\
380
+ "@\${prompt_file}" \\
381
+ 2>&1 | tee -a "\${output_file}" || pi_exit_code=\$?
382
+ else
383
+ "\${pi_bin}" --print --no-session \\
384
+ --model "\${pi_model}" \\
385
+ --thinking "\${pi_thinking}" \\
386
+ "@\${prompt_file}" \\
387
+ 2>&1 >> "\${output_file}" &
388
+ _pi_bgpid=\$!
389
+ # Hard timeout watchdog
390
+ ( sleep "\${pi_timeout_seconds}" && kill "\${_pi_bgpid}" 2>/dev/null \\
391
+ && printf '[pi] timed out after %s seconds\n' "\${pi_timeout_seconds}" >> "\${output_file}" ) &
392
+ _pi_timeout_wd=\$!
393
+ # Stall watchdog: kill if output file stops growing for pi_stall_seconds
394
+ if [[ "\${pi_stall_seconds}" -gt 0 ]]; then
395
+ (
396
+ _prev_size=-1
397
+ while kill -0 "\${_pi_bgpid}" 2>/dev/null; do
398
+ _cur_size="\$(wc -c < "\${output_file}" 2>/dev/null || echo 0)"
399
+ if [[ "\${_cur_size}" -eq "\${_prev_size}" ]]; then
400
+ if [[ -z "\${_idle_since:-}" ]]; then _idle_since="\$(date +%s)"; fi
401
+ if (( \$(date +%s) - _idle_since >= pi_stall_seconds )); then
402
+ printf '[pi] no output for %s seconds — aborting stalled worker\n' "\${pi_stall_seconds}" >> "\${output_file}"
403
+ kill "\${_pi_bgpid}" 2>/dev/null
404
+ break
405
+ fi
406
+ else
407
+ _idle_since=""
408
+ _prev_size="\${_cur_size}"
409
+ fi
410
+ sleep 5
411
+ done
412
+ ) &
413
+ _pi_stall_wd=\$!
414
+ fi
415
+ wait "\${_pi_bgpid}" || pi_exit_code=\$?
416
+ kill "\${_pi_timeout_wd}" 2>/dev/null || true
417
+ wait "\${_pi_timeout_wd}" 2>/dev/null || true
418
+ if [[ -n "\${_pi_stall_wd:-}" ]]; then
419
+ kill "\${_pi_stall_wd}" 2>/dev/null || true
420
+ wait "\${_pi_stall_wd}" 2>/dev/null || true
421
+ fi
422
+ if [[ "\${pi_exit_code}" -eq 143 ]]; then
423
+ pi_exit_code=124
424
+ fi
425
+ fi
426
+
427
+ if [[ "\${pi_exit_code}" -eq 0 ]]; then
428
+ # Pi runs in --print mode and cannot write result.env itself.
429
+ # If the result file is missing, write a blocked fallback so reconcile
430
+ # sees a valid contract instead of an invalid-result-contract failure.
431
+ if [[ ! -f "\${result_file_path}" ]]; then
432
+ write_result_fallback "missing-result-contract"
433
+ fi
434
+ write_state succeeded 0
435
+ else
436
+ failure_reason="pi-exit-\${pi_exit_code}"
437
+ if [[ "\${pi_exit_code}" -eq 124 ]]; then
438
+ failure_reason="timeout"
439
+ fi
440
+ if [[ ! -f "\${result_file_path}" ]]; then
441
+ write_result_fallback "\${failure_reason}"
442
+ fi
443
+ write_state failed "\${pi_exit_code}" "\${failure_reason}"
444
+ fi
445
+
446
+ record_final_git_state
447
+
448
+ ${collect_copy_snippet}
449
+ printf '\n__CODEX_EXIT__:%s\n' "\${pi_exit_code}" | tee -a "\${output_file}"
450
+ exit "\${pi_exit_code}"
451
+ EOF
452
+
453
+ chmod +x "$inner_script"
454
+
455
+ # Write initial runner state
456
+ {
457
+ printf 'RUNNER_STATE=%q\n' "running"
458
+ printf 'ATTEMPT=1\n'
459
+ printf 'RESUME_COUNT=0\n'
460
+ printf "LAST_EXIT_CODE=''\n"
461
+ printf "LAST_FAILURE_REASON=''\n"
462
+ printf "LAST_TRIGGER_REASON=''\n"
463
+ printf "AUTH_WAIT_STARTED_AT=''\n"
464
+ printf 'UPDATED_AT=%q\n' "$started_at"
465
+ printf 'RUNNER_STATE_FILE=%q\n' "$runner_state_file"
466
+ } >"$runner_state_file"
467
+
468
+ # Append pi-specific metadata to run.env for dashboard/status visibility
469
+ {
470
+ printf 'PI_MODEL=%s\n' "$pi_model_q"
471
+ printf 'PI_THINKING=%s\n' "$pi_thinking_q"
472
+ printf 'PI_TIMEOUT_SECONDS=%s\n' "$pi_timeout_q"
473
+ printf 'PI_STALL_SECONDS=%s\n' "$pi_stall_q"
474
+ printf 'PI_BIN=%s\n' "$pi_bin_q"
475
+ } >>"$meta_file"
476
+
477
+ tmux new-session -d -s "$session" -x 220 -y 50 \
478
+ "bash -l $script_q >> $output_q 2>&1; tmux wait-for -S $session_q-done" \; \
479
+ wait-for "$session-done" || true
@@ -13,7 +13,7 @@ EOF
13
13
 
14
14
  runs_root="${AGENT_PROJECT_RUNS_ROOT:-}"
15
15
  session=""
16
- exit_marker="__CODEX_EXIT__:"
16
+ exit_marker="__\\w+_EXIT__:"
17
17
 
18
18
  while [[ $# -gt 0 ]]; do
19
19
  case "$1" in
@@ -1699,6 +1699,48 @@ flow_provider_pool_openclaw_timeout_seconds() {
1699
1699
  flow_provider_pool_value "${config_file}" "${pool_name}" "openclaw.timeout_seconds"
1700
1700
  }
1701
1701
 
1702
+ flow_provider_pool_ollama_model() {
1703
+ local config_file="${1:?config file required}"
1704
+ local pool_name="${2:?pool name required}"
1705
+
1706
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.model"
1707
+ }
1708
+
1709
+ flow_provider_pool_ollama_base_url() {
1710
+ local config_file="${1:?config file required}"
1711
+ local pool_name="${2:?pool name required}"
1712
+
1713
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.base_url"
1714
+ }
1715
+
1716
+ flow_provider_pool_ollama_timeout_seconds() {
1717
+ local config_file="${1:?config file required}"
1718
+ local pool_name="${2:?pool name required}"
1719
+
1720
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.timeout_seconds"
1721
+ }
1722
+
1723
+ flow_provider_pool_pi_model() {
1724
+ local config_file="${1:?config file required}"
1725
+ local pool_name="${2:?pool name required}"
1726
+
1727
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.model"
1728
+ }
1729
+
1730
+ flow_provider_pool_pi_thinking() {
1731
+ local config_file="${1:?config file required}"
1732
+ local pool_name="${2:?pool name required}"
1733
+
1734
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.thinking"
1735
+ }
1736
+
1737
+ flow_provider_pool_pi_timeout_seconds() {
1738
+ local config_file="${1:?config file required}"
1739
+ local pool_name="${2:?pool name required}"
1740
+
1741
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.timeout_seconds"
1742
+ }
1743
+
1702
1744
  flow_sanitize_provider_key() {
1703
1745
  local raw_key="${1:?raw key required}"
1704
1746
 
@@ -1723,6 +1765,12 @@ flow_provider_pool_model_identity() {
1723
1765
  openclaw)
1724
1766
  flow_provider_pool_openclaw_model "${config_file}" "${pool_name}"
1725
1767
  ;;
1768
+ ollama)
1769
+ flow_provider_pool_ollama_model "${config_file}" "${pool_name}"
1770
+ ;;
1771
+ pi)
1772
+ flow_provider_pool_pi_model "${config_file}" "${pool_name}"
1773
+ ;;
1726
1774
  *)
1727
1775
  printf '\n'
1728
1776
  ;;
@@ -1756,6 +1804,12 @@ flow_provider_pool_state_get() {
1756
1804
  local openclaw_model=""
1757
1805
  local openclaw_thinking=""
1758
1806
  local openclaw_timeout_seconds=""
1807
+ local ollama_model=""
1808
+ local ollama_base_url=""
1809
+ local ollama_timeout_seconds=""
1810
+ local pi_model=""
1811
+ local pi_thinking=""
1812
+ local pi_timeout_seconds=""
1759
1813
 
1760
1814
  backend="$(flow_provider_pool_backend "${config_file}" "${pool_name}")"
1761
1815
  safe_profile="$(flow_provider_pool_safe_profile "${config_file}" "${pool_name}")"
@@ -1769,6 +1823,12 @@ flow_provider_pool_state_get() {
1769
1823
  openclaw_model="$(flow_provider_pool_openclaw_model "${config_file}" "${pool_name}")"
1770
1824
  openclaw_thinking="$(flow_provider_pool_openclaw_thinking "${config_file}" "${pool_name}")"
1771
1825
  openclaw_timeout_seconds="$(flow_provider_pool_openclaw_timeout_seconds "${config_file}" "${pool_name}")"
1826
+ ollama_model="$(flow_provider_pool_ollama_model "${config_file}" "${pool_name}")"
1827
+ ollama_base_url="$(flow_provider_pool_ollama_base_url "${config_file}" "${pool_name}")"
1828
+ ollama_timeout_seconds="$(flow_provider_pool_ollama_timeout_seconds "${config_file}" "${pool_name}")"
1829
+ pi_model="$(flow_provider_pool_pi_model "${config_file}" "${pool_name}")"
1830
+ pi_thinking="$(flow_provider_pool_pi_thinking "${config_file}" "${pool_name}")"
1831
+ pi_timeout_seconds="$(flow_provider_pool_pi_timeout_seconds "${config_file}" "${pool_name}")"
1772
1832
  model="$(flow_provider_pool_model_identity "${config_file}" "${pool_name}")"
1773
1833
 
1774
1834
  case "${backend}" in
@@ -1781,6 +1841,12 @@ flow_provider_pool_state_get() {
1781
1841
  openclaw)
1782
1842
  [[ -n "${openclaw_model}" && -n "${openclaw_thinking}" && -n "${openclaw_timeout_seconds}" ]] || valid="no"
1783
1843
  ;;
1844
+ ollama)
1845
+ [[ -n "${ollama_model}" ]] || valid="no"
1846
+ ;;
1847
+ pi)
1848
+ [[ -n "${pi_model}" ]] || valid="no"
1849
+ ;;
1784
1850
  *)
1785
1851
  valid="no"
1786
1852
  ;;
@@ -1833,6 +1899,12 @@ flow_provider_pool_state_get() {
1833
1899
  printf 'OPENCLAW_MODEL=%s\n' "${openclaw_model}"
1834
1900
  printf 'OPENCLAW_THINKING=%s\n' "${openclaw_thinking}"
1835
1901
  printf 'OPENCLAW_TIMEOUT_SECONDS=%s\n' "${openclaw_timeout_seconds}"
1902
+ printf 'OLLAMA_MODEL=%s\n' "${ollama_model}"
1903
+ printf 'OLLAMA_BASE_URL=%s\n' "${ollama_base_url}"
1904
+ printf 'OLLAMA_TIMEOUT_SECONDS=%s\n' "${ollama_timeout_seconds}"
1905
+ printf 'PI_MODEL=%s\n' "${pi_model}"
1906
+ printf 'PI_THINKING=%s\n' "${pi_thinking}"
1907
+ printf 'PI_TIMEOUT_SECONDS=%s\n' "${pi_timeout_seconds}"
1836
1908
  }
1837
1909
 
1838
1910
  flow_selected_provider_pool_env() {
@@ -2040,11 +2112,17 @@ flow_export_execution_env() {
2040
2112
  local openclaw_thinking=""
2041
2113
  local openclaw_timeout=""
2042
2114
  local openclaw_stall=""
2115
+ local ollama_model=""
2116
+ local ollama_base_url=""
2117
+ local ollama_timeout=""
2118
+ local pi_model=""
2119
+ local pi_thinking=""
2120
+ local pi_timeout=""
2043
2121
 
2044
2122
  repo_id="$(flow_resolve_repo_id "${config_file}")"
2045
2123
  provider_quota_cooldowns="$(flow_resolve_provider_quota_cooldowns "${config_file}")"
2046
2124
  provider_pool_order="$(flow_resolve_provider_pool_order "${config_file}")"
2047
- explicit_coding_worker="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-}}"
2125
+ explicit_coding_worker="${ACP_CODING_WORKER:-}"
2048
2126
  if [[ -z "${explicit_coding_worker}" && -n "${provider_pool_order}" ]]; then
2049
2127
  provider_pool_selection="$(flow_selected_provider_pool_env "${config_file}" || true)"
2050
2128
  fi
@@ -2073,11 +2151,17 @@ flow_export_execution_env() {
2073
2151
  openclaw_thinking="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_THINKING")"
2074
2152
  openclaw_timeout="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_TIMEOUT_SECONDS")"
2075
2153
  openclaw_stall="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_STALL_SECONDS")"
2154
+ ollama_model="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_MODEL")"
2155
+ ollama_base_url="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_BASE_URL")"
2156
+ ollama_timeout="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_TIMEOUT_SECONDS")"
2157
+ pi_model="$(flow_kv_get "${provider_pool_selection}" "PI_MODEL")"
2158
+ pi_thinking="$(flow_kv_get "${provider_pool_selection}" "PI_THINKING")"
2159
+ pi_timeout="$(flow_kv_get "${provider_pool_selection}" "PI_TIMEOUT_SECONDS")"
2076
2160
  else
2077
2161
  if [[ -n "${explicit_coding_worker}" ]]; then
2078
2162
  active_provider_selection_reason="env-override"
2079
2163
  fi
2080
- coding_worker="$(flow_env_or_config "${config_file}" "ACP_CODING_WORKER F_LOSNING_CODING_WORKER" "execution.coding_worker" "")"
2164
+ coding_worker="$(flow_env_or_config "${config_file}" "ACP_CODING_WORKER" "execution.coding_worker" "")"
2081
2165
  safe_profile="$(flow_env_or_config "${config_file}" "ACP_CODEX_PROFILE_SAFE F_LOSNING_CODEX_PROFILE_SAFE" "execution.safe_profile" "")"
2082
2166
  bypass_profile="$(flow_env_or_config "${config_file}" "ACP_CODEX_PROFILE_BYPASS F_LOSNING_CODEX_PROFILE_BYPASS" "execution.bypass_profile" "")"
2083
2167
  claude_model="$(flow_env_or_config "${config_file}" "ACP_CLAUDE_MODEL F_LOSNING_CLAUDE_MODEL" "execution.claude.model" "")"
@@ -2090,10 +2174,15 @@ flow_export_execution_env() {
2090
2174
  openclaw_thinking="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING" "execution.openclaw.thinking" "")"
2091
2175
  openclaw_timeout="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS" "execution.openclaw.timeout_seconds" "")"
2092
2176
  openclaw_stall="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS" "execution.openclaw.stall_seconds" "")"
2177
+ ollama_model="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_MODEL F_LOSNING_OLLAMA_MODEL" "execution.ollama.model" "")"
2178
+ ollama_base_url="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_BASE_URL F_LOSNING_OLLAMA_BASE_URL" "execution.ollama.base_url" "")"
2179
+ ollama_timeout="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_TIMEOUT_SECONDS F_LOSNING_OLLAMA_TIMEOUT_SECONDS" "execution.ollama.timeout_seconds" "")"
2180
+ pi_model="$(flow_env_or_config "${config_file}" "ACP_PI_MODEL F_LOSNING_PI_MODEL" "execution.pi.model" "")"
2181
+ pi_thinking="$(flow_env_or_config "${config_file}" "ACP_PI_THINKING F_LOSNING_PI_THINKING" "execution.pi.thinking" "")"
2182
+ pi_timeout="$(flow_env_or_config "${config_file}" "ACP_PI_TIMEOUT_SECONDS F_LOSNING_PI_TIMEOUT_SECONDS" "execution.pi.timeout_seconds" "")"
2093
2183
  fi
2094
2184
 
2095
2185
  if [[ -n "${coding_worker}" ]]; then
2096
- export F_LOSNING_CODING_WORKER="${coding_worker}"
2097
2186
  export ACP_CODING_WORKER="${coding_worker}"
2098
2187
  fi
2099
2188
  if [[ -n "${repo_id}" ]]; then
@@ -2174,6 +2263,30 @@ flow_export_execution_env() {
2174
2263
  export F_LOSNING_OPENCLAW_STALL_SECONDS="${openclaw_stall}"
2175
2264
  export ACP_OPENCLAW_STALL_SECONDS="${openclaw_stall}"
2176
2265
  fi
2266
+ if [[ -n "${ollama_model}" ]]; then
2267
+ export F_LOSNING_OLLAMA_MODEL="${ollama_model}"
2268
+ export ACP_OLLAMA_MODEL="${ollama_model}"
2269
+ fi
2270
+ if [[ -n "${ollama_base_url}" ]]; then
2271
+ export F_LOSNING_OLLAMA_BASE_URL="${ollama_base_url}"
2272
+ export ACP_OLLAMA_BASE_URL="${ollama_base_url}"
2273
+ fi
2274
+ if [[ -n "${ollama_timeout}" ]]; then
2275
+ export F_LOSNING_OLLAMA_TIMEOUT_SECONDS="${ollama_timeout}"
2276
+ export ACP_OLLAMA_TIMEOUT_SECONDS="${ollama_timeout}"
2277
+ fi
2278
+ if [[ -n "${pi_model}" ]]; then
2279
+ export F_LOSNING_PI_MODEL="${pi_model}"
2280
+ export ACP_PI_MODEL="${pi_model}"
2281
+ fi
2282
+ if [[ -n "${pi_thinking}" ]]; then
2283
+ export F_LOSNING_PI_THINKING="${pi_thinking}"
2284
+ export ACP_PI_THINKING="${pi_thinking}"
2285
+ fi
2286
+ if [[ -n "${pi_timeout}" ]]; then
2287
+ export F_LOSNING_PI_TIMEOUT_SECONDS="${pi_timeout}"
2288
+ export ACP_PI_TIMEOUT_SECONDS="${pi_timeout}"
2289
+ fi
2177
2290
 
2178
2291
  flow_export_github_cli_auth_env "$(flow_resolve_repo_slug "${config_file}")"
2179
2292
  flow_export_project_env_aliases