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,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"