agent-control-plane 0.4.9 → 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 (80) hide show
  1. package/README.md +72 -9
  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-runtime-doctor-linux.sh +136 -0
  6. package/tools/bin/flow-runtime-doctor.sh +5 -1
  7. package/tools/bin/install-project-systemd.sh +255 -0
  8. package/tools/bin/project-runtimectl.sh +45 -0
  9. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  10. package/tools/bin/uninstall-project-systemd.sh +87 -0
  11. package/tools/dashboard/app.js +198 -5
  12. package/tools/dashboard/issue_queue_state.py +101 -0
  13. package/tools/dashboard/server.py +123 -1
  14. package/tools/dashboard/styles.css +526 -455
  15. package/tools/bin/agent-cleanup-worktree +0 -247
  16. package/tools/bin/agent-github-update-labels +0 -105
  17. package/tools/bin/agent-init-worktree +0 -216
  18. package/tools/bin/agent-project-archive-run +0 -52
  19. package/tools/bin/agent-project-capture-worker +0 -46
  20. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  21. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  22. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  23. package/tools/bin/agent-project-cleanup-session +0 -513
  24. package/tools/bin/agent-project-detached-launch +0 -127
  25. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  26. package/tools/bin/agent-project-open-issue-worktree +0 -89
  27. package/tools/bin/agent-project-open-pr-worktree +0 -80
  28. package/tools/bin/agent-project-publish-issue-pr +0 -468
  29. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  30. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  31. package/tools/bin/agent-project-retry-state +0 -158
  32. package/tools/bin/agent-project-run-claude-session +0 -805
  33. package/tools/bin/agent-project-run-codex-resilient +0 -963
  34. package/tools/bin/agent-project-run-codex-session +0 -435
  35. package/tools/bin/agent-project-run-kilo-session +0 -369
  36. package/tools/bin/agent-project-run-ollama-session +0 -658
  37. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  38. package/tools/bin/agent-project-run-opencode-session +0 -377
  39. package/tools/bin/agent-project-run-pi-session +0 -479
  40. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  41. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  42. package/tools/bin/agent-project-worker-status +0 -188
  43. package/tools/bin/branch-verification-guard.sh +0 -364
  44. package/tools/bin/capture-worker.sh +0 -18
  45. package/tools/bin/cleanup-worktree.sh +0 -52
  46. package/tools/bin/codex-quota +0 -31
  47. package/tools/bin/create-follow-up-issue.sh +0 -114
  48. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  49. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  50. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  51. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  52. package/tools/bin/issue-resource-class.sh +0 -12
  53. package/tools/bin/kick-scheduler.sh +0 -75
  54. package/tools/bin/label-follow-up-issues.sh +0 -14
  55. package/tools/bin/new-pr-worktree.sh +0 -50
  56. package/tools/bin/new-worktree.sh +0 -49
  57. package/tools/bin/pr-risk.sh +0 -12
  58. package/tools/bin/prepare-worktree.sh +0 -142
  59. package/tools/bin/provider-cooldown-state.sh +0 -204
  60. package/tools/bin/publish-issue-worker.sh +0 -31
  61. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  62. package/tools/bin/reconcile-issue-worker.sh +0 -34
  63. package/tools/bin/reconcile-pr-worker.sh +0 -34
  64. package/tools/bin/record-verification.sh +0 -71
  65. package/tools/bin/render-flow-config.sh +0 -98
  66. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  67. package/tools/bin/retry-state.sh +0 -31
  68. package/tools/bin/reuse-issue-worktree.sh +0 -121
  69. package/tools/bin/run-codex-bypass.sh +0 -3
  70. package/tools/bin/run-codex-safe.sh +0 -3
  71. package/tools/bin/run-codex-task.sh +0 -280
  72. package/tools/bin/serve-dashboard.sh +0 -5
  73. package/tools/bin/start-issue-worker.sh +0 -943
  74. package/tools/bin/start-pr-fix-worker.sh +0 -528
  75. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  76. package/tools/bin/start-pr-review-worker.sh +0 -261
  77. package/tools/bin/start-resident-issue-loop.sh +0 -499
  78. package/tools/bin/update-github-labels.sh +0 -14
  79. package/tools/bin/worker-status.sh +0 -19
  80. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,89 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-open-issue-worktree --repo-root <path> --worktree-root <path> --issue-id <id> [--slug <text>] [--branch-prefix <prefix>] [--base <ref>] [--remote <name>] [--prepare-script <path>] [--stamp <yyyymmdd-hhmmss>]
8
-
9
- Create a dedicated issue branch + worktree for a project adapter using the
10
- shared agent-init-worktree helper.
11
- EOF
12
- }
13
-
14
- shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
15
- repo_root="${AGENT_PROJECT_REPO_ROOT:-}"
16
- worktree_root="${AGENT_PROJECT_WORKTREE_ROOT:-}"
17
- issue_id=""
18
- slug_input="task"
19
- branch_prefix="agent/project/issue"
20
- base_ref="origin/main"
21
- remote_name="origin"
22
- prepare_script=""
23
- stamp=""
24
-
25
- while [[ $# -gt 0 ]]; do
26
- case "$1" in
27
- --repo-root) repo_root="${2:-}"; shift 2 ;;
28
- --worktree-root) worktree_root="${2:-}"; shift 2 ;;
29
- --issue-id) issue_id="${2:-}"; shift 2 ;;
30
- --slug) slug_input="${2:-}"; shift 2 ;;
31
- --branch-prefix) branch_prefix="${2:-}"; shift 2 ;;
32
- --base) base_ref="${2:-}"; shift 2 ;;
33
- --remote) remote_name="${2:-}"; shift 2 ;;
34
- --prepare-script) prepare_script="${2:-}"; shift 2 ;;
35
- --stamp) stamp="${2:-}"; shift 2 ;;
36
- --help|-h) usage; exit 0 ;;
37
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
38
- esac
39
- done
40
-
41
- if [[ -z "$repo_root" || -z "$worktree_root" || -z "$issue_id" ]]; then
42
- usage >&2
43
- exit 1
44
- fi
45
-
46
- if [[ -z "$stamp" ]]; then
47
- stamp="$(date +%Y%m%d-%H%M%S)"
48
- fi
49
-
50
- safe_slug="$(printf '%s' "$slug_input" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')"
51
- safe_slug="${safe_slug#-}"
52
- safe_slug="${safe_slug%-}"
53
- if [[ -z "$safe_slug" ]]; then
54
- safe_slug="task"
55
- fi
56
-
57
- branch_name="${branch_prefix}-${issue_id}-${safe_slug}-${stamp}"
58
- worktree_path="${worktree_root}/issue-${issue_id}-${stamp}"
59
- init_tool="${shared_agent_home}/tools/bin/agent-init-worktree"
60
- fetch_ref="$base_ref"
61
-
62
- if [[ "$fetch_ref" == "${remote_name}/"* ]]; then
63
- fetch_ref="${fetch_ref#${remote_name}/}"
64
- git -C "$repo_root" fetch \
65
- "$remote_name" \
66
- "+refs/heads/${fetch_ref}:refs/remotes/${remote_name}/${fetch_ref}" \
67
- --prune
68
- else
69
- git -C "$repo_root" fetch "$remote_name" "$fetch_ref" --prune
70
- fi
71
-
72
- mkdir -p "$worktree_root"
73
- (
74
- cd "$repo_root"
75
- "$init_tool" \
76
- --branch "$branch_name" \
77
- --base "$base_ref" \
78
- --path "$worktree_path" \
79
- --allow-unclaimed \
80
- --no-bootstrap-deps >/dev/null
81
- )
82
-
83
- if [[ -n "$prepare_script" ]]; then
84
- "$prepare_script" "$worktree_path" >/dev/null
85
- fi
86
-
87
- printf 'WORKTREE=%s\n' "$worktree_path"
88
- printf 'BRANCH=%s\n' "$branch_name"
89
- printf 'BASE_REF=%s\n' "$base_ref"
@@ -1,80 +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
-
8
- usage() {
9
- cat <<'EOF'
10
- Usage:
11
- agent-project-open-pr-worktree --repo-root <path> --worktree-root <path> --pr-number <id> --head-ref <ref> [--local-branch-prefix <prefix>] [--remote <name>] [--prepare-script <path>] [--stamp <yyyymmdd-hhmmss>]
12
-
13
- Create a dedicated review/fix worktree for a PR using the shared
14
- agent-init-worktree helper.
15
- EOF
16
- }
17
-
18
- flow_skill_dir="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
19
- config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
20
- repo_root="${AGENT_PROJECT_REPO_ROOT:-}"
21
- worktree_root="${AGENT_PROJECT_WORKTREE_ROOT:-}"
22
- pr_number=""
23
- head_ref=""
24
- local_branch_prefix="$(flow_resolve_pr_worktree_branch_prefix "${config_yaml}")"
25
- remote_name="origin"
26
- prepare_script=""
27
- stamp=""
28
-
29
- while [[ $# -gt 0 ]]; do
30
- case "$1" in
31
- --repo-root) repo_root="${2:-}"; shift 2 ;;
32
- --worktree-root) worktree_root="${2:-}"; shift 2 ;;
33
- --pr-number) pr_number="${2:-}"; shift 2 ;;
34
- --head-ref) head_ref="${2:-}"; shift 2 ;;
35
- --local-branch-prefix) local_branch_prefix="${2:-}"; shift 2 ;;
36
- --remote) remote_name="${2:-}"; shift 2 ;;
37
- --prepare-script) prepare_script="${2:-}"; shift 2 ;;
38
- --stamp) stamp="${2:-}"; shift 2 ;;
39
- --help|-h) usage; exit 0 ;;
40
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
41
- esac
42
- done
43
-
44
- if [[ -z "$repo_root" || -z "$worktree_root" || -z "$pr_number" || -z "$head_ref" ]]; then
45
- usage >&2
46
- exit 1
47
- fi
48
-
49
- if [[ -z "$stamp" ]]; then
50
- stamp="$(date +%Y%m%d-%H%M%S)"
51
- fi
52
-
53
- worktree_path="${worktree_root}/pr-${pr_number}-${stamp}"
54
- local_branch="${local_branch_prefix}-${pr_number}-${stamp}"
55
- base_ref="${remote_name}/${head_ref}"
56
- init_tool="${flow_skill_dir}/tools/bin/agent-init-worktree"
57
-
58
- mkdir -p "$worktree_root"
59
- git -C "$repo_root" fetch \
60
- "$remote_name" \
61
- "+refs/heads/main:refs/remotes/${remote_name}/main" \
62
- "+refs/heads/${head_ref}:refs/remotes/${remote_name}/${head_ref}" \
63
- --prune
64
- (
65
- cd "$repo_root"
66
- "$init_tool" \
67
- --branch "$local_branch" \
68
- --base "$base_ref" \
69
- --path "$worktree_path" \
70
- --allow-unclaimed \
71
- --no-bootstrap-deps >/dev/null
72
- )
73
-
74
- if [[ -n "$prepare_script" ]]; then
75
- "$prepare_script" "$worktree_path" >/dev/null
76
- fi
77
-
78
- printf 'WORKTREE=%s\n' "$worktree_path"
79
- printf 'REF=%s\n' "$base_ref"
80
- printf 'BRANCH=%s\n' "$local_branch"
@@ -1,468 +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
- CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
8
-
9
- usage() {
10
- cat <<'EOF'
11
- Usage:
12
- agent-project-publish-issue-pr --repo-slug <owner/repo> --runs-root <path> --session <id> [--history-root <path>] [--base-branch <name>] [--remote <name>] [--keep-open-label <label>] [--dry-run]
13
-
14
- Publish an issue worker branch as a PR and ensure the linked issue contains a
15
- single host-side PR comment.
16
- EOF
17
- }
18
-
19
- repo_slug=""
20
- runs_root=""
21
- session=""
22
- history_root="$(flow_resolve_history_root "${CONFIG_YAML}")"
23
- base_branch="main"
24
- remote_name="origin"
25
- dry_run="false"
26
- keep_open_label=""
27
- meta_file_from_archive="no"
28
- shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
29
- scope_guard_script="${shared_agent_home}/tools/bin/issue-publish-scope-guard.sh"
30
- localization_guard_script="${shared_agent_home}/tools/bin/issue-publish-localization-guard.sh"
31
- verification_guard_script="${shared_agent_home}/tools/bin/branch-verification-guard.sh"
32
-
33
- while [[ $# -gt 0 ]]; do
34
- case "$1" in
35
- --repo-slug) repo_slug="${2:-}"; shift 2 ;;
36
- --runs-root) runs_root="${2:-}"; shift 2 ;;
37
- --session) session="${2:-}"; shift 2 ;;
38
- --history-root) history_root="${2:-}"; shift 2 ;;
39
- --base-branch) base_branch="${2:-}"; shift 2 ;;
40
- --remote) remote_name="${2:-}"; shift 2 ;;
41
- --keep-open-label) keep_open_label="${2:-}"; shift 2 ;;
42
- --dry-run) dry_run="true"; shift ;;
43
- --help|-h) usage; exit 0 ;;
44
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
45
- esac
46
- done
47
-
48
- if [[ -z "$repo_slug" || -z "$runs_root" || -z "$session" ]]; then
49
- usage >&2
50
- exit 1
51
- fi
52
-
53
- find_archived_session_dir() {
54
- local root="${1:-}"
55
- local target_session="${2:-}"
56
- local latest=""
57
- [[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
58
-
59
- latest="$(
60
- command find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
61
- | LC_ALL=C sort -r \
62
- | sed -n '1p'
63
- )"
64
- [[ -n "$latest" ]] || return 1
65
- printf '%s\n' "$latest"
66
- }
67
-
68
- find_existing_worktree_for_branch() {
69
- local repo_root="${1:-}"
70
- local branch_name="${2:-}"
71
- local current_worktree=""
72
- local current_branch=""
73
- [[ -n "$repo_root" && -d "$repo_root" && -n "$branch_name" ]] || return 1
74
-
75
- while IFS= read -r line; do
76
- case "$line" in
77
- worktree\ *)
78
- current_worktree="${line#worktree }"
79
- current_branch=""
80
- ;;
81
- branch\ refs/heads/*)
82
- current_branch="${line#branch refs/heads/}"
83
- if [[ "$current_branch" == "$branch_name" && -n "$current_worktree" ]]; then
84
- printf '%s\n' "$current_worktree"
85
- return 0
86
- fi
87
- ;;
88
- "")
89
- current_worktree=""
90
- current_branch=""
91
- ;;
92
- esac
93
- done < <(git -C "$repo_root" worktree list --porcelain 2>/dev/null || true)
94
-
95
- return 1
96
- }
97
-
98
- worktree_is_git_repo() {
99
- local worktree_path="${1:-}"
100
- [[ -n "$worktree_path" && -d "$worktree_path" ]] || return 1
101
- git -C "$worktree_path" rev-parse --is-inside-work-tree >/dev/null 2>&1
102
- }
103
-
104
- worktree_matches_publish_target() {
105
- local worktree_path="${1:-}"
106
- local current_branch=""
107
- local current_head=""
108
-
109
- worktree_is_git_repo "$worktree_path" || return 1
110
-
111
- current_head="$(git -C "$worktree_path" rev-parse HEAD 2>/dev/null || true)"
112
- current_branch="$(git -C "$worktree_path" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
113
-
114
- if [[ -n "${FINAL_HEAD:-}" && -n "$current_head" && "$current_head" == "$FINAL_HEAD" ]]; then
115
- return 0
116
- fi
117
-
118
- if [[ -n "${BRANCH:-}" && -n "$current_branch" && "$current_branch" == "$BRANCH" ]]; then
119
- return 0
120
- fi
121
-
122
- return 1
123
- }
124
-
125
- recover_worktree_from_remote_branch() {
126
- local repo_root="${1:-}"
127
- local worktree_root="${2:-}"
128
- local branch_name="${3:-}"
129
- local remote="${4:-}"
130
- local remote_ref=""
131
- local fallback_worktree=""
132
-
133
- [[ -n "$repo_root" && -d "$repo_root" && -n "$worktree_root" && -n "$branch_name" && -n "$remote" ]] || return 1
134
-
135
- remote_ref="refs/remotes/${remote}/${branch_name}"
136
- git -C "$repo_root" fetch "$remote" "$branch_name" >/dev/null 2>&1 || true
137
- git -C "$repo_root" rev-parse --verify "$remote_ref" >/dev/null 2>&1 || return 1
138
-
139
- fallback_worktree="$(mktemp -d "${worktree_root}/recovery-XXXXXX")"
140
- git -C "$repo_root" worktree add --detach "$fallback_worktree" "$remote_ref" >/dev/null 2>&1 || {
141
- rm -rf "$fallback_worktree" 2>/dev/null || true
142
- return 1
143
- }
144
- worktree_is_git_repo "$fallback_worktree" || {
145
- rm -rf "$fallback_worktree" 2>/dev/null || true
146
- return 1
147
- }
148
- git -C "$fallback_worktree" checkout -B "$branch_name" "$remote_ref" >/dev/null 2>&1 || {
149
- rm -rf "$fallback_worktree" 2>/dev/null || true
150
- return 1
151
- }
152
- git -C "$fallback_worktree" branch --set-upstream-to "${remote}/${branch_name}" "$branch_name" >/dev/null 2>&1 || true
153
- printf '%s\n' "$fallback_worktree"
154
- }
155
-
156
- status_out="$(
157
- "${shared_agent_home}/tools/bin/agent-project-worker-status" \
158
- --runs-root "$runs_root" \
159
- --session "$session"
160
- )"
161
- meta_file="$(awk -F= '/^META_FILE=/{print $2}' <<<"$status_out")"
162
- if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
163
- archived_run_dir="$(find_archived_session_dir "$history_root" "$session" || true)"
164
- if [[ -n "$archived_run_dir" && -f "${archived_run_dir}/run.env" ]]; then
165
- meta_file="${archived_run_dir}/run.env"
166
- meta_file_from_archive="yes"
167
- fi
168
- fi
169
- if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
170
- echo "missing metadata for session $session" >&2
171
- exit 1
172
- fi
173
-
174
- run_dir="$(dirname "$meta_file")"
175
-
176
- set -a
177
- # shellcheck source=/dev/null
178
- source "$meta_file"
179
- set +a
180
-
181
- if [[ -z "${ISSUE_ID:-}" || -z "${BRANCH:-}" ]]; then
182
- echo "session $session is missing ISSUE_ID or BRANCH" >&2
183
- exit 1
184
- fi
185
-
186
- resolve_actor_login() {
187
- local login=""
188
-
189
- login="$(flow_github_current_login 2>/dev/null || true)"
190
- if [[ -z "${login}" && ! "$(flow_forge_provider)" =~ ^gitea$ ]]; then
191
- flow_export_github_cli_auth_env "${repo_slug}"
192
- login="$(gh api user --jq .login 2>/dev/null || true)"
193
- fi
194
- if [[ -z "${login}" && ! "$(flow_forge_provider)" =~ ^gitea$ ]]; then
195
- login="$(
196
- gh auth status 2>/dev/null \
197
- | sed -n 's/^ ✓ Logged in to github.com account \([^ ]*\) (.*/\1/p' \
198
- | head -n 1
199
- )"
200
- fi
201
- if [[ -z "${login}" ]]; then
202
- login="${USER:-}"
203
- fi
204
- printf '%s\n' "${login}"
205
- }
206
-
207
- issue_json="$(flow_github_issue_view_json "$repo_slug" "$ISSUE_ID")"
208
- issue_title="$(jq -r '.title' <<<"$issue_json")"
209
- issue_url="$(jq -r '.url' <<<"$issue_json")"
210
- if [[ -z "${issue_title}" || "${issue_title}" == "null" ]]; then
211
- issue_title="Issue #${ISSUE_ID}"
212
- fi
213
- if [[ -z "${issue_url}" || "${issue_url}" == "null" ]]; then
214
- issue_url="${ISSUE_URL:-}"
215
- fi
216
- actor_login="${GITHUB_ACTOR:-$(resolve_actor_login)}"
217
- issue_keep_open="no"
218
- if [[ -n "$keep_open_label" ]] && jq -e --arg label "$keep_open_label" 'any(.labels[]?; .name == $label)' >/dev/null <<<"$issue_json"; then
219
- issue_keep_open="yes"
220
- fi
221
-
222
- sanitize_body_to_keep_issue_open() {
223
- local body_file="${1:?body file required}"
224
- BODY_FILE="$body_file" ISSUE_ID="$ISSUE_ID" node <<'EOF'
225
- const fs = require('fs');
226
- const bodyFile = process.env.BODY_FILE;
227
- const issueId = process.env.ISSUE_ID;
228
- let text = fs.readFileSync(bodyFile, 'utf8');
229
- const pattern = new RegExp(`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueId}\\b`, 'gi');
230
- text = text.replace(pattern, `Related to #${issueId}`);
231
- fs.writeFileSync(bodyFile, text);
232
- EOF
233
- }
234
-
235
- ensure_body_closes_issue() {
236
- local body_file="${1:?body file required}"
237
- if [[ "$issue_keep_open" == "yes" ]]; then
238
- sanitize_body_to_keep_issue_open "$body_file"
239
- return 0
240
- fi
241
-
242
- if rg -qi '\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#'"${ISSUE_ID}"'\b' "$body_file"; then
243
- return 0
244
- fi
245
-
246
- printf '\nCloses #%s\n' "$ISSUE_ID" >>"$body_file"
247
- }
248
-
249
- find_pr_for_branch() {
250
- flow_github_pr_list_json "$repo_slug" open 100 \
251
- | jq -c --arg branch "${BRANCH}" '.[] | select(.headRefName == $branch)' \
252
- | head -n 1
253
- }
254
-
255
- ensure_issue_pr_comment() {
256
- local pr_number="${1:?pr number required}"
257
- local pr_url="${2:?pr url required}"
258
- local existing body
259
-
260
- existing="$(
261
- flow_github_api_repo "${repo_slug}" "issues/${ISSUE_ID}/comments?per_page=100" --paginate \
262
- | jq -r --arg actor "$actor_login" --arg pr_url "$pr_url" '
263
- .[]
264
- | select(.user.login == $actor)
265
- | select((.body // "") | contains($pr_url))
266
- | .id
267
- ' \
268
- | head -n 1
269
- )"
270
- if [[ -n "$existing" ]]; then
271
- return 0
272
- fi
273
-
274
- body=$(
275
- cat <<EOF
276
- Opened PR #${pr_number}: ${pr_url}
277
-
278
- - Issue: #${ISSUE_ID} (${issue_title})
279
- - Session: \`${session}\`
280
- EOF
281
- )
282
-
283
- if [[ "$dry_run" == "true" ]]; then
284
- printf 'COMMENT_ACTION=would-create\n'
285
- printf 'COMMENT_BODY=%s\n' "$(printf '%s' "$body" | tr '\n' ' ' | sed 's/ */ /g')"
286
- return 0
287
- fi
288
-
289
- flow_github_api_repo "${repo_slug}" "issues/${ISSUE_ID}/comments" --method POST -f body="$body" >/dev/null
290
- }
291
-
292
- pr_json="$(find_pr_for_branch || true)"
293
- publish_status=""
294
- pr_number=""
295
- pr_url=""
296
-
297
- if [[ -n "$pr_json" ]]; then
298
- publish_status="existing-pr"
299
- pr_number="$(jq -r '.number' <<<"$pr_json")"
300
- pr_url="$(jq -r '.url' <<<"$pr_json")"
301
- else
302
- local_repo_root="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
303
- local_worktree_root="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
304
- remote_branch_ref="refs/remotes/${remote_name}/${BRANCH}"
305
- existing_worktree=""
306
- fallback_worktree=""
307
- remote_head=""
308
- current_head=""
309
-
310
- if [[ "${meta_file_from_archive}" == "yes" && -n "${WORKTREE:-}" ]]; then
311
- printf 'WORKTREE_RECOVERY=ignored-archived-pointer\n' >&2
312
- printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
313
- WORKTREE=""
314
- fi
315
-
316
- if [[ -n "${WORKTREE:-}" ]] && ! worktree_matches_publish_target "${WORKTREE}"; then
317
- printf 'WORKTREE_RECOVERY=ignored-stale\n' >&2
318
- printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
319
- WORKTREE=""
320
- fi
321
-
322
- git -C "$local_repo_root" worktree prune >/dev/null 2>&1 || true
323
-
324
- if [[ -z "${WORKTREE:-}" ]]; then
325
- existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
326
- if [[ -n "$existing_worktree" ]] && worktree_matches_publish_target "$existing_worktree"; then
327
- WORKTREE="$existing_worktree"
328
- printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
329
- printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
330
- fi
331
- fi
332
-
333
- if [[ -z "${WORKTREE:-}" && "${meta_file_from_archive}" == "yes" ]]; then
334
- fallback_worktree="$(recover_worktree_from_remote_branch "$local_repo_root" "$local_worktree_root" "$BRANCH" "$remote_name" || true)"
335
- if [[ -n "$fallback_worktree" ]]; then
336
- WORKTREE="$fallback_worktree"
337
- printf 'WORKTREE_RECOVERY=from-remote\n' >&2
338
- printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
339
- fi
340
- fi
341
-
342
- if [[ -z "${WORKTREE:-}" ]]; then
343
- if ! git -C "$local_repo_root" rev-parse --verify "${BRANCH}" >/dev/null 2>&1; then
344
- if [[ -n "${FINAL_HEAD:-}" ]] && git -C "$local_repo_root" cat-file -e "${FINAL_HEAD}^{commit}" >/dev/null 2>&1; then
345
- git -C "$local_repo_root" branch -f "$BRANCH" "$FINAL_HEAD" >/dev/null 2>&1 || true
346
- printf 'BRANCH_RECOVERY=from-final-head\n' >&2
347
- printf 'RECOVERY_HEAD=%s\n' "$FINAL_HEAD" >&2
348
- fi
349
- fi
350
-
351
- existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
352
- if [[ -n "$existing_worktree" ]] && worktree_is_git_repo "$existing_worktree"; then
353
- WORKTREE="$existing_worktree"
354
- printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
355
- printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
356
- else
357
- fallback_worktree="$(mktemp -d "${local_worktree_root}/recovery-XXXXXX")"
358
- if git -C "$local_repo_root" worktree add "$fallback_worktree" "$BRANCH" >/dev/null 2>&1 \
359
- || git -C "$local_repo_root" worktree add "$fallback_worktree" "$remote_branch_ref" >/dev/null 2>&1; then
360
- if worktree_is_git_repo "$fallback_worktree"; then
361
- WORKTREE="$fallback_worktree"
362
- printf 'WORKTREE_RECOVERY=created\n' >&2
363
- printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
364
- else
365
- rm -rf "$fallback_worktree" 2>/dev/null || true
366
- fi
367
- else
368
- rm -rf "$fallback_worktree" 2>/dev/null || true
369
- fi
370
- fi
371
- fi
372
-
373
- if [[ -z "${WORKTREE:-}" || ! -d "${WORKTREE:-}" ]]; then
374
- echo "session $session is missing a publishable worktree (branch ${BRANCH:-unknown} not found locally or on remote ${remote_name})" >&2
375
- exit 1
376
- fi
377
-
378
- git -C "$WORKTREE" fetch "$remote_name" "$base_branch" "$BRANCH" --prune >/dev/null 2>&1 || git -C "$WORKTREE" fetch "$remote_name" "$base_branch" --prune >/dev/null 2>&1 || true
379
- if git -C "$local_repo_root" rev-parse --verify "${remote_branch_ref}" >/dev/null 2>&1; then
380
- remote_head="$(git -C "$local_repo_root" rev-parse "${remote_branch_ref}" 2>/dev/null || true)"
381
- current_head="$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null || true)"
382
- if [[ -n "${remote_head}" && "${current_head}" != "${remote_head}" ]]; then
383
- git -C "$WORKTREE" checkout -B "$BRANCH" "${remote_branch_ref}" >/dev/null 2>&1 || true
384
- git -C "$WORKTREE" branch --set-upstream-to "${remote_name}/${BRANCH}" "$BRANCH" >/dev/null 2>&1 || true
385
- printf 'WORKTREE_RECOVERY=aligned-remote\n' >&2
386
- printf 'RECOVERY_HEAD=%s\n' "${remote_head}" >&2
387
- fi
388
- fi
389
-
390
- head_sha="$(git -C "$WORKTREE" rev-parse HEAD)"
391
- ahead_count="$(git -C "$WORKTREE" rev-list --count "${remote_name}/${base_branch}"..HEAD)"
392
- if [[ "$ahead_count" == "0" ]]; then
393
- echo "branch $BRANCH has no commits ahead of ${remote_name}/${base_branch}; nothing to publish" >&2
394
- exit 1
395
- fi
396
-
397
- "${scope_guard_script}" \
398
- --worktree "$WORKTREE" \
399
- --base-ref "${remote_name}/${base_branch}" \
400
- --issue-id "$ISSUE_ID"
401
-
402
- if [[ -x "${localization_guard_script}" ]]; then
403
- "${localization_guard_script}" \
404
- --worktree "$WORKTREE" \
405
- --base-ref "${remote_name}/${base_branch}"
406
- fi
407
-
408
- "${verification_guard_script}" \
409
- --worktree "$WORKTREE" \
410
- --base-ref "${remote_name}/${base_branch}" \
411
- --run-dir "$run_dir"
412
-
413
- pr_title="$(git -C "$WORKTREE" log -1 --pretty=%s)"
414
- body_file="${run_dir}/pr-body.md"
415
- tmp_body_file=""
416
- if [[ ! -s "$body_file" ]]; then
417
- tmp_body_file="$(mktemp)"
418
- body_file="$tmp_body_file"
419
- cat >"$body_file" <<EOF
420
- ## Summary
421
-
422
- - Implements #${ISSUE_ID}: ${issue_title}
423
- - Host-side publication for session \`${session}\`
424
- - Commit: \`${head_sha}\`
425
-
426
- ## Verification
427
-
428
- - Local verification was executed by the sandboxed issue worker in its isolated worktree.
429
- - Detailed command output is available in the run artifacts for \`${session}\`.
430
- EOF
431
- fi
432
-
433
- ensure_body_closes_issue "$body_file"
434
-
435
- if [[ "$dry_run" == "true" ]]; then
436
- printf 'PUBLISH_STATUS=would-create-pr\n'
437
- printf 'ISSUE_ID=%s\n' "$ISSUE_ID"
438
- printf 'ISSUE_URL=%s\n' "$issue_url"
439
- printf 'BRANCH=%s\n' "$BRANCH"
440
- printf 'PR_TITLE=%s\n' "$pr_title"
441
- printf 'PR_BODY_FILE=%s\n' "$body_file"
442
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
443
- exit 0
444
- fi
445
-
446
- git -C "$WORKTREE" push -u "$remote_name" "$BRANCH"
447
- pr_url="$(flow_github_pr_create "$repo_slug" "$base_branch" "$BRANCH" "$pr_title" "$body_file")"
448
- pr_json="$(find_pr_for_branch || true)"
449
- if [[ -z "$pr_json" ]]; then
450
- echo "PR creation returned ${pr_url}, but no open PR was found for branch ${BRANCH}" >&2
451
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
452
- exit 1
453
- fi
454
-
455
- pr_number="$(jq -r '.number' <<<"$pr_json")"
456
- pr_url="$(jq -r '.url' <<<"$pr_json")"
457
- publish_status="created-pr"
458
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
459
- fi
460
-
461
- ensure_issue_pr_comment "$pr_number" "$pr_url"
462
-
463
- printf 'PUBLISH_STATUS=%s\n' "$publish_status"
464
- printf 'ISSUE_ID=%s\n' "$ISSUE_ID"
465
- printf 'ISSUE_URL=%s\n' "$issue_url"
466
- printf 'PR_NUMBER=%s\n' "$pr_number"
467
- printf 'PR_URL=%s\n' "$pr_url"
468
- printf 'BRANCH=%s\n' "$BRANCH"