agent-control-plane 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,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,465 +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
- flow_export_github_cli_auth_env "${repo_slug}"
190
- login="$(gh api user --jq .login 2>/dev/null || true)"
191
- if [[ -z "${login}" ]]; then
192
- login="$(
193
- gh auth status 2>/dev/null \
194
- | sed -n 's/^ ✓ Logged in to github.com account \([^ ]*\) (.*/\1/p' \
195
- | head -n 1
196
- )"
197
- fi
198
- if [[ -z "${login}" ]]; then
199
- login="${USER:-}"
200
- fi
201
- printf '%s\n' "${login}"
202
- }
203
-
204
- issue_json="$(flow_github_issue_view_json "$repo_slug" "$ISSUE_ID")"
205
- issue_title="$(jq -r '.title' <<<"$issue_json")"
206
- issue_url="$(jq -r '.url' <<<"$issue_json")"
207
- if [[ -z "${issue_title}" || "${issue_title}" == "null" ]]; then
208
- issue_title="Issue #${ISSUE_ID}"
209
- fi
210
- if [[ -z "${issue_url}" || "${issue_url}" == "null" ]]; then
211
- issue_url="${ISSUE_URL:-}"
212
- fi
213
- actor_login="${GITHUB_ACTOR:-$(resolve_actor_login)}"
214
- issue_keep_open="no"
215
- if [[ -n "$keep_open_label" ]] && jq -e --arg label "$keep_open_label" 'any(.labels[]?; .name == $label)' >/dev/null <<<"$issue_json"; then
216
- issue_keep_open="yes"
217
- fi
218
-
219
- sanitize_body_to_keep_issue_open() {
220
- local body_file="${1:?body file required}"
221
- BODY_FILE="$body_file" ISSUE_ID="$ISSUE_ID" node <<'EOF'
222
- const fs = require('fs');
223
- const bodyFile = process.env.BODY_FILE;
224
- const issueId = process.env.ISSUE_ID;
225
- let text = fs.readFileSync(bodyFile, 'utf8');
226
- const pattern = new RegExp(`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s+#${issueId}\\b`, 'gi');
227
- text = text.replace(pattern, `Related to #${issueId}`);
228
- fs.writeFileSync(bodyFile, text);
229
- EOF
230
- }
231
-
232
- ensure_body_closes_issue() {
233
- local body_file="${1:?body file required}"
234
- if [[ "$issue_keep_open" == "yes" ]]; then
235
- sanitize_body_to_keep_issue_open "$body_file"
236
- return 0
237
- fi
238
-
239
- if rg -qi '\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#'"${ISSUE_ID}"'\b' "$body_file"; then
240
- return 0
241
- fi
242
-
243
- printf '\nCloses #%s\n' "$ISSUE_ID" >>"$body_file"
244
- }
245
-
246
- find_pr_for_branch() {
247
- flow_github_pr_list_json "$repo_slug" open 100 \
248
- | jq -c --arg branch "${BRANCH}" '.[] | select(.headRefName == $branch)' \
249
- | head -n 1
250
- }
251
-
252
- ensure_issue_pr_comment() {
253
- local pr_number="${1:?pr number required}"
254
- local pr_url="${2:?pr url required}"
255
- local existing body
256
-
257
- existing="$(
258
- flow_github_api_repo "${repo_slug}" "issues/${ISSUE_ID}/comments?per_page=100" --paginate \
259
- | jq -r --arg actor "$actor_login" --arg pr_url "$pr_url" '
260
- .[]
261
- | select(.user.login == $actor)
262
- | select((.body // "") | contains($pr_url))
263
- | .id
264
- ' \
265
- | head -n 1
266
- )"
267
- if [[ -n "$existing" ]]; then
268
- return 0
269
- fi
270
-
271
- body=$(
272
- cat <<EOF
273
- Opened PR #${pr_number}: ${pr_url}
274
-
275
- - Issue: #${ISSUE_ID} (${issue_title})
276
- - Session: \`${session}\`
277
- EOF
278
- )
279
-
280
- if [[ "$dry_run" == "true" ]]; then
281
- printf 'COMMENT_ACTION=would-create\n'
282
- printf 'COMMENT_BODY=%s\n' "$(printf '%s' "$body" | tr '\n' ' ' | sed 's/ */ /g')"
283
- return 0
284
- fi
285
-
286
- flow_github_api_repo "${repo_slug}" "issues/${ISSUE_ID}/comments" --method POST -f body="$body" >/dev/null
287
- }
288
-
289
- pr_json="$(find_pr_for_branch || true)"
290
- publish_status=""
291
- pr_number=""
292
- pr_url=""
293
-
294
- if [[ -n "$pr_json" ]]; then
295
- publish_status="existing-pr"
296
- pr_number="$(jq -r '.number' <<<"$pr_json")"
297
- pr_url="$(jq -r '.url' <<<"$pr_json")"
298
- else
299
- local_repo_root="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
300
- local_worktree_root="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
301
- remote_branch_ref="refs/remotes/${remote_name}/${BRANCH}"
302
- existing_worktree=""
303
- fallback_worktree=""
304
- remote_head=""
305
- current_head=""
306
-
307
- if [[ "${meta_file_from_archive}" == "yes" && -n "${WORKTREE:-}" ]]; then
308
- printf 'WORKTREE_RECOVERY=ignored-archived-pointer\n' >&2
309
- printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
310
- WORKTREE=""
311
- fi
312
-
313
- if [[ -n "${WORKTREE:-}" ]] && ! worktree_matches_publish_target "${WORKTREE}"; then
314
- printf 'WORKTREE_RECOVERY=ignored-stale\n' >&2
315
- printf 'RECOVERY_WORKTREE=%s\n' "${WORKTREE}" >&2
316
- WORKTREE=""
317
- fi
318
-
319
- git -C "$local_repo_root" worktree prune >/dev/null 2>&1 || true
320
-
321
- if [[ -z "${WORKTREE:-}" ]]; then
322
- existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
323
- if [[ -n "$existing_worktree" ]] && worktree_matches_publish_target "$existing_worktree"; then
324
- WORKTREE="$existing_worktree"
325
- printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
326
- printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
327
- fi
328
- fi
329
-
330
- if [[ -z "${WORKTREE:-}" && "${meta_file_from_archive}" == "yes" ]]; then
331
- fallback_worktree="$(recover_worktree_from_remote_branch "$local_repo_root" "$local_worktree_root" "$BRANCH" "$remote_name" || true)"
332
- if [[ -n "$fallback_worktree" ]]; then
333
- WORKTREE="$fallback_worktree"
334
- printf 'WORKTREE_RECOVERY=from-remote\n' >&2
335
- printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
336
- fi
337
- fi
338
-
339
- if [[ -z "${WORKTREE:-}" ]]; then
340
- if ! git -C "$local_repo_root" rev-parse --verify "${BRANCH}" >/dev/null 2>&1; then
341
- if [[ -n "${FINAL_HEAD:-}" ]] && git -C "$local_repo_root" cat-file -e "${FINAL_HEAD}^{commit}" >/dev/null 2>&1; then
342
- git -C "$local_repo_root" branch -f "$BRANCH" "$FINAL_HEAD" >/dev/null 2>&1 || true
343
- printf 'BRANCH_RECOVERY=from-final-head\n' >&2
344
- printf 'RECOVERY_HEAD=%s\n' "$FINAL_HEAD" >&2
345
- fi
346
- fi
347
-
348
- existing_worktree="$(find_existing_worktree_for_branch "$local_repo_root" "$BRANCH" || true)"
349
- if [[ -n "$existing_worktree" ]] && worktree_is_git_repo "$existing_worktree"; then
350
- WORKTREE="$existing_worktree"
351
- printf 'WORKTREE_RECOVERY=reused-existing\n' >&2
352
- printf 'RECOVERY_WORKTREE=%s\n' "$existing_worktree" >&2
353
- else
354
- fallback_worktree="$(mktemp -d "${local_worktree_root}/recovery-XXXXXX")"
355
- if git -C "$local_repo_root" worktree add "$fallback_worktree" "$BRANCH" >/dev/null 2>&1 \
356
- || git -C "$local_repo_root" worktree add "$fallback_worktree" "$remote_branch_ref" >/dev/null 2>&1; then
357
- if worktree_is_git_repo "$fallback_worktree"; then
358
- WORKTREE="$fallback_worktree"
359
- printf 'WORKTREE_RECOVERY=created\n' >&2
360
- printf 'RECOVERY_WORKTREE=%s\n' "$fallback_worktree" >&2
361
- else
362
- rm -rf "$fallback_worktree" 2>/dev/null || true
363
- fi
364
- else
365
- rm -rf "$fallback_worktree" 2>/dev/null || true
366
- fi
367
- fi
368
- fi
369
-
370
- if [[ -z "${WORKTREE:-}" || ! -d "${WORKTREE:-}" ]]; then
371
- echo "session $session is missing a publishable worktree (branch ${BRANCH:-unknown} not found locally or on remote ${remote_name})" >&2
372
- exit 1
373
- fi
374
-
375
- 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
376
- if git -C "$local_repo_root" rev-parse --verify "${remote_branch_ref}" >/dev/null 2>&1; then
377
- remote_head="$(git -C "$local_repo_root" rev-parse "${remote_branch_ref}" 2>/dev/null || true)"
378
- current_head="$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null || true)"
379
- if [[ -n "${remote_head}" && "${current_head}" != "${remote_head}" ]]; then
380
- git -C "$WORKTREE" checkout -B "$BRANCH" "${remote_branch_ref}" >/dev/null 2>&1 || true
381
- git -C "$WORKTREE" branch --set-upstream-to "${remote_name}/${BRANCH}" "$BRANCH" >/dev/null 2>&1 || true
382
- printf 'WORKTREE_RECOVERY=aligned-remote\n' >&2
383
- printf 'RECOVERY_HEAD=%s\n' "${remote_head}" >&2
384
- fi
385
- fi
386
-
387
- head_sha="$(git -C "$WORKTREE" rev-parse HEAD)"
388
- ahead_count="$(git -C "$WORKTREE" rev-list --count "${remote_name}/${base_branch}"..HEAD)"
389
- if [[ "$ahead_count" == "0" ]]; then
390
- echo "branch $BRANCH has no commits ahead of ${remote_name}/${base_branch}; nothing to publish" >&2
391
- exit 1
392
- fi
393
-
394
- "${scope_guard_script}" \
395
- --worktree "$WORKTREE" \
396
- --base-ref "${remote_name}/${base_branch}" \
397
- --issue-id "$ISSUE_ID"
398
-
399
- if [[ -x "${localization_guard_script}" ]]; then
400
- "${localization_guard_script}" \
401
- --worktree "$WORKTREE" \
402
- --base-ref "${remote_name}/${base_branch}"
403
- fi
404
-
405
- "${verification_guard_script}" \
406
- --worktree "$WORKTREE" \
407
- --base-ref "${remote_name}/${base_branch}" \
408
- --run-dir "$run_dir"
409
-
410
- pr_title="$(git -C "$WORKTREE" log -1 --pretty=%s)"
411
- body_file="${run_dir}/pr-body.md"
412
- tmp_body_file=""
413
- if [[ ! -s "$body_file" ]]; then
414
- tmp_body_file="$(mktemp)"
415
- body_file="$tmp_body_file"
416
- cat >"$body_file" <<EOF
417
- ## Summary
418
-
419
- - Implements #${ISSUE_ID}: ${issue_title}
420
- - Host-side publication for session \`${session}\`
421
- - Commit: \`${head_sha}\`
422
-
423
- ## Verification
424
-
425
- - Local verification was executed by the sandboxed issue worker in its isolated worktree.
426
- - Detailed command output is available in the run artifacts for \`${session}\`.
427
- EOF
428
- fi
429
-
430
- ensure_body_closes_issue "$body_file"
431
-
432
- if [[ "$dry_run" == "true" ]]; then
433
- printf 'PUBLISH_STATUS=would-create-pr\n'
434
- printf 'ISSUE_ID=%s\n' "$ISSUE_ID"
435
- printf 'ISSUE_URL=%s\n' "$issue_url"
436
- printf 'BRANCH=%s\n' "$BRANCH"
437
- printf 'PR_TITLE=%s\n' "$pr_title"
438
- printf 'PR_BODY_FILE=%s\n' "$body_file"
439
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
440
- exit 0
441
- fi
442
-
443
- git -C "$WORKTREE" push -u "$remote_name" "$BRANCH"
444
- pr_url="$(flow_github_pr_create "$repo_slug" "$base_branch" "$BRANCH" "$pr_title" "$body_file")"
445
- pr_json="$(find_pr_for_branch || true)"
446
- if [[ -z "$pr_json" ]]; then
447
- echo "PR creation returned ${pr_url}, but no open PR was found for branch ${BRANCH}" >&2
448
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
449
- exit 1
450
- fi
451
-
452
- pr_number="$(jq -r '.number' <<<"$pr_json")"
453
- pr_url="$(jq -r '.url' <<<"$pr_json")"
454
- publish_status="created-pr"
455
- [[ -n "$tmp_body_file" ]] && rm -f "$tmp_body_file"
456
- fi
457
-
458
- ensure_issue_pr_comment "$pr_number" "$pr_url"
459
-
460
- printf 'PUBLISH_STATUS=%s\n' "$publish_status"
461
- printf 'ISSUE_ID=%s\n' "$ISSUE_ID"
462
- printf 'ISSUE_URL=%s\n' "$issue_url"
463
- printf 'PR_NUMBER=%s\n' "$pr_number"
464
- printf 'PR_URL=%s\n' "$pr_url"
465
- printf 'BRANCH=%s\n' "$BRANCH"