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,479 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-run-pi-session --mode safe|bypass --session <id> --worktree <path> --prompt-file <path> --runs-root <path> --adapter-id <id> --task-kind <kind> --task-id <id> [options]
8
-
9
- Launch a Pi coding agent worker session inside tmux for a project adapter and
10
- persist the standard run artifacts.
11
-
12
- Pi supports any OpenRouter model (or other providers) via --model provider/id.
13
-
14
- Options:
15
- --env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
16
- --context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
17
- --collect-file <name> Copy sandbox artifact file into the host run dir after execution
18
- --reconcile-command <cmd> Host-side command queued after the worker exits
19
- --sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .pi-artifacts)
20
- --pi-model <id> Model ID for pi (e.g. openrouter/qwen/qwen3.6-plus:free)
21
- --pi-thinking <level> Thinking level: off, minimal, low, medium, high, xhigh (default: low)
22
- --pi-timeout-seconds <secs> Hard timeout in seconds (default: 900)
23
- --pi-stall-seconds <secs> Abort if no output for this long, 0 disables (default: 300)
24
- --help Show this help
25
- EOF
26
- }
27
-
28
- mode=""
29
- session=""
30
- worktree=""
31
- prompt_file=""
32
- runs_root=""
33
- adapter_id=""
34
- task_kind=""
35
- task_id=""
36
- env_prefix=""
37
- sandbox_subdir=".pi-artifacts"
38
- reconcile_command=""
39
- pi_model="${ACP_PI_MODEL:-${F_LOSNING_PI_MODEL:-openrouter/qwen/qwen3.6-plus:free}}"
40
- pi_thinking="${ACP_PI_THINKING:-${F_LOSNING_PI_THINKING:-low}}"
41
- pi_timeout_seconds="${ACP_PI_TIMEOUT_SECONDS:-${F_LOSNING_PI_TIMEOUT_SECONDS:-900}}"
42
- pi_stall_seconds="${ACP_PI_STALL_SECONDS:-${F_LOSNING_PI_STALL_SECONDS:-300}}"
43
- declare -a context_items=()
44
- declare -a collect_files=()
45
-
46
- while [[ $# -gt 0 ]]; do
47
- case "$1" in
48
- --mode) mode="${2:-}"; shift 2 ;;
49
- --session) session="${2:-}"; shift 2 ;;
50
- --worktree) worktree="${2:-}"; shift 2 ;;
51
- --prompt-file) prompt_file="${2:-}"; shift 2 ;;
52
- --runs-root) runs_root="${2:-}"; shift 2 ;;
53
- --adapter-id) adapter_id="${2:-}"; shift 2 ;;
54
- --task-kind) task_kind="${2:-}"; shift 2 ;;
55
- --task-id) task_id="${2:-}"; shift 2 ;;
56
- --env-prefix) env_prefix="${2:-}"; shift 2 ;;
57
- --context) context_items+=("${2:-}"); shift 2 ;;
58
- --collect-file) collect_files+=("${2:-}"); shift 2 ;;
59
- --reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
60
- --sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
61
- --pi-model) pi_model="${2:-}"; shift 2 ;;
62
- --pi-thinking) pi_thinking="${2:-}"; shift 2 ;;
63
- --pi-timeout-seconds) pi_timeout_seconds="${2:-}"; shift 2 ;;
64
- --pi-stall-seconds) pi_stall_seconds="${2:-}"; shift 2 ;;
65
- --help|-h) usage; exit 0 ;;
66
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
67
- esac
68
- done
69
-
70
- if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
71
- usage >&2
72
- exit 1
73
- fi
74
-
75
- case "$mode" in
76
- safe|bypass) ;;
77
- *) echo "--mode must be safe or bypass" >&2; exit 1 ;;
78
- esac
79
-
80
- case "$pi_thinking" in
81
- off|minimal|low|medium|high|xhigh) ;;
82
- *) echo "--pi-thinking must be one of: off, minimal, low, medium, high, xhigh" >&2; exit 1 ;;
83
- esac
84
-
85
- case "$pi_timeout_seconds" in
86
- ''|*[!0-9]*|0) echo "--pi-timeout-seconds must be a positive integer" >&2; exit 1 ;;
87
- esac
88
-
89
- case "$pi_stall_seconds" in
90
- ''|*[!0-9]*) echo "--pi-stall-seconds must be numeric" >&2; exit 1 ;;
91
- esac
92
-
93
- resolve_pi_bin() {
94
- local configured_bin="${PI_BIN:-${ACP_PI_BIN:-${F_LOSNING_PI_BIN:-}}}"
95
- if [[ -n "${configured_bin}" && -x "${configured_bin}" ]]; then
96
- printf '%s\n' "${configured_bin}"
97
- return 0
98
- fi
99
- if command -v pi >/dev/null 2>&1; then
100
- command -v pi
101
- return 0
102
- fi
103
- local -a fallback_paths=(
104
- "/opt/homebrew/bin/pi"
105
- "/usr/local/bin/pi"
106
- "${HOME}/.local/bin/pi"
107
- "${HOME}/.nvm/versions/node/$(node --version 2>/dev/null || true)/bin/pi"
108
- )
109
- local p
110
- for p in "${fallback_paths[@]}"; do
111
- if [[ -x "${p}" ]]; then
112
- printf '%s\n' "${p}"
113
- return 0
114
- fi
115
- done
116
- return 1
117
- }
118
-
119
- pi_bin="$(resolve_pi_bin || true)"
120
- if [[ -z "${pi_bin}" || ! -x "${pi_bin}" ]]; then
121
- echo "unable to resolve a runnable pi binary — install with: npm install -g @mariozechner/pi-coding-agent" >&2
122
- exit 1
123
- fi
124
-
125
- artifact_dir="${runs_root}/${session}"
126
- output_file="${artifact_dir}/${session}.log"
127
- inner_script="${artifact_dir}/${session}.sh"
128
- meta_file="${artifact_dir}/run.env"
129
- result_file="${artifact_dir}/result.env"
130
- runner_state_file="${artifact_dir}/runner.env"
131
- sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
132
- retained_repo_root="${ACP_RETAINED_REPO_ROOT:-${F_LOSNING_RETAINED_REPO_ROOT:-}}"
133
- started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
134
-
135
- mkdir -p "$artifact_dir"
136
- mkdir -p "$sandbox_run_dir"
137
-
138
- if tmux has-session -t "$session" 2>/dev/null; then
139
- echo "tmux session already exists: $session" >&2
140
- exit 1
141
- fi
142
-
143
- branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
144
-
145
- printf -v session_q '%q' "$session"
146
- printf -v task_kind_q '%q' "$task_kind"
147
- printf -v task_id_q '%q' "$task_id"
148
- printf -v mode_q '%q' "$mode"
149
- printf -v worktree_q '%q' "$worktree"
150
- printf -v prompt_q '%q' "$prompt_file"
151
- printf -v output_q '%q' "$output_file"
152
- printf -v artifact_dir_q '%q' "$artifact_dir"
153
- printf -v script_q '%q' "$inner_script"
154
- printf -v result_q '%q' "$result_file"
155
- printf -v meta_file_q '%q' "$meta_file"
156
- printf -v runner_state_q '%q' "$runner_state_file"
157
- printf -v branch_q '%q' "$branch_name"
158
- printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
159
- printf -v retained_repo_root_q '%q' "$retained_repo_root"
160
- printf -v adapter_id_q '%q' "$adapter_id"
161
- printf -v started_at_q '%q' "$started_at"
162
- printf -v pi_bin_q '%q' "$pi_bin"
163
- printf -v pi_model_q '%q' "$pi_model"
164
- printf -v pi_thinking_q '%q' "$pi_thinking"
165
- printf -v pi_timeout_q '%q' "$pi_timeout_seconds"
166
- printf -v pi_stall_q '%q' "$pi_stall_seconds"
167
-
168
- {
169
- printf 'TASK_KIND=%s\n' "$task_kind_q"
170
- printf 'TASK_ID=%s\n' "$task_id_q"
171
- printf 'SESSION=%s\n' "$session_q"
172
- printf 'MODE=%s\n' "$mode_q"
173
- printf 'WORKTREE=%s\n' "$worktree_q"
174
- printf 'PROMPT_FILE=%s\n' "$prompt_q"
175
- printf 'OUTPUT_FILE=%s\n' "$output_q"
176
- printf 'SCRIPT=%s\n' "$script_q"
177
- printf 'BRANCH=%s\n' "$branch_q"
178
- printf 'RESULT_FILE=%s\n' "$result_q"
179
- printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
180
- printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
181
- printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
182
- printf 'STARTED_AT=%s\n' "$started_at_q"
183
- printf 'PI_BIN=%s\n' "$pi_bin_q"
184
- printf 'PI_MODEL=%s\n' "$pi_model_q"
185
- printf 'PI_THINKING=%s\n' "$pi_thinking_q"
186
- printf 'PI_TIMEOUT_SECONDS=%s\n' "$pi_timeout_q"
187
- printf 'PI_STALL_SECONDS=%s\n' "$pi_stall_q"
188
- } >"$meta_file"
189
-
190
- context_exports=""
191
- if ((${#context_items[@]} > 0)); then
192
- for item in "${context_items[@]}"; do
193
- if [[ "$item" != *=* ]]; then
194
- echo "--context must use KEY=VALUE syntax: $item" >&2
195
- exit 1
196
- fi
197
- key="${item%%=*}"
198
- value="${item#*=}"
199
- if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
200
- echo "Invalid context key: $key" >&2
201
- exit 1
202
- fi
203
- printf -v value_q '%q' "$value"
204
- printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
205
- if [[ -n "$env_prefix" ]]; then
206
- context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
207
- fi
208
- context_exports+="export ACP_${key}=${value_q}"$'\n'
209
- if [[ "$env_prefix" != "F_LOSNING_" ]]; then
210
- context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
211
- fi
212
- done
213
- fi
214
-
215
- runtime_exports=$(
216
- cat <<EOF
217
- export AGENT_PROJECT_SESSION=${session_q}
218
- export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
219
- export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
220
- export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
221
- export AGENT_PROJECT_PI_BIN=${pi_bin_q}
222
- export AGENT_PROJECT_RETAINED_REPO_ROOT=${retained_repo_root_q}
223
- export ACP_SESSION=${session_q}
224
- export ACP_RUN_DIR=${sandbox_run_dir_q}
225
- export ACP_HOST_RUN_DIR=${artifact_dir_q}
226
- export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
227
- export ACP_PI_BIN=${pi_bin_q}
228
- export ACP_PI_MODEL=${pi_model_q}
229
- export ACP_PI_THINKING=${pi_thinking_q}
230
- export ACP_PI_TIMEOUT_SECONDS=${pi_timeout_q}
231
- export ACP_RETAINED_REPO_ROOT=${retained_repo_root_q}
232
- export F_LOSNING_SESSION=${session_q}
233
- export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
234
- export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
235
- export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
236
- export F_LOSNING_PI_BIN=${pi_bin_q}
237
- export F_LOSNING_PI_MODEL=${pi_model_q}
238
- export F_LOSNING_PI_THINKING=${pi_thinking_q}
239
- export F_LOSNING_PI_TIMEOUT_SECONDS=${pi_timeout_q}
240
- export F_LOSNING_RETAINED_REPO_ROOT=${retained_repo_root_q}
241
- EOF
242
- )
243
-
244
- if [[ -n "$env_prefix" ]]; then
245
- runtime_exports+=$'\n'
246
- runtime_exports+=$(cat <<EOF
247
- export ${env_prefix}SESSION=${session_q}
248
- export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
249
- export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
250
- export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
251
- export ${env_prefix}PI_BIN=${pi_bin_q}
252
- export ${env_prefix}PI_MODEL=${pi_model_q}
253
- export ${env_prefix}PI_THINKING=${pi_thinking_q}
254
- export ${env_prefix}PI_TIMEOUT_SECONDS=${pi_timeout_q}
255
- EOF
256
- )
257
- fi
258
-
259
- collect_copy_snippet=""
260
- if ((${#collect_files[@]} > 0)); then
261
- for artifact_name in "${collect_files[@]}"; do
262
- [[ -z "$artifact_name" ]] && continue
263
- printf -v artifact_q '%q' "$artifact_name"
264
- collect_copy_snippet+=$(
265
- cat <<EOF
266
- if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
267
- cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
268
- fi
269
- EOF
270
- )
271
- collect_copy_snippet+=$'\n'
272
- done
273
- fi
274
-
275
- # Always collect result.env from sandbox to artifact_dir
276
- collect_copy_snippet+=$(
277
- cat <<EOF
278
- if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
279
- cp ${sandbox_run_dir_q}/result.env ${result_q}
280
- fi
281
- EOF
282
- )
283
-
284
- reconcile_snippet=""
285
- if [[ -n "$reconcile_command" ]]; then
286
- printf -v delayed_reconcile_q '%q' "export ACP_EXPECTED_RUN_STARTED_AT=${started_at_q}; export F_LOSNING_EXPECTED_RUN_STARTED_AT=${started_at_q}; while tmux has-session -t ${session_q} 2>/dev/null; do sleep 1; done; sleep 2; $reconcile_command"
287
- reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
288
- fi
289
-
290
- cat >"$inner_script" <<EOF
291
- #!/usr/bin/env bash
292
- set -euo pipefail
293
- ${runtime_exports}
294
- ${context_exports}cd ${worktree_q}
295
-
296
- runner_state_file=${runner_state_q}
297
- output_file=${output_q}
298
- sandbox_run_dir=${sandbox_run_dir_q}
299
- artifact_dir=${artifact_dir_q}
300
- result_file_path=${sandbox_run_dir_q}/result.env
301
- host_result_file=${result_q}
302
- pi_bin=${pi_bin_q}
303
- pi_model=${pi_model_q}
304
- pi_thinking=${pi_thinking_q}
305
- pi_timeout_seconds=${pi_timeout_q}
306
- pi_stall_seconds=${pi_stall_q}
307
- prompt_file=${prompt_q}
308
- worktree=${worktree_q}
309
-
310
- write_state() {
311
- local runner_state="\${1:?runner state required}"
312
- local last_exit_code="\${2:-}"
313
- local failure_reason="\${3:-}"
314
- local updated_at tmp_file
315
-
316
- updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
317
- tmp_file="\${runner_state_file}.tmp.\$\$"
318
- {
319
- printf 'RUNNER_STATE=%q\n' "\${runner_state}"
320
- printf 'ATTEMPT=1\n'
321
- printf 'RESUME_COUNT=0\n'
322
- printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
323
- printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
324
- printf 'LAST_TRIGGER_REASON=%q\n' ''
325
- printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
326
- printf 'UPDATED_AT=%q\n' "\${updated_at}"
327
- } >"\${tmp_file}"
328
- mv "\${tmp_file}" "\${runner_state_file}"
329
- }
330
-
331
- write_result_fallback() {
332
- local detail="\${1:-missing-result-contract}"
333
- local tmp_file
334
- tmp_file="\${result_file_path}.tmp.\$\$"
335
- {
336
- printf 'OUTCOME=blocked\n'
337
- printf 'ACTION=host-comment-blocker\n'
338
- printf 'DETAIL=%s\n' "\${detail}"
339
- } >"\${tmp_file}"
340
- mv "\${tmp_file}" "\${result_file_path}"
341
- }
342
-
343
- record_final_git_state() {
344
- local final_head final_branch tmp_file
345
- final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
346
- final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
347
- tmp_file=${meta_file_q}.tmp.final.$$
348
- grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
349
- {
350
- printf 'FINAL_HEAD=%q\n' "\${final_head}"
351
- printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
352
- } >>"\${tmp_file}"
353
- mv "\${tmp_file}" ${meta_file_q}
354
- }
355
-
356
- ${reconcile_snippet}
357
-
358
- write_state running
359
-
360
- mkdir -p "\${sandbox_run_dir}"
361
-
362
- # Run pi in print mode (non-interactive, single-shot)
363
- # --no-session: ephemeral, don't persist session state
364
- # --thinking: reasoning depth
365
- # @prompt_file: pass prompt as file reference
366
- pi_exit_code=0
367
- # macOS does not ship 'timeout'; prefer it when available, else use background watchdog
368
- if command -v timeout >/dev/null 2>&1; then
369
- timeout "\${pi_timeout_seconds}" \\
370
- "\${pi_bin}" --print --no-session \\
371
- --model "\${pi_model}" \\
372
- --thinking "\${pi_thinking}" \\
373
- "@\${prompt_file}" \\
374
- 2>&1 | tee -a "\${output_file}" || pi_exit_code=\$?
375
- elif command -v gtimeout >/dev/null 2>&1; then
376
- gtimeout "\${pi_timeout_seconds}" \\
377
- "\${pi_bin}" --print --no-session \\
378
- --model "\${pi_model}" \\
379
- --thinking "\${pi_thinking}" \\
380
- "@\${prompt_file}" \\
381
- 2>&1 | tee -a "\${output_file}" || pi_exit_code=\$?
382
- else
383
- "\${pi_bin}" --print --no-session \\
384
- --model "\${pi_model}" \\
385
- --thinking "\${pi_thinking}" \\
386
- "@\${prompt_file}" \\
387
- 2>&1 >> "\${output_file}" &
388
- _pi_bgpid=\$!
389
- # Hard timeout watchdog
390
- ( sleep "\${pi_timeout_seconds}" && kill "\${_pi_bgpid}" 2>/dev/null \\
391
- && printf '[pi] timed out after %s seconds\n' "\${pi_timeout_seconds}" >> "\${output_file}" ) &
392
- _pi_timeout_wd=\$!
393
- # Stall watchdog: kill if output file stops growing for pi_stall_seconds
394
- if [[ "\${pi_stall_seconds}" -gt 0 ]]; then
395
- (
396
- _prev_size=-1
397
- while kill -0 "\${_pi_bgpid}" 2>/dev/null; do
398
- _cur_size="\$(wc -c < "\${output_file}" 2>/dev/null || echo 0)"
399
- if [[ "\${_cur_size}" -eq "\${_prev_size}" ]]; then
400
- if [[ -z "\${_idle_since:-}" ]]; then _idle_since="\$(date +%s)"; fi
401
- if (( \$(date +%s) - _idle_since >= pi_stall_seconds )); then
402
- printf '[pi] no output for %s seconds — aborting stalled worker\n' "\${pi_stall_seconds}" >> "\${output_file}"
403
- kill "\${_pi_bgpid}" 2>/dev/null
404
- break
405
- fi
406
- else
407
- _idle_since=""
408
- _prev_size="\${_cur_size}"
409
- fi
410
- sleep 5
411
- done
412
- ) &
413
- _pi_stall_wd=\$!
414
- fi
415
- wait "\${_pi_bgpid}" || pi_exit_code=\$?
416
- kill "\${_pi_timeout_wd}" 2>/dev/null || true
417
- wait "\${_pi_timeout_wd}" 2>/dev/null || true
418
- if [[ -n "\${_pi_stall_wd:-}" ]]; then
419
- kill "\${_pi_stall_wd}" 2>/dev/null || true
420
- wait "\${_pi_stall_wd}" 2>/dev/null || true
421
- fi
422
- if [[ "\${pi_exit_code}" -eq 143 ]]; then
423
- pi_exit_code=124
424
- fi
425
- fi
426
-
427
- if [[ "\${pi_exit_code}" -eq 0 ]]; then
428
- # Pi runs in --print mode and cannot write result.env itself.
429
- # If the result file is missing, write a blocked fallback so reconcile
430
- # sees a valid contract instead of an invalid-result-contract failure.
431
- if [[ ! -f "\${result_file_path}" ]]; then
432
- write_result_fallback "missing-result-contract"
433
- fi
434
- write_state succeeded 0
435
- else
436
- failure_reason="pi-exit-\${pi_exit_code}"
437
- if [[ "\${pi_exit_code}" -eq 124 ]]; then
438
- failure_reason="timeout"
439
- fi
440
- if [[ ! -f "\${result_file_path}" ]]; then
441
- write_result_fallback "\${failure_reason}"
442
- fi
443
- write_state failed "\${pi_exit_code}" "\${failure_reason}"
444
- fi
445
-
446
- record_final_git_state
447
-
448
- ${collect_copy_snippet}
449
- printf '\n__CODEX_EXIT__:%s\n' "\${pi_exit_code}" | tee -a "\${output_file}"
450
- exit "\${pi_exit_code}"
451
- EOF
452
-
453
- chmod +x "$inner_script"
454
-
455
- # Write initial runner state
456
- {
457
- printf 'RUNNER_STATE=%q\n' "running"
458
- printf 'ATTEMPT=1\n'
459
- printf 'RESUME_COUNT=0\n'
460
- printf "LAST_EXIT_CODE=''\n"
461
- printf "LAST_FAILURE_REASON=''\n"
462
- printf "LAST_TRIGGER_REASON=''\n"
463
- printf "AUTH_WAIT_STARTED_AT=''\n"
464
- printf 'UPDATED_AT=%q\n' "$started_at"
465
- printf 'RUNNER_STATE_FILE=%q\n' "$runner_state_file"
466
- } >"$runner_state_file"
467
-
468
- # Append pi-specific metadata to run.env for dashboard/status visibility
469
- {
470
- printf 'PI_MODEL=%s\n' "$pi_model_q"
471
- printf 'PI_THINKING=%s\n' "$pi_thinking_q"
472
- printf 'PI_TIMEOUT_SECONDS=%s\n' "$pi_timeout_q"
473
- printf 'PI_STALL_SECONDS=%s\n' "$pi_stall_q"
474
- printf 'PI_BIN=%s\n' "$pi_bin_q"
475
- } >>"$meta_file"
476
-
477
- tmux new-session -d -s "$session" -x 220 -y 50 \
478
- "bash -l $script_q >> $output_q 2>&1; tmux wait-for -S $session_q-done" \; \
479
- wait-for "$session-done" || true
@@ -1,139 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-sync-anchor-repo --canonical-root <path> --anchor-root <path> [--remote <name>] [--default-branch <name>]
8
-
9
- Initialize or refresh a local agent-owned anchor repository that is safe to use
10
- as the Git parent for per-task worktrees. The canonical checkout is only used to
11
- discover the authoritative remote URL.
12
- EOF
13
- }
14
-
15
- canonical_root=""
16
- anchor_root=""
17
- remote_name="origin"
18
- default_branch="main"
19
- dirty_state_stashed="no"
20
- dirty_stash_message=""
21
-
22
- while [[ $# -gt 0 ]]; do
23
- case "$1" in
24
- --canonical-root) canonical_root="${2:-}"; shift 2 ;;
25
- --anchor-root) anchor_root="${2:-}"; shift 2 ;;
26
- --remote) remote_name="${2:-}"; shift 2 ;;
27
- --default-branch) default_branch="${2:-}"; shift 2 ;;
28
- --help|-h) usage; exit 0 ;;
29
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
30
- esac
31
- done
32
-
33
- if [[ -z "$canonical_root" || -z "$anchor_root" ]]; then
34
- usage >&2
35
- exit 1
36
- fi
37
-
38
- if [[ ! -d "$canonical_root/.git" && ! -f "$canonical_root/.git" ]]; then
39
- echo "[agent-sync-anchor] canonical root is not a Git checkout: $canonical_root" >&2
40
- exit 1
41
- fi
42
-
43
- origin_url="$(git -C "$canonical_root" remote get-url "$remote_name" 2>/dev/null || true)"
44
- if [[ -z "$origin_url" ]]; then
45
- echo "[agent-sync-anchor] missing remote '$remote_name' in canonical repo: $canonical_root" >&2
46
- exit 1
47
- fi
48
-
49
- anchor_parent="$(dirname "$anchor_root")"
50
- lock_dir="${anchor_root}.sync.lock"
51
- pid_file="${lock_dir}/pid"
52
-
53
- mkdir -p "$anchor_parent"
54
-
55
- acquire_lock() {
56
- local attempts=0
57
- while ! mkdir "$lock_dir" 2>/dev/null; do
58
- if [[ -f "$pid_file" ]]; then
59
- local existing_pid
60
- existing_pid="$(cat "$pid_file" 2>/dev/null || true)"
61
- if [[ -n "$existing_pid" ]] && ! kill -0 "$existing_pid" 2>/dev/null; then
62
- rm -rf "$lock_dir"
63
- continue
64
- fi
65
- else
66
- rm -rf "$lock_dir"
67
- continue
68
- fi
69
-
70
- attempts=$((attempts + 1))
71
- if (( attempts >= 60 )); then
72
- echo "[agent-sync-anchor] timed out waiting for anchor lock: $lock_dir" >&2
73
- exit 1
74
- fi
75
- sleep 1
76
- done
77
- printf '%s\n' "$$" >"$pid_file"
78
- }
79
-
80
- cleanup() {
81
- rm -rf "$lock_dir"
82
- }
83
-
84
- trap cleanup EXIT
85
- acquire_lock
86
-
87
- if [[ -e "$anchor_root" && ! -d "$anchor_root/.git" && ! -f "$anchor_root/.git" ]]; then
88
- echo "[agent-sync-anchor] anchor root exists but is not a Git checkout: $anchor_root" >&2
89
- exit 1
90
- fi
91
-
92
- if [[ ! -d "$anchor_root/.git" && ! -f "$anchor_root/.git" ]]; then
93
- git clone \
94
- --origin "$remote_name" \
95
- --branch "$default_branch" \
96
- --single-branch \
97
- --no-tags \
98
- "$origin_url" \
99
- "$anchor_root" >/dev/null
100
- fi
101
-
102
- git -C "$anchor_root" remote set-url "$remote_name" "$origin_url"
103
- git -C "$anchor_root" config "remote.${remote_name}.prune" true
104
- git -C "$anchor_root" fetch "$remote_name" --prune >/dev/null
105
- git -C "$anchor_root" worktree prune >/dev/null 2>&1 || true
106
-
107
- if [[ -n "$(git -C "$anchor_root" status --porcelain --untracked-files=no)" ]]; then
108
- dirty_stash_message="acp-anchor-sync-$(date -u +%Y%m%dT%H%M%SZ)"
109
- git -C "$anchor_root" stash push --message "$dirty_stash_message" >/dev/null
110
- if [[ -n "$(git -C "$anchor_root" status --porcelain --untracked-files=no)" ]]; then
111
- echo "[agent-sync-anchor] anchor repo is dirty; refuse to reset: $anchor_root" >&2
112
- exit 1
113
- fi
114
- dirty_state_stashed="yes"
115
- fi
116
-
117
- default_ref="${remote_name}/${default_branch}"
118
- if git -C "$anchor_root" show-ref --verify --quiet "refs/remotes/${default_ref}"; then
119
- current_branch="$(git -C "$anchor_root" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
120
- if [[ "$current_branch" != "$default_branch" ]]; then
121
- if git -C "$anchor_root" show-ref --verify --quiet "refs/heads/${default_branch}"; then
122
- git -C "$anchor_root" checkout "$default_branch" >/dev/null
123
- else
124
- git -C "$anchor_root" checkout -b "$default_branch" "$default_ref" >/dev/null
125
- fi
126
- fi
127
- git -C "$anchor_root" reset --hard "$default_ref" >/dev/null
128
- git -C "$anchor_root" branch --set-upstream-to="$default_ref" "$default_branch" >/dev/null 2>&1 || true
129
- fi
130
-
131
- printf 'CANONICAL_ROOT=%s\n' "$canonical_root"
132
- printf 'ANCHOR_ROOT=%s\n' "$anchor_root"
133
- printf 'REMOTE=%s\n' "$remote_name"
134
- printf 'DEFAULT_BRANCH=%s\n' "$default_branch"
135
- printf 'ORIGIN_URL=%s\n' "$origin_url"
136
- printf 'DIRTY_STATE_STASHED=%s\n' "$dirty_state_stashed"
137
- if [[ "$dirty_state_stashed" == "yes" ]]; then
138
- printf 'DIRTY_STASH_MESSAGE=%s\n' "$dirty_stash_message"
139
- fi