agent-control-plane 0.1.16 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -14
- package/bin/pr-risk.sh +28 -6
- package/hooks/heartbeat-hooks.sh +62 -22
- package/npm/bin/agent-control-plane.js +360 -10
- package/package.json +6 -3
- package/references/architecture.md +8 -0
- package/references/control-plane-map.md +6 -2
- package/references/release-checklist.md +0 -2
- package/tools/bin/agent-github-update-labels +6 -1
- package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
- package/tools/bin/agent-project-catch-up-merged-prs +78 -21
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
- package/tools/bin/agent-project-cleanup-session +132 -4
- package/tools/bin/agent-project-heartbeat-loop +116 -1461
- package/tools/bin/agent-project-reconcile-issue-session +90 -117
- package/tools/bin/agent-project-reconcile-pr-session +76 -111
- package/tools/bin/agent-project-run-claude-session +12 -2
- package/tools/bin/agent-project-run-codex-resilient +86 -9
- package/tools/bin/agent-project-run-codex-session +16 -5
- package/tools/bin/agent-project-run-kilo-session +356 -14
- package/tools/bin/agent-project-run-ollama-session +658 -0
- package/tools/bin/agent-project-run-openclaw-session +37 -25
- package/tools/bin/agent-project-run-opencode-session +364 -14
- package/tools/bin/agent-project-run-pi-session +479 -0
- package/tools/bin/agent-project-worker-status +11 -8
- package/tools/bin/cleanup-worktree.sh +6 -1
- package/tools/bin/flow-config-lib.sh +196 -3
- package/tools/bin/flow-resident-worker-lib.sh +120 -2
- package/tools/bin/flow-shell-lib.sh +29 -2
- package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
- package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
- package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
- package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
- package/tools/bin/heartbeat-recovery-preflight.sh +13 -1
- package/tools/bin/heartbeat-safe-auto.sh +119 -20
- package/tools/bin/install-project-launchd.sh +19 -2
- package/tools/bin/prepare-worktree.sh +4 -4
- package/tools/bin/profile-activate.sh +2 -2
- package/tools/bin/profile-adopt.sh +2 -2
- package/tools/bin/project-init.sh +1 -1
- package/tools/bin/project-launchd-bootstrap.sh +11 -8
- package/tools/bin/project-runtimectl.sh +90 -7
- package/tools/bin/provider-cooldown-state.sh +14 -14
- package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
- package/tools/bin/render-flow-config.sh +30 -33
- package/tools/bin/resident-issue-controller-lib.sh +448 -0
- package/tools/bin/resident-issue-queue-status.py +35 -0
- package/tools/bin/run-codex-task.sh +53 -4
- package/tools/bin/scaffold-profile.sh +18 -3
- package/tools/bin/start-issue-worker.sh +1 -1
- package/tools/bin/start-pr-fix-worker.sh +30 -0
- package/tools/bin/start-pr-review-worker.sh +31 -0
- package/tools/bin/start-resident-issue-loop.sh +27 -438
- package/tools/bin/sync-agent-repo.sh +2 -2
- package/tools/bin/sync-dependency-baseline.sh +3 -3
- package/tools/bin/sync-shared-agent-home.sh +4 -1
- package/tools/dashboard/app.js +7 -0
- package/tools/dashboard/dashboard_snapshot.py +13 -29
- package/tools/templates/pr-fix-template.md +3 -7
- package/tools/templates/pr-merge-repair-template.md +3 -7
- package/tools/templates/pr-review-template.md +2 -1
- package/SKILL.md +0 -149
|
@@ -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="
|
|
16
|
+
exit_marker="__\\w+_EXIT__:"
|
|
17
17
|
|
|
18
18
|
while [[ $# -gt 0 ]]; do
|
|
19
19
|
case "$1" in
|
|
@@ -117,13 +117,6 @@ if [[ "$status" == "UNKNOWN" && -f "$output_file" ]]; then
|
|
|
117
117
|
fi
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
-
if [[ "$status" == "UNKNOWN" && -z "$failure_reason" ]]; then
|
|
121
|
-
failure_reason="$(failure_reason_from_output || true)"
|
|
122
|
-
if [[ -n "$failure_reason" ]]; then
|
|
123
|
-
status="FAILED"
|
|
124
|
-
fi
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
120
|
if [[ "$status" == "UNKNOWN" && -n "$runner_state" ]]; then
|
|
128
121
|
case "$runner_state" in
|
|
129
122
|
running|waiting-auth-refresh|switching-account)
|
|
@@ -133,6 +126,7 @@ if [[ "$status" == "UNKNOWN" && -n "$runner_state" ]]; then
|
|
|
133
126
|
# Check BEFORE stale result.env to avoid false SUCCEEDED when a prior
|
|
134
127
|
# cycle's result.env happens to exist.
|
|
135
128
|
status="FAILED"
|
|
129
|
+
failure_reason="$(failure_reason_from_output || true)"
|
|
136
130
|
if [[ -z "$failure_reason" ]]; then
|
|
137
131
|
failure_reason="runner-aborted-before-completion"
|
|
138
132
|
fi
|
|
@@ -146,10 +140,19 @@ fi
|
|
|
146
140
|
if [[ "$status" == "UNKNOWN" && -f "$result_file" ]]; then
|
|
147
141
|
# A worker that managed to persist result.env already completed its contract,
|
|
148
142
|
# even if the tmux session disappeared before the exit marker was flushed.
|
|
143
|
+
# Check BEFORE failure_reason_from_output so that a completed result.env
|
|
144
|
+
# is not overridden by transient failure text in the log.
|
|
149
145
|
status="SUCCEEDED"
|
|
150
146
|
result_only_completion="yes"
|
|
151
147
|
fi
|
|
152
148
|
|
|
149
|
+
if [[ "$status" == "UNKNOWN" && -z "$failure_reason" ]]; then
|
|
150
|
+
failure_reason="$(failure_reason_from_output || true)"
|
|
151
|
+
if [[ -n "$failure_reason" ]]; then
|
|
152
|
+
status="FAILED"
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
153
156
|
if [[ "$status" == "UNKNOWN" && -f "$output_file" ]]; then
|
|
154
157
|
if rg -qi "You've hit your usage limit|You have reached your Codex usage limits|visit https://chatgpt.com/codex/settings/usage|Upgrade to Pro|rate limit exceeded|quota exceeded|usage cap (reached|exceeded)|usage quota (reached|exceeded)" "$output_file"; then
|
|
155
158
|
status="FAILED"
|
|
@@ -37,11 +37,16 @@ if [[ -n "$SESSION" ]]; then
|
|
|
37
37
|
ARGS+=(--session "$SESSION")
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
+
cleanup_exit=0
|
|
40
41
|
AGENT_PROJECT_WORKTREE_ROOT="$WORKTREE_ROOT" \
|
|
41
42
|
F_LOSNING_WORKTREE_ROOT="$WORKTREE_ROOT" \
|
|
42
|
-
bash "${FLOW_TOOLS_DIR}/agent-project-cleanup-session" "${ARGS[@]}" >/dev/null
|
|
43
|
+
bash "${FLOW_TOOLS_DIR}/agent-project-cleanup-session" "${ARGS[@]}" >/dev/null || cleanup_exit=$?
|
|
43
44
|
|
|
44
45
|
F_LOSNING_AGENT_REPO_ROOT="$AGENT_REPO_ROOT" \
|
|
45
46
|
F_LOSNING_RETAINED_REPO_ROOT="$RETAINED_REPO_ROOT" \
|
|
46
47
|
F_LOSNING_VSCODE_WORKSPACE_FILE="$VSCODE_WORKSPACE_FILE" \
|
|
47
48
|
"${FLOW_TOOLS_DIR}/sync-vscode-workspace.sh" >/dev/null 2>&1 || true
|
|
49
|
+
|
|
50
|
+
if [[ "$cleanup_exit" -ne 0 ]]; then
|
|
51
|
+
exit "$cleanup_exit"
|
|
52
|
+
fi
|