agent-control-plane 0.3.0 → 0.6.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 (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,377 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-run-opencode-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 Crush (formerly OpenCode) worker session inside tmux for a project
10
- adapter and persist the standard run artifacts.
11
-
12
- Crush is the Go-based coding agent by Charm (charmbracelet/crush).
13
- It executes via `crush run` 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: .opencode-artifacts)
21
- --opencode-model <id> Model in provider/name format (default: anthropic/claude-sonnet-4-20250514)
22
- --opencode-timeout-seconds <secs> Hard timeout in seconds (default: 900)
23
- --help Show this help
24
- EOF
25
- }
26
-
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=".opencode-artifacts"
37
- reconcile_command=""
38
- opencode_model="${ACP_OPENCODE_MODEL:-${F_LOSNING_OPENCODE_MODEL:-anthropic/claude-sonnet-4-20250514}}"
39
- opencode_timeout_seconds="${ACP_OPENCODE_TIMEOUT_SECONDS:-${F_LOSNING_OPENCODE_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
- --opencode-model) opencode_model="${2:-}"; shift 2 ;;
59
- --opencode-timeout-seconds) opencode_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 "$opencode_timeout_seconds" in
76
- ''|*[!0-9]*|0) echo "--opencode-timeout-seconds must be a positive integer" >&2; exit 1 ;;
77
- esac
78
-
79
- resolve_crush_bin() {
80
- local configured_bin="${CRUSH_BIN:-${ACP_CRUSH_BIN:-${ACP_OPENCODE_BIN:-}}}"
81
- if [[ -n "${configured_bin}" && -x "${configured_bin}" ]]; then
82
- printf '%s\n' "${configured_bin}"
83
- return 0
84
- fi
85
- # Try crush first (current name), then opencode (legacy name)
86
- if command -v crush >/dev/null 2>&1; then
87
- command -v crush
88
- return 0
89
- fi
90
- if command -v opencode >/dev/null 2>&1; then
91
- command -v opencode
92
- return 0
93
- fi
94
- local -a fallback_paths=(
95
- "/opt/homebrew/bin/crush"
96
- "/usr/local/bin/crush"
97
- "${HOME}/.local/bin/crush"
98
- "${HOME}/go/bin/crush"
99
- "/opt/homebrew/bin/opencode"
100
- "/usr/local/bin/opencode"
101
- )
102
- local p
103
- for p in "${fallback_paths[@]}"; do
104
- if [[ -x "${p}" ]]; then
105
- printf '%s\n' "${p}"
106
- return 0
107
- fi
108
- done
109
- return 1
110
- }
111
-
112
- crush_bin="$(resolve_crush_bin || true)"
113
- if [[ -z "${crush_bin}" || ! -x "${crush_bin}" ]]; then
114
- echo "unable to resolve a runnable crush/opencode binary — install with: brew install charmbracelet/tap/crush" >&2
115
- exit 1
116
- fi
117
-
118
- artifact_dir="${runs_root}/${session}"
119
- output_file="${artifact_dir}/${session}.log"
120
- inner_script="${artifact_dir}/${session}.sh"
121
- meta_file="${artifact_dir}/run.env"
122
- result_file="${artifact_dir}/result.env"
123
- runner_state_file="${artifact_dir}/runner.env"
124
- sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
125
- started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
126
-
127
- mkdir -p "$artifact_dir" "$sandbox_run_dir"
128
-
129
- if tmux has-session -t "$session" 2>/dev/null; then
130
- echo "tmux session already exists: $session" >&2
131
- exit 1
132
- fi
133
-
134
- branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
135
-
136
- printf -v session_q '%q' "$session"
137
- printf -v task_kind_q '%q' "$task_kind"
138
- printf -v task_id_q '%q' "$task_id"
139
- printf -v mode_q '%q' "$mode"
140
- printf -v worktree_q '%q' "$worktree"
141
- printf -v prompt_q '%q' "$prompt_file"
142
- printf -v output_q '%q' "$output_file"
143
- printf -v artifact_dir_q '%q' "$artifact_dir"
144
- printf -v script_q '%q' "$inner_script"
145
- printf -v result_q '%q' "$result_file"
146
- printf -v meta_file_q '%q' "$meta_file"
147
- printf -v runner_state_q '%q' "$runner_state_file"
148
- printf -v branch_q '%q' "$branch_name"
149
- printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
150
- printf -v adapter_id_q '%q' "$adapter_id"
151
- printf -v started_at_q '%q' "$started_at"
152
- printf -v crush_bin_q '%q' "$crush_bin"
153
- printf -v opencode_model_q '%q' "$opencode_model"
154
- printf -v opencode_timeout_q '%q' "$opencode_timeout_seconds"
155
-
156
- {
157
- printf 'TASK_KIND=%s\n' "$task_kind_q"
158
- printf 'TASK_ID=%s\n' "$task_id_q"
159
- printf 'SESSION=%s\n' "$session_q"
160
- printf 'MODE=%s\n' "$mode_q"
161
- printf 'WORKTREE=%s\n' "$worktree_q"
162
- printf 'PROMPT_FILE=%s\n' "$prompt_q"
163
- printf 'OUTPUT_FILE=%s\n' "$output_q"
164
- printf 'BRANCH=%s\n' "$branch_q"
165
- printf 'RESULT_FILE=%s\n' "$result_q"
166
- printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
167
- printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
168
- printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
169
- printf 'STARTED_AT=%s\n' "$started_at_q"
170
- printf 'CRUSH_BIN=%s\n' "$crush_bin_q"
171
- printf 'OPENCODE_MODEL=%s\n' "$opencode_model_q"
172
- printf 'OPENCODE_TIMEOUT_SECONDS=%s\n' "$opencode_timeout_q"
173
- } >"$meta_file"
174
-
175
- context_exports=""
176
- if ((${#context_items[@]} > 0)); then
177
- for item in "${context_items[@]}"; do
178
- if [[ "$item" != *=* ]]; then
179
- echo "--context must use KEY=VALUE syntax: $item" >&2
180
- exit 1
181
- fi
182
- key="${item%%=*}"
183
- value="${item#*=}"
184
- if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
185
- echo "Invalid context key: $key" >&2
186
- exit 1
187
- fi
188
- printf -v value_q '%q' "$value"
189
- printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
190
- if [[ -n "$env_prefix" ]]; then
191
- context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
192
- fi
193
- context_exports+="export ACP_${key}=${value_q}"$'\n'
194
- if [[ "$env_prefix" != "F_LOSNING_" ]]; then
195
- context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
196
- fi
197
- done
198
- fi
199
-
200
- runtime_exports=$(
201
- cat <<EOF
202
- export AGENT_PROJECT_SESSION=${session_q}
203
- export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
204
- export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
205
- export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
206
- export ACP_SESSION=${session_q}
207
- export ACP_RUN_DIR=${sandbox_run_dir_q}
208
- export ACP_HOST_RUN_DIR=${artifact_dir_q}
209
- export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
210
- export F_LOSNING_SESSION=${session_q}
211
- export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
212
- export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
213
- export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
214
- EOF
215
- )
216
-
217
- if [[ -n "$env_prefix" ]]; then
218
- runtime_exports+=$'\n'
219
- runtime_exports+=$(cat <<EOF
220
- export ${env_prefix}SESSION=${session_q}
221
- export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
222
- export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
223
- export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
224
- EOF
225
- )
226
- fi
227
-
228
- collect_copy_snippet=""
229
- if ((${#collect_files[@]} > 0)); then
230
- for artifact_name in "${collect_files[@]}"; do
231
- [[ -z "$artifact_name" ]] && continue
232
- printf -v artifact_q '%q' "$artifact_name"
233
- collect_copy_snippet+=$(cat <<EOF
234
- if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
235
- cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
236
- fi
237
- EOF
238
- )
239
- collect_copy_snippet+=$'\n'
240
- done
241
- fi
242
-
243
- # Always collect result.env from sandbox to artifact_dir
244
- collect_copy_snippet+=$(
245
- cat <<EOF
246
- if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
247
- cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
248
- fi
249
- EOF
250
- )
251
- collect_copy_snippet+=$'\n'
252
-
253
- reconcile_snippet=""
254
- if [[ -n "$reconcile_command" ]]; then
255
- 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"
256
- reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
257
- fi
258
-
259
- cat >"$inner_script" <<EOF
260
- #!/usr/bin/env bash
261
- set -euo pipefail
262
- ${runtime_exports}
263
- ${context_exports}cd ${worktree_q}
264
-
265
- runner_state_file=${runner_state_q}
266
- output_file=${output_q}
267
- sandbox_run_dir=${sandbox_run_dir_q}
268
- artifact_dir=${artifact_dir_q}
269
- result_file_path=${sandbox_run_dir_q}/result.env
270
- host_result_file=${result_q}
271
- crush_bin=${crush_bin_q}
272
- opencode_model=${opencode_model_q}
273
- opencode_timeout=${opencode_timeout_q}
274
- prompt_file=${prompt_q}
275
- worktree=${worktree_q}
276
-
277
- write_state() {
278
- local runner_state="\${1:?runner state required}"
279
- local last_exit_code="\${2:-}"
280
- local failure_reason="\${3:-}"
281
- local updated_at tmp_file
282
-
283
- updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
284
- tmp_file="\${runner_state_file}.tmp.\$\$"
285
- {
286
- printf 'RUNNER_STATE=%q\n' "\${runner_state}"
287
- printf 'ATTEMPT=1\n'
288
- printf 'RESUME_COUNT=0\n'
289
- printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
290
- printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
291
- printf 'UPDATED_AT=%q\n' "\${updated_at}"
292
- } >"\${tmp_file}"
293
- mv "\${tmp_file}" "\${runner_state_file}"
294
- }
295
-
296
- record_final_git_state() {
297
- local final_head final_branch tmp_file
298
- final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
299
- final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
300
- tmp_file=${meta_file_q}.tmp.final.\$\$
301
- grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
302
- {
303
- printf 'FINAL_HEAD=%q\n' "\${final_head}"
304
- printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
305
- } >>"\${tmp_file}"
306
- mv "\${tmp_file}" ${meta_file_q}
307
- }
308
-
309
- write_state running
310
-
311
- mkdir -p "\${sandbox_run_dir}"
312
-
313
- # Crush (opencode) runs via 'crush run' in non-interactive mode.
314
- # --quiet suppresses spinner, --yolo auto-approves all tool permissions,
315
- # --model selects the provider/model, --cwd sets the working directory.
316
- # Prompt is provided via stdin.
317
- crush_exit_code=0
318
- crush_args=(run --quiet --model "\${opencode_model}" --cwd ${worktree_q})
319
- if [[ "${mode_q}" == "bypass" ]]; then
320
- crush_args+=(--yolo)
321
- fi
322
-
323
- if command -v timeout >/dev/null 2>&1; then
324
- timeout "\${opencode_timeout}" "\${crush_bin}" "\${crush_args[@]}" <"\${prompt_file}" 2>&1 | tee -a "\${output_file}" || crush_exit_code=\$?
325
- elif command -v gtimeout >/dev/null 2>&1; then
326
- gtimeout "\${opencode_timeout}" "\${crush_bin}" "\${crush_args[@]}" <"\${prompt_file}" 2>&1 | tee -a "\${output_file}" || crush_exit_code=\$?
327
- else
328
- "\${crush_bin}" "\${crush_args[@]}" <"\${prompt_file}" 2>&1 | tee -a "\${output_file}" &
329
- _crush_pid=\$!
330
- ( sleep "\${opencode_timeout}" && kill "\${_crush_pid}" 2>/dev/null ) &
331
- _wd=\$!
332
- wait "\${_crush_pid}" || crush_exit_code=\$?
333
- kill "\${_wd}" 2>/dev/null || true; wait "\${_wd}" 2>/dev/null || true
334
- [[ "\${crush_exit_code}" -eq 143 ]] && crush_exit_code=124
335
- fi
336
-
337
- if [[ "\${crush_exit_code}" -eq 0 ]]; then
338
- write_state succeeded 0
339
- # Crush has full tool access — it can write files and make commits.
340
- # Infer result from git state if result.env was not written by the agent.
341
- if [[ ! -f "\${result_file_path}" ]]; then
342
- if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
343
- || git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \\
344
- || git -C ${worktree_q} diff --name-only origin/main..HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null; then
345
- printf 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n' >"\${result_file_path}"
346
- else
347
- printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=missing-result-contract\n' >"\${result_file_path}"
348
- fi
349
- fi
350
- else
351
- failure_reason="opencode-exit-\${crush_exit_code}"
352
- [[ "\${crush_exit_code}" -eq 124 ]] && failure_reason="timeout"
353
- write_state failed "\${crush_exit_code}" "\${failure_reason}"
354
- if [[ ! -f "\${result_file_path}" ]]; then
355
- printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=%s\n' "\${failure_reason}" >"\${result_file_path}"
356
- fi
357
- fi
358
-
359
- record_final_git_state
360
-
361
- if [[ -f "\${result_file_path}" ]]; then
362
- cp "\${result_file_path}" "\${host_result_file}"
363
- fi
364
- ${collect_copy_snippet}
365
- ${reconcile_snippet}
366
- printf '\n__CODEX_EXIT__:%s\n' "\${crush_exit_code}" | tee -a "\${output_file}"
367
- exit "\${crush_exit_code}"
368
- EOF
369
-
370
- chmod +x "$inner_script"
371
- tmux new-session -d -s "$session" "$inner_script"
372
-
373
- printf 'SESSION=%s\n' "$session"
374
- printf 'TASK_KIND=%s\n' "$task_kind"
375
- printf 'TASK_ID=%s\n' "$task_id"
376
- printf 'WORKTREE=%s\n' "$worktree"
377
- printf 'OUTPUT=%s\n' "$output_file"