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.
- package/README.md +141 -28
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +257 -59
- package/package.json +39 -32
- package/tools/bin/debug-session.sh +106 -0
- package/tools/bin/flow-config-lib.sh +1203 -60
- package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
- package/tools/bin/flow-runtime-doctor.sh +5 -1
- package/tools/bin/flow-shell-lib.sh +32 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
- package/tools/bin/heartbeat-safe-auto.sh +42 -0
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/install-project-systemd.sh +255 -0
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +5 -1
- package/tools/bin/project-runtimectl.sh +91 -2
- package/tools/bin/project-systemd-bootstrap.sh +74 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/uninstall-project-systemd.sh +87 -0
- package/tools/dashboard/app.js +228 -6
- package/tools/dashboard/dashboard_snapshot.py +55 -0
- package/tools/dashboard/issue_queue_state.py +101 -0
- package/tools/dashboard/server.py +123 -1
- package/tools/dashboard/styles.css +526 -455
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/agent-cleanup-worktree +0 -247
- package/tools/bin/agent-github-update-labels +0 -71
- package/tools/bin/agent-init-worktree +0 -216
- package/tools/bin/agent-project-archive-run +0 -52
- package/tools/bin/agent-project-capture-worker +0 -46
- package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
- package/tools/bin/agent-project-catch-up-merged-prs +0 -194
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
- package/tools/bin/agent-project-cleanup-session +0 -513
- package/tools/bin/agent-project-detached-launch +0 -127
- package/tools/bin/agent-project-heartbeat-loop +0 -1029
- package/tools/bin/agent-project-open-issue-worktree +0 -89
- package/tools/bin/agent-project-open-pr-worktree +0 -80
- package/tools/bin/agent-project-publish-issue-pr +0 -465
- package/tools/bin/agent-project-reconcile-issue-session +0 -1398
- package/tools/bin/agent-project-reconcile-pr-session +0 -1230
- package/tools/bin/agent-project-retry-state +0 -147
- package/tools/bin/agent-project-run-claude-session +0 -805
- package/tools/bin/agent-project-run-codex-resilient +0 -955
- package/tools/bin/agent-project-run-codex-session +0 -435
- package/tools/bin/agent-project-run-kilo-session +0 -369
- package/tools/bin/agent-project-run-ollama-session +0 -658
- package/tools/bin/agent-project-run-openclaw-session +0 -1309
- package/tools/bin/agent-project-run-opencode-session +0 -377
- package/tools/bin/agent-project-run-pi-session +0 -479
- package/tools/bin/agent-project-sync-anchor-repo +0 -139
- package/tools/bin/agent-project-worker-status +0 -188
- package/tools/bin/branch-verification-guard.sh +0 -364
- package/tools/bin/capture-worker.sh +0 -18
- package/tools/bin/cleanup-worktree.sh +0 -52
- package/tools/bin/codex-quota +0 -31
- package/tools/bin/create-follow-up-issue.sh +0 -114
- package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
- package/tools/bin/issue-publish-localization-guard.sh +0 -142
- package/tools/bin/issue-publish-scope-guard.sh +0 -242
- package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
- package/tools/bin/issue-resource-class.sh +0 -12
- package/tools/bin/kick-scheduler.sh +0 -75
- package/tools/bin/label-follow-up-issues.sh +0 -14
- package/tools/bin/new-pr-worktree.sh +0 -50
- package/tools/bin/new-worktree.sh +0 -49
- package/tools/bin/pr-risk.sh +0 -12
- package/tools/bin/prepare-worktree.sh +0 -142
- package/tools/bin/provider-cooldown-state.sh +0 -204
- package/tools/bin/publish-issue-worker.sh +0 -31
- package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
- package/tools/bin/reconcile-issue-worker.sh +0 -34
- package/tools/bin/reconcile-pr-worker.sh +0 -34
- package/tools/bin/record-verification.sh +0 -71
- package/tools/bin/render-flow-config.sh +0 -98
- package/tools/bin/resident-issue-controller-lib.sh +0 -448
- package/tools/bin/resident-issue-queue-status.py +0 -35
- package/tools/bin/retry-state.sh +0 -31
- package/tools/bin/reuse-issue-worktree.sh +0 -121
- package/tools/bin/run-codex-bypass.sh +0 -3
- package/tools/bin/run-codex-safe.sh +0 -3
- package/tools/bin/run-codex-task.sh +0 -280
- package/tools/bin/serve-dashboard.sh +0 -5
- package/tools/bin/split-retained-slice.sh +0 -124
- package/tools/bin/start-issue-worker.sh +0 -943
- package/tools/bin/start-pr-fix-worker.sh +0 -491
- package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
- package/tools/bin/start-pr-review-worker.sh +0 -261
- package/tools/bin/start-resident-issue-loop.sh +0 -499
- package/tools/bin/update-github-labels.sh +0 -14
- package/tools/bin/worker-status.sh +0 -19
- package/tools/bin/workflow-catalog.sh +0 -77
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
usage() {
|
|
5
|
-
cat <<'EOF'
|
|
6
|
-
Usage:
|
|
7
|
-
agent-project-run-ollama-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 an Ollama local model worker inside tmux for a project adapter and
|
|
10
|
-
persist the standard run artifacts. Requires Ollama running at the configured
|
|
11
|
-
base URL with the specified model pulled.
|
|
12
|
-
|
|
13
|
-
Options:
|
|
14
|
-
--env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
|
|
15
|
-
--context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
|
|
16
|
-
--collect-file <name> Copy worker artifact file into the host run dir after execution
|
|
17
|
-
--reconcile-command <cmd> Host-side command queued after the worker exits
|
|
18
|
-
--sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .ollama-artifacts)
|
|
19
|
-
--ollama-model <id> Ollama model name (default: qwen2.5-coder:7b)
|
|
20
|
-
--ollama-base-url <url> Ollama API base URL (default: http://localhost:11434)
|
|
21
|
-
--ollama-timeout-seconds <secs> Max wall-clock time for the agent run (default: 900)
|
|
22
|
-
--help Show this help
|
|
23
|
-
EOF
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
mode=""
|
|
27
|
-
session=""
|
|
28
|
-
worktree=""
|
|
29
|
-
prompt_file=""
|
|
30
|
-
runs_root=""
|
|
31
|
-
adapter_id=""
|
|
32
|
-
task_kind=""
|
|
33
|
-
task_id=""
|
|
34
|
-
env_prefix=""
|
|
35
|
-
sandbox_subdir=".ollama-artifacts"
|
|
36
|
-
reconcile_command=""
|
|
37
|
-
ollama_model="${ACP_OLLAMA_MODEL:-${F_LOSNING_OLLAMA_MODEL:-qwen2.5-coder:7b}}"
|
|
38
|
-
ollama_base_url="${ACP_OLLAMA_BASE_URL:-${F_LOSNING_OLLAMA_BASE_URL:-http://localhost:11434}}"
|
|
39
|
-
ollama_timeout_seconds="${ACP_OLLAMA_TIMEOUT_SECONDS:-${F_LOSNING_OLLAMA_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
|
-
--ollama-model) ollama_model="${2:-}"; shift 2 ;;
|
|
59
|
-
--ollama-base-url) ollama_base_url="${2:-}"; shift 2 ;;
|
|
60
|
-
--ollama-timeout-seconds) ollama_timeout_seconds="${2:-}"; shift 2 ;;
|
|
61
|
-
--help|-h) usage; exit 0 ;;
|
|
62
|
-
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
63
|
-
esac
|
|
64
|
-
done
|
|
65
|
-
|
|
66
|
-
if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
|
|
67
|
-
usage >&2
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
case "$mode" in
|
|
72
|
-
safe|bypass) ;;
|
|
73
|
-
*)
|
|
74
|
-
echo "--mode must be safe or bypass" >&2
|
|
75
|
-
exit 1
|
|
76
|
-
;;
|
|
77
|
-
esac
|
|
78
|
-
|
|
79
|
-
case "$ollama_timeout_seconds" in
|
|
80
|
-
''|*[!0-9]*) echo "--ollama-timeout-seconds must be numeric" >&2; exit 1 ;;
|
|
81
|
-
esac
|
|
82
|
-
|
|
83
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
84
|
-
echo "agent-project-run-ollama-session: node is required but not found in PATH" >&2
|
|
85
|
-
exit 1
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
artifact_dir="${runs_root}/${session}"
|
|
89
|
-
output_file="${artifact_dir}/${session}.log"
|
|
90
|
-
inner_script="${artifact_dir}/${session}.sh"
|
|
91
|
-
meta_file="${artifact_dir}/run.env"
|
|
92
|
-
result_file="${artifact_dir}/result.env"
|
|
93
|
-
runner_state_file="${artifact_dir}/runner.env"
|
|
94
|
-
sandbox_artifact_dir="${worktree%/}/${sandbox_subdir}"
|
|
95
|
-
sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
|
|
96
|
-
retained_repo_root="${ACP_RETAINED_REPO_ROOT:-${F_LOSNING_RETAINED_REPO_ROOT:-}}"
|
|
97
|
-
started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
98
|
-
|
|
99
|
-
mkdir -p "$artifact_dir"
|
|
100
|
-
mkdir -p "$sandbox_run_dir"
|
|
101
|
-
|
|
102
|
-
if tmux has-session -t "$session" 2>/dev/null; then
|
|
103
|
-
echo "tmux session already exists: $session" >&2
|
|
104
|
-
exit 1
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
|
|
108
|
-
|
|
109
|
-
printf -v session_q '%q' "$session"
|
|
110
|
-
printf -v task_kind_q '%q' "$task_kind"
|
|
111
|
-
printf -v task_id_q '%q' "$task_id"
|
|
112
|
-
printf -v mode_q '%q' "$mode"
|
|
113
|
-
printf -v worktree_q '%q' "$worktree"
|
|
114
|
-
printf -v prompt_q '%q' "$prompt_file"
|
|
115
|
-
printf -v output_q '%q' "$output_file"
|
|
116
|
-
printf -v artifact_dir_q '%q' "$artifact_dir"
|
|
117
|
-
printf -v script_q '%q' "$inner_script"
|
|
118
|
-
printf -v result_q '%q' "$result_file"
|
|
119
|
-
printf -v meta_file_q '%q' "$meta_file"
|
|
120
|
-
printf -v runner_state_q '%q' "$runner_state_file"
|
|
121
|
-
printf -v branch_q '%q' "$branch_name"
|
|
122
|
-
printf -v sandbox_artifact_dir_q '%q' "$sandbox_artifact_dir"
|
|
123
|
-
printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
|
|
124
|
-
printf -v retained_repo_root_q '%q' "$retained_repo_root"
|
|
125
|
-
printf -v adapter_id_q '%q' "$adapter_id"
|
|
126
|
-
printf -v started_at_q '%q' "$started_at"
|
|
127
|
-
printf -v ollama_model_q '%q' "$ollama_model"
|
|
128
|
-
printf -v ollama_base_url_q '%q' "$ollama_base_url"
|
|
129
|
-
printf -v ollama_timeout_q '%q' "$ollama_timeout_seconds"
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
printf 'TASK_KIND=%s\n' "$task_kind_q"
|
|
133
|
-
printf 'TASK_ID=%s\n' "$task_id_q"
|
|
134
|
-
printf 'SESSION=%s\n' "$session_q"
|
|
135
|
-
printf 'MODE=%s\n' "$mode_q"
|
|
136
|
-
printf 'WORKTREE=%s\n' "$worktree_q"
|
|
137
|
-
printf 'PROMPT_FILE=%s\n' "$prompt_q"
|
|
138
|
-
printf 'OUTPUT_FILE=%s\n' "$output_q"
|
|
139
|
-
printf 'SCRIPT=%s\n' "$script_q"
|
|
140
|
-
printf 'BRANCH=%s\n' "$branch_q"
|
|
141
|
-
printf 'RESULT_FILE=%s\n' "$result_q"
|
|
142
|
-
printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
|
|
143
|
-
printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
|
|
144
|
-
printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
|
|
145
|
-
printf 'STARTED_AT=%s\n' "$started_at_q"
|
|
146
|
-
printf 'OLLAMA_MODEL=%s\n' "$ollama_model_q"
|
|
147
|
-
printf 'OLLAMA_BASE_URL=%s\n' "$ollama_base_url_q"
|
|
148
|
-
printf 'OLLAMA_TIMEOUT_SECONDS=%s\n' "$ollama_timeout_q"
|
|
149
|
-
} >"$meta_file"
|
|
150
|
-
|
|
151
|
-
context_exports=""
|
|
152
|
-
if ((${#context_items[@]} > 0)); then
|
|
153
|
-
for item in "${context_items[@]}"; do
|
|
154
|
-
if [[ "$item" != *=* ]]; then
|
|
155
|
-
echo "--context must use KEY=VALUE syntax: $item" >&2
|
|
156
|
-
exit 1
|
|
157
|
-
fi
|
|
158
|
-
key="${item%%=*}"
|
|
159
|
-
value="${item#*=}"
|
|
160
|
-
if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
|
|
161
|
-
echo "Invalid context key: $key" >&2
|
|
162
|
-
exit 1
|
|
163
|
-
fi
|
|
164
|
-
printf -v value_q '%q' "$value"
|
|
165
|
-
printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
|
|
166
|
-
if [[ -n "$env_prefix" ]]; then
|
|
167
|
-
context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
|
|
168
|
-
fi
|
|
169
|
-
context_exports+="export ACP_${key}=${value_q}"$'\n'
|
|
170
|
-
if [[ "$env_prefix" != "F_LOSNING_" ]]; then
|
|
171
|
-
context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
|
|
172
|
-
fi
|
|
173
|
-
done
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
runtime_exports=$(
|
|
177
|
-
cat <<EOF
|
|
178
|
-
export AGENT_PROJECT_SESSION=${session_q}
|
|
179
|
-
export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
|
|
180
|
-
export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
|
|
181
|
-
export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
182
|
-
export AGENT_PROJECT_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
183
|
-
export ACP_SESSION=${session_q}
|
|
184
|
-
export ACP_RUN_DIR=${sandbox_run_dir_q}
|
|
185
|
-
export ACP_HOST_RUN_DIR=${artifact_dir_q}
|
|
186
|
-
export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
187
|
-
export ACP_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
188
|
-
export ACP_WORKTREE=${worktree_q}
|
|
189
|
-
export ACP_PROMPT_FILE=${prompt_q}
|
|
190
|
-
export ACP_OLLAMA_MODEL=${ollama_model_q}
|
|
191
|
-
export ACP_OLLAMA_BASE_URL=${ollama_base_url_q}
|
|
192
|
-
export ACP_OLLAMA_TIMEOUT_SECONDS=${ollama_timeout_q}
|
|
193
|
-
export F_LOSNING_SESSION=${session_q}
|
|
194
|
-
export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
|
|
195
|
-
export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
|
|
196
|
-
export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
197
|
-
export F_LOSNING_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
198
|
-
EOF
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
if [[ -n "$env_prefix" ]]; then
|
|
202
|
-
runtime_exports+=$'\n'
|
|
203
|
-
runtime_exports+=$(cat <<EOF
|
|
204
|
-
export ${env_prefix}SESSION=${session_q}
|
|
205
|
-
export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
|
|
206
|
-
export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
|
|
207
|
-
export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
208
|
-
EOF
|
|
209
|
-
)
|
|
210
|
-
fi
|
|
211
|
-
|
|
212
|
-
collect_copy_snippet=""
|
|
213
|
-
if ((${#collect_files[@]} > 0)); then
|
|
214
|
-
for artifact_name in "${collect_files[@]}"; do
|
|
215
|
-
[[ -z "$artifact_name" ]] && continue
|
|
216
|
-
printf -v artifact_q '%q' "$artifact_name"
|
|
217
|
-
collect_copy_snippet+=$(
|
|
218
|
-
cat <<EOF
|
|
219
|
-
if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
|
|
220
|
-
cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
|
|
221
|
-
fi
|
|
222
|
-
EOF
|
|
223
|
-
)
|
|
224
|
-
collect_copy_snippet+=$'\n'
|
|
225
|
-
done
|
|
226
|
-
fi
|
|
227
|
-
|
|
228
|
-
# Always collect result.env from sandbox to artifact_dir
|
|
229
|
-
collect_copy_snippet+=$(
|
|
230
|
-
cat <<EOF
|
|
231
|
-
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
232
|
-
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
233
|
-
fi
|
|
234
|
-
EOF
|
|
235
|
-
)
|
|
236
|
-
collect_copy_snippet+=$'\n'
|
|
237
|
-
|
|
238
|
-
reconcile_snippet=""
|
|
239
|
-
if [[ -n "$reconcile_command" ]]; then
|
|
240
|
-
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"
|
|
241
|
-
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
242
|
-
fi
|
|
243
|
-
|
|
244
|
-
cat >"$inner_script" <<EOF
|
|
245
|
-
#!/usr/bin/env bash
|
|
246
|
-
set -euo pipefail
|
|
247
|
-
${runtime_exports}
|
|
248
|
-
${context_exports}cd ${worktree_q}
|
|
249
|
-
|
|
250
|
-
runner_state_file=${runner_state_q}
|
|
251
|
-
output_file=${output_q}
|
|
252
|
-
sandbox_artifact_dir=${sandbox_artifact_dir_q}
|
|
253
|
-
sandbox_run_dir=${sandbox_run_dir_q}
|
|
254
|
-
retained_repo_root=${retained_repo_root_q}
|
|
255
|
-
artifact_dir=${artifact_dir_q}
|
|
256
|
-
run_dir=${artifact_dir_q}
|
|
257
|
-
task_kind=${task_kind_q}
|
|
258
|
-
worktree=${worktree_q}
|
|
259
|
-
prompt_file_path=${prompt_q}
|
|
260
|
-
ollama_model=${ollama_model_q}
|
|
261
|
-
ollama_base_url=${ollama_base_url_q}
|
|
262
|
-
ollama_timeout=${ollama_timeout_q}
|
|
263
|
-
|
|
264
|
-
write_state() {
|
|
265
|
-
local runner_state="\${1:?runner state required}"
|
|
266
|
-
local last_exit_code="\${2:-}"
|
|
267
|
-
local failure_reason="\${3:-}"
|
|
268
|
-
local updated_at tmp_file
|
|
269
|
-
|
|
270
|
-
updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
271
|
-
tmp_file="\${runner_state_file}.tmp.\$\$"
|
|
272
|
-
{
|
|
273
|
-
printf 'RUNNER_STATE=%q\n' "\${runner_state}"
|
|
274
|
-
printf 'ATTEMPT=1\n'
|
|
275
|
-
printf 'RESUME_COUNT=0\n'
|
|
276
|
-
printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
|
|
277
|
-
printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
|
|
278
|
-
printf 'LAST_TRIGGER_REASON=%q\n' ''
|
|
279
|
-
printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
|
|
280
|
-
printf 'UPDATED_AT=%q\n' "\${updated_at}"
|
|
281
|
-
} >"\${tmp_file}"
|
|
282
|
-
mv "\${tmp_file}" "\${runner_state_file}"
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
record_final_git_state() {
|
|
286
|
-
local final_head final_branch tmp_file
|
|
287
|
-
|
|
288
|
-
final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
|
|
289
|
-
final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
|
|
290
|
-
tmp_file=${meta_file_q}.tmp.final.$$
|
|
291
|
-
grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
|
|
292
|
-
{
|
|
293
|
-
printf 'FINAL_HEAD=%q\n' "\${final_head}"
|
|
294
|
-
printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
|
|
295
|
-
} >>"\${tmp_file}"
|
|
296
|
-
mv "\${tmp_file}" ${meta_file_q}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
write_state "running" "" ""
|
|
300
|
-
mkdir -p "\${sandbox_run_dir}"
|
|
301
|
-
|
|
302
|
-
exit_code=0
|
|
303
|
-
failure_reason=""
|
|
304
|
-
|
|
305
|
-
node - <<'AGENT_EOF'
|
|
306
|
-
const fs = require('node:fs');
|
|
307
|
-
const path = require('node:path');
|
|
308
|
-
const { execSync } = require('node:child_process');
|
|
309
|
-
|
|
310
|
-
const worktree = process.env.ACP_WORKTREE || process.cwd();
|
|
311
|
-
const runDir = process.env.ACP_RUN_DIR || process.env.AGENT_PROJECT_RUN_DIR;
|
|
312
|
-
const promptFilePath = process.env.ACP_PROMPT_FILE;
|
|
313
|
-
const ollamaBaseUrl = (process.env.ACP_OLLAMA_BASE_URL || 'http://localhost:11434').replace(/\/$/, '');
|
|
314
|
-
const ollamaModel = process.env.ACP_OLLAMA_MODEL || 'qwen2.5-coder:7b';
|
|
315
|
-
const ollamaTimeoutMs = Number(process.env.ACP_OLLAMA_TIMEOUT_SECONDS || '900') * 1000;
|
|
316
|
-
const resultFile = process.env.ACP_RESULT_FILE;
|
|
317
|
-
const maxIterations = 30;
|
|
318
|
-
|
|
319
|
-
function writeResult(status, reason) {
|
|
320
|
-
if (!resultFile) return;
|
|
321
|
-
if (status === 'success') {
|
|
322
|
-
// Do not write result.env on success — let the host bash wrapper infer
|
|
323
|
-
// the correct OUTCOME from git state (product changes → implemented,
|
|
324
|
-
// no changes → blocked). Writing a blanket blocked contract here would
|
|
325
|
-
// prevent the reconcile from ever publishing agent work.
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const detail = reason || 'agent-exit-failed';
|
|
329
|
-
const content = \`OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=\${detail}\n\`;
|
|
330
|
-
fs.writeFileSync(resultFile, content);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async function ollamaChat(messages, tools) {
|
|
334
|
-
const body = JSON.stringify({
|
|
335
|
-
model: ollamaModel,
|
|
336
|
-
messages,
|
|
337
|
-
tools: tools || undefined,
|
|
338
|
-
stream: false,
|
|
339
|
-
think: false,
|
|
340
|
-
options: { num_ctx: 32768 },
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
const url = new URL('/api/chat', ollamaBaseUrl);
|
|
344
|
-
const { default: https } = await import(url.protocol === 'https:' ? 'node:https' : 'node:http');
|
|
345
|
-
|
|
346
|
-
return new Promise((resolve, reject) => {
|
|
347
|
-
const deadline = setTimeout(() => reject(new Error('ollama request timed out')), Math.min(ollamaTimeoutMs, 300_000));
|
|
348
|
-
const req = https.request(
|
|
349
|
-
{ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
|
|
350
|
-
(res) => {
|
|
351
|
-
let data = '';
|
|
352
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
353
|
-
res.on('end', () => {
|
|
354
|
-
clearTimeout(deadline);
|
|
355
|
-
try { resolve(JSON.parse(data)); } catch (e) { reject(new Error(\`failed to parse ollama response: \${e.message}\`)); }
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
);
|
|
359
|
-
req.on('error', (e) => { clearTimeout(deadline); reject(e); });
|
|
360
|
-
req.write(body);
|
|
361
|
-
req.end();
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const tools = [
|
|
366
|
-
{
|
|
367
|
-
type: 'function',
|
|
368
|
-
function: {
|
|
369
|
-
name: 'read_file',
|
|
370
|
-
description: 'Read the contents of a file in the worktree.',
|
|
371
|
-
parameters: {
|
|
372
|
-
type: 'object',
|
|
373
|
-
properties: { path: { type: 'string', description: 'File path relative to worktree root.' } },
|
|
374
|
-
required: ['path'],
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
type: 'function',
|
|
380
|
-
function: {
|
|
381
|
-
name: 'write_file',
|
|
382
|
-
description: 'Write or overwrite a file in the worktree with the given content.',
|
|
383
|
-
parameters: {
|
|
384
|
-
type: 'object',
|
|
385
|
-
properties: {
|
|
386
|
-
path: { type: 'string', description: 'File path relative to worktree root.' },
|
|
387
|
-
content: { type: 'string', description: 'Full content to write.' },
|
|
388
|
-
},
|
|
389
|
-
required: ['path', 'content'],
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
type: 'function',
|
|
395
|
-
function: {
|
|
396
|
-
name: 'run_command',
|
|
397
|
-
description: 'Run a shell command in the worktree directory. Use for git, tests, or inspecting state.',
|
|
398
|
-
parameters: {
|
|
399
|
-
type: 'object',
|
|
400
|
-
properties: { command: { type: 'string', description: 'Shell command to execute.' } },
|
|
401
|
-
required: ['command'],
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
type: 'function',
|
|
407
|
-
function: {
|
|
408
|
-
name: 'list_directory',
|
|
409
|
-
description: 'List files and directories at the given path.',
|
|
410
|
-
parameters: {
|
|
411
|
-
type: 'object',
|
|
412
|
-
properties: { path: { type: 'string', description: 'Directory path relative to worktree root. Defaults to root.' } },
|
|
413
|
-
required: [],
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
type: 'function',
|
|
419
|
-
function: {
|
|
420
|
-
name: 'task_complete',
|
|
421
|
-
description: 'Signal that the task is done. Provide a summary of what was accomplished.',
|
|
422
|
-
parameters: {
|
|
423
|
-
type: 'object',
|
|
424
|
-
properties: { summary: { type: 'string', description: 'Summary of changes made.' } },
|
|
425
|
-
required: ['summary'],
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
const retainedRepoRoot = (process.env.ACP_RETAINED_REPO_ROOT || '').replace(/\/$/, '');
|
|
432
|
-
|
|
433
|
-
function isAllowedPath(abs) {
|
|
434
|
-
const sep = path.sep;
|
|
435
|
-
if (abs === worktree || abs.startsWith(worktree + sep)) return true;
|
|
436
|
-
if (retainedRepoRoot && (abs === retainedRepoRoot || abs.startsWith(retainedRepoRoot + sep))) return true;
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function resolvePath(p) {
|
|
441
|
-
// Absolute paths are used as-is; relative paths resolve against worktree
|
|
442
|
-
return path.isAbsolute(p) ? p : path.resolve(worktree, p);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function executeTool(name, args) {
|
|
446
|
-
try {
|
|
447
|
-
if (name === 'read_file') {
|
|
448
|
-
const abs = resolvePath(args.path);
|
|
449
|
-
if (!isAllowedPath(abs)) throw new Error('path outside worktree');
|
|
450
|
-
return fs.readFileSync(abs, 'utf8');
|
|
451
|
-
}
|
|
452
|
-
if (name === 'write_file') {
|
|
453
|
-
// Writes only allowed in worktree (not retained read-only root)
|
|
454
|
-
const abs = resolvePath(args.path);
|
|
455
|
-
if (abs !== worktree && !abs.startsWith(worktree + path.sep)) throw new Error('path outside worktree');
|
|
456
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
457
|
-
fs.writeFileSync(abs, args.content, 'utf8');
|
|
458
|
-
return \`wrote \${args.path}\`;
|
|
459
|
-
}
|
|
460
|
-
if (name === 'run_command') {
|
|
461
|
-
const out = execSync(args.command, { cwd: worktree, timeout: 60_000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
462
|
-
return out || '(no output)';
|
|
463
|
-
}
|
|
464
|
-
if (name === 'list_directory') {
|
|
465
|
-
const abs = resolvePath(args.path || '.');
|
|
466
|
-
if (!isAllowedPath(abs)) throw new Error('path outside worktree');
|
|
467
|
-
return fs.readdirSync(abs).join('\n');
|
|
468
|
-
}
|
|
469
|
-
if (name === 'task_complete') {
|
|
470
|
-
return '__TASK_COMPLETE__';
|
|
471
|
-
}
|
|
472
|
-
return \`unknown tool: \${name}\`;
|
|
473
|
-
} catch (err) {
|
|
474
|
-
return \`error: \${err.message}\`;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async function main() {
|
|
479
|
-
const prompt = promptFilePath ? fs.readFileSync(promptFilePath, 'utf8') : '';
|
|
480
|
-
const messages = [
|
|
481
|
-
{
|
|
482
|
-
role: 'system',
|
|
483
|
-
content: 'You are an autonomous coding agent. You have access to tools to read/write files and run commands in the repository. Complete the task described by the user. When done, call task_complete with a summary.',
|
|
484
|
-
},
|
|
485
|
-
{ role: 'user', content: prompt },
|
|
486
|
-
];
|
|
487
|
-
|
|
488
|
-
const deadline = Date.now() + ollamaTimeoutMs;
|
|
489
|
-
let taskDone = false;
|
|
490
|
-
let summary = '';
|
|
491
|
-
let lastToolSig = '';
|
|
492
|
-
let repeatCount = 0;
|
|
493
|
-
const toolHistory = []; // sliding window for cycle detection
|
|
494
|
-
|
|
495
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
496
|
-
if (Date.now() > deadline) {
|
|
497
|
-
writeResult('failure', 'timeout');
|
|
498
|
-
process.stderr.write('ollama agent: timeout reached\n');
|
|
499
|
-
process.exit(1);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
let response;
|
|
503
|
-
try {
|
|
504
|
-
response = await ollamaChat(messages, tools);
|
|
505
|
-
} catch (err) {
|
|
506
|
-
writeResult('failure', \`ollama-api-error: \${err.message}\`);
|
|
507
|
-
process.stderr.write(\`ollama agent: API error: \${err.message}\n\`);
|
|
508
|
-
process.exit(1);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const msg = response?.message;
|
|
512
|
-
if (!msg) {
|
|
513
|
-
writeResult('failure', 'empty-response');
|
|
514
|
-
process.stderr.write('ollama agent: empty response from model\n');
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
messages.push(msg);
|
|
519
|
-
|
|
520
|
-
// Some models (e.g. qwen2.5-coder:7b) return tool calls as JSON text
|
|
521
|
-
// in content instead of the tool_calls array — normalise both formats.
|
|
522
|
-
let toolCalls = msg.tool_calls || [];
|
|
523
|
-
if (toolCalls.length === 0 && msg.content) {
|
|
524
|
-
const text = String(msg.content).trim();
|
|
525
|
-
const extracted = [];
|
|
526
|
-
// Extract all top-level JSON objects from the text (handles multiple objects)
|
|
527
|
-
let pos = 0;
|
|
528
|
-
while (pos < text.length) {
|
|
529
|
-
const start = text.indexOf('{', pos);
|
|
530
|
-
if (start === -1) break;
|
|
531
|
-
let depth = 0;
|
|
532
|
-
let end = -1;
|
|
533
|
-
for (let i = start; i < text.length; i++) {
|
|
534
|
-
if (text[i] === '{') depth++;
|
|
535
|
-
else if (text[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
536
|
-
}
|
|
537
|
-
if (end === -1) break;
|
|
538
|
-
try {
|
|
539
|
-
const obj = JSON.parse(text.slice(start, end + 1));
|
|
540
|
-
if (obj.name && obj.arguments !== undefined) {
|
|
541
|
-
extracted.push({ function: { name: obj.name, arguments: obj.arguments } });
|
|
542
|
-
}
|
|
543
|
-
} catch {}
|
|
544
|
-
pos = end + 1;
|
|
545
|
-
}
|
|
546
|
-
// Also try top-level array format
|
|
547
|
-
if (extracted.length === 0 && text.startsWith('[')) {
|
|
548
|
-
try {
|
|
549
|
-
const arr = JSON.parse(text);
|
|
550
|
-
if (Array.isArray(arr) && arr[0]?.name) {
|
|
551
|
-
arr.forEach((c) => extracted.push({ function: { name: c.name, arguments: c.arguments } }));
|
|
552
|
-
}
|
|
553
|
-
} catch {}
|
|
554
|
-
}
|
|
555
|
-
if (extracted.length > 0) toolCalls = extracted;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (toolCalls.length === 0) {
|
|
559
|
-
if (msg.content) process.stdout.write(\`[agent] \${msg.content}\n\`);
|
|
560
|
-
// No tool calls — model finished without task_complete
|
|
561
|
-
taskDone = true;
|
|
562
|
-
summary = msg.content || '(no summary)';
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
for (const call of toolCalls) {
|
|
567
|
-
const name = call?.function?.name || '';
|
|
568
|
-
const args = call?.function?.arguments || {};
|
|
569
|
-
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
|
|
570
|
-
process.stdout.write(\`[tool] \${name}(\${JSON.stringify(parsedArgs)})\n\`);
|
|
571
|
-
|
|
572
|
-
// Detect repetitive tool calls (stuck loop guard)
|
|
573
|
-
const sig = \`\${name}:\${JSON.stringify(parsedArgs)}\`;
|
|
574
|
-
if (sig === lastToolSig) {
|
|
575
|
-
repeatCount++;
|
|
576
|
-
if (repeatCount >= 3) {
|
|
577
|
-
process.stdout.write('[agent] stuck in repetitive loop, aborting\n');
|
|
578
|
-
writeResult('failure', 'stuck-loop');
|
|
579
|
-
process.exit(1);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
lastToolSig = sig;
|
|
583
|
-
repeatCount = 0;
|
|
584
|
-
}
|
|
585
|
-
// Cycle detection: abort if last 6 tool sigs appeared in same order before
|
|
586
|
-
toolHistory.push(sig);
|
|
587
|
-
const W = 6;
|
|
588
|
-
if (toolHistory.length >= W * 2) {
|
|
589
|
-
const recent = toolHistory.slice(-W).join('|');
|
|
590
|
-
const prev = toolHistory.slice(-W * 2, -W).join('|');
|
|
591
|
-
if (recent === prev) {
|
|
592
|
-
process.stdout.write('[agent] detected repeating cycle, aborting\n');
|
|
593
|
-
writeResult('failure', 'stuck-cycle');
|
|
594
|
-
process.exit(1);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const result = executeTool(name, parsedArgs);
|
|
599
|
-
if (result === '__TASK_COMPLETE__') {
|
|
600
|
-
taskDone = true;
|
|
601
|
-
summary = parsedArgs.summary || '(task complete)';
|
|
602
|
-
}
|
|
603
|
-
messages.push({ role: 'tool', content: String(result), name });
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (taskDone) break;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Write issue-comment.md with summary
|
|
610
|
-
if (runDir && summary) {
|
|
611
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
612
|
-
fs.writeFileSync(path.join(runDir, 'issue-comment.md'), \`## Ollama Agent Summary\n\n\${summary}\n\`);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
writeResult('success', '');
|
|
616
|
-
process.stdout.write('[agent] task complete\n');
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
main().catch((err) => {
|
|
620
|
-
process.stderr.write(\`ollama agent: unhandled error: \${err.message}\n\`);
|
|
621
|
-
writeResult('failure', \`unhandled-error: \${err.message}\`);
|
|
622
|
-
process.exit(1);
|
|
623
|
-
});
|
|
624
|
-
AGENT_EOF
|
|
625
|
-
exit_code=\$?
|
|
626
|
-
|
|
627
|
-
if [[ \${exit_code} -ne 0 ]]; then
|
|
628
|
-
failure_reason="agent-exit-\${exit_code}"
|
|
629
|
-
write_state "failed" "\${exit_code}" "\${failure_reason}"
|
|
630
|
-
if [[ ! -f "\${sandbox_run_dir}/result.env" ]]; then
|
|
631
|
-
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=%s\n' "\${failure_reason}" >"\${sandbox_run_dir}/result.env"
|
|
632
|
-
fi
|
|
633
|
-
else
|
|
634
|
-
write_state "succeeded" "0" ""
|
|
635
|
-
if [[ ! -f "\${sandbox_run_dir}/result.env" ]]; then
|
|
636
|
-
# Infer outcome from git state: product changes → implemented, else → blocked
|
|
637
|
-
if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \
|
|
638
|
-
|| git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.md$' 2>/dev/null \
|
|
639
|
-
|| git -C ${worktree_q} diff --name-only origin/main..HEAD 2>/dev/null | grep -qvE '\.md$' 2>/dev/null; then
|
|
640
|
-
printf 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n' >"\${sandbox_run_dir}/result.env"
|
|
641
|
-
else
|
|
642
|
-
printf 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=missing-result-contract\n' >"\${sandbox_run_dir}/result.env"
|
|
643
|
-
fi
|
|
644
|
-
fi
|
|
645
|
-
fi
|
|
646
|
-
|
|
647
|
-
record_final_git_state
|
|
648
|
-
${collect_copy_snippet}
|
|
649
|
-
printf '\n__CODEX_EXIT__:%s\n' "\${exit_code}" | tee -a "\${output_file}"
|
|
650
|
-
EOF
|
|
651
|
-
|
|
652
|
-
chmod +x "$inner_script"
|
|
653
|
-
|
|
654
|
-
${reconcile_snippet}
|
|
655
|
-
|
|
656
|
-
tmux new-session -d -s "$session" \
|
|
657
|
-
"bash ${script_q} 2>&1 | tee ${output_q}; tmux wait-for -S ${session_q}-done" \; \
|
|
658
|
-
wait-for "${session}-done"
|