agent-control-plane 0.1.14 → 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.
- package/README.md +323 -349
- package/bin/pr-risk.sh +28 -6
- package/hooks/heartbeat-hooks.sh +62 -22
- package/npm/bin/agent-control-plane.js +434 -12
- package/package.json +1 -1
- 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 +77 -21
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
- package/tools/bin/agent-project-cleanup-session +84 -0
- package/tools/bin/agent-project-heartbeat-loop +10 -3
- package/tools/bin/agent-project-reconcile-issue-session +45 -12
- package/tools/bin/agent-project-reconcile-pr-session +25 -0
- package/tools/bin/agent-project-run-claude-session +2 -2
- package/tools/bin/agent-project-run-codex-resilient +57 -2
- package/tools/bin/agent-project-run-kilo-session +346 -14
- package/tools/bin/agent-project-run-ollama-session +658 -0
- package/tools/bin/agent-project-run-openclaw-session +73 -25
- package/tools/bin/agent-project-run-opencode-session +354 -14
- package/tools/bin/agent-project-run-pi-session +479 -0
- package/tools/bin/agent-project-worker-status +38 -1
- package/tools/bin/flow-config-lib.sh +123 -3
- package/tools/bin/flow-resident-worker-lib.sh +1 -1
- package/tools/bin/flow-shell-lib.sh +7 -2
- package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
- package/tools/bin/heartbeat-safe-auto.sh +105 -17
- 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-runtimectl.sh +90 -7
- package/tools/bin/provider-cooldown-state.sh +14 -14
- package/tools/bin/render-flow-config.sh +30 -33
- package/tools/bin/run-codex-task.sh +53 -4
- package/tools/bin/scaffold-profile.sh +18 -3
- package/tools/bin/start-issue-worker.sh +4 -1
- package/tools/bin/start-pr-fix-worker.sh +33 -0
- package/tools/bin/start-pr-review-worker.sh +34 -0
- package/tools/bin/start-resident-issue-loop.sh +5 -4
- 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 +62 -0
- package/tools/dashboard/dashboard_snapshot.py +53 -4
- package/tools/dashboard/index.html +5 -1
- package/tools/dashboard/styles.css +97 -20
- package/tools/templates/pr-fix-template.md +4 -8
- package/tools/templates/pr-merge-repair-template.md +4 -8
- package/tools/templates/pr-review-template.md +2 -1
|
@@ -774,12 +774,12 @@ fi
|
|
|
774
774
|
|
|
775
775
|
${collect_copy_snippet}
|
|
776
776
|
if [[ "\${status}" -eq 0 ]]; then
|
|
777
|
-
write_state
|
|
777
|
+
write_state succeeded "\${status}" '' "\${attempt}" "\$((attempt - 1))"
|
|
778
778
|
else
|
|
779
779
|
write_state failed "\${status}" "\${failure_reason}" "\${attempt}" "\$((attempt - 1))"
|
|
780
780
|
fi
|
|
781
781
|
${reconcile_snippet}
|
|
782
|
-
printf '\
|
|
782
|
+
printf '\n__CODEX_EXIT__:%s\n' "\${status}" | tee -a "\${output_file}"
|
|
783
783
|
exit "\${status}"
|
|
784
784
|
EOF
|
|
785
785
|
|
|
@@ -618,6 +618,22 @@ classify_failure_reason() {
|
|
|
618
618
|
fi
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
+
failure_chunk_indicates_startup_stall() {
|
|
622
|
+
local chunk="${1:-}"
|
|
623
|
+
local recent_chunk
|
|
624
|
+
|
|
625
|
+
recent_chunk="$(tail -n 120 <<<"$chunk")"
|
|
626
|
+
grep -q '"type":"thread.started"' <<<"$recent_chunk" || return 1
|
|
627
|
+
grep -q '"type":"turn.started"' <<<"$recent_chunk" || return 1
|
|
628
|
+
if grep -Eq '"type":"item\.(started|completed)"' <<<"$recent_chunk"; then
|
|
629
|
+
return 1
|
|
630
|
+
fi
|
|
631
|
+
if grep -q '"type":"turn.completed"' <<<"$recent_chunk"; then
|
|
632
|
+
return 1
|
|
633
|
+
fi
|
|
634
|
+
return 0
|
|
635
|
+
}
|
|
636
|
+
|
|
621
637
|
resume_prompt() {
|
|
622
638
|
cat <<EOF
|
|
623
639
|
The previous Codex exec turn in this same thread was interrupted because the host refreshed Codex authentication after a quota or auth failure.
|
|
@@ -729,7 +745,7 @@ run_resume_exec() {
|
|
|
729
745
|
}
|
|
730
746
|
|
|
731
747
|
attempt_run() {
|
|
732
|
-
local reason auth_before_switch quota_label_before_switch quota_switch_signature_before_switch quota_switch_result shell_flags_before_quota_switch
|
|
748
|
+
local reason auth_before_switch quota_label_before_switch quota_switch_signature_before_switch quota_switch_result shell_flags_before_quota_switch failure_chunk startup_stall
|
|
733
749
|
|
|
734
750
|
attempt=$((attempt + 1))
|
|
735
751
|
last_quota_switch_status=""
|
|
@@ -750,8 +766,15 @@ attempt_run() {
|
|
|
750
766
|
return 0
|
|
751
767
|
fi
|
|
752
768
|
|
|
753
|
-
|
|
769
|
+
failure_chunk="$(new_output_since "$last_attempt_start_size")"
|
|
770
|
+
reason="$(classify_failure_reason "$failure_chunk")"
|
|
754
771
|
last_failure_reason="${reason:-worker-exit-failed}"
|
|
772
|
+
startup_stall="no"
|
|
773
|
+
if [[ "$last_failure_reason" == "no-codex-output-before-stall-threshold" || "$last_failure_reason" == "no-codex-progress-before-stall-threshold" ]]; then
|
|
774
|
+
if failure_chunk_indicates_startup_stall "$failure_chunk"; then
|
|
775
|
+
startup_stall="yes"
|
|
776
|
+
fi
|
|
777
|
+
fi
|
|
755
778
|
|
|
756
779
|
case "$last_failure_reason" in
|
|
757
780
|
usage-limit|auth-failure|auth-401|account-banned)
|
|
@@ -796,6 +819,38 @@ attempt_run() {
|
|
|
796
819
|
resume_count=$((resume_count + 1))
|
|
797
820
|
return 2
|
|
798
821
|
;;
|
|
822
|
+
no-codex-output-before-stall-threshold|no-codex-progress-before-stall-threshold)
|
|
823
|
+
if [[ "$startup_stall" == "yes" && $quota_autoswitch_attempt_count -lt $max_quota_autoswitch_attempts ]]; then
|
|
824
|
+
auth_before_switch="$(auth_fingerprint)"
|
|
825
|
+
quota_label_before_switch="$last_attempt_start_quota_label"
|
|
826
|
+
quota_switch_signature_before_switch="$(quota_switch_signature)"
|
|
827
|
+
last_auth_fingerprint="$auth_before_switch"
|
|
828
|
+
write_state "switching-account" "$last_failure_reason"
|
|
829
|
+
log_runner "startup-stall detected before first Codex tool activity; attempting Codex account rotation"
|
|
830
|
+
shell_flags_before_quota_switch="$-"
|
|
831
|
+
set +e
|
|
832
|
+
run_quota_autoswitch
|
|
833
|
+
quota_switch_result=$?
|
|
834
|
+
case "$shell_flags_before_quota_switch" in
|
|
835
|
+
*e*) set -e ;;
|
|
836
|
+
*) set +e ;;
|
|
837
|
+
esac
|
|
838
|
+
if [[ "$quota_switch_result" == "0" ]]; then
|
|
839
|
+
thread_id=""
|
|
840
|
+
auth_wait_started_at=""
|
|
841
|
+
write_state "running" ""
|
|
842
|
+
return 2
|
|
843
|
+
fi
|
|
844
|
+
if [[ "$quota_switch_result" == "10" ]]; then
|
|
845
|
+
log_runner "startup-stall rotation deferred until ${last_quota_next_retry_at:-unknown}"
|
|
846
|
+
last_failure_reason="quota-switch-deferred"
|
|
847
|
+
write_state "failed" "$last_failure_reason"
|
|
848
|
+
return 1
|
|
849
|
+
fi
|
|
850
|
+
fi
|
|
851
|
+
write_state "failed" "$last_failure_reason"
|
|
852
|
+
return 1
|
|
853
|
+
;;
|
|
799
854
|
*)
|
|
800
855
|
write_state "failed" "$last_failure_reason"
|
|
801
856
|
return 1
|
|
@@ -4,24 +4,356 @@ 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
|
+
reconcile_snippet=""
|
|
237
|
+
if [[ -n "$reconcile_command" ]]; then
|
|
238
|
+
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"
|
|
239
|
+
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
cat >"$inner_script" <<EOF
|
|
243
|
+
#!/usr/bin/env bash
|
|
244
|
+
set -euo pipefail
|
|
245
|
+
${runtime_exports}
|
|
246
|
+
${context_exports}cd ${worktree_q}
|
|
247
|
+
|
|
248
|
+
runner_state_file=${runner_state_q}
|
|
249
|
+
output_file=${output_q}
|
|
250
|
+
sandbox_run_dir=${sandbox_run_dir_q}
|
|
251
|
+
artifact_dir=${artifact_dir_q}
|
|
252
|
+
result_file_path=${sandbox_run_dir_q}/result.env
|
|
253
|
+
host_result_file=${result_q}
|
|
254
|
+
kilo_bin=${kilo_bin_q}
|
|
255
|
+
kilo_model=${kilo_model_q}
|
|
256
|
+
kilo_timeout=${kilo_timeout_q}
|
|
257
|
+
prompt_file=${prompt_q}
|
|
258
|
+
worktree=${worktree_q}
|
|
259
|
+
|
|
260
|
+
write_state() {
|
|
261
|
+
local runner_state="\${1:?runner state required}"
|
|
262
|
+
local last_exit_code="\${2:-}"
|
|
263
|
+
local failure_reason="\${3:-}"
|
|
264
|
+
local updated_at tmp_file
|
|
265
|
+
|
|
266
|
+
updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
267
|
+
tmp_file="\${runner_state_file}.tmp.\$\$"
|
|
268
|
+
{
|
|
269
|
+
printf 'RUNNER_STATE=%q\n' "\${runner_state}"
|
|
270
|
+
printf 'ATTEMPT=1\n'
|
|
271
|
+
printf 'RESUME_COUNT=0\n'
|
|
272
|
+
printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
|
|
273
|
+
printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
|
|
274
|
+
printf 'UPDATED_AT=%q\n' "\${updated_at}"
|
|
275
|
+
} >"\${tmp_file}"
|
|
276
|
+
mv "\${tmp_file}" "\${runner_state_file}"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
record_final_git_state() {
|
|
280
|
+
local final_head final_branch tmp_file
|
|
281
|
+
final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
|
|
282
|
+
final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
|
|
283
|
+
tmp_file=${meta_file_q}.tmp.final.\$\$
|
|
284
|
+
grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
|
|
285
|
+
{
|
|
286
|
+
printf 'FINAL_HEAD=%q\n' "\${final_head}"
|
|
287
|
+
printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
|
|
288
|
+
} >>"\${tmp_file}"
|
|
289
|
+
mv "\${tmp_file}" ${meta_file_q}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
write_state running
|
|
293
|
+
|
|
294
|
+
mkdir -p "\${sandbox_run_dir}"
|
|
295
|
+
|
|
296
|
+
# Kilo runs via 'kilo run' in non-interactive mode.
|
|
297
|
+
# --auto auto-approves all tool permissions (CI mode).
|
|
298
|
+
# --format json emits structured JSON events (parseable for result inference).
|
|
299
|
+
# --model selects the provider/model, --dir sets the working directory.
|
|
300
|
+
# Prompt is passed as the positional argument.
|
|
301
|
+
prompt_content="\$(cat "\${prompt_file}")"
|
|
302
|
+
kilo_exit_code=0
|
|
303
|
+
kilo_args=(run --auto --format json --model "\${kilo_model}" --dir ${worktree_q})
|
|
304
|
+
|
|
305
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
306
|
+
timeout "\${kilo_timeout}" "\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" || kilo_exit_code=\$?
|
|
307
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
308
|
+
gtimeout "\${kilo_timeout}" "\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" || kilo_exit_code=\$?
|
|
309
|
+
else
|
|
310
|
+
"\${kilo_bin}" "\${kilo_args[@]}" "\${prompt_content}" 2>&1 | tee -a "\${output_file}" &
|
|
311
|
+
_kilo_pid=\$!
|
|
312
|
+
( sleep "\${kilo_timeout}" && kill "\${_kilo_pid}" 2>/dev/null ) &
|
|
313
|
+
_wd=\$!
|
|
314
|
+
wait "\${_kilo_pid}" || kilo_exit_code=\$?
|
|
315
|
+
kill "\${_wd}" 2>/dev/null || true; wait "\${_wd}" 2>/dev/null || true
|
|
316
|
+
[[ "\${kilo_exit_code}" -eq 143 ]] && kilo_exit_code=124
|
|
317
|
+
fi
|
|
318
|
+
|
|
319
|
+
if [[ "\${kilo_exit_code}" -eq 0 ]]; then
|
|
320
|
+
write_state succeeded 0
|
|
321
|
+
# Kilo has full tool access (bash, write, edit) — it can modify the worktree.
|
|
322
|
+
# Infer result from git state if result.env was not written by the agent.
|
|
323
|
+
if [[ ! -f "\${result_file_path}" ]]; then
|
|
324
|
+
if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
|
|
325
|
+
|| git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
|
|
326
|
+
|| git -C ${worktree_q} diff --name-only origin/main..HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null; then
|
|
327
|
+
printf 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n' >"\${result_file_path}"
|
|
328
|
+
else
|
|
329
|
+
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=missing-result-contract\n' >"\${result_file_path}"
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
else
|
|
333
|
+
failure_reason="kilo-exit-\${kilo_exit_code}"
|
|
334
|
+
[[ "\${kilo_exit_code}" -eq 124 ]] && failure_reason="timeout"
|
|
335
|
+
write_state failed "\${kilo_exit_code}" "\${failure_reason}"
|
|
336
|
+
if [[ ! -f "\${result_file_path}" ]]; then
|
|
337
|
+
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=%s\n' "\${failure_reason}" >"\${result_file_path}"
|
|
338
|
+
fi
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
record_final_git_state
|
|
342
|
+
|
|
343
|
+
if [[ -f "\${result_file_path}" ]]; then
|
|
344
|
+
cp "\${result_file_path}" "\${host_result_file}"
|
|
345
|
+
fi
|
|
346
|
+
${collect_copy_snippet}
|
|
347
|
+
${reconcile_snippet}
|
|
348
|
+
printf '\n__CODEX_EXIT__:%s\n' "\${kilo_exit_code}" | tee -a "\${output_file}"
|
|
349
|
+
exit "\${kilo_exit_code}"
|
|
350
|
+
EOF
|
|
351
|
+
|
|
352
|
+
chmod +x "$inner_script"
|
|
353
|
+
tmux new-session -d -s "$session" "$inner_script"
|
|
354
|
+
|
|
355
|
+
printf 'SESSION=%s\n' "$session"
|
|
356
|
+
printf 'TASK_KIND=%s\n' "$task_kind"
|
|
357
|
+
printf 'TASK_ID=%s\n' "$task_id"
|
|
358
|
+
printf 'WORKTREE=%s\n' "$worktree"
|
|
359
|
+
printf 'OUTPUT=%s\n' "$output_file"
|