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.
Files changed (53) hide show
  1. package/README.md +323 -349
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +434 -12
  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 +45 -12
  16. package/tools/bin/agent-project-reconcile-pr-session +25 -0
  17. package/tools/bin/agent-project-run-claude-session +2 -2
  18. package/tools/bin/agent-project-run-codex-resilient +57 -2
  19. package/tools/bin/agent-project-run-kilo-session +346 -14
  20. package/tools/bin/agent-project-run-ollama-session +658 -0
  21. package/tools/bin/agent-project-run-openclaw-session +73 -25
  22. package/tools/bin/agent-project-run-opencode-session +354 -14
  23. package/tools/bin/agent-project-run-pi-session +479 -0
  24. package/tools/bin/agent-project-worker-status +38 -1
  25. package/tools/bin/flow-config-lib.sh +123 -3
  26. package/tools/bin/flow-resident-worker-lib.sh +1 -1
  27. package/tools/bin/flow-shell-lib.sh +7 -2
  28. package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
  29. package/tools/bin/heartbeat-safe-auto.sh +105 -17
  30. package/tools/bin/install-project-launchd.sh +19 -2
  31. package/tools/bin/prepare-worktree.sh +4 -4
  32. package/tools/bin/profile-activate.sh +2 -2
  33. package/tools/bin/profile-adopt.sh +2 -2
  34. package/tools/bin/project-init.sh +1 -1
  35. package/tools/bin/project-runtimectl.sh +90 -7
  36. package/tools/bin/provider-cooldown-state.sh +14 -14
  37. package/tools/bin/render-flow-config.sh +30 -33
  38. package/tools/bin/run-codex-task.sh +53 -4
  39. package/tools/bin/scaffold-profile.sh +18 -3
  40. package/tools/bin/start-issue-worker.sh +4 -1
  41. package/tools/bin/start-pr-fix-worker.sh +33 -0
  42. package/tools/bin/start-pr-review-worker.sh +34 -0
  43. package/tools/bin/start-resident-issue-loop.sh +5 -4
  44. package/tools/bin/sync-agent-repo.sh +2 -2
  45. package/tools/bin/sync-dependency-baseline.sh +3 -3
  46. package/tools/bin/sync-shared-agent-home.sh +4 -1
  47. package/tools/dashboard/app.js +62 -0
  48. package/tools/dashboard/dashboard_snapshot.py +53 -4
  49. package/tools/dashboard/index.html +5 -1
  50. package/tools/dashboard/styles.css +97 -20
  51. package/tools/templates/pr-fix-template.md +4 -8
  52. package/tools/templates/pr-merge-repair-template.md +4 -8
  53. 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 completed "\${status}" '' "\${attempt}" "\$((attempt - 1))"
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 '\n__CLAUDE_EXIT__:%s\n' "\${status}" | tee -a "\${output_file}"
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
- reason="$(classify_failure_reason "$(new_output_since "$last_attempt_start_size")")"
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 [shared session-wrapper options]
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
- Placeholder adapter for the roadmap. The public package ships this command so
10
- ACP can expose and test worker routing for `kilo`, but live session execution
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
- Use `codex`, `claude`, or `openclaw` for real runs today. See ROADMAP.md for
14
- current backend maturity.
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
- case "${1:-}" in
19
- --help|-h)
20
- usage
21
- exit 0
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
- echo "agent-project-run-kilo-session: kilo support is scaffolded, but execution is not implemented yet" >&2
26
- echo "Choose codex, claude, or openclaw for live runs today." >&2
27
- exit 1
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"