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
|
@@ -38,6 +38,23 @@ auth_refresh_poll_seconds="${ACP_CODEX_AUTH_REFRESH_POLL_SECONDS:-${F_LOSNING_CO
|
|
|
38
38
|
max_quota_autoswitch_attempts="${ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-${F_LOSNING_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-1}}"
|
|
39
39
|
codex_progress_heartbeat_seconds="${ACP_CODEX_PROGRESS_HEARTBEAT_SECONDS:-${F_LOSNING_CODEX_PROGRESS_HEARTBEAT_SECONDS:-30}}"
|
|
40
40
|
codex_stall_seconds="${ACP_CODEX_STALL_SECONDS:-${F_LOSNING_CODEX_STALL_SECONDS:-300}}"
|
|
41
|
+
python_bin=""
|
|
42
|
+
|
|
43
|
+
resolve_python_bin() {
|
|
44
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
45
|
+
command -v python3
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
if [[ -x /opt/homebrew/bin/python3 ]]; then
|
|
49
|
+
printf '%s\n' "/opt/homebrew/bin/python3"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
if command -v python >/dev/null 2>&1; then
|
|
53
|
+
command -v python
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
41
58
|
|
|
42
59
|
while [[ $# -gt 0 ]]; do
|
|
43
60
|
case "$1" in
|
|
@@ -92,6 +109,12 @@ case "$codex_stall_seconds" in
|
|
|
92
109
|
''|*[!0-9]*) echo "ACP_CODEX_STALL_SECONDS must be numeric" >&2; exit 1 ;;
|
|
93
110
|
esac
|
|
94
111
|
|
|
112
|
+
python_bin="$(resolve_python_bin || true)"
|
|
113
|
+
if [[ -z "$python_bin" || ! -x "$python_bin" ]]; then
|
|
114
|
+
echo "unable to resolve a runnable python interpreter for codex supervision" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
95
118
|
FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
|
|
96
119
|
state_file="${host_run_dir}/runner.env"
|
|
97
120
|
auth_file="${HOME}/.codex/auth.json"
|
|
@@ -177,7 +200,7 @@ run_with_timeout() {
|
|
|
177
200
|
local timeout_seconds="${1:?timeout seconds required}"
|
|
178
201
|
shift
|
|
179
202
|
|
|
180
|
-
|
|
203
|
+
"$python_bin" - "$timeout_seconds" "$@" <<'PY'
|
|
181
204
|
import os
|
|
182
205
|
import signal
|
|
183
206
|
import subprocess
|
|
@@ -220,6 +243,60 @@ sys.exit(proc.returncode)
|
|
|
220
243
|
PY
|
|
221
244
|
}
|
|
222
245
|
|
|
246
|
+
stat_file_size() {
|
|
247
|
+
local path="${1:?path required}"
|
|
248
|
+
local value=""
|
|
249
|
+
|
|
250
|
+
value="$(stat -f %z "$path" 2>/dev/null || true)"
|
|
251
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
252
|
+
printf '%s\n' "$value"
|
|
253
|
+
return 0
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
value="$(stat -c %s "$path" 2>/dev/null || true)"
|
|
257
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
258
|
+
printf '%s\n' "$value"
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
"$python_bin" - "$path" <<'PY'
|
|
263
|
+
import os
|
|
264
|
+
import sys
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
print(os.path.getsize(sys.argv[1]))
|
|
268
|
+
except OSError:
|
|
269
|
+
print("0")
|
|
270
|
+
PY
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
stat_file_mtime() {
|
|
274
|
+
local path="${1:?path required}"
|
|
275
|
+
local value=""
|
|
276
|
+
|
|
277
|
+
value="$(stat -f %m "$path" 2>/dev/null || true)"
|
|
278
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
279
|
+
printf '%s\n' "$value"
|
|
280
|
+
return 0
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
value="$(stat -c %Y "$path" 2>/dev/null || true)"
|
|
284
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
285
|
+
printf '%s\n' "$value"
|
|
286
|
+
return 0
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
"$python_bin" - "$path" <<'PY'
|
|
290
|
+
import os
|
|
291
|
+
import sys
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
print(int(os.path.getmtime(sys.argv[1])))
|
|
295
|
+
except OSError:
|
|
296
|
+
print("0")
|
|
297
|
+
PY
|
|
298
|
+
}
|
|
299
|
+
|
|
223
300
|
auth_fingerprint() {
|
|
224
301
|
if [[ ! -f "$auth_file" ]]; then
|
|
225
302
|
printf 'missing\n'
|
|
@@ -227,8 +304,8 @@ auth_fingerprint() {
|
|
|
227
304
|
fi
|
|
228
305
|
|
|
229
306
|
local mtime size sha
|
|
230
|
-
mtime="$(
|
|
231
|
-
size="$(
|
|
307
|
+
mtime="$(stat_file_mtime "$auth_file" 2>/dev/null || printf '0')"
|
|
308
|
+
size="$(stat_file_size "$auth_file" 2>/dev/null || printf '0')"
|
|
232
309
|
sha="$(shasum -a 256 "$auth_file" | awk '{print $1}')"
|
|
233
310
|
printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
|
|
234
311
|
}
|
|
@@ -256,8 +333,8 @@ quota_switch_signature() {
|
|
|
256
333
|
fi
|
|
257
334
|
|
|
258
335
|
local mtime size sha
|
|
259
|
-
mtime="$(
|
|
260
|
-
size="$(
|
|
336
|
+
mtime="$(stat_file_mtime "$quota_switch_state_file" 2>/dev/null || printf '0')"
|
|
337
|
+
size="$(stat_file_size "$quota_switch_state_file" 2>/dev/null || printf '0')"
|
|
261
338
|
sha="$(shasum -a 256 "$quota_switch_state_file" | awk '{print $1}')"
|
|
262
339
|
printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
|
|
263
340
|
}
|
|
@@ -390,7 +467,7 @@ run_quota_autoswitch() {
|
|
|
390
467
|
new_output_since() {
|
|
391
468
|
local start_size="${1:?start size required}"
|
|
392
469
|
local file_size
|
|
393
|
-
file_size="$(
|
|
470
|
+
file_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
|
|
394
471
|
if (( file_size <= start_size )); then
|
|
395
472
|
return 0
|
|
396
473
|
fi
|
|
@@ -456,7 +533,7 @@ stream_codex_exec() {
|
|
|
456
533
|
local progress_file=""
|
|
457
534
|
local line=""
|
|
458
535
|
|
|
459
|
-
last_attempt_start_size="$(
|
|
536
|
+
last_attempt_start_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
|
|
460
537
|
last_attempt_started_epoch="$(date +%s)"
|
|
461
538
|
progress_file="${host_run_dir}/.codex-progress.$$"
|
|
462
539
|
rm -f "$progress_file"
|
|
@@ -514,7 +591,7 @@ stream_codex_exec() {
|
|
|
514
591
|
break
|
|
515
592
|
fi
|
|
516
593
|
else
|
|
517
|
-
last_progress_epoch="$(
|
|
594
|
+
last_progress_epoch="$(stat_file_mtime "$progress_file" 2>/dev/null || printf '0')"
|
|
518
595
|
if [[ -n "$last_progress_epoch" && "$last_progress_epoch" != "0" ]]; then
|
|
519
596
|
idle_for=$((now - last_progress_epoch))
|
|
520
597
|
if (( idle_for >= codex_stall_seconds )); then
|
|
@@ -556,7 +633,7 @@ stream_codex_exec() {
|
|
|
556
633
|
}
|
|
557
634
|
|
|
558
635
|
extract_thread_id() {
|
|
559
|
-
|
|
636
|
+
"$python_bin" -c '
|
|
560
637
|
import json
|
|
561
638
|
import sys
|
|
562
639
|
|
|
@@ -39,10 +39,16 @@ declare -a context_items=()
|
|
|
39
39
|
declare -a collect_files=()
|
|
40
40
|
|
|
41
41
|
resolve_codex_bin() {
|
|
42
|
-
local configured_bin="${CODEX_BIN:-}"
|
|
42
|
+
local configured_bin="${CODEX_BIN:-${ACP_CODEX_BIN:-${F_LOSNING_CODEX_BIN:-}}}"
|
|
43
43
|
local best_version=""
|
|
44
44
|
local best_bin=""
|
|
45
45
|
local candidate version_line version
|
|
46
|
+
local -a fallback_paths=(
|
|
47
|
+
"${HOME}/.local/bin/codex"
|
|
48
|
+
"${HOME}/.codex/local/bin/codex"
|
|
49
|
+
"/usr/local/bin/codex"
|
|
50
|
+
"/opt/homebrew/bin/codex"
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
if [[ -n "$configured_bin" && -x "$configured_bin" ]]; then
|
|
48
54
|
printf '%s\n' "$configured_bin"
|
|
@@ -50,11 +56,16 @@ resolve_codex_bin() {
|
|
|
50
56
|
fi
|
|
51
57
|
|
|
52
58
|
if command -v codex >/dev/null 2>&1; then
|
|
53
|
-
|
|
59
|
+
command -v codex
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
for candidate in "${fallback_paths[@]}"; do
|
|
54
64
|
if [[ -x "$candidate" ]]; then
|
|
55
|
-
|
|
65
|
+
printf '%s\n' "$candidate"
|
|
66
|
+
return 0
|
|
56
67
|
fi
|
|
57
|
-
|
|
68
|
+
done
|
|
58
69
|
|
|
59
70
|
if [[ -d "${HOME:-}/.nvm/versions/node" ]]; then
|
|
60
71
|
while IFS= read -r candidate; do
|
|
@@ -347,7 +358,7 @@ find_logged_artifact_path() {
|
|
|
347
358
|
if [[ "\$(basename "\${candidate}")" == "\${artifact_name}" && -f "\${candidate}" ]]; then
|
|
348
359
|
printf '%s\n' "\${candidate}"
|
|
349
360
|
fi
|
|
350
|
-
done < <(grep -oE '/
|
|
361
|
+
done < <(grep -oE '/[^[:space:])"]+' ${output_q} 2>/dev/null || true)
|
|
351
362
|
}
|
|
352
363
|
recover_logged_artifact() {
|
|
353
364
|
local artifact_name="\${1:?artifact name required}"
|
|
@@ -4,24 +4,366 @@ set -euo pipefail
|
|
|
4
4
|
usage() {
|
|
5
5
|
cat <<'EOF'
|
|
6
6
|
Usage:
|
|
7
|
-
agent-project-run-kilo-session
|
|
7
|
+
agent-project-run-kilo-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
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
is not implemented yet.
|
|
9
|
+
Launch a Kilo Code worker session inside tmux for a project adapter and persist
|
|
10
|
+
the standard run artifacts.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Kilo is a TypeScript/Bun coding agent (kilocode/cli). It executes via
|
|
13
|
+
`kilo run --auto --format json` in non-interactive mode.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
|
|
17
|
+
--context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
|
|
18
|
+
--collect-file <name> Copy sandbox artifact file into the host run dir after execution
|
|
19
|
+
--reconcile-command <cmd> Host-side command queued after the worker exits
|
|
20
|
+
--sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .kilo-artifacts)
|
|
21
|
+
--kilo-model <id> Model in provider/name format (default: anthropic/claude-sonnet-4-20250514)
|
|
22
|
+
--kilo-timeout-seconds <secs> Hard timeout in seconds (default: 900)
|
|
23
|
+
--help Show this help
|
|
15
24
|
EOF
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
mode=""
|
|
28
|
+
session=""
|
|
29
|
+
worktree=""
|
|
30
|
+
prompt_file=""
|
|
31
|
+
runs_root=""
|
|
32
|
+
adapter_id=""
|
|
33
|
+
task_kind=""
|
|
34
|
+
task_id=""
|
|
35
|
+
env_prefix=""
|
|
36
|
+
sandbox_subdir=".kilo-artifacts"
|
|
37
|
+
reconcile_command=""
|
|
38
|
+
kilo_model="${ACP_KILO_MODEL:-${F_LOSNING_KILO_MODEL:-anthropic/claude-sonnet-4-20250514}}"
|
|
39
|
+
kilo_timeout_seconds="${ACP_KILO_TIMEOUT_SECONDS:-${F_LOSNING_KILO_TIMEOUT_SECONDS:-900}}"
|
|
40
|
+
declare -a context_items=()
|
|
41
|
+
declare -a collect_files=()
|
|
42
|
+
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--mode) mode="${2:-}"; shift 2 ;;
|
|
46
|
+
--session) session="${2:-}"; shift 2 ;;
|
|
47
|
+
--worktree) worktree="${2:-}"; shift 2 ;;
|
|
48
|
+
--prompt-file) prompt_file="${2:-}"; shift 2 ;;
|
|
49
|
+
--runs-root) runs_root="${2:-}"; shift 2 ;;
|
|
50
|
+
--adapter-id) adapter_id="${2:-}"; shift 2 ;;
|
|
51
|
+
--task-kind) task_kind="${2:-}"; shift 2 ;;
|
|
52
|
+
--task-id) task_id="${2:-}"; shift 2 ;;
|
|
53
|
+
--env-prefix) env_prefix="${2:-}"; shift 2 ;;
|
|
54
|
+
--context) context_items+=("${2:-}"); shift 2 ;;
|
|
55
|
+
--collect-file) collect_files+=("${2:-}"); shift 2 ;;
|
|
56
|
+
--reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
|
|
57
|
+
--sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
|
|
58
|
+
--kilo-model) kilo_model="${2:-}"; shift 2 ;;
|
|
59
|
+
--kilo-timeout-seconds) kilo_timeout_seconds="${2:-}"; shift 2 ;;
|
|
60
|
+
--help|-h) usage; exit 0 ;;
|
|
61
|
+
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
|
|
66
|
+
usage >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
case "$mode" in
|
|
71
|
+
safe|bypass) ;;
|
|
72
|
+
*) echo "--mode must be safe or bypass" >&2; exit 1 ;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
case "$kilo_timeout_seconds" in
|
|
76
|
+
''|*[!0-9]*|0) echo "--kilo-timeout-seconds must be a positive integer" >&2; exit 1 ;;
|
|
23
77
|
esac
|
|
24
78
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
79
|
+
resolve_kilo_bin() {
|
|
80
|
+
local configured_bin="${KILO_BIN:-${ACP_KILO_BIN:-}}"
|
|
81
|
+
if [[ -n "${configured_bin}" && -x "${configured_bin}" ]]; then
|
|
82
|
+
printf '%s\n' "${configured_bin}"
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
if command -v kilo >/dev/null 2>&1; then
|
|
86
|
+
command -v kilo
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
local -a fallback_paths=(
|
|
90
|
+
"/opt/homebrew/bin/kilo"
|
|
91
|
+
"/usr/local/bin/kilo"
|
|
92
|
+
"${HOME}/.local/bin/kilo"
|
|
93
|
+
"${HOME}/.bun/bin/kilo"
|
|
94
|
+
)
|
|
95
|
+
local p
|
|
96
|
+
for p in "${fallback_paths[@]}"; do
|
|
97
|
+
if [[ -x "${p}" ]]; then
|
|
98
|
+
printf '%s\n' "${p}"
|
|
99
|
+
return 0
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
kilo_bin="$(resolve_kilo_bin || true)"
|
|
106
|
+
if [[ -z "${kilo_bin}" || ! -x "${kilo_bin}" ]]; then
|
|
107
|
+
echo "unable to resolve a runnable kilo binary — install with: npm install -g @kilocode/cli" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
artifact_dir="${runs_root}/${session}"
|
|
112
|
+
output_file="${artifact_dir}/${session}.log"
|
|
113
|
+
inner_script="${artifact_dir}/${session}.sh"
|
|
114
|
+
meta_file="${artifact_dir}/run.env"
|
|
115
|
+
result_file="${artifact_dir}/result.env"
|
|
116
|
+
runner_state_file="${artifact_dir}/runner.env"
|
|
117
|
+
sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
|
|
118
|
+
started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
119
|
+
|
|
120
|
+
mkdir -p "$artifact_dir" "$sandbox_run_dir"
|
|
121
|
+
|
|
122
|
+
if tmux has-session -t "$session" 2>/dev/null; then
|
|
123
|
+
echo "tmux session already exists: $session" >&2
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
|
|
128
|
+
|
|
129
|
+
printf -v session_q '%q' "$session"
|
|
130
|
+
printf -v task_kind_q '%q' "$task_kind"
|
|
131
|
+
printf -v task_id_q '%q' "$task_id"
|
|
132
|
+
printf -v mode_q '%q' "$mode"
|
|
133
|
+
printf -v worktree_q '%q' "$worktree"
|
|
134
|
+
printf -v prompt_q '%q' "$prompt_file"
|
|
135
|
+
printf -v output_q '%q' "$output_file"
|
|
136
|
+
printf -v artifact_dir_q '%q' "$artifact_dir"
|
|
137
|
+
printf -v script_q '%q' "$inner_script"
|
|
138
|
+
printf -v result_q '%q' "$result_file"
|
|
139
|
+
printf -v meta_file_q '%q' "$meta_file"
|
|
140
|
+
printf -v runner_state_q '%q' "$runner_state_file"
|
|
141
|
+
printf -v branch_q '%q' "$branch_name"
|
|
142
|
+
printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
|
|
143
|
+
printf -v adapter_id_q '%q' "$adapter_id"
|
|
144
|
+
printf -v started_at_q '%q' "$started_at"
|
|
145
|
+
printf -v kilo_bin_q '%q' "$kilo_bin"
|
|
146
|
+
printf -v kilo_model_q '%q' "$kilo_model"
|
|
147
|
+
printf -v kilo_timeout_q '%q' "$kilo_timeout_seconds"
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
printf 'TASK_KIND=%s\n' "$task_kind_q"
|
|
151
|
+
printf 'TASK_ID=%s\n' "$task_id_q"
|
|
152
|
+
printf 'SESSION=%s\n' "$session_q"
|
|
153
|
+
printf 'MODE=%s\n' "$mode_q"
|
|
154
|
+
printf 'WORKTREE=%s\n' "$worktree_q"
|
|
155
|
+
printf 'PROMPT_FILE=%s\n' "$prompt_q"
|
|
156
|
+
printf 'OUTPUT_FILE=%s\n' "$output_q"
|
|
157
|
+
printf 'BRANCH=%s\n' "$branch_q"
|
|
158
|
+
printf 'RESULT_FILE=%s\n' "$result_q"
|
|
159
|
+
printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
|
|
160
|
+
printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
|
|
161
|
+
printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
|
|
162
|
+
printf 'STARTED_AT=%s\n' "$started_at_q"
|
|
163
|
+
printf 'KILO_BIN=%s\n' "$kilo_bin_q"
|
|
164
|
+
printf 'KILO_MODEL=%s\n' "$kilo_model_q"
|
|
165
|
+
printf 'KILO_TIMEOUT_SECONDS=%s\n' "$kilo_timeout_q"
|
|
166
|
+
} >"$meta_file"
|
|
167
|
+
|
|
168
|
+
context_exports=""
|
|
169
|
+
if ((${#context_items[@]} > 0)); then
|
|
170
|
+
for item in "${context_items[@]}"; do
|
|
171
|
+
if [[ "$item" != *=* ]]; then
|
|
172
|
+
echo "--context must use KEY=VALUE syntax: $item" >&2
|
|
173
|
+
exit 1
|
|
174
|
+
fi
|
|
175
|
+
key="${item%%=*}"
|
|
176
|
+
value="${item#*=}"
|
|
177
|
+
if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
|
|
178
|
+
echo "Invalid context key: $key" >&2
|
|
179
|
+
exit 1
|
|
180
|
+
fi
|
|
181
|
+
printf -v value_q '%q' "$value"
|
|
182
|
+
printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
|
|
183
|
+
if [[ -n "$env_prefix" ]]; then
|
|
184
|
+
context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
|
|
185
|
+
fi
|
|
186
|
+
context_exports+="export ACP_${key}=${value_q}"$'\n'
|
|
187
|
+
if [[ "$env_prefix" != "F_LOSNING_" ]]; then
|
|
188
|
+
context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
|
|
189
|
+
fi
|
|
190
|
+
done
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
runtime_exports=$(
|
|
194
|
+
cat <<EOF
|
|
195
|
+
export AGENT_PROJECT_SESSION=${session_q}
|
|
196
|
+
export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
|
|
197
|
+
export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
|
|
198
|
+
export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
199
|
+
export ACP_SESSION=${session_q}
|
|
200
|
+
export ACP_RUN_DIR=${sandbox_run_dir_q}
|
|
201
|
+
export ACP_HOST_RUN_DIR=${artifact_dir_q}
|
|
202
|
+
export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
203
|
+
export F_LOSNING_SESSION=${session_q}
|
|
204
|
+
export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
|
|
205
|
+
export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
|
|
206
|
+
export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
207
|
+
EOF
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if [[ -n "$env_prefix" ]]; then
|
|
211
|
+
runtime_exports+=$'\n'
|
|
212
|
+
runtime_exports+=$(cat <<EOF
|
|
213
|
+
export ${env_prefix}SESSION=${session_q}
|
|
214
|
+
export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
|
|
215
|
+
export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
|
|
216
|
+
export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
217
|
+
EOF
|
|
218
|
+
)
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
collect_copy_snippet=""
|
|
222
|
+
if ((${#collect_files[@]} > 0)); then
|
|
223
|
+
for artifact_name in "${collect_files[@]}"; do
|
|
224
|
+
[[ -z "$artifact_name" ]] && continue
|
|
225
|
+
printf -v artifact_q '%q' "$artifact_name"
|
|
226
|
+
collect_copy_snippet+=$(cat <<EOF
|
|
227
|
+
if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
|
|
228
|
+
cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
|
|
229
|
+
fi
|
|
230
|
+
EOF
|
|
231
|
+
)
|
|
232
|
+
collect_copy_snippet+=$'\n'
|
|
233
|
+
done
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Always collect result.env from sandbox to artifact_dir
|
|
237
|
+
collect_copy_snippet+=$(
|
|
238
|
+
cat <<EOF
|
|
239
|
+
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
240
|
+
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
241
|
+
fi
|
|
242
|
+
EOF
|
|
243
|
+
)
|
|
244
|
+
collect_copy_snippet+=$'\n'
|
|
245
|
+
|
|
246
|
+
reconcile_snippet=""
|
|
247
|
+
if [[ -n "$reconcile_command" ]]; then
|
|
248
|
+
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"
|
|
249
|
+
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
cat >"$inner_script" <<EOF
|
|
253
|
+
#!/usr/bin/env bash
|
|
254
|
+
set -euo pipefail
|
|
255
|
+
${runtime_exports}
|
|
256
|
+
${context_exports}cd ${worktree_q}
|
|
257
|
+
|
|
258
|
+
runner_state_file=${runner_state_q}
|
|
259
|
+
output_file=${output_q}
|
|
260
|
+
sandbox_run_dir=${sandbox_run_dir_q}
|
|
261
|
+
artifact_dir=${artifact_dir_q}
|
|
262
|
+
result_file_path=${sandbox_run_dir_q}/result.env
|
|
263
|
+
host_result_file=${result_q}
|
|
264
|
+
kilo_bin=${kilo_bin_q}
|
|
265
|
+
kilo_model=${kilo_model_q}
|
|
266
|
+
kilo_timeout=${kilo_timeout_q}
|
|
267
|
+
prompt_file=${prompt_q}
|
|
268
|
+
worktree=${worktree_q}
|
|
269
|
+
|
|
270
|
+
write_state() {
|
|
271
|
+
local runner_state="\${1:?runner state required}"
|
|
272
|
+
local last_exit_code="\${2:-}"
|
|
273
|
+
local failure_reason="\${3:-}"
|
|
274
|
+
local updated_at tmp_file
|
|
275
|
+
|
|
276
|
+
updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
277
|
+
tmp_file="\${runner_state_file}.tmp.\$\$"
|
|
278
|
+
{
|
|
279
|
+
printf 'RUNNER_STATE=%q\n' "\${runner_state}"
|
|
280
|
+
printf 'ATTEMPT=1\n'
|
|
281
|
+
printf 'RESUME_COUNT=0\n'
|
|
282
|
+
printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
|
|
283
|
+
printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
|
|
284
|
+
printf 'UPDATED_AT=%q\n' "\${updated_at}"
|
|
285
|
+
} >"\${tmp_file}"
|
|
286
|
+
mv "\${tmp_file}" "\${runner_state_file}"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
record_final_git_state() {
|
|
290
|
+
local final_head final_branch tmp_file
|
|
291
|
+
final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
|
|
292
|
+
final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
|
|
293
|
+
tmp_file=${meta_file_q}.tmp.final.\$\$
|
|
294
|
+
grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
|
|
295
|
+
{
|
|
296
|
+
printf 'FINAL_HEAD=%q\n' "\${final_head}"
|
|
297
|
+
printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
|
|
298
|
+
} >>"\${tmp_file}"
|
|
299
|
+
mv "\${tmp_file}" ${meta_file_q}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
write_state running
|
|
303
|
+
|
|
304
|
+
mkdir -p "\${sandbox_run_dir}"
|
|
305
|
+
|
|
306
|
+
# Kilo runs via 'kilo run' in non-interactive mode.
|
|
307
|
+
# --auto auto-approves all tool permissions (CI mode).
|
|
308
|
+
# --format json emits structured JSON events (parseable for result inference).
|
|
309
|
+
# --model selects the provider/model, --dir sets the working directory.
|
|
310
|
+
# Prompt is passed as the positional argument.
|
|
311
|
+
prompt_content="\$(cat "\${prompt_file}")"
|
|
312
|
+
kilo_exit_code=0
|
|
313
|
+
kilo_args=(run --auto --format json --model "\${kilo_model}" --dir ${worktree_q})
|
|
314
|
+
|
|
315
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
316
|
+
timeout "\${kilo_timeout}" "\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" || kilo_exit_code=\$?
|
|
317
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
318
|
+
gtimeout "\${kilo_timeout}" "\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" || kilo_exit_code=\$?
|
|
319
|
+
else
|
|
320
|
+
"\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" &
|
|
321
|
+
_kilo_pid=\$!
|
|
322
|
+
( sleep "\${kilo_timeout}" && kill "\${_kilo_pid}" 2>/dev/null ) &
|
|
323
|
+
_wd=\$!
|
|
324
|
+
wait "\${_kilo_pid}" || kilo_exit_code=\$?
|
|
325
|
+
kill "\${_wd}" 2>/dev/null || true; wait "\${_wd}" 2>/dev/null || true
|
|
326
|
+
[[ "\${kilo_exit_code}" -eq 143 ]] && kilo_exit_code=124
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
if [[ "\${kilo_exit_code}" -eq 0 ]]; then
|
|
330
|
+
write_state succeeded 0
|
|
331
|
+
# Kilo has full tool access (bash, write, edit) — it can modify the worktree.
|
|
332
|
+
# Infer result from git state if result.env was not written by the agent.
|
|
333
|
+
if [[ ! -f "\${result_file_path}" ]]; then
|
|
334
|
+
if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
|
|
335
|
+
|| git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
|
|
336
|
+
|| git -C ${worktree_q} diff --name-only origin/main..HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null; then
|
|
337
|
+
printf 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n' >"\${result_file_path}"
|
|
338
|
+
else
|
|
339
|
+
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=missing-result-contract\n' >"\${result_file_path}"
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
342
|
+
else
|
|
343
|
+
failure_reason="kilo-exit-\${kilo_exit_code}"
|
|
344
|
+
[[ "\${kilo_exit_code}" -eq 124 ]] && failure_reason="timeout"
|
|
345
|
+
write_state failed "\${kilo_exit_code}" "\${failure_reason}"
|
|
346
|
+
if [[ ! -f "\${result_file_path}" ]]; then
|
|
347
|
+
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=%s\n' "\${failure_reason}" >"\${result_file_path}"
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
record_final_git_state
|
|
352
|
+
|
|
353
|
+
if [[ -f "\${result_file_path}" ]]; then
|
|
354
|
+
cp "\${result_file_path}" "\${host_result_file}"
|
|
355
|
+
fi
|
|
356
|
+
${collect_copy_snippet}
|
|
357
|
+
${reconcile_snippet}
|
|
358
|
+
printf '\n__CODEX_EXIT__:%s\n' "\${kilo_exit_code}" | tee -a "\${output_file}"
|
|
359
|
+
exit "\${kilo_exit_code}"
|
|
360
|
+
EOF
|
|
361
|
+
|
|
362
|
+
chmod +x "$inner_script"
|
|
363
|
+
tmux new-session -d -s "$session" "$inner_script"
|
|
364
|
+
|
|
365
|
+
printf 'SESSION=%s\n' "$session"
|
|
366
|
+
printf 'TASK_KIND=%s\n' "$task_kind"
|
|
367
|
+
printf 'TASK_ID=%s\n' "$task_id"
|
|
368
|
+
printf 'WORKTREE=%s\n' "$worktree"
|
|
369
|
+
printf 'OUTPUT=%s\n' "$output_file"
|