agent-control-plane 0.4.9 → 0.7.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 (87) hide show
  1. package/README.md +109 -13
  2. package/npm/bin/agent-control-plane.js +1 -1
  3. package/package.json +39 -33
  4. package/tools/bin/debug-session.sh +106 -0
  5. package/tools/bin/flow-config-lib.sh +13 -3508
  6. package/tools/bin/flow-execution-lib.sh +243 -0
  7. package/tools/bin/flow-forge-lib.sh +1770 -0
  8. package/tools/bin/flow-profile-lib.sh +335 -0
  9. package/tools/bin/flow-provider-lib.sh +981 -0
  10. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  11. package/tools/bin/flow-runtime-doctor.sh +5 -1
  12. package/tools/bin/flow-session-lib.sh +317 -0
  13. package/tools/bin/install-project-systemd.sh +255 -0
  14. package/tools/bin/project-runtimectl.sh +45 -0
  15. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  16. package/tools/bin/uninstall-project-systemd.sh +87 -0
  17. package/tools/dashboard/app.js +238 -8
  18. package/tools/dashboard/issue_queue_state.py +101 -0
  19. package/tools/dashboard/requirements.txt +3 -0
  20. package/tools/dashboard/server.py +250 -30
  21. package/tools/dashboard/styles.css +526 -455
  22. package/tools/bin/agent-cleanup-worktree +0 -247
  23. package/tools/bin/agent-github-update-labels +0 -105
  24. package/tools/bin/agent-init-worktree +0 -216
  25. package/tools/bin/agent-project-archive-run +0 -52
  26. package/tools/bin/agent-project-capture-worker +0 -46
  27. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  28. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  29. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  30. package/tools/bin/agent-project-cleanup-session +0 -513
  31. package/tools/bin/agent-project-detached-launch +0 -127
  32. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  33. package/tools/bin/agent-project-open-issue-worktree +0 -89
  34. package/tools/bin/agent-project-open-pr-worktree +0 -80
  35. package/tools/bin/agent-project-publish-issue-pr +0 -468
  36. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  37. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  38. package/tools/bin/agent-project-retry-state +0 -158
  39. package/tools/bin/agent-project-run-claude-session +0 -805
  40. package/tools/bin/agent-project-run-codex-resilient +0 -963
  41. package/tools/bin/agent-project-run-codex-session +0 -435
  42. package/tools/bin/agent-project-run-kilo-session +0 -369
  43. package/tools/bin/agent-project-run-ollama-session +0 -658
  44. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  45. package/tools/bin/agent-project-run-opencode-session +0 -377
  46. package/tools/bin/agent-project-run-pi-session +0 -479
  47. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  48. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  49. package/tools/bin/agent-project-worker-status +0 -188
  50. package/tools/bin/branch-verification-guard.sh +0 -364
  51. package/tools/bin/capture-worker.sh +0 -18
  52. package/tools/bin/cleanup-worktree.sh +0 -52
  53. package/tools/bin/codex-quota +0 -31
  54. package/tools/bin/create-follow-up-issue.sh +0 -114
  55. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  56. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  57. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  58. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  59. package/tools/bin/issue-resource-class.sh +0 -12
  60. package/tools/bin/kick-scheduler.sh +0 -75
  61. package/tools/bin/label-follow-up-issues.sh +0 -14
  62. package/tools/bin/new-pr-worktree.sh +0 -50
  63. package/tools/bin/new-worktree.sh +0 -49
  64. package/tools/bin/pr-risk.sh +0 -12
  65. package/tools/bin/prepare-worktree.sh +0 -142
  66. package/tools/bin/provider-cooldown-state.sh +0 -204
  67. package/tools/bin/publish-issue-worker.sh +0 -31
  68. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  69. package/tools/bin/reconcile-issue-worker.sh +0 -34
  70. package/tools/bin/reconcile-pr-worker.sh +0 -34
  71. package/tools/bin/record-verification.sh +0 -71
  72. package/tools/bin/render-flow-config.sh +0 -98
  73. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  74. package/tools/bin/retry-state.sh +0 -31
  75. package/tools/bin/reuse-issue-worktree.sh +0 -121
  76. package/tools/bin/run-codex-bypass.sh +0 -3
  77. package/tools/bin/run-codex-safe.sh +0 -3
  78. package/tools/bin/run-codex-task.sh +0 -280
  79. package/tools/bin/serve-dashboard.sh +0 -5
  80. package/tools/bin/start-issue-worker.sh +0 -943
  81. package/tools/bin/start-pr-fix-worker.sh +0 -528
  82. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  83. package/tools/bin/start-pr-review-worker.sh +0 -261
  84. package/tools/bin/start-resident-issue-loop.sh +0 -499
  85. package/tools/bin/update-github-labels.sh +0 -14
  86. package/tools/bin/worker-status.sh +0 -19
  87. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,943 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- # shellcheck source=/dev/null
6
- source "${SCRIPT_DIR}/flow-config-lib.sh"
7
- # shellcheck source=/dev/null
8
- source "${SCRIPT_DIR}/flow-resident-worker-lib.sh"
9
-
10
- ISSUE_ID="${1:?usage: start-issue-worker.sh ISSUE_ID [safe|bypass]}"
11
- MODE="${2:-safe}"
12
- WORKSPACE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13
- FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
14
- if ! flow_require_explicit_profile_selection "${FLOW_SKILL_DIR}" "start-issue-worker.sh"; then
15
- exit 64
16
- fi
17
- CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
18
- flow_export_execution_env "${CONFIG_YAML}"
19
- flow_export_project_env_aliases
20
- AGENT_ROOT="$(flow_resolve_agent_root "${CONFIG_YAML}")"
21
- ISSUE_SESSION_PREFIX="$(flow_resolve_issue_session_prefix "${CONFIG_YAML}")"
22
- RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
23
- HISTORY_ROOT="$(flow_resolve_history_root "${CONFIG_YAML}")"
24
- REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
25
- AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
26
- FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
27
- TEMPLATE_FILE="$(flow_resolve_template_file "issue-prompt-template.md" "${WORKSPACE_DIR}" "${CONFIG_YAML}")"
28
- SCHEDULED_TEMPLATE_FILE="$(flow_resolve_template_file "scheduled-issue-prompt-template.md" "${WORKSPACE_DIR}" "${CONFIG_YAML}")"
29
- LOCAL_INSTALL_POLICY_BIN="${WORKSPACE_DIR}/bin/issue-requires-local-workspace-install.sh"
30
- SESSION="${ISSUE_SESSION_PREFIX}${ISSUE_ID}"
31
- RUN_DIR="${RUNS_ROOT}/${SESSION}"
32
- UPDATE_LABELS_BIN="${WORKSPACE_DIR}/bin/agent-github-update-labels"
33
- CODING_WORKER="${ACP_CODING_WORKER:-codex}"
34
- launch_success="no"
35
- label_rollback_armed="no"
36
- RECURRING_CHECKLIST_SYNC_BIN="${FLOW_TOOLS_DIR}/sync-recurring-issue-checklist.sh"
37
-
38
- issue_block_and_exit() {
39
- local comment_body="${1:?comment body required}"
40
- local comment_marker="${2:?comment marker required}"
41
-
42
- if ! jq -e --arg marker "$comment_marker" 'any(.comments[]?; (.body // "") | contains($marker))' >/dev/null <<<"$ISSUE_JSON"; then
43
- flow_github_api_repo "$REPO_SLUG" "issues/${ISSUE_ID}/comments" --method POST -f body="$comment_body" >/dev/null 2>&1 || true
44
- fi
45
- if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
46
- bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${ISSUE_ID}" --add agent-blocked --remove agent-running >/dev/null 2>&1 || true
47
- fi
48
- label_rollback_armed="no"
49
- launch_success="yes"
50
- exit 0
51
- }
52
-
53
- rollback_labels_on_failure() {
54
- if [[ "${label_rollback_armed}" != "yes" || "${launch_success}" == "yes" ]]; then
55
- return 0
56
- fi
57
- if [[ -d "${RUN_DIR}" && ! -f "${RUN_DIR}/run.env" && ! -f "${RUN_DIR}/runner.env" && ! -f "${RUN_DIR}/result.env" ]]; then
58
- rm -rf "${RUN_DIR}" >/dev/null 2>&1 || true
59
- fi
60
- if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
61
- bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${ISSUE_ID}" --remove agent-running >/dev/null 2>&1 || true
62
- fi
63
- }
64
-
65
- recurring_checklist_total="0"
66
- recurring_checklist_unchecked="0"
67
- recurring_checklist_matched_pr_numbers=""
68
-
69
- refresh_recurring_issue_from_github() {
70
- ISSUE_JSON="$(flow_github_issue_view_json "$REPO_SLUG" "$ISSUE_ID")"
71
- ISSUE_TITLE="$(jq -r '.title' <<<"$ISSUE_JSON")"
72
- ISSUE_BODY="$(jq -r '.body // ""' <<<"$ISSUE_JSON")"
73
- ISSUE_URL="$(jq -r '.url' <<<"$ISSUE_JSON")"
74
- ISSUE_AUTOMERGE="$(jq -r 'if any(.labels[]?; .name == "agent-automerge") then "yes" else "no" end' <<<"$ISSUE_JSON")"
75
- }
76
-
77
- refresh_recurring_issue_checklist_state() {
78
- local sync_out=""
79
-
80
- recurring_checklist_total="0"
81
- recurring_checklist_unchecked="0"
82
- recurring_checklist_matched_pr_numbers=""
83
-
84
- [[ "${ISSUE_IS_KEEP_OPEN:-no}" == "yes" ]] || return 0
85
- [[ -x "${RECURRING_CHECKLIST_SYNC_BIN}" ]] || return 0
86
-
87
- sync_out="$(
88
- ACP_REPO_ID="${ACP_REPO_ID:-${F_LOSNING_REPO_ID:-}}" \
89
- bash "${RECURRING_CHECKLIST_SYNC_BIN}" \
90
- --repo-slug "${REPO_SLUG}" \
91
- --issue-id "${ISSUE_ID}" 2>/dev/null || true
92
- )"
93
-
94
- recurring_checklist_total="$(awk -F= '/^CHECKLIST_TOTAL=/{print $2; exit}' <<<"${sync_out}")"
95
- recurring_checklist_unchecked="$(awk -F= '/^CHECKLIST_UNCHECKED=/{print $2; exit}' <<<"${sync_out}")"
96
- recurring_checklist_matched_pr_numbers="$(awk -F= '/^CHECKLIST_MATCHED_PR_NUMBERS=/{print $2; exit}' <<<"${sync_out}")"
97
-
98
- case "${recurring_checklist_total}" in
99
- ''|*[!0-9]*) recurring_checklist_total="0" ;;
100
- esac
101
- case "${recurring_checklist_unchecked}" in
102
- ''|*[!0-9]*) recurring_checklist_unchecked="0" ;;
103
- esac
104
-
105
- refresh_recurring_issue_from_github
106
- }
107
-
108
- block_if_recurring_checklist_complete() {
109
- [[ "${ISSUE_IS_KEEP_OPEN:-no}" == "yes" ]] || return 0
110
-
111
- refresh_recurring_issue_checklist_state
112
-
113
- if [[ "${recurring_checklist_total}" -gt 0 && "${recurring_checklist_unchecked}" -eq 0 ]]; then
114
- local blocker_comment=""
115
-
116
- blocker_comment="$(cat <<EOF
117
- # Blocker: All checklist items already completed
118
-
119
- All checklist items for issue #${ISSUE_ID} appear to be satisfied on the current baseline.
120
-
121
- Why this run was stopped early:
122
- - recurring automation should not spend another worker cycle when every listed improvement is already checked off
123
- - the issue body was refreshed against merged PR history before this decision
124
-
125
- Required next step:
126
- - refresh the issue body with new unchecked improvement items before re-queueing this issue
127
- EOF
128
- )"
129
-
130
- if [[ -n "${recurring_checklist_matched_pr_numbers}" ]]; then
131
- blocker_comment="${blocker_comment}"$'\n\n'"Recently matched PRs: #$(sed 's/,/, #/g' <<<"${recurring_checklist_matched_pr_numbers}")"
132
- fi
133
-
134
- issue_block_and_exit "${blocker_comment}" "# Blocker: All checklist items already completed"
135
- fi
136
- }
137
-
138
- reap_stale_run_dir() {
139
- if [[ ! -d "$RUN_DIR" ]]; then
140
- return 0
141
- fi
142
- if [[ -f "$RUN_DIR/run.env" ]]; then
143
- if grep -q '^RESIDENT_WORKER_ENABLED=yes$' "$RUN_DIR/run.env" 2>/dev/null; then
144
- if "${FLOW_TOOLS_DIR}/agent-project-archive-run" \
145
- --runs-root "$RUNS_ROOT" \
146
- --history-root "$HISTORY_ROOT" \
147
- --session "$SESSION" >/dev/null 2>&1; then
148
- return 0
149
- fi
150
- fi
151
- if "${WORKSPACE_DIR}/bin/cleanup-worktree.sh" "" "$SESSION" >/dev/null 2>&1; then
152
- return 0
153
- fi
154
- fi
155
- mkdir -p "$HISTORY_ROOT"
156
- mv "$RUN_DIR" "${HISTORY_ROOT}/${SESSION}-stale-$(date +%Y%m%d-%H%M%S)"
157
- }
158
-
159
- find_archived_issue_session_dir() {
160
- local root="${1:-}"
161
- local target_session="${2:-}"
162
- [[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
163
-
164
- find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
165
- | sort -r \
166
- | head -n 1
167
- }
168
-
169
- issue_retry_state_value() {
170
- local key="${1:?retry-state key required}"
171
- awk -F= -v target_key="$key" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${ISSUE_RETRY_STATE:-}"
172
- }
173
-
174
- issue_host_publish_replay_dir() {
175
- local last_reason=""
176
- local archived_dir=""
177
- local runner_state=""
178
- local result_outcome=""
179
- local result_action=""
180
-
181
- last_reason="$(issue_retry_state_value LAST_REASON)"
182
- case "${last_reason}" in
183
- host-publish-failed|issue-worker-blocked) ;;
184
- *) return 1 ;;
185
- esac
186
-
187
- archived_dir="$(find_archived_issue_session_dir "$HISTORY_ROOT" "$SESSION" || true)"
188
- [[ -n "${archived_dir}" && -f "${archived_dir}/run.env" && -f "${archived_dir}/runner.env" && -f "${archived_dir}/result.env" ]] || return 1
189
-
190
- runner_state="$(awk -F= '/^RUNNER_STATE=/{print $2; exit}' "${archived_dir}/runner.env")"
191
- result_outcome="$(awk -F= '/^OUTCOME=/{print $2; exit}' "${archived_dir}/result.env")"
192
- result_action="$(awk -F= '/^ACTION=/{print $2; exit}' "${archived_dir}/result.env")"
193
-
194
- [[ "${runner_state}" == "succeeded" ]] || return 1
195
- [[ "${result_outcome}" == "implemented" ]] || return 1
196
- [[ "${result_action}" == "host-publish-issue-pr" ]] || return 1
197
-
198
- printf '%s\n' "${archived_dir}"
199
- }
200
-
201
- replay_issue_host_publish_retry() {
202
- local archived_dir="${1:?archived dir required}"
203
- printf 'ISSUE_HOST_PUBLISH_REPLAY=session=%s archived_run_dir=%s\n' "${SESSION}" "${archived_dir}" >&2
204
- bash "${WORKSPACE_DIR}/bin/reconcile-issue-worker.sh" "${SESSION}"
205
- }
206
-
207
- if tmux has-session -t "$SESSION" 2>/dev/null; then
208
- echo "worker session already exists: $SESSION" >&2
209
- exit 1
210
- fi
211
-
212
- label_rollback_armed="yes"
213
- trap rollback_labels_on_failure EXIT INT TERM
214
-
215
- refresh_recurring_issue_from_github
216
- ISSUE_SCHEDULE_INTERVAL_SECONDS="$(
217
- ISSUE_BODY="$ISSUE_BODY" node <<'EOF'
218
- const body = process.env.ISSUE_BODY || '';
219
- const match = body.match(/^\s*(?:Agent schedule|Schedule|Cadence)\s*:\s*(?:every\s+)?(\d+)\s*([mhd])\s*$/im);
220
- if (!match) {
221
- process.stdout.write('0\n');
222
- process.exit(0);
223
- }
224
- const value = Number(match[1]);
225
- const unit = String(match[2] || '').toLowerCase();
226
- const multiplier = { m: 60, h: 3600, d: 86400 }[unit] || 0;
227
- const seconds = Number.isFinite(value) && value > 0 ? value * multiplier : 0;
228
- process.stdout.write(`${seconds}\n`);
229
- EOF
230
- )"
231
- ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL="$(
232
- ISSUE_BODY="$ISSUE_BODY" bash "$LOCAL_INSTALL_POLICY_BIN"
233
- )"
234
- ISSUE_RETRY_STATE="$(
235
- bash "${WORKSPACE_DIR}/bin/retry-state.sh" issue "$ISSUE_ID" get 2>/dev/null || true
236
- )"
237
- if [[ "${ISSUE_SCHEDULE_INTERVAL_SECONDS}" =~ ^[1-9][0-9]*$ ]]; then
238
- TEMPLATE_FILE="${SCHEDULED_TEMPLATE_FILE}"
239
- fi
240
-
241
- ISSUE_IS_KEEP_OPEN="$(jq -r 'if any(.labels[]?; .name == "agent-keep-open") then "yes" else "no" end' <<<"$ISSUE_JSON")"
242
- RESIDENT_WORKER_ENABLED="no"
243
- RESIDENT_WORKER_KEY=""
244
- RESIDENT_WORKER_DIR=""
245
- RESIDENT_WORKER_META_FILE=""
246
- RESIDENT_LANE_KIND=""
247
- RESIDENT_LANE_VALUE=""
248
- RESIDENT_WORKTREE_REUSED="no"
249
- RESIDENT_TASK_COUNT="0"
250
- RESIDENT_OPENCLAW_AGENT_ID=""
251
- RESIDENT_OPENCLAW_SESSION_ID=""
252
- RESIDENT_OPENCLAW_AGENT_DIR=""
253
- RESIDENT_OPENCLAW_STATE_DIR=""
254
- RESIDENT_OPENCLAW_CONFIG_PATH=""
255
- RESIDENT_WORKTREE_REALPATH=""
256
-
257
- if flow_resident_issue_backend_supported "${CODING_WORKER}" \
258
- && flow_is_truthy "$(flow_resident_issue_workers_enabled "${CONFIG_YAML}")" \
259
- && ( [[ "${ISSUE_IS_KEEP_OPEN}" == "yes" ]] || [[ "${ISSUE_SCHEDULE_INTERVAL_SECONDS}" =~ ^[1-9][0-9]*$ ]] ); then
260
- RESIDENT_WORKER_ENABLED="yes"
261
- if [[ "${ISSUE_SCHEDULE_INTERVAL_SECONDS}" =~ ^[1-9][0-9]*$ ]]; then
262
- RESIDENT_LANE_KIND="scheduled"
263
- RESIDENT_LANE_VALUE="${ISSUE_SCHEDULE_INTERVAL_SECONDS}"
264
- else
265
- RESIDENT_LANE_KIND="recurring"
266
- RESIDENT_LANE_VALUE="general"
267
- fi
268
- RESIDENT_WORKER_KEY="$(flow_resident_issue_lane_key "${CODING_WORKER}" "${MODE}" "${RESIDENT_LANE_KIND}" "${RESIDENT_LANE_VALUE}")"
269
- RESIDENT_WORKER_DIR="$(flow_resident_issue_lane_dir "${CONFIG_YAML}" "${RESIDENT_WORKER_KEY}")"
270
- RESIDENT_WORKER_META_FILE="$(flow_resident_issue_lane_meta_file "${CONFIG_YAML}" "${RESIDENT_WORKER_KEY}")"
271
- if [[ "${CODING_WORKER}" == "openclaw" ]]; then
272
- RESIDENT_OPENCLAW_AGENT_ID="$(flow_resident_issue_lane_openclaw_agent_id "${CONFIG_YAML}" "${RESIDENT_WORKER_KEY}")"
273
- RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_lane_openclaw_session_id "${CONFIG_YAML}" "${RESIDENT_WORKER_KEY}")"
274
- RESIDENT_OPENCLAW_AGENT_DIR="${RESIDENT_WORKER_DIR}/openclaw-agent"
275
- RESIDENT_OPENCLAW_STATE_DIR="${RESIDENT_WORKER_DIR}/openclaw-state"
276
- RESIDENT_OPENCLAW_CONFIG_PATH="${RESIDENT_WORKER_DIR}/openclaw-config/openclaw.json"
277
- fi
278
- fi
279
-
280
- if [[ -d "$RUN_DIR" ]]; then
281
- reap_stale_run_dir
282
- fi
283
-
284
- ISSUE_HOST_PUBLISH_REPLAY_DIR="$(issue_host_publish_replay_dir || true)"
285
- if [[ -n "${ISSUE_HOST_PUBLISH_REPLAY_DIR}" ]]; then
286
- if ! replay_issue_host_publish_retry "${ISSUE_HOST_PUBLISH_REPLAY_DIR}"; then
287
- echo "host publish replay failed for session ${SESSION}" >&2
288
- exit 1
289
- fi
290
- launch_success="yes"
291
- exit 0
292
- fi
293
-
294
- block_if_recurring_checklist_complete
295
-
296
- mkdir -p "$RUN_DIR"
297
-
298
- MISSING_CHANGE_PATHS="$(
299
- ISSUE_BODY="$ISSUE_BODY" AGENT_REPO_ROOT="$AGENT_REPO_ROOT" node <<'EOF'
300
- const fs = require('fs');
301
- const path = require('path');
302
-
303
- const body = process.env.ISSUE_BODY || '';
304
- const repoRoot = process.env.AGENT_REPO_ROOT || '';
305
- const matches = Array.from(body.matchAll(/openspec\/changes\/[A-Za-z0-9._/-]+/g))
306
- .map((match) => match[0]);
307
- const unique = [...new Set(matches)];
308
- const missing = unique.filter((relativePath) => !fs.existsSync(path.join(repoRoot, relativePath)));
309
- process.stdout.write(missing.join('\n'));
310
- EOF
311
- )"
312
-
313
- if [[ -n "$MISSING_CHANGE_PATHS" ]]; then
314
- missing_bullets="$(printf '%s\n' "$MISSING_CHANGE_PATHS" | sed 's/^/- /')"
315
- issue_block_and_exit \
316
- "Blocked on missing referenced OpenSpec paths for issue #${ISSUE_ID}.
317
-
318
- The issue body points at change-package paths that do not exist in the clean baseline checkout used by automation:
319
- ${missing_bullets}
320
-
321
- Why this blocks safe implementation:
322
- - the issue is asking automation to follow a planning/change artifact that is not present in the repo baseline
323
- - without a real canonical source, the worker would have to invent scope or requirements
324
-
325
- Fastest unblock:
326
- - update the issue to point at existing canonical specs/paths, or
327
- - restore the missing OpenSpec change package before re-queueing this issue." \
328
- "Blocked on missing referenced OpenSpec paths for issue #${ISSUE_ID}."
329
- fi
330
-
331
- ISSUE_RECURRING_CONTEXT_FILE="${RUN_DIR}/issue-recurring-context.md"
332
- ISSUE_JSON="$ISSUE_JSON" REPO_SLUG="$REPO_SLUG" FLOW_CONFIG_LIB_PATH="${SCRIPT_DIR}/flow-config-lib.sh" node <<'EOF' >"$ISSUE_RECURRING_CONTEXT_FILE"
333
- const { execFileSync } = require('child_process');
334
-
335
- const issue = JSON.parse(process.env.ISSUE_JSON || '{}');
336
- const repoSlug = process.env.REPO_SLUG || '';
337
- const isRecurring = Array.isArray(issue.labels)
338
- && issue.labels.some((label) => label && label.name === 'agent-keep-open');
339
-
340
- if (!isRecurring) {
341
- process.exit(0);
342
- }
343
-
344
- const prNumbers = [];
345
- for (const comment of issue.comments || []) {
346
- const body = comment?.body || '';
347
- for (const match of body.matchAll(/Opened PR #(\d+)/g)) {
348
- const number = Number(match[1]);
349
- if (number && !prNumbers.includes(number)) {
350
- prNumbers.push(number);
351
- }
352
- }
353
- }
354
-
355
- const recentNumbers = prNumbers.slice(-5).reverse();
356
- const recentPrs = recentNumbers.map((number) => {
357
- try {
358
- const raw = execFileSync(
359
- 'bash',
360
- ['-lc', `source "${process.env.FLOW_CONFIG_LIB_PATH}"; flow_github_pr_view_json "${repoSlug}" "${number}"`],
361
- { encoding: 'utf8' },
362
- );
363
- const pr = JSON.parse(raw);
364
- return {
365
- number: pr.number,
366
- title: pr.title,
367
- url: pr.url,
368
- state: pr.isDraft ? 'draft' : String(pr.state || '').toLowerCase(),
369
- };
370
- } catch (error) {
371
- return {
372
- number,
373
- title: 'Unable to load PR details',
374
- url: '',
375
- state: 'unknown',
376
- };
377
- }
378
- });
379
-
380
- const activePrs = recentPrs.filter((pr) => pr.state === 'open' || pr.state === 'draft');
381
- const completedPrs = recentPrs.filter((pr) => pr.state !== 'open' && pr.state !== 'draft');
382
-
383
- const recentCycleNotes = [];
384
- for (const comment of [...(issue.comments || [])].reverse()) {
385
- const body = String(comment?.body || '').trim();
386
- if (!body) {
387
- continue;
388
- }
389
- if (!/^(Completed|Blocked on|# Blocker:|Host-side publish blocked|Host-side publish failed)/im.test(body)) {
390
- continue;
391
- }
392
- const summaryLines = body
393
- .split(/\r?\n/)
394
- .map((line) => line.trim())
395
- .filter(Boolean)
396
- .slice(0, 6);
397
- if (summaryLines.length === 0) {
398
- continue;
399
- }
400
- const summary = summaryLines.join(' | ');
401
- recentCycleNotes.push(summary.length > 420 ? `${summary.slice(0, 417)}...` : summary);
402
- if (recentCycleNotes.length >= 3) {
403
- break;
404
- }
405
- }
406
-
407
- const formatPr = (pr) => {
408
- const suffix = pr.url ? ` ${pr.url}` : '';
409
- return `- #${pr.number} (${pr.state}): ${pr.title}${suffix}`;
410
- };
411
-
412
- const lines = [
413
- '',
414
- '## Recurring Issue Guardrails',
415
- 'Because this issue carries `agent-keep-open`:',
416
- '- Before editing, choose exactly one concrete target module, screen, or flow and keep the cycle limited to that target.',
417
- '- Do not work on a target already covered by an open or draft PR for this issue, or by the most recent completed cycles listed below, unless you are explicitly fixing a regression introduced there.',
418
- '- If you cannot identify a small non-overlapping target after reviewing recent cycle history, stop blocked using the blocker contract instead of forcing another PR.',
419
- '- Prefer the recent cycle notes below over repeating broad web research; only fetch outside context when the local baseline or linked advisories materially changed.',
420
- '- In your final worker output, start with `Target:` and `Why now:` lines before the changed-files list.',
421
- ];
422
-
423
- if (activePrs.length > 0) {
424
- lines.push('', '### Active PRs to avoid overlapping');
425
- for (const pr of activePrs) {
426
- lines.push(formatPr(pr));
427
- }
428
- }
429
-
430
- if (completedPrs.length > 0) {
431
- lines.push('', '### Most recent completed cycles');
432
- for (const pr of completedPrs) {
433
- lines.push(formatPr(pr));
434
- }
435
- }
436
-
437
- if (recentCycleNotes.length > 0) {
438
- lines.push('', '### Recent cycle notes from issue comments');
439
- for (const note of recentCycleNotes) {
440
- lines.push(`- ${note}`);
441
- }
442
- }
443
-
444
- process.stdout.write(`${lines.join('\n')}\n`);
445
- EOF
446
- ISSUE_RECURRING_CONTEXT="$(cat "$ISSUE_RECURRING_CONTEXT_FILE")"
447
- ISSUE_BLOCKER_CONTEXT="$(
448
- ISSUE_JSON="$ISSUE_JSON" ISSUE_RETRY_STATE="$ISSUE_RETRY_STATE" node <<'EOF'
449
- const issue = JSON.parse(process.env.ISSUE_JSON || '{}');
450
- const labels = new Set((issue.labels || []).map((label) => label?.name).filter(Boolean));
451
- const retryState = String(process.env.ISSUE_RETRY_STATE || '');
452
-
453
- const retryMap = Object.fromEntries(
454
- retryState
455
- .split(/\r?\n/)
456
- .map((line) => line.trim())
457
- .filter(Boolean)
458
- .map((line) => {
459
- const idx = line.indexOf('=');
460
- if (idx === -1) {
461
- return [line, ''];
462
- }
463
- return [line.slice(0, idx), line.slice(idx + 1)];
464
- }),
465
- );
466
- const attempts = Number.parseInt(retryMap.ATTEMPTS || '0', 10);
467
- const lastReason = String(retryMap.LAST_REASON || '').trim();
468
- const nextAttemptAt = String(retryMap.NEXT_ATTEMPT_AT || '').trim();
469
-
470
- if (!labels.has('agent-blocked')) {
471
- process.exit(0);
472
- }
473
-
474
- const blockerComment = [...(issue.comments || [])]
475
- .reverse()
476
- .find((comment) =>
477
- /Host-side publish blocked for session|Host-side publish failed for session|Blocked on missing referenced OpenSpec paths for issue|Superseded by focused follow-up issues:|Why it was blocked:|^# Blocker:/i.test(
478
- comment?.body || '',
479
- ),
480
- );
481
-
482
- const inferCommentReason = (bodyText) => {
483
- const body = String(bodyText || '');
484
- const marker = 'Failure reason:';
485
- const markerIndex = body.search(/Failure reason:/i);
486
- if (markerIndex !== -1) {
487
- const backtick = String.fromCharCode(96);
488
- const tail = body.slice(markerIndex + marker.length);
489
- const firstQuoted = tail.split(backtick)[1];
490
- if (firstQuoted) {
491
- return firstQuoted.trim();
492
- }
493
- }
494
- if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
495
- return 'verification-guard-blocked';
496
- }
497
- if (/^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
498
- return 'localization-guard-blocked';
499
- }
500
- if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
501
- return 'no-publishable-commits';
502
- }
503
- if (/scope guard/i.test(body)) {
504
- return 'scope-guard-blocked';
505
- }
506
- if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
507
- return 'provider-quota-limit';
508
- }
509
- return '';
510
- };
511
-
512
- const effectiveLastReason =
513
- lastReason && lastReason !== 'issue-worker-blocked'
514
- ? lastReason
515
- : inferCommentReason(blockerComment?.body || '') || lastReason;
516
-
517
- if (!blockerComment || !blockerComment.body) {
518
- const fallbackLines = [
519
- '',
520
- '## Prior Blocker Context',
521
- 'This issue is being retried after an `agent-blocked` stop.',
522
- '- First resolve the prior blocker instead of repeating the same broad implementation path.',
523
- ];
524
- if (effectiveLastReason) {
525
- fallbackLines.push('- Last recorded blocker: `' + effectiveLastReason + '`.');
526
- }
527
- if (attempts > 0) {
528
- fallbackLines.push('- Blocked retries so far: ' + attempts + '.');
529
- }
530
- if (effectiveLastReason === 'scope-guard-blocked' && attempts >= 2) {
531
- fallbackLines.push(
532
- '- This issue has already hit the scope guard multiple times. Do not attempt another broad multi-surface patch.',
533
- `- Either ship one focused slice that stays under the scope guard, or create focused follow-up issues with \`bash "$FLOW_TOOLS_DIR/create-follow-up-issue.sh" --parent ${issue.number} --title "..." --body-file /tmp/follow-up.md\` and supersede the umbrella.`,
534
- );
535
- }
536
- process.stdout.write(fallbackLines.join('\n'));
537
- process.exit(0);
538
- }
539
-
540
- const normalizedBody = String(blockerComment.body).trim();
541
- const clippedBody =
542
- normalizedBody.length > 1600 ? `${normalizedBody.slice(0, 1600).trimEnd()}\n\n[truncated]` : normalizedBody;
543
-
544
- const lines = [
545
- '',
546
- '## Prior Blocker Context',
547
- 'This issue is being retried after an `agent-blocked` stop.',
548
- '- Address the blocker below before attempting a new implementation/publish cycle.',
549
- ];
550
-
551
- if (effectiveLastReason) {
552
- lines.push('- Last recorded blocker: `' + effectiveLastReason + '`.');
553
- }
554
- if (attempts > 0) {
555
- lines.push('- Blocked retries so far: ' + attempts + '.');
556
- }
557
- if (nextAttemptAt) {
558
- lines.push('- Last scheduled retry target was ' + nextAttemptAt + '.');
559
- }
560
- if (effectiveLastReason === 'scope-guard-blocked') {
561
- lines.push('- Treat this as a scope problem first: narrow to one safe slice or decompose into focused follow-up issues.');
562
- if (attempts >= 2) {
563
- lines.push(`- Because the scope guard has already fired multiple times, do not retry the same umbrella patch. Use \`bash "$FLOW_TOOLS_DIR/create-follow-up-issue.sh" --parent ${issue.number} --title "..." --body-file /tmp/follow-up.md\` for the remaining slices, then supersede the umbrella if you covered the full decomposition.`);
564
- }
565
- } else if (effectiveLastReason === 'verification-guard-blocked') {
566
- lines.push('- Add the missing verification or shrink the touched surface before attempting another publish cycle.');
567
- } else if (effectiveLastReason === 'localization-guard-blocked') {
568
- lines.push('- Finish moving the remaining user-facing literals behind translation keys before attempting another publish cycle.');
569
- }
570
-
571
- lines.push('', clippedBody);
572
- process.stdout.write(lines.join('\n'));
573
- EOF
574
- )"
575
-
576
- ISSUE_SLUG="$(printf '%s' "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' | cut -c1-48)"
577
- if [[ -z "$ISSUE_SLUG" ]]; then
578
- ISSUE_SLUG="issue"
579
- fi
580
-
581
- ensure_resident_issue_worktree_alias() {
582
- local target_worktree=""
583
- local alias_path=""
584
-
585
- [[ "${RESIDENT_WORKER_ENABLED}" == "yes" ]] || return 0
586
- [[ -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]] || return 0
587
- [[ -n "${RESIDENT_WORKER_DIR:-}" ]] || return 0
588
-
589
- target_worktree="$(cd "${WORKTREE}" && pwd -P)"
590
- alias_path="${RESIDENT_WORKER_DIR}/worktree"
591
- mkdir -p "${RESIDENT_WORKER_DIR}"
592
- rm -rf "${alias_path}" 2>/dev/null || true
593
- ln -s "${target_worktree}" "${alias_path}"
594
- WORKTREE="${alias_path}"
595
- RESIDENT_WORKTREE_REALPATH="${target_worktree}"
596
- }
597
-
598
- write_resident_issue_metadata_started() {
599
- local started_at="${1:?started_at required}"
600
- local resident_lane_kind="${RESIDENT_LANE_KIND:-}"
601
- local resident_lane_value="${RESIDENT_LANE_VALUE:-}"
602
- [[ "${RESIDENT_WORKER_ENABLED}" == "yes" ]] || return 0
603
-
604
- if [[ -z "${resident_lane_kind}" ]]; then
605
- resident_lane_kind="$(flow_resident_issue_lane_field_from_key "${RESIDENT_WORKER_KEY:-}" kind 2>/dev/null || true)"
606
- fi
607
- if [[ -z "${resident_lane_value}" ]]; then
608
- resident_lane_value="$(flow_resident_issue_lane_field_from_key "${RESIDENT_WORKER_KEY:-}" value 2>/dev/null || true)"
609
- fi
610
-
611
- flow_resident_write_metadata "${RESIDENT_WORKER_META_FILE}" \
612
- "RESIDENT_WORKER_KIND=issue" \
613
- "RESIDENT_WORKER_SCOPE=lane" \
614
- "RESIDENT_WORKER_KEY=${RESIDENT_WORKER_KEY}" \
615
- "RESIDENT_LANE_KIND=${resident_lane_kind}" \
616
- "RESIDENT_LANE_VALUE=${resident_lane_value}" \
617
- "ISSUE_ID=${ISSUE_ID}" \
618
- "ADAPTER_ID=$(flow_resolve_adapter_id "${CONFIG_YAML}")" \
619
- "CODING_WORKER=${CODING_WORKER}" \
620
- "WORKTREE=${WORKTREE}" \
621
- "WORKTREE_REALPATH=${RESIDENT_WORKTREE_REALPATH:-${WORKTREE}}" \
622
- "LAST_BRANCH=${BRANCH}" \
623
- "OPENCLAW_AGENT_ID=${RESIDENT_OPENCLAW_AGENT_ID}" \
624
- "OPENCLAW_SESSION_ID=${RESIDENT_OPENCLAW_SESSION_ID}" \
625
- "OPENCLAW_AGENT_DIR=${RESIDENT_OPENCLAW_AGENT_DIR}" \
626
- "OPENCLAW_STATE_DIR=${RESIDENT_OPENCLAW_STATE_DIR}" \
627
- "OPENCLAW_CONFIG_PATH=${RESIDENT_OPENCLAW_CONFIG_PATH}" \
628
- "TASK_COUNT=${RESIDENT_TASK_COUNT}" \
629
- "LAST_STARTED_AT=${started_at}" \
630
- "LAST_FINISHED_AT=${LAST_FINISHED_AT:-}" \
631
- "LAST_RUN_SESSION=${SESSION}" \
632
- "LAST_WORKTREE_REUSED=${RESIDENT_WORKTREE_REUSED}" \
633
- "LAST_STATUS=running"
634
- }
635
-
636
- open_or_reuse_issue_worktree() {
637
- local resident_started_at=""
638
- local max_tasks=""
639
- local max_age_seconds=""
640
- local reuse_output=""
641
- local current_issue_id="${ISSUE_ID}"
642
- local current_session="${SESSION}"
643
- local previous_issue_id=""
644
- local current_resident_worker_scope="${RESIDENT_WORKER_SCOPE:-lane}"
645
- local current_resident_worker_key="${RESIDENT_WORKER_KEY}"
646
- local current_resident_worker_dir="${RESIDENT_WORKER_DIR}"
647
- local current_resident_worker_meta_file="${RESIDENT_WORKER_META_FILE}"
648
- local current_resident_lane_kind="${RESIDENT_LANE_KIND}"
649
- local current_resident_lane_value="${RESIDENT_LANE_VALUE}"
650
- local current_resident_openclaw_agent_id="${RESIDENT_OPENCLAW_AGENT_ID}"
651
- local current_resident_openclaw_session_id="${RESIDENT_OPENCLAW_SESSION_ID}"
652
- local current_resident_openclaw_agent_dir="${RESIDENT_OPENCLAW_AGENT_DIR}"
653
- local current_resident_openclaw_state_dir="${RESIDENT_OPENCLAW_STATE_DIR}"
654
- local current_resident_openclaw_config_path="${RESIDENT_OPENCLAW_CONFIG_PATH}"
655
-
656
- if [[ "${RESIDENT_WORKER_ENABLED}" == "yes" ]]; then
657
- max_tasks="$(flow_resident_issue_worker_max_tasks "${CONFIG_YAML}")"
658
- max_age_seconds="$(flow_resident_issue_worker_max_age_seconds "${CONFIG_YAML}")"
659
- if flow_resident_issue_can_reuse "${RESIDENT_WORKER_META_FILE}" "${max_tasks}" "${max_age_seconds}"; then
660
- set -a
661
- # shellcheck source=/dev/null
662
- source "${RESIDENT_WORKER_META_FILE}"
663
- set +a
664
- previous_issue_id="${ISSUE_ID:-}"
665
- ISSUE_ID="${current_issue_id}"
666
- SESSION="${current_session}"
667
- RESIDENT_WORKER_SCOPE="${current_resident_worker_scope}"
668
- RESIDENT_WORKER_KEY="${current_resident_worker_key}"
669
- RESIDENT_WORKER_DIR="${current_resident_worker_dir}"
670
- RESIDENT_WORKER_META_FILE="${current_resident_worker_meta_file}"
671
- RESIDENT_LANE_KIND="${current_resident_lane_kind}"
672
- RESIDENT_LANE_VALUE="${current_resident_lane_value}"
673
- RESIDENT_OPENCLAW_AGENT_ID="${current_resident_openclaw_agent_id}"
674
- RESIDENT_OPENCLAW_SESSION_ID="${current_resident_openclaw_session_id}"
675
- RESIDENT_OPENCLAW_AGENT_DIR="${current_resident_openclaw_agent_dir}"
676
- RESIDENT_OPENCLAW_STATE_DIR="${current_resident_openclaw_state_dir}"
677
- RESIDENT_OPENCLAW_CONFIG_PATH="${current_resident_openclaw_config_path}"
678
- RESIDENT_TASK_COUNT="$(( ${TASK_COUNT:-0} + 1 ))"
679
- RESIDENT_WORKTREE_REUSED="yes"
680
- if [[ "${CODING_WORKER}" == "openclaw" ]]; then
681
- # Keep the resident lane's warm workspace/agent files, but rotate the
682
- # OpenClaw conversation thread every cycle so a new task does not inherit
683
- # stale conversational context from the previous one.
684
- RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
685
- fi
686
- if reuse_output="$("${WORKSPACE_DIR}/bin/reuse-issue-worktree.sh" "${WORKTREE}" "${ISSUE_ID}" "${ISSUE_SLUG}" 2>&1)"; then
687
- WORKTREE_OUT="${reuse_output}"
688
- else
689
- printf 'RESIDENT_REUSE_FALLBACK=issue-%s reason=%s\n' "${ISSUE_ID}" "$(printf '%s' "${reuse_output}" | tr '\n' ' ' | sed 's/ */ /g')" >&2
690
- RESIDENT_TASK_COUNT="1"
691
- RESIDENT_WORKTREE_REUSED="no"
692
- if [[ "${CODING_WORKER}" == "openclaw" ]]; then
693
- RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
694
- fi
695
- if [[ "$ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL" == "yes" ]]; then
696
- WORKTREE_OUT="$(ACP_WORKTREE_LOCAL_INSTALL=true F_LOSNING_WORKTREE_LOCAL_INSTALL=true "${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
697
- else
698
- WORKTREE_OUT="$("${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
699
- fi
700
- fi
701
- else
702
- RESIDENT_TASK_COUNT="1"
703
- RESIDENT_WORKTREE_REUSED="no"
704
- if [[ "${CODING_WORKER}" == "openclaw" ]]; then
705
- RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
706
- fi
707
- if [[ "$ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL" == "yes" ]]; then
708
- WORKTREE_OUT="$(ACP_WORKTREE_LOCAL_INSTALL=true F_LOSNING_WORKTREE_LOCAL_INSTALL=true "${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
709
- else
710
- WORKTREE_OUT="$("${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
711
- fi
712
- fi
713
- else
714
- if [[ "$ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL" == "yes" ]]; then
715
- WORKTREE_OUT="$(ACP_WORKTREE_LOCAL_INSTALL=true F_LOSNING_WORKTREE_LOCAL_INSTALL=true "${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
716
- else
717
- WORKTREE_OUT="$("${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
718
- fi
719
- fi
720
-
721
- WORKTREE="$(awk -F= '/^WORKTREE=/{print $2}' <<<"$WORKTREE_OUT")"
722
- BRANCH="$(awk -F= '/^BRANCH=/{print $2}' <<<"$WORKTREE_OUT")"
723
- ensure_resident_issue_worktree_alias
724
- ISSUE_BASELINE_HEAD_SHA="$(git -C "$WORKTREE" rev-parse HEAD)"
725
-
726
- if [[ "${RESIDENT_WORKER_ENABLED}" == "yes" ]]; then
727
- resident_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
728
- write_resident_issue_metadata_started "${resident_started_at}"
729
- fi
730
- }
731
-
732
- open_or_reuse_issue_worktree
733
-
734
- PROMPT_FILE="${RUN_DIR}/prompt.md"
735
-
736
- build_issue_verification_command_snippet() {
737
- ISSUE_BODY="$ISSUE_BODY" AGENT_REPO_ROOT="$AGENT_REPO_ROOT" node <<'EOF'
738
- const fs = require('fs');
739
- const path = require('path');
740
-
741
- const body = String(process.env.ISSUE_BODY || '');
742
- const repoRoot = String(process.env.AGENT_REPO_ROOT || '');
743
- const commands = [];
744
- const seen = new Set();
745
- const backtick = String.fromCharCode(96);
746
-
747
- const addCommand = (value) => {
748
- const command = String(value || '').trim();
749
- if (!command || seen.has(command)) {
750
- return;
751
- }
752
- seen.add(command);
753
- commands.push(command);
754
- };
755
-
756
- for (const line of body.split(/\r?\n/).slice(0, 40)) {
757
- if (!/^\s*-\s+/.test(line)) continue;
758
- if (!/(?:\bRun\b|\balso run\b|\bafter code changes\b|\bevery completed cycle\b)/i.test(line)) continue;
759
- const matches = line.matchAll(new RegExp(backtick + '([^' + backtick + ']+)' + backtick, 'g'));
760
- for (const match of matches) {
761
- addCommand(match[1]);
762
- }
763
- }
764
-
765
- if (commands.length === 0 && repoRoot) {
766
- const packageJsonPath = path.join(repoRoot, 'package.json');
767
- if (fs.existsSync(packageJsonPath)) {
768
- try {
769
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
770
- if (packageJson?.scripts?.smoke) {
771
- addCommand('# If this cycle changes smoke runners, CLI entrypoints, or operator commands, also run the matching smoke command from the repo instructions.');
772
- }
773
- } catch (_error) {
774
- // Ignore parse errors and fall through to generic guidance.
775
- }
776
- }
777
- }
778
-
779
- if (commands.length === 0) {
780
- addCommand('# Pick the narrowest relevant local verification for the files you touch.');
781
- addCommand('# Do not default to repo-wide pnpm test unless the issue body explicitly requires it.');
782
- addCommand('# Examples:');
783
- addCommand('# pnpm --filter @<repo>/api test -- --runInBand <target-spec>');
784
- addCommand('# pnpm --filter @<repo>/api typecheck');
785
- addCommand('# pnpm --filter @<repo>/web test -- --run <target-spec>');
786
- addCommand('# pnpm --filter @<repo>/web typecheck');
787
- addCommand('# pnpm --filter @<repo>/mobile test -- --runInBand <target-spec>');
788
- addCommand('# pnpm --filter @<repo>/mobile typecheck');
789
- addCommand('# pnpm --filter @<repo>/<package> test -- --run <target-spec>');
790
- addCommand('# pnpm --filter @<repo>/<package> typecheck');
791
- addCommand('# After each successful command, record it with record-verification.sh exactly as shown below.');
792
- }
793
-
794
- const escapeDoubleQuotes = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
795
- const snippet = commands
796
- .map((command) => {
797
- if (command.startsWith('#')) {
798
- return command;
799
- }
800
- return (
801
- command + '\n' +
802
- 'bash "$ACP_FLOW_TOOLS_DIR/record-verification.sh" --run-dir "$ACP_RUN_DIR" --status pass --command "' +
803
- escapeDoubleQuotes(command) +
804
- '"'
805
- );
806
- })
807
- .join('\n\n');
808
-
809
- process.stdout.write(snippet);
810
- EOF
811
- }
812
-
813
- ISSUE_VERIFICATION_COMMAND_SNIPPET="$(build_issue_verification_command_snippet)"
814
- ISSUE_RESIDENT_CONTEXT=""
815
- if [[ "${RESIDENT_WORKER_ENABLED}" == "yes" ]]; then
816
- ISSUE_RESIDENT_CONTEXT="$(cat <<EOF
817
-
818
- ## Resident Worker Context
819
-
820
- - This recurring/scheduled issue is running in resident-worker mode for the same issue lane.
821
- - Resident task count for this lane: ${RESIDENT_TASK_COUNT}.
822
- - Worktree reused from a prior cycle: ${RESIDENT_WORKTREE_REUSED}.
823
- - Reuse the saved context to avoid rereading the full repo, but first verify the latest repo state before editing so you do not act on stale assumptions.
824
- - Treat this cycle as one focused slice. Do not reopen broad scope just because prior context is available.
825
- EOF
826
- )"
827
- fi
828
- ISSUE_QUALITY_GUARDRAILS="$(cat <<'EOF'
829
-
830
- ## Host Quality Guardrails
831
-
832
- - Before committing, run `git status --short` and remove unrelated generated files or local bootstrap artifacts so only intended code/test/doc changes remain.
833
- - Do not commit `.agent-session.env`, `.openclaw*`, or incidental lockfile churn unless the issue explicitly changes dependency manifests or package-manager/tooling files.
834
- - If you changed CLI or operator-facing commands, flags, argument parsing, usage/help text, or entrypoint scripts, add/update regression coverage and record at least one direct invocation that exercises the changed path before you commit.
835
- EOF
836
- )"
837
-
838
- ISSUE_ID="$ISSUE_ID" \
839
- ISSUE_TITLE="$ISSUE_TITLE" \
840
- ISSUE_URL="$ISSUE_URL" \
841
- ISSUE_AUTOMERGE="$ISSUE_AUTOMERGE" \
842
- ISSUE_BASELINE_HEAD_SHA="$ISSUE_BASELINE_HEAD_SHA" \
843
- ISSUE_BODY="$ISSUE_BODY" \
844
- ISSUE_RECURRING_CONTEXT="$ISSUE_RECURRING_CONTEXT" \
845
- ISSUE_BLOCKER_CONTEXT="$ISSUE_BLOCKER_CONTEXT" \
846
- ISSUE_VERIFICATION_COMMAND_SNIPPET="$ISSUE_VERIFICATION_COMMAND_SNIPPET" \
847
- ISSUE_RESIDENT_CONTEXT="$ISSUE_RESIDENT_CONTEXT" \
848
- ISSUE_QUALITY_GUARDRAILS="$ISSUE_QUALITY_GUARDRAILS" \
849
- REPO_SLUG="$REPO_SLUG" \
850
- TEMPLATE_FILE="$TEMPLATE_FILE" \
851
- node <<'EOF' >"$PROMPT_FILE"
852
- const fs = require('fs');
853
-
854
- const template = fs.readFileSync(process.env.TEMPLATE_FILE, 'utf8');
855
- const replacements = {
856
- '{ISSUE_ID}': process.env.ISSUE_ID || '',
857
- '{ISSUE_TITLE}': process.env.ISSUE_TITLE || '',
858
- '{ISSUE_URL}': process.env.ISSUE_URL || '',
859
- '{ISSUE_AUTOMERGE}': process.env.ISSUE_AUTOMERGE || 'no',
860
- '{ISSUE_BASELINE_HEAD_SHA}': process.env.ISSUE_BASELINE_HEAD_SHA || '',
861
- '{REPO_SLUG}': process.env.REPO_SLUG || '',
862
- '{ISSUE_BODY}': process.env.ISSUE_BODY || '',
863
- '{ISSUE_RECURRING_CONTEXT}': process.env.ISSUE_RECURRING_CONTEXT || '',
864
- '{ISSUE_BLOCKER_CONTEXT}': process.env.ISSUE_BLOCKER_CONTEXT || '',
865
- '{ISSUE_VERIFICATION_COMMAND_SNIPPET}': process.env.ISSUE_VERIFICATION_COMMAND_SNIPPET || '',
866
- };
867
-
868
- let rendered = template;
869
- for (const [key, value] of Object.entries(replacements)) {
870
- rendered = rendered.split(key).join(value);
871
- }
872
- const addendum = String(process.env.ISSUE_QUALITY_GUARDRAILS || '').trim();
873
- const residentContext = String(process.env.ISSUE_RESIDENT_CONTEXT || '').trim();
874
- const addendumParts = [residentContext, addendum].filter(Boolean);
875
- if (addendumParts.length > 0) {
876
- rendered = `${rendered.trimEnd()}\n\n${addendumParts.join('\n\n')}\n`;
877
- }
878
- process.stdout.write(rendered);
879
- EOF
880
-
881
- launch_issue_worker() {
882
- local runner="${1:?runner required}"
883
-
884
- ACP_ISSUE_ID="$ISSUE_ID" \
885
- ACP_ISSUE_URL="$ISSUE_URL" \
886
- ACP_ISSUE_AUTOMERGE="$ISSUE_AUTOMERGE" \
887
- ACP_RESIDENT_WORKER_ENABLED="$RESIDENT_WORKER_ENABLED" \
888
- ACP_RESIDENT_WORKER_SCOPE="lane" \
889
- ACP_RESIDENT_WORKER_KEY="$RESIDENT_WORKER_KEY" \
890
- ACP_RESIDENT_WORKER_DIR="$RESIDENT_WORKER_DIR" \
891
- ACP_RESIDENT_WORKER_META_FILE="$RESIDENT_WORKER_META_FILE" \
892
- ACP_RESIDENT_LANE_KIND="$RESIDENT_LANE_KIND" \
893
- ACP_RESIDENT_LANE_VALUE="$RESIDENT_LANE_VALUE" \
894
- ACP_RESIDENT_TASK_COUNT="$RESIDENT_TASK_COUNT" \
895
- ACP_RESIDENT_WORKTREE_REUSED="$RESIDENT_WORKTREE_REUSED" \
896
- ACP_RESIDENT_OPENCLAW_AGENT_ID="$RESIDENT_OPENCLAW_AGENT_ID" \
897
- ACP_RESIDENT_OPENCLAW_SESSION_ID="$RESIDENT_OPENCLAW_SESSION_ID" \
898
- ACP_RESIDENT_OPENCLAW_AGENT_DIR="$RESIDENT_OPENCLAW_AGENT_DIR" \
899
- ACP_RESIDENT_OPENCLAW_STATE_DIR="$RESIDENT_OPENCLAW_STATE_DIR" \
900
- ACP_RESIDENT_OPENCLAW_CONFIG_PATH="$RESIDENT_OPENCLAW_CONFIG_PATH" \
901
- F_LOSNING_ISSUE_ID="$ISSUE_ID" \
902
- F_LOSNING_ISSUE_URL="$ISSUE_URL" \
903
- F_LOSNING_ISSUE_AUTOMERGE="$ISSUE_AUTOMERGE" \
904
- F_LOSNING_RESIDENT_WORKER_ENABLED="$RESIDENT_WORKER_ENABLED" \
905
- F_LOSNING_RESIDENT_WORKER_SCOPE="lane" \
906
- F_LOSNING_RESIDENT_WORKER_KEY="$RESIDENT_WORKER_KEY" \
907
- F_LOSNING_RESIDENT_WORKER_DIR="$RESIDENT_WORKER_DIR" \
908
- F_LOSNING_RESIDENT_WORKER_META_FILE="$RESIDENT_WORKER_META_FILE" \
909
- F_LOSNING_RESIDENT_LANE_KIND="$RESIDENT_LANE_KIND" \
910
- F_LOSNING_RESIDENT_LANE_VALUE="$RESIDENT_LANE_VALUE" \
911
- F_LOSNING_RESIDENT_TASK_COUNT="$RESIDENT_TASK_COUNT" \
912
- F_LOSNING_RESIDENT_WORKTREE_REUSED="$RESIDENT_WORKTREE_REUSED" \
913
- F_LOSNING_RESIDENT_OPENCLAW_AGENT_ID="$RESIDENT_OPENCLAW_AGENT_ID" \
914
- F_LOSNING_RESIDENT_OPENCLAW_SESSION_ID="$RESIDENT_OPENCLAW_SESSION_ID" \
915
- F_LOSNING_RESIDENT_OPENCLAW_AGENT_DIR="$RESIDENT_OPENCLAW_AGENT_DIR" \
916
- F_LOSNING_RESIDENT_OPENCLAW_STATE_DIR="$RESIDENT_OPENCLAW_STATE_DIR" \
917
- F_LOSNING_RESIDENT_OPENCLAW_CONFIG_PATH="$RESIDENT_OPENCLAW_CONFIG_PATH" \
918
- "$runner" "$SESSION" "$WORKTREE" "$PROMPT_FILE"
919
- }
920
-
921
- case "$MODE" in
922
- safe)
923
- launch_issue_worker "${WORKSPACE_DIR}/bin/run-codex-safe.sh"
924
- ;;
925
- bypass)
926
- launch_issue_worker "${WORKSPACE_DIR}/bin/run-codex-bypass.sh"
927
- ;;
928
- *)
929
- echo "unknown mode: $MODE" >&2
930
- exit 1
931
- ;;
932
- esac
933
-
934
- launch_success="yes"
935
-
936
- printf 'ISSUE_ID=%s\n' "$ISSUE_ID"
937
- printf 'TITLE=%s\n' "$ISSUE_TITLE"
938
- printf 'URL=%s\n' "$ISSUE_URL"
939
- printf 'AUTOMERGE=%s\n' "$ISSUE_AUTOMERGE"
940
- printf 'SESSION=%s\n' "$SESSION"
941
- printf 'WORKTREE=%s\n' "$WORKTREE"
942
- printf 'BRANCH=%s\n' "$BRANCH"
943
- printf 'PROMPT=%s\n' "$PROMPT_FILE"