agent-control-plane 0.1.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 (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +589 -0
  3. package/SKILL.md +149 -0
  4. package/assets/workflow-catalog.json +57 -0
  5. package/bin/audit-issue-routing.sh +74 -0
  6. package/bin/issue-resource-class.sh +58 -0
  7. package/bin/label-follow-up-issues.sh +114 -0
  8. package/bin/pr-risk.sh +532 -0
  9. package/bin/sync-pr-labels.sh +112 -0
  10. package/hooks/heartbeat-hooks.sh +573 -0
  11. package/hooks/issue-reconcile-hooks.sh +217 -0
  12. package/hooks/pr-reconcile-hooks.sh +225 -0
  13. package/npm/bin/agent-control-plane.js +1984 -0
  14. package/npm/public-bin/agent-control-plane +3 -0
  15. package/package.json +61 -0
  16. package/tools/bin/agent-cleanup-worktree +247 -0
  17. package/tools/bin/agent-github-update-labels +66 -0
  18. package/tools/bin/agent-init-worktree +216 -0
  19. package/tools/bin/agent-project-archive-run +52 -0
  20. package/tools/bin/agent-project-capture-worker +46 -0
  21. package/tools/bin/agent-project-catch-up-merged-prs +137 -0
  22. package/tools/bin/agent-project-cleanup-session +244 -0
  23. package/tools/bin/agent-project-detached-launch +107 -0
  24. package/tools/bin/agent-project-heartbeat-loop +2347 -0
  25. package/tools/bin/agent-project-open-issue-worktree +89 -0
  26. package/tools/bin/agent-project-open-pr-worktree +80 -0
  27. package/tools/bin/agent-project-publish-issue-pr +349 -0
  28. package/tools/bin/agent-project-reconcile-issue-session +1128 -0
  29. package/tools/bin/agent-project-reconcile-pr-session +1005 -0
  30. package/tools/bin/agent-project-retry-state +147 -0
  31. package/tools/bin/agent-project-run-claude-session +657 -0
  32. package/tools/bin/agent-project-run-codex-resilient +718 -0
  33. package/tools/bin/agent-project-run-codex-session +316 -0
  34. package/tools/bin/agent-project-run-kilo-session +27 -0
  35. package/tools/bin/agent-project-run-openclaw-session +984 -0
  36. package/tools/bin/agent-project-run-opencode-session +27 -0
  37. package/tools/bin/agent-project-sync-anchor-repo +128 -0
  38. package/tools/bin/agent-project-worker-status +143 -0
  39. package/tools/bin/audit-agent-worktrees.sh +310 -0
  40. package/tools/bin/audit-issue-routing.sh +11 -0
  41. package/tools/bin/audit-retained-layout.sh +58 -0
  42. package/tools/bin/audit-retained-overlap.sh +135 -0
  43. package/tools/bin/audit-retained-worktrees.sh +228 -0
  44. package/tools/bin/branch-verification-guard.sh +351 -0
  45. package/tools/bin/capture-worker.sh +18 -0
  46. package/tools/bin/check-skill-contracts.sh +324 -0
  47. package/tools/bin/cleanup-worktree.sh +44 -0
  48. package/tools/bin/codex-quota +31 -0
  49. package/tools/bin/create-follow-up-issue.sh +114 -0
  50. package/tools/bin/dashboard-launchd-bootstrap.sh +38 -0
  51. package/tools/bin/flow-config-lib.sh +2127 -0
  52. package/tools/bin/flow-resident-worker-lib.sh +683 -0
  53. package/tools/bin/flow-runtime-doctor.sh +97 -0
  54. package/tools/bin/flow-shell-lib.sh +266 -0
  55. package/tools/bin/heartbeat-recovery-preflight.sh +106 -0
  56. package/tools/bin/heartbeat-safe-auto.sh +551 -0
  57. package/tools/bin/install-dashboard-launchd.sh +152 -0
  58. package/tools/bin/install-project-launchd.sh +219 -0
  59. package/tools/bin/issue-publish-scope-guard.sh +242 -0
  60. package/tools/bin/issue-requires-local-workspace-install.sh +31 -0
  61. package/tools/bin/issue-resource-class.sh +12 -0
  62. package/tools/bin/kick-scheduler.sh +75 -0
  63. package/tools/bin/label-follow-up-issues.sh +14 -0
  64. package/tools/bin/new-pr-worktree.sh +50 -0
  65. package/tools/bin/new-worktree.sh +49 -0
  66. package/tools/bin/pr-risk.sh +12 -0
  67. package/tools/bin/prepare-worktree.sh +140 -0
  68. package/tools/bin/profile-activate.sh +109 -0
  69. package/tools/bin/profile-adopt.sh +219 -0
  70. package/tools/bin/profile-smoke.sh +461 -0
  71. package/tools/bin/project-init.sh +189 -0
  72. package/tools/bin/project-launchd-bootstrap.sh +54 -0
  73. package/tools/bin/project-remove.sh +155 -0
  74. package/tools/bin/project-runtime-supervisor.sh +56 -0
  75. package/tools/bin/project-runtimectl.sh +586 -0
  76. package/tools/bin/provider-cooldown-state.sh +166 -0
  77. package/tools/bin/publish-issue-worker.sh +31 -0
  78. package/tools/bin/reconcile-issue-worker.sh +34 -0
  79. package/tools/bin/reconcile-pr-worker.sh +34 -0
  80. package/tools/bin/record-verification.sh +71 -0
  81. package/tools/bin/render-architecture-infographics.sh +110 -0
  82. package/tools/bin/render-dashboard-demo-media.sh +333 -0
  83. package/tools/bin/render-dashboard-snapshot.py +16 -0
  84. package/tools/bin/render-flow-config.sh +86 -0
  85. package/tools/bin/retry-state.sh +31 -0
  86. package/tools/bin/reuse-issue-worktree.sh +75 -0
  87. package/tools/bin/run-codex-bypass.sh +3 -0
  88. package/tools/bin/run-codex-safe.sh +3 -0
  89. package/tools/bin/run-codex-task.sh +231 -0
  90. package/tools/bin/scaffold-profile.sh +374 -0
  91. package/tools/bin/serve-dashboard.sh +5 -0
  92. package/tools/bin/split-retained-slice.sh +124 -0
  93. package/tools/bin/start-issue-worker.sh +796 -0
  94. package/tools/bin/start-pr-fix-worker.sh +458 -0
  95. package/tools/bin/start-pr-merge-repair-worker.sh +8 -0
  96. package/tools/bin/start-pr-review-worker.sh +227 -0
  97. package/tools/bin/start-resident-issue-loop.sh +908 -0
  98. package/tools/bin/sync-agent-repo.sh +52 -0
  99. package/tools/bin/sync-dependency-baseline.sh +247 -0
  100. package/tools/bin/sync-pr-labels.sh +12 -0
  101. package/tools/bin/sync-recurring-issue-checklist.sh +274 -0
  102. package/tools/bin/sync-shared-agent-home.sh +214 -0
  103. package/tools/bin/sync-vscode-workspace.sh +157 -0
  104. package/tools/bin/test-smoke.sh +63 -0
  105. package/tools/bin/uninstall-project-launchd.sh +55 -0
  106. package/tools/bin/update-github-labels.sh +14 -0
  107. package/tools/bin/worker-status.sh +19 -0
  108. package/tools/bin/workflow-catalog.sh +77 -0
  109. package/tools/dashboard/app.js +286 -0
  110. package/tools/dashboard/dashboard_snapshot.py +466 -0
  111. package/tools/dashboard/index.html +41 -0
  112. package/tools/dashboard/server.py +64 -0
  113. package/tools/dashboard/styles.css +351 -0
  114. package/tools/templates/issue-prompt-template.md +109 -0
  115. package/tools/templates/pr-fix-template.md +120 -0
  116. package/tools/templates/pr-merge-repair-template.md +91 -0
  117. package/tools/templates/pr-review-template.md +62 -0
  118. package/tools/templates/scheduled-issue-prompt-template.md +62 -0
  119. package/tools/tests/test-agent-control-plane-npm-cli.sh +279 -0
  120. package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +56 -0
  121. package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +89 -0
  122. package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +82 -0
  123. package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +90 -0
  124. package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +73 -0
  125. package/tools/tests/test-agent-project-claude-session-wrapper.sh +103 -0
  126. package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +90 -0
  127. package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +90 -0
  128. package/tools/tests/test-agent-project-codex-live-thread-persist.sh +76 -0
  129. package/tools/tests/test-agent-project-codex-recovery.sh +731 -0
  130. package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +105 -0
  131. package/tools/tests/test-agent-project-codex-session-wrapper.sh +97 -0
  132. package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +81 -0
  133. package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +109 -0
  134. package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +89 -0
  135. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +113 -0
  136. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +135 -0
  137. package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +179 -0
  138. package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +119 -0
  139. package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +91 -0
  140. package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +117 -0
  141. package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +148 -0
  142. package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +146 -0
  143. package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +145 -0
  144. package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +147 -0
  145. package/tools/tests/test-agent-project-reconcile-failure-reason.sh +456 -0
  146. package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +96 -0
  147. package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +90 -0
  148. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +212 -0
  149. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +207 -0
  150. package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +101 -0
  151. package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +113 -0
  152. package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +117 -0
  153. package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +55 -0
  154. package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +125 -0
  155. package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +118 -0
  156. package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +144 -0
  157. package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +145 -0
  158. package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +139 -0
  159. package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +242 -0
  160. package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +142 -0
  161. package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +106 -0
  162. package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +66 -0
  163. package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +129 -0
  164. package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +69 -0
  165. package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +43 -0
  166. package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +46 -0
  167. package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +79 -0
  168. package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +56 -0
  169. package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +72 -0
  170. package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +125 -0
  171. package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +178 -0
  172. package/tools/tests/test-codex-quota-wrapper.sh +37 -0
  173. package/tools/tests/test-contribution-docs.sh +18 -0
  174. package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +343 -0
  175. package/tools/tests/test-create-follow-up-issue.sh +73 -0
  176. package/tools/tests/test-dashboard-launchd-bootstrap.sh +55 -0
  177. package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +30 -0
  178. package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +48 -0
  179. package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +85 -0
  180. package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +60 -0
  181. package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +64 -0
  182. package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +77 -0
  183. package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +52 -0
  184. package/tools/tests/test-flow-resident-reap-stale-controllers.sh +63 -0
  185. package/tools/tests/test-flow-resolve-codex-quota-tools.sh +104 -0
  186. package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +27 -0
  187. package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +79 -0
  188. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +115 -0
  189. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +117 -0
  190. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +96 -0
  191. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +96 -0
  192. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +96 -0
  193. package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +170 -0
  194. package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +201 -0
  195. package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +201 -0
  196. package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +160 -0
  197. package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +133 -0
  198. package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +157 -0
  199. package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +181 -0
  200. package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +160 -0
  201. package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +134 -0
  202. package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +162 -0
  203. package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +136 -0
  204. package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +139 -0
  205. package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +119 -0
  206. package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +140 -0
  207. package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +142 -0
  208. package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +141 -0
  209. package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +65 -0
  210. package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +179 -0
  211. package/tools/tests/test-install-dashboard-launchd.sh +78 -0
  212. package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +87 -0
  213. package/tools/tests/test-install-project-launchd.sh +110 -0
  214. package/tools/tests/test-issue-local-workspace-install-policy.sh +81 -0
  215. package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +70 -0
  216. package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +36 -0
  217. package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +47 -0
  218. package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +132 -0
  219. package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +64 -0
  220. package/tools/tests/test-package-funding-metadata.sh +21 -0
  221. package/tools/tests/test-package-public-metadata.sh +62 -0
  222. package/tools/tests/test-placeholder-worker-adapters.sh +38 -0
  223. package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +110 -0
  224. package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +70 -0
  225. package/tools/tests/test-pr-risk-fix-label-semantics.sh +114 -0
  226. package/tools/tests/test-pr-risk-local-first-no-checks.sh +70 -0
  227. package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +67 -0
  228. package/tools/tests/test-profile-activate.sh +33 -0
  229. package/tools/tests/test-profile-adopt-allow-missing-repo.sh +68 -0
  230. package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +61 -0
  231. package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +90 -0
  232. package/tools/tests/test-profile-smoke-collision.sh +44 -0
  233. package/tools/tests/test-profile-smoke-invalid-claude-config.sh +31 -0
  234. package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +68 -0
  235. package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +36 -0
  236. package/tools/tests/test-profile-smoke.sh +45 -0
  237. package/tools/tests/test-project-init-force-and-skip-sync.sh +61 -0
  238. package/tools/tests/test-project-init-repo-slug-mismatch.sh +29 -0
  239. package/tools/tests/test-project-init.sh +66 -0
  240. package/tools/tests/test-project-launchd-bootstrap.sh +66 -0
  241. package/tools/tests/test-project-remove.sh +150 -0
  242. package/tools/tests/test-project-runtime-supervisor.sh +47 -0
  243. package/tools/tests/test-project-runtimectl-launchd.sh +115 -0
  244. package/tools/tests/test-project-runtimectl-missing-profile.sh +54 -0
  245. package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +108 -0
  246. package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +95 -0
  247. package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +59 -0
  248. package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +85 -0
  249. package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +78 -0
  250. package/tools/tests/test-project-runtimectl.sh +212 -0
  251. package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +39 -0
  252. package/tools/tests/test-provider-cooldown-state.sh +59 -0
  253. package/tools/tests/test-public-repo-docs.sh +159 -0
  254. package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +75 -0
  255. package/tools/tests/test-render-dashboard-snapshot.sh +149 -0
  256. package/tools/tests/test-render-flow-config-demo-profile.sh +36 -0
  257. package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +81 -0
  258. package/tools/tests/test-render-flow-config.sh +52 -0
  259. package/tools/tests/test-run-codex-task-claude-routing.sh +125 -0
  260. package/tools/tests/test-run-codex-task-codex-resident-routing.sh +108 -0
  261. package/tools/tests/test-run-codex-task-kilo-routing.sh +98 -0
  262. package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +117 -0
  263. package/tools/tests/test-run-codex-task-openclaw-routing.sh +113 -0
  264. package/tools/tests/test-run-codex-task-opencode-routing.sh +98 -0
  265. package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +146 -0
  266. package/tools/tests/test-scaffold-profile.sh +108 -0
  267. package/tools/tests/test-serve-dashboard.sh +93 -0
  268. package/tools/tests/test-start-issue-worker-blocked-context.sh +129 -0
  269. package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +189 -0
  270. package/tools/tests/test-start-issue-worker-local-install-routing.sh +157 -0
  271. package/tools/tests/test-start-issue-worker-profile-template-routing.sh +149 -0
  272. package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +212 -0
  273. package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +219 -0
  274. package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +155 -0
  275. package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +199 -0
  276. package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +275 -0
  277. package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +185 -0
  278. package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +152 -0
  279. package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +186 -0
  280. package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +212 -0
  281. package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +148 -0
  282. package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +194 -0
  283. package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +198 -0
  284. package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +145 -0
  285. package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +67 -0
  286. package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +70 -0
  287. package/tools/tests/test-sync-recurring-issue-checklist.sh +95 -0
  288. package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +66 -0
  289. package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +47 -0
  290. package/tools/tests/test-test-smoke.sh +86 -0
  291. package/tools/tests/test-uninstall-project-launchd.sh +37 -0
  292. package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +49 -0
  293. package/tools/tests/test-workflow-catalog.sh +43 -0
  294. package/tools/vendor/codex-quota/LICENSE +21 -0
  295. package/tools/vendor/codex-quota/README.md +459 -0
  296. package/tools/vendor/codex-quota/codex-quota.js +261 -0
  297. package/tools/vendor/codex-quota/lib/claude-accounts.js +226 -0
  298. package/tools/vendor/codex-quota/lib/claude-oauth.js +174 -0
  299. package/tools/vendor/codex-quota/lib/claude-tokens.js +471 -0
  300. package/tools/vendor/codex-quota/lib/claude-usage.js +929 -0
  301. package/tools/vendor/codex-quota/lib/codex-accounts.js +205 -0
  302. package/tools/vendor/codex-quota/lib/codex-tokens.js +326 -0
  303. package/tools/vendor/codex-quota/lib/codex-usage.js +32 -0
  304. package/tools/vendor/codex-quota/lib/color.js +72 -0
  305. package/tools/vendor/codex-quota/lib/constants.js +57 -0
  306. package/tools/vendor/codex-quota/lib/container.js +143 -0
  307. package/tools/vendor/codex-quota/lib/display.js +1111 -0
  308. package/tools/vendor/codex-quota/lib/fs.js +63 -0
  309. package/tools/vendor/codex-quota/lib/handlers.js +2060 -0
  310. package/tools/vendor/codex-quota/lib/jwt.js +33 -0
  311. package/tools/vendor/codex-quota/lib/oauth.js +486 -0
  312. package/tools/vendor/codex-quota/lib/paths.js +34 -0
  313. package/tools/vendor/codex-quota/lib/prompts.js +44 -0
  314. package/tools/vendor/codex-quota/lib/sync.js +1438 -0
  315. package/tools/vendor/codex-quota/lib/token-match.js +96 -0
  316. package/tools/vendor/codex-quota-manager/scripts/auto-switch.sh +500 -0
  317. package/tools/vendor/codex-quota-manager/scripts/batch-add.sh +123 -0
@@ -0,0 +1,1128 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
6
+ bootstrap_flow_shell_lib() {
7
+ local candidate=""
8
+ local skill_name=""
9
+
10
+ for candidate in \
11
+ "${SCRIPT_DIR}/flow-shell-lib.sh" \
12
+ "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
13
+ "${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
14
+ "${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
15
+ "${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
16
+ "${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
17
+ "$(pwd)/tools/bin/flow-shell-lib.sh"; do
18
+ if [[ -n "${candidate}" && -f "${candidate}" ]]; then
19
+ printf '%s\n' "${candidate}"
20
+ return 0
21
+ fi
22
+ done
23
+
24
+ if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
25
+ for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
26
+ [[ -n "${skill_name}" ]] || continue
27
+ candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
28
+ if [[ -f "${candidate}" ]]; then
29
+ printf '%s\n' "${candidate}"
30
+ return 0
31
+ fi
32
+ done
33
+ fi
34
+
35
+ echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
36
+ return 1
37
+ }
38
+
39
+ FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
40
+ BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
41
+ # shellcheck source=/dev/null
42
+ source "${FLOW_SHELL_LIB_PATH}"
43
+
44
+ resolve_reconcile_tools_dir() {
45
+ local candidate_root=""
46
+ local skill_name=""
47
+
48
+ for candidate_root in \
49
+ "${AGENT_CONTROL_PLANE_ROOT:-}" \
50
+ "${ACP_ROOT:-}" \
51
+ "${F_LOSNING_FLOW_ROOT:-}" \
52
+ "${AGENT_FLOW_SKILL_ROOT:-}"; do
53
+ if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
54
+ printf '%s/tools/bin\n' "${candidate_root}"
55
+ return 0
56
+ fi
57
+ done
58
+
59
+ if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
60
+ if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
61
+ printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
62
+ return 0
63
+ fi
64
+ for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
65
+ [[ -n "${skill_name}" ]] || continue
66
+ candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
67
+ if [[ -d "${candidate_root}/tools/bin" ]]; then
68
+ printf '%s/tools/bin\n' "${candidate_root}"
69
+ return 0
70
+ fi
71
+ done
72
+ fi
73
+
74
+ if [[ -d "${SCRIPT_DIR}" ]]; then
75
+ printf '%s\n' "${SCRIPT_DIR}"
76
+ return 0
77
+ fi
78
+
79
+ printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
80
+ }
81
+
82
+ usage() {
83
+ cat <<'EOF'
84
+ Usage:
85
+ agent-project-reconcile-issue-session --session <id> --repo-slug <owner/repo> --repo-root <path> --runs-root <path> --history-root <path> [--hook-file <path>]
86
+
87
+ Reconcile a completed issue worker run using shared lifecycle control flow while
88
+ allowing project adapters to inject policy hooks.
89
+ EOF
90
+ }
91
+
92
+ shared_tools_dir="$(resolve_reconcile_tools_dir)"
93
+ resolve_reconcile_helper_path() {
94
+ local helper_name="${1:?helper name required}"
95
+ local candidate=""
96
+
97
+ for candidate in \
98
+ "${SCRIPT_DIR}/${helper_name}" \
99
+ "${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
100
+ "${shared_tools_dir}/${helper_name}"; do
101
+ if [[ -n "${candidate}" && -f "${candidate}" ]]; then
102
+ printf '%s\n' "${candidate}"
103
+ return 0
104
+ fi
105
+ done
106
+
107
+ echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
108
+ return 1
109
+ }
110
+
111
+ FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
112
+ FLOW_RESIDENT_WORKER_LIB_PATH="$(resolve_reconcile_helper_path "flow-resident-worker-lib.sh")"
113
+ # shellcheck source=/dev/null
114
+ source "${FLOW_CONFIG_LIB_PATH}"
115
+ # shellcheck source=/dev/null
116
+ source "${FLOW_RESIDENT_WORKER_LIB_PATH}"
117
+ session=""
118
+ repo_slug=""
119
+ repo_root=""
120
+ runs_root=""
121
+ history_root=""
122
+ hook_file=""
123
+ record_verification_script="${shared_tools_dir}/record-verification.sh"
124
+
125
+ while [[ $# -gt 0 ]]; do
126
+ case "$1" in
127
+ --session) session="${2:-}"; shift 2 ;;
128
+ --repo-slug) repo_slug="${2:-}"; shift 2 ;;
129
+ --repo-root) repo_root="${2:-}"; shift 2 ;;
130
+ --runs-root) runs_root="${2:-}"; shift 2 ;;
131
+ --history-root) history_root="${2:-}"; shift 2 ;;
132
+ --hook-file) hook_file="${2:-}"; shift 2 ;;
133
+ --help|-h) usage; exit 0 ;;
134
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
135
+ esac
136
+ done
137
+
138
+ if [[ -z "$session" || -z "$repo_slug" || -z "$repo_root" || -z "$runs_root" || -z "$history_root" ]]; then
139
+ usage >&2
140
+ exit 1
141
+ fi
142
+
143
+ status_out="$(
144
+ "${shared_tools_dir}/agent-project-worker-status" \
145
+ --runs-root "$runs_root" \
146
+ --session "$session"
147
+ )"
148
+ status="$(awk -F= '/^STATUS=/{print $2}' <<<"$status_out")"
149
+ failure_reason="$(awk -F= '/^FAILURE_REASON=/{print $2}' <<<"$status_out" | tail -n 1)"
150
+
151
+ if [[ "$status" == "RUNNING" ]]; then
152
+ printf 'STATUS=%s\n' "$status"
153
+ exit 0
154
+ fi
155
+
156
+ find_archived_session_dir() {
157
+ local root="${1:-}"
158
+ local target_session="${2:-}"
159
+ [[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
160
+
161
+ find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
162
+ | sort -r \
163
+ | head -n 1
164
+ }
165
+
166
+ meta_file="$(awk -F= '/^META_FILE=/{print $2}' <<<"$status_out")"
167
+ if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
168
+ archived_run_dir="$(find_archived_session_dir "$history_root" "$session" || true)"
169
+ if [[ -n "$archived_run_dir" && -f "${archived_run_dir}/run.env" ]]; then
170
+ meta_file="${archived_run_dir}/run.env"
171
+ if [[ "$status" == "UNKNOWN" && -f "${archived_run_dir}/runner.env" ]]; then
172
+ set -a
173
+ # shellcheck source=/dev/null
174
+ source "${archived_run_dir}/runner.env"
175
+ set +a
176
+ case "${RUNNER_STATE:-}" in
177
+ succeeded)
178
+ status="SUCCEEDED"
179
+ ;;
180
+ failed)
181
+ status="FAILED"
182
+ failure_reason="${LAST_FAILURE_REASON:-${failure_reason:-}}"
183
+ ;;
184
+ esac
185
+ fi
186
+ if [[ "$status" == "UNKNOWN" && -f "${archived_run_dir}/result.env" ]]; then
187
+ status="SUCCEEDED"
188
+ fi
189
+ fi
190
+ fi
191
+ if [[ -z "$meta_file" || ! -f "$meta_file" ]]; then
192
+ echo "missing metadata for session $session" >&2
193
+ exit 1
194
+ fi
195
+
196
+ run_dir="$(dirname "$meta_file")"
197
+
198
+ set -a
199
+ # shellcheck source=/dev/null
200
+ source "$meta_file"
201
+ set +a
202
+
203
+ result_outcome=""
204
+ result_action=""
205
+ result_file_candidate="${run_dir}/result.env"
206
+ if [[ ! -f "$result_file_candidate" && -n "${RESULT_FILE:-}" && -f "${RESULT_FILE:-}" ]]; then
207
+ result_file_candidate="${RESULT_FILE}"
208
+ fi
209
+ if [[ -f "$result_file_candidate" ]]; then
210
+ set -a
211
+ # shellcheck source=/dev/null
212
+ source "$result_file_candidate"
213
+ set +a
214
+ result_outcome="${OUTCOME:-}"
215
+ result_action="${ACTION:-}"
216
+ fi
217
+
218
+ issue_summary_outcome=""
219
+ issue_summary_action=""
220
+ issue_summary_failure_reason=""
221
+
222
+ issue_set_reconcile_summary() {
223
+ local summary_status="${1:-${status:-}}"
224
+ local summary_outcome="__ISSUE_DEFAULT__"
225
+ local summary_action="__ISSUE_DEFAULT__"
226
+ local summary_failure_reason="__ISSUE_DEFAULT__"
227
+
228
+ if [[ $# -ge 2 ]]; then
229
+ summary_outcome="${2}"
230
+ fi
231
+ if [[ $# -ge 3 ]]; then
232
+ summary_action="${3}"
233
+ fi
234
+ if [[ $# -ge 4 ]]; then
235
+ summary_failure_reason="${4}"
236
+ fi
237
+
238
+ if [[ "${summary_outcome}" == "__ISSUE_DEFAULT__" ]]; then
239
+ if [[ "${summary_status}" == "SUCCEEDED" ]]; then
240
+ summary_outcome="${result_outcome:-}"
241
+ else
242
+ summary_outcome=""
243
+ fi
244
+ fi
245
+
246
+ if [[ "${summary_action}" == "__ISSUE_DEFAULT__" ]]; then
247
+ if [[ "${summary_status}" == "SUCCEEDED" ]]; then
248
+ summary_action="${result_action:-}"
249
+ else
250
+ summary_action=""
251
+ fi
252
+ fi
253
+
254
+ if [[ "${summary_failure_reason}" == "__ISSUE_DEFAULT__" ]]; then
255
+ summary_failure_reason="${failure_reason:-}"
256
+ fi
257
+
258
+ issue_summary_outcome="${summary_outcome}"
259
+ issue_summary_action="${summary_action}"
260
+ issue_summary_failure_reason="${summary_failure_reason}"
261
+ }
262
+
263
+ issue_set_reconcile_summary "$status"
264
+
265
+ issue_id="${ISSUE_ID:-}"
266
+ if [[ -z "$issue_id" ]]; then
267
+ echo "session $session is missing ISSUE_ID" >&2
268
+ exit 1
269
+ fi
270
+
271
+ owner="${repo_slug%%/*}"
272
+ repo="${repo_slug#*/}"
273
+ pr_number=""
274
+
275
+ issue_before_success() { :; }
276
+ issue_before_blocked() { :; }
277
+ issue_schedule_retry() { :; }
278
+ issue_mark_ready() { :; }
279
+ issue_clear_retry() { :; }
280
+ issue_remove_running() { :; }
281
+ issue_mark_blocked() { issue_remove_running; }
282
+ issue_should_close_as_superseded() { return 1; }
283
+ issue_close_as_superseded() { :; }
284
+ issue_after_pr_created() { :; }
285
+ issue_after_reconciled() { :; }
286
+ issue_publish_extra_args() { :; }
287
+ issue_result_contract_note=""
288
+
289
+ if [[ -n "$hook_file" && -f "$hook_file" ]]; then
290
+ # shellcheck source=/dev/null
291
+ source "$hook_file"
292
+ fi
293
+
294
+ provider_cooldown_script="${shared_tools_dir}/provider-cooldown-state.sh"
295
+
296
+ schedule_provider_quota_cooldown() {
297
+ local reason="${1:-provider-quota-limit}"
298
+ [[ "${failure_reason:-}" == "provider-quota-limit" ]] || return 0
299
+ [[ -x "${provider_cooldown_script}" ]] || return 0
300
+
301
+ "${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
302
+ }
303
+
304
+ clear_provider_quota_cooldown() {
305
+ [[ -x "${provider_cooldown_script}" ]] || return 0
306
+
307
+ "${provider_cooldown_script}" clear >/dev/null || true
308
+ }
309
+
310
+ normalize_issue_result_contract() {
311
+ [[ "$status" == "SUCCEEDED" ]] || return 0
312
+
313
+ case "${result_outcome:-}:${result_action:-}" in
314
+ implemented:host-publish-issue-pr)
315
+ return 0
316
+ ;;
317
+ blocked:host-comment-blocker)
318
+ return 0
319
+ ;;
320
+ reported:host-comment-scheduled-report)
321
+ return 0
322
+ ;;
323
+ reported:host-comment-scheduled-alert)
324
+ return 0
325
+ ;;
326
+ *)
327
+ echo "invalid issue worker result contract for session ${session}: OUTCOME='${result_outcome:-}' ACTION='${result_action:-}'" >&2
328
+ return 1
329
+ ;;
330
+ esac
331
+ }
332
+
333
+ post_issue_comment_if_present() {
334
+ local comment_file="${run_dir}/issue-comment.md"
335
+ [[ -s "$comment_file" ]] || return 0
336
+ if issue_latest_comment_matches_artifact; then
337
+ return 0
338
+ fi
339
+ flow_github_api_repo "${repo_slug}" "issues/${issue_id}/comments" --method POST -f body="$(cat "$comment_file")" >/dev/null || true
340
+ }
341
+
342
+ issue_latest_comment_matches_artifact() {
343
+ local comment_file="${run_dir}/issue-comment.md"
344
+ local comment_body issue_json
345
+ [[ -s "${comment_file}" ]] || return 1
346
+ comment_body="$(cat "${comment_file}")"
347
+ issue_json="$(flow_github_issue_view_json "${repo_slug}" "${issue_id}" 2>/dev/null || true)"
348
+ [[ -n "${issue_json}" ]] || return 1
349
+ jq -e --arg body "${comment_body}" '((.comments // [])[-1]?.body // "") == $body' >/dev/null <<<"${issue_json}"
350
+ }
351
+
352
+ write_issue_comment_artifact() {
353
+ local comment_body="${1:-}"
354
+ local comment_file="${run_dir}/issue-comment.md"
355
+ [[ -n "${comment_body}" ]] || return 1
356
+ printf '%s\n' "${comment_body}" >"${comment_file}"
357
+ }
358
+
359
+ issue_has_no_publishable_delta() {
360
+ local worktree_path="${WORKTREE:-}"
361
+ local default_branch="${ACP_DEFAULT_BRANCH:-${F_LOSNING_DEFAULT_BRANCH:-main}}"
362
+ local baseline_ref=""
363
+ local ahead_count=""
364
+ local dirty_state=""
365
+ local ref=""
366
+ local candidate_refs=(
367
+ "origin/${default_branch}"
368
+ "${default_branch}"
369
+ "origin/main"
370
+ "main"
371
+ "origin/master"
372
+ "master"
373
+ )
374
+ local seen_refs=" "
375
+
376
+ [[ -n "${worktree_path}" && -d "${worktree_path}" ]] || return 1
377
+ git -C "${worktree_path}" rev-parse --git-dir >/dev/null 2>&1 || return 1
378
+
379
+ for ref in "${candidate_refs[@]}"; do
380
+ [[ -n "${ref}" ]] || continue
381
+ if [[ "${seen_refs}" == *" ${ref} "* ]]; then
382
+ continue
383
+ fi
384
+ seen_refs="${seen_refs}${ref} "
385
+ if git -C "${worktree_path}" rev-parse --verify "${ref}" >/dev/null 2>&1; then
386
+ baseline_ref="${ref}"
387
+ break
388
+ fi
389
+ done
390
+
391
+ [[ -n "${baseline_ref}" ]] || return 1
392
+
393
+ ahead_count="$(git -C "${worktree_path}" rev-list --count "${baseline_ref}..HEAD" 2>/dev/null || true)"
394
+ case "${ahead_count}" in
395
+ 0) ;;
396
+ *) return 1 ;;
397
+ esac
398
+
399
+ dirty_state="$(git -C "${worktree_path}" status --porcelain --untracked-files=no 2>/dev/null || true)"
400
+ [[ -z "${dirty_state}" ]] || return 1
401
+
402
+ return 0
403
+ }
404
+
405
+ ensure_issue_blocked_comment_artifact() {
406
+ local comment_file="${run_dir}/issue-comment.md"
407
+ local blocker_reason=""
408
+ local verification_file=""
409
+ local comment_body=""
410
+
411
+ [[ -s "${comment_file}" ]] && return 0
412
+
413
+ if issue_has_no_publishable_delta; then
414
+ blocker_reason="no-publishable-commits"
415
+ fi
416
+
417
+ if [[ -z "${blocker_reason}" ]]; then
418
+ verification_file="$(issue_verification_file)"
419
+ if [[ ! -f "${verification_file}" ]] || ! grep -q '"status":"pass"' "${verification_file}" 2>/dev/null; then
420
+ blocker_reason="verification-guard-blocked"
421
+ fi
422
+ fi
423
+
424
+ comment_body="$(build_issue_publish_blocker_comment "${blocker_reason}" "")"
425
+ write_issue_comment_artifact "${comment_body}" || true
426
+ }
427
+
428
+ issue_verification_file() {
429
+ printf '%s\n' "${run_dir}/verification.jsonl"
430
+ }
431
+
432
+ normalize_issue_runner_state() {
433
+ local normalized_state="${1:?normalized state required}"
434
+ local normalized_exit_code="${2:-}"
435
+ local normalized_failure_reason="${3:-}"
436
+ local runner_state_file="${run_dir}/runner.env"
437
+ local thread_id=""
438
+ local attempt="1"
439
+ local resume_count="0"
440
+ local last_exit_code=""
441
+ local last_failure_reason=""
442
+ local last_trigger_reason=""
443
+ local auth_wait_started_at=""
444
+ local last_auth_fingerprint=""
445
+
446
+ [[ -f "${runner_state_file}" ]] || return 0
447
+
448
+ set +u
449
+ set -a
450
+ # shellcheck source=/dev/null
451
+ source "${runner_state_file}"
452
+ set +a
453
+ set -u
454
+
455
+ thread_id="${THREAD_ID:-}"
456
+ attempt="${ATTEMPT:-1}"
457
+ resume_count="${RESUME_COUNT:-0}"
458
+ last_exit_code="${LAST_EXIT_CODE:-}"
459
+ last_failure_reason="${LAST_FAILURE_REASON:-}"
460
+ last_trigger_reason="${LAST_TRIGGER_REASON:-}"
461
+ auth_wait_started_at="${AUTH_WAIT_STARTED_AT:-}"
462
+ last_auth_fingerprint="${LAST_AUTH_FINGERPRINT:-}"
463
+
464
+ if [[ -n "${normalized_exit_code}" ]]; then
465
+ last_exit_code="${normalized_exit_code}"
466
+ fi
467
+ if [[ -n "${normalized_failure_reason}" || "${normalized_state}" == "succeeded" ]]; then
468
+ last_failure_reason="${normalized_failure_reason}"
469
+ fi
470
+
471
+ flow_resident_write_metadata "${runner_state_file}" \
472
+ "RUNNER_STATE=${normalized_state}" \
473
+ "THREAD_ID=${thread_id}" \
474
+ "ATTEMPT=${attempt}" \
475
+ "RESUME_COUNT=${resume_count}" \
476
+ "LAST_EXIT_CODE=${last_exit_code}" \
477
+ "LAST_FAILURE_REASON=${last_failure_reason}" \
478
+ "LAST_TRIGGER_REASON=${last_trigger_reason}" \
479
+ "AUTH_WAIT_STARTED_AT=${auth_wait_started_at}" \
480
+ "LAST_AUTH_FINGERPRINT=${last_auth_fingerprint}" \
481
+ "UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
482
+ }
483
+
484
+ issue_has_recorded_verification() {
485
+ local verification_file
486
+ verification_file="$(issue_verification_file)"
487
+ [[ -f "$verification_file" ]] || return 1
488
+ grep -q '"status":"pass"' "$verification_file" 2>/dev/null
489
+ }
490
+
491
+ extract_issue_host_recovery_commands() {
492
+ local prompt_file="${run_dir}/prompt.md"
493
+ local worktree_path="${WORKTREE:-}"
494
+ local verification_file
495
+ verification_file="$(issue_verification_file)"
496
+
497
+ [[ -n "$worktree_path" && -d "$worktree_path" ]] || return 0
498
+
499
+ PROMPT_FILE="$prompt_file" \
500
+ WORKTREE_PATH="$worktree_path" \
501
+ VERIFICATION_FILE="$verification_file" \
502
+ node <<'EOF'
503
+ const fs = require('fs');
504
+ const path = require('path');
505
+ const cp = require('child_process');
506
+
507
+ const promptFile = process.env.PROMPT_FILE || '';
508
+ const worktreePath = process.env.WORKTREE_PATH || '';
509
+ const verificationFile = process.env.VERIFICATION_FILE || '';
510
+
511
+ const commands = [];
512
+ const seen = new Set();
513
+
514
+ const addCommand = (value) => {
515
+ const command = String(value || '').trim();
516
+ if (!command) return;
517
+ if (seen.has(command)) return;
518
+ seen.add(command);
519
+ commands.push(command);
520
+ };
521
+
522
+ let recordedPassCommands = new Set();
523
+ if (verificationFile && fs.existsSync(verificationFile)) {
524
+ const raw = fs.readFileSync(verificationFile, 'utf8');
525
+ for (const line of raw.split('\n')) {
526
+ const trimmed = line.trim();
527
+ if (!trimmed) continue;
528
+ try {
529
+ const entry = JSON.parse(trimmed);
530
+ if (entry && entry.status === 'pass' && typeof entry.command === 'string') {
531
+ recordedPassCommands.add(entry.command.trim());
532
+ }
533
+ } catch (_error) {
534
+ // Ignore malformed history entries during recovery.
535
+ }
536
+ }
537
+ }
538
+
539
+ let packageJson = null;
540
+ const packageJsonPath = path.join(worktreePath, 'package.json');
541
+ if (fs.existsSync(packageJsonPath)) {
542
+ try {
543
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
544
+ } catch (_error) {
545
+ packageJson = null;
546
+ }
547
+ }
548
+
549
+ const gitChangedFiles = [];
550
+ try {
551
+ const raw = cp.execFileSync(
552
+ 'git',
553
+ [
554
+ '-C',
555
+ worktreePath,
556
+ 'show',
557
+ '--pretty=',
558
+ '--name-only',
559
+ 'HEAD',
560
+ ],
561
+ { encoding: 'utf8' },
562
+ );
563
+ for (const line of raw.split('\n')) {
564
+ const file = line.trim();
565
+ if (file) gitChangedFiles.push(file);
566
+ }
567
+ } catch (_error) {
568
+ // Ignore; recovery can still use prompt-derived commands.
569
+ }
570
+
571
+ const changedFilesLower = gitChangedFiles.map((file) => file.toLowerCase());
572
+ const repoHasScript = (scriptName) => Boolean(packageJson?.scripts && Object.prototype.hasOwnProperty.call(packageJson.scripts, scriptName));
573
+ const commandLooksRunnable = (command) => {
574
+ if (/^npm test(?:\s|$)?/.test(command)) return repoHasScript('test');
575
+ if (/^pnpm test(?:\s|$)?/.test(command)) return repoHasScript('test');
576
+ if (/^npm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^npm run\s+([a-z0-9:_-]+)/i)[1]);
577
+ if (/^pnpm run\s+([a-z0-9:_-]+)/i.test(command)) return repoHasScript(command.match(/^pnpm run\s+([a-z0-9:_-]+)/i)[1]);
578
+ if (/^node\s+--test(?:\s|$)/.test(command)) return true;
579
+ return true;
580
+ };
581
+
582
+ if (promptFile && fs.existsSync(promptFile)) {
583
+ const lines = fs.readFileSync(promptFile, 'utf8').split(/\r?\n/).slice(0, 40);
584
+ for (const line of lines) {
585
+ if (!/^\s*-\s+/.test(line)) continue;
586
+ if (!/(?:\bRun\b|\balso run\b|\bafter code changes\b|\bevery completed cycle\b)/i.test(line)) continue;
587
+
588
+ const trimmed = line.trim();
589
+ if (/^- If /i.test(trimmed)) {
590
+ const lowered = trimmed.toLowerCase();
591
+ if (lowered.includes('cli') || lowered.includes('fixture')) {
592
+ const cliOrFixtureTouched = changedFilesLower.some((file) => /(?:^|\/)(src\/summary-cli|fixtures?\/|fixtures?\.|cli\b)/.test(file));
593
+ if (!cliOrFixtureTouched) {
594
+ continue;
595
+ }
596
+ } else {
597
+ continue;
598
+ }
599
+ }
600
+
601
+ for (const match of line.matchAll(/`([^`]+)`/g)) {
602
+ const command = String(match[1] || '').trim();
603
+ if (commandLooksRunnable(command)) {
604
+ addCommand(command);
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ const changedTestFiles = [...new Set(gitChangedFiles.filter((file) => /\.(?:spec|test)\.[cm]?[jt]sx?$/.test(file)))];
611
+ const rootTestScript = String(packageJson?.scripts?.test || '').trim();
612
+ if (/^node\s+--test(?:\s|$)/.test(rootTestScript)) {
613
+ for (const file of changedTestFiles) {
614
+ addCommand(`node --test ${file}`);
615
+ }
616
+ }
617
+
618
+ if (commands.length === 0 && rootTestScript) {
619
+ addCommand('npm test');
620
+ }
621
+
622
+ const filtered = commands.filter((command) => !recordedPassCommands.has(command));
623
+ process.stdout.write(filtered.join('\n'));
624
+ EOF
625
+ }
626
+
627
+ run_issue_host_verification_command() {
628
+ local command_text="${1:?command text required}"
629
+ local session_log_file="${run_dir}/${session}.log"
630
+
631
+ {
632
+ printf '\n[host-issue-recovery] command=%s\n' "$command_text"
633
+ } >>"$session_log_file"
634
+
635
+ if WORKTREE_DIR="${WORKTREE:?missing worktree for host recovery}" HOST_COMMAND="$command_text" bash -lc 'set -euo pipefail; cd "$WORKTREE_DIR"; eval "$HOST_COMMAND"' >>"$session_log_file" 2>&1; then
636
+ if [[ -x "$record_verification_script" ]]; then
637
+ bash "$record_verification_script" --run-dir "$run_dir" --status pass --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
638
+ fi
639
+ return 0
640
+ fi
641
+
642
+ if [[ -x "$record_verification_script" ]]; then
643
+ bash "$record_verification_script" --run-dir "$run_dir" --status fail --command "$command_text" --note "host-recovery-after-missing-worker-verification" >/dev/null
644
+ fi
645
+ printf '[host-issue-recovery] command failed=%s\n' "$command_text" >>"$session_log_file"
646
+ return 1
647
+ }
648
+
649
+ attempt_issue_host_verification_recovery() {
650
+ local recovery_reason="${1:-missing-worker-verification}"
651
+ local recovery_commands=""
652
+ local command_text=""
653
+
654
+ [[ "${result_outcome:-}" == "implemented" ]] || return 1
655
+ [[ -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]] || return 1
656
+
657
+ recovery_commands="$(extract_issue_host_recovery_commands)"
658
+ [[ -n "$recovery_commands" ]] || return 1
659
+
660
+ while IFS= read -r command_text; do
661
+ [[ -n "$command_text" ]] || continue
662
+ if ! run_issue_host_verification_command "$command_text"; then
663
+ return 1
664
+ fi
665
+ done <<<"$recovery_commands"
666
+
667
+ issue_result_contract_note="host-recovered-${recovery_reason}"
668
+ return 0
669
+ }
670
+
671
+ classify_issue_publish_blocker() {
672
+ local publish_out="${1:-}"
673
+
674
+ if grep -Fq 'Scope guard blocked issue' <<<"$publish_out"; then
675
+ printf 'scope-guard-blocked\n'
676
+ return 0
677
+ fi
678
+
679
+ if grep -Fq 'Verification guard blocked branch publication.' <<<"$publish_out"; then
680
+ printf 'verification-guard-blocked\n'
681
+ return 0
682
+ fi
683
+
684
+ if grep -Fq 'has no commits ahead of' <<<"$publish_out"; then
685
+ printf 'no-publishable-commits\n'
686
+ return 0
687
+ fi
688
+
689
+ return 1
690
+ }
691
+
692
+ refresh_recurring_issue_checklist() {
693
+ local sync_script="${shared_tools_dir}/sync-recurring-issue-checklist.sh"
694
+ [[ -x "${sync_script}" ]] || return 1
695
+ bash "${sync_script}" --repo-slug "${repo_slug}" --issue-id "${issue_id}" 2>/dev/null || return 1
696
+ }
697
+
698
+ build_issue_publish_blocker_comment() {
699
+ local blocker_reason="${1:-}"
700
+ local publish_out="${2:-}"
701
+ local sync_out=""
702
+ local checklist_total="0"
703
+ local checklist_unchecked="0"
704
+ local checklist_matched_prs=""
705
+
706
+ if [[ "${blocker_reason}" == "no-publishable-commits" ]]; then
707
+ sync_out="$(refresh_recurring_issue_checklist || true)"
708
+ checklist_total="$(awk -F= '/^CHECKLIST_TOTAL=/{print $2; exit}' <<<"${sync_out:-}")"
709
+ checklist_unchecked="$(awk -F= '/^CHECKLIST_UNCHECKED=/{print $2; exit}' <<<"${sync_out:-}")"
710
+ checklist_matched_prs="$(awk -F= '/^CHECKLIST_MATCHED_PR_NUMBERS=/{print $2; exit}' <<<"${sync_out:-}")"
711
+
712
+ case "${checklist_total}" in
713
+ ''|*[!0-9]*) checklist_total="0" ;;
714
+ esac
715
+ case "${checklist_unchecked}" in
716
+ ''|*[!0-9]*) checklist_unchecked="0" ;;
717
+ esac
718
+
719
+ if [[ "${checklist_total}" -gt 0 && "${checklist_unchecked}" -eq 0 ]]; then
720
+ cat <<EOF
721
+ # Blocker: All checklist items already completed
722
+
723
+ All checklist items for issue #${issue_id} appear to be satisfied on the current baseline.
724
+
725
+ Why it was blocked:
726
+ - the worker completed a cycle, but the resulting branch had no commits ahead of \`origin/main\`
727
+ - recurring automation should not force another PR when the requested checklist is already done
728
+
729
+ Next step:
730
+ - refresh the issue body with new unchecked improvement items before re-queueing this issue
731
+ EOF
732
+ if [[ -n "${checklist_matched_prs}" ]]; then
733
+ printf '\nRecently matched PRs: #%s\n' "$(sed 's/,/, #/g' <<<"${checklist_matched_prs}")"
734
+ fi
735
+ return 0
736
+ fi
737
+
738
+ cat <<EOF
739
+ # Blocker: Worker produced no publishable delta
740
+
741
+ The worker finished its cycle, but the resulting branch had no commits ahead of \`origin/main\`.
742
+
743
+ Why it was blocked:
744
+ - the selected target likely overlapped work that is already on the current baseline, or
745
+ - the worker ended with no net code/doc/test changes to publish
746
+
747
+ Next step:
748
+ - pick one remaining unchecked checklist item that is still missing on \`main\`
749
+ - if the checklist is stale, refresh the issue body before re-queueing
750
+ EOF
751
+ return 0
752
+ fi
753
+
754
+ if [[ "${blocker_reason}" == "scope-guard-blocked" ]]; then
755
+ cat <<EOF
756
+ # Blocker: Change scope was too broad
757
+
758
+ Host publication stopped this cycle because the branch touched too much surface area for a safe recurring issue PR.
759
+
760
+ Why it was blocked:
761
+ - recurring issues should ship one focused slice at a time
762
+ - the publish scope guard detected a multi-surface change set
763
+
764
+ \`\`\`text
765
+ ${publish_out}
766
+ \`\`\`
767
+ EOF
768
+ return 0
769
+ fi
770
+
771
+ if [[ "${blocker_reason}" == "verification-guard-blocked" ]]; then
772
+ cat <<EOF
773
+ # Blocker: Verification requirements were not satisfied
774
+
775
+ Host publication stopped this cycle because the branch did not carry the required verification signal for a safe recurring issue PR.
776
+
777
+ Why it was blocked:
778
+ - the verification guard could not confirm the expected checks for this change
779
+ - recurring issue publication should stop rather than open an unverifiable PR
780
+
781
+ \`\`\`text
782
+ ${publish_out}
783
+ \`\`\`
784
+ EOF
785
+ return 0
786
+ fi
787
+
788
+ cat <<EOF
789
+ Host-side publish blocked for session \`${session}\`.
790
+
791
+ \`\`\`text
792
+ ${publish_out}
793
+ \`\`\`
794
+ EOF
795
+ }
796
+
797
+ extract_recovery_worktree_from_publish_output() {
798
+ local publish_out="${1:-}"
799
+ awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
800
+ }
801
+
802
+ require_transition() {
803
+ local step="${1:?step required}"
804
+ shift
805
+ if ! "$@"; then
806
+ echo "reconcile transition failed: ${step}" >&2
807
+ exit 1
808
+ fi
809
+ }
810
+
811
+ mark_reconciled() {
812
+ if [[ -d "$run_dir" ]]; then
813
+ touch "${run_dir}/reconciled.ok"
814
+ fi
815
+ }
816
+
817
+ update_resident_issue_metadata() {
818
+ local metadata_file="${RESIDENT_WORKER_META_FILE:-}"
819
+ local finished_at=""
820
+ local task_count="${RESIDENT_TASK_COUNT:-1}"
821
+ local resident_worker_scope=""
822
+ local resident_worker_key=""
823
+ local resident_lane_kind=""
824
+ local resident_lane_value=""
825
+ local openclaw_agent_id=""
826
+ local openclaw_session_id=""
827
+ local openclaw_agent_dir=""
828
+ local openclaw_state_dir=""
829
+ local openclaw_config_path=""
830
+ local worktree_realpath=""
831
+ local last_worktree_reused=""
832
+
833
+ [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]] || return 0
834
+ [[ -n "${metadata_file}" ]] || return 0
835
+
836
+ finished_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
837
+ resident_worker_scope="${RESIDENT_WORKER_SCOPE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_SCOPE" 2>/dev/null || true)}"
838
+ resident_worker_key="${RESIDENT_WORKER_KEY:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_WORKER_KEY" 2>/dev/null || true)}"
839
+ resident_lane_kind="${RESIDENT_LANE_KIND:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_KIND" 2>/dev/null || true)}"
840
+ resident_lane_value="${RESIDENT_LANE_VALUE:-$(flow_resident_metadata_value "${metadata_file}" "RESIDENT_LANE_VALUE" 2>/dev/null || true)}"
841
+ if [[ -z "${resident_lane_kind}" ]]; then
842
+ resident_lane_kind="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" kind 2>/dev/null || true)"
843
+ fi
844
+ if [[ -z "${resident_lane_value}" ]]; then
845
+ resident_lane_value="$(flow_resident_issue_lane_field_from_key "${resident_worker_key:-}" value 2>/dev/null || true)"
846
+ fi
847
+ openclaw_agent_id="${RESIDENT_OPENCLAW_AGENT_ID:-${OPENCLAW_AGENT_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_ID" 2>/dev/null || true)}}"
848
+ openclaw_session_id="${RESIDENT_OPENCLAW_SESSION_ID:-${OPENCLAW_SESSION_ID:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_SESSION_ID" 2>/dev/null || true)}}"
849
+ openclaw_agent_dir="${RESIDENT_OPENCLAW_AGENT_DIR:-${OPENCLAW_AGENT_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_AGENT_DIR" 2>/dev/null || true)}}"
850
+ openclaw_state_dir="${RESIDENT_OPENCLAW_STATE_DIR:-${OPENCLAW_STATE_DIR:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_STATE_DIR" 2>/dev/null || true)}}"
851
+ openclaw_config_path="${RESIDENT_OPENCLAW_CONFIG_PATH:-${OPENCLAW_CONFIG_PATH:-$(flow_resident_metadata_value "${metadata_file}" "OPENCLAW_CONFIG_PATH" 2>/dev/null || true)}}"
852
+ worktree_realpath="${RESIDENT_WORKTREE_REALPATH:-$(flow_resident_metadata_value "${metadata_file}" "WORKTREE_REALPATH" 2>/dev/null || true)}"
853
+ last_worktree_reused="${RESIDENT_WORKTREE_REUSED:-$(flow_resident_metadata_value "${metadata_file}" "LAST_WORKTREE_REUSED" 2>/dev/null || true)}"
854
+
855
+ flow_resident_write_metadata "${metadata_file}" \
856
+ "RESIDENT_WORKER_KIND=issue" \
857
+ "RESIDENT_WORKER_SCOPE=${resident_worker_scope:-lane}" \
858
+ "RESIDENT_WORKER_KEY=${resident_worker_key:-issue-${issue_id}}" \
859
+ "RESIDENT_LANE_KIND=${resident_lane_kind:-}" \
860
+ "RESIDENT_LANE_VALUE=${resident_lane_value:-}" \
861
+ "ISSUE_ID=${issue_id}" \
862
+ "ADAPTER_ID=${ADAPTER_ID:-}" \
863
+ "CODING_WORKER=${CODING_WORKER:-openclaw}" \
864
+ "WORKTREE=${WORKTREE:-}" \
865
+ "WORKTREE_REALPATH=${worktree_realpath:-${WORKTREE:-}}" \
866
+ "LAST_BRANCH=${BRANCH:-}" \
867
+ "OPENCLAW_AGENT_ID=${openclaw_agent_id:-}" \
868
+ "OPENCLAW_SESSION_ID=${openclaw_session_id:-}" \
869
+ "OPENCLAW_AGENT_DIR=${openclaw_agent_dir:-}" \
870
+ "OPENCLAW_STATE_DIR=${openclaw_state_dir:-}" \
871
+ "OPENCLAW_CONFIG_PATH=${openclaw_config_path:-}" \
872
+ "TASK_COUNT=${task_count}" \
873
+ "LAST_STARTED_AT=${STARTED_AT:-}" \
874
+ "LAST_FINISHED_AT=${finished_at}" \
875
+ "LAST_RUN_SESSION=${session}" \
876
+ "LAST_STATUS=${status}" \
877
+ "LAST_OUTCOME=${issue_summary_outcome:-}" \
878
+ "LAST_ACTION=${issue_summary_action:-}" \
879
+ "LAST_FAILURE_REASON=${issue_summary_failure_reason:-}" \
880
+ "LAST_WORKTREE_REUSED=${last_worktree_reused:-no}"
881
+ }
882
+
883
+ cleanup_issue_session() {
884
+ local -a cleanup_args=(
885
+ --repo-root "$repo_root"
886
+ --runs-root "$runs_root"
887
+ --history-root "$history_root"
888
+ --session "$session"
889
+ --worktree "${WORKTREE:-}"
890
+ --mode issue
891
+ )
892
+
893
+ update_resident_issue_metadata
894
+ if [[ "${RESIDENT_WORKER_ENABLED:-}" == "yes" ]]; then
895
+ cleanup_args+=(--skip-worktree-cleanup)
896
+ fi
897
+
898
+ "${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" >/dev/null
899
+ }
900
+
901
+ notify_issue_reconciled() {
902
+ issue_after_reconciled "$status" "${issue_summary_outcome:-}" "${issue_summary_action:-}" "${pr_number:-}" || true
903
+ }
904
+
905
+ case "$status" in
906
+ SUCCEEDED)
907
+ clear_provider_quota_cooldown
908
+ if ! normalize_issue_result_contract; then
909
+ status="FAILED"
910
+ failure_reason="invalid-result-contract"
911
+ issue_result_contract_note="invalid-result-contract"
912
+ normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
913
+ post_issue_comment_if_present
914
+ require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
915
+ require_transition "issue_mark_ready" issue_mark_ready
916
+ result_outcome="invalid-contract"
917
+ result_action="queued-issue-retry"
918
+ issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
919
+ cleanup_issue_session
920
+ notify_issue_reconciled
921
+ mark_reconciled
922
+ printf 'STATUS=%s\n' "$status"
923
+ printf 'ISSUE_ID=%s\n' "$issue_id"
924
+ printf 'PR_NUMBER=%s\n' "$pr_number"
925
+ printf 'OUTCOME=%s\n' "$result_outcome"
926
+ printf 'ACTION=%s\n' "$result_action"
927
+ printf 'FAILURE_REASON=%s\n' "$failure_reason"
928
+ printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
929
+ exit 0
930
+ fi
931
+ require_transition "issue_before_success" issue_before_success
932
+ if [[ "$result_outcome" == "blocked" ]]; then
933
+ ensure_issue_blocked_comment_artifact
934
+ require_transition "issue_before_blocked" issue_before_blocked
935
+ post_issue_comment_if_present
936
+ if issue_should_close_as_superseded; then
937
+ require_transition "issue_clear_retry" issue_clear_retry
938
+ require_transition "issue_remove_running" issue_remove_running
939
+ require_transition "issue_close_as_superseded" issue_close_as_superseded
940
+ result_action="closed-superseded"
941
+ issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "${failure_reason:-}"
942
+ cleanup_issue_session
943
+ notify_issue_reconciled
944
+ mark_reconciled
945
+ printf 'STATUS=%s\n' "$status"
946
+ printf 'ISSUE_ID=%s\n' "$issue_id"
947
+ printf 'PR_NUMBER=%s\n' "$pr_number"
948
+ printf 'OUTCOME=%s\n' "$result_outcome"
949
+ printf 'ACTION=%s\n' "$result_action"
950
+ exit 0
951
+ fi
952
+ failure_reason="issue-worker-blocked"
953
+ normalize_issue_runner_state "succeeded" "0" ""
954
+ require_transition "issue_schedule_retry" issue_schedule_retry "$failure_reason"
955
+ require_transition "issue_mark_blocked" issue_mark_blocked
956
+ issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
957
+ cleanup_issue_session
958
+ notify_issue_reconciled
959
+ mark_reconciled
960
+ printf 'STATUS=%s\n' "$status"
961
+ printf 'ISSUE_ID=%s\n' "$issue_id"
962
+ printf 'PR_NUMBER=%s\n' "$pr_number"
963
+ printf 'OUTCOME=%s\n' "$result_outcome"
964
+ printf 'ACTION=%s\n' "$result_action"
965
+ printf 'FAILURE_REASON=%s\n' "$failure_reason"
966
+ exit 0
967
+ fi
968
+
969
+ if [[ "$result_outcome" == "reported" ]]; then
970
+ normalize_issue_runner_state "succeeded" "0" ""
971
+ post_issue_comment_if_present
972
+ require_transition "issue_clear_retry" issue_clear_retry
973
+ require_transition "issue_remove_running" issue_remove_running
974
+ cleanup_issue_session
975
+ notify_issue_reconciled
976
+ mark_reconciled
977
+ printf 'STATUS=%s\n' "$status"
978
+ printf 'ISSUE_ID=%s\n' "$issue_id"
979
+ printf 'PR_NUMBER=%s\n' "$pr_number"
980
+ printf 'OUTCOME=%s\n' "$result_outcome"
981
+ printf 'ACTION=%s\n' "$result_action"
982
+ exit 0
983
+ fi
984
+
985
+ # Push branch to remote BEFORE publish to preserve commits
986
+ # even if worktree gets cleaned up during publish process
987
+ if [[ -n "${BRANCH:-}" && -n "${WORKTREE:-}" && -d "${WORKTREE:-}" ]]; then
988
+ if git -C "${WORKTREE}" rev-parse --git-dir >/dev/null 2>&1; then
989
+ if ! git -C "${WORKTREE}" push -u origin "${BRANCH}" 2>/dev/null; then
990
+ printf 'PRE_PUBLISH_PUSH=failed branch=%s\n' "${BRANCH}" >&2
991
+ else
992
+ printf 'PRE_PUBLISH_PUSH=ok branch=%s\n' "${BRANCH}" >&2
993
+ fi
994
+ fi
995
+ fi
996
+
997
+ if ! issue_has_recorded_verification; then
998
+ attempt_issue_host_verification_recovery "missing-worker-verification" || true
999
+ fi
1000
+
1001
+ publish_args=()
1002
+ while IFS= read -r publish_arg; do
1003
+ [[ -n "$publish_arg" ]] || continue
1004
+ publish_args+=("$publish_arg")
1005
+ done < <(issue_publish_extra_args || true)
1006
+
1007
+ publish_cmd=(
1008
+ "${shared_tools_dir}/agent-project-publish-issue-pr"
1009
+ --repo-slug "$repo_slug"
1010
+ --runs-root "$runs_root"
1011
+ --history-root "$history_root"
1012
+ --session "$session"
1013
+ )
1014
+ if [[ ${#publish_args[@]} -gt 0 ]]; then
1015
+ publish_cmd+=("${publish_args[@]}")
1016
+ fi
1017
+
1018
+ if ! publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1019
+ publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1020
+ if [[ "$publish_blocker_reason" == "verification-guard-blocked" ]]; then
1021
+ recovered_worktree="$(extract_recovery_worktree_from_publish_output "$publish_out" || true)"
1022
+ if [[ -n "$recovered_worktree" && -d "$recovered_worktree" ]]; then
1023
+ WORKTREE="$recovered_worktree"
1024
+ fi
1025
+ if attempt_issue_host_verification_recovery "verification-guard-blocked"; then
1026
+ if publish_out="$("${publish_cmd[@]}" 2>&1)"; then
1027
+ pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1028
+ normalize_issue_runner_state "succeeded" "0" ""
1029
+ require_transition "issue_clear_retry" issue_clear_retry
1030
+ require_transition "issue_remove_running" issue_remove_running
1031
+ if [[ -n "$pr_number" ]]; then
1032
+ require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1033
+ fi
1034
+ cleanup_issue_session
1035
+ notify_issue_reconciled
1036
+ mark_reconciled
1037
+ printf 'STATUS=%s\n' "$status"
1038
+ printf 'ISSUE_ID=%s\n' "$issue_id"
1039
+ printf 'PR_NUMBER=%s\n' "$pr_number"
1040
+ printf 'OUTCOME=%s\n' "$result_outcome"
1041
+ printf 'ACTION=%s\n' "$result_action"
1042
+ if [[ -n "$issue_result_contract_note" ]]; then
1043
+ printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1044
+ fi
1045
+ exit 0
1046
+ fi
1047
+ publish_blocker_reason="$(classify_issue_publish_blocker "$publish_out" || true)"
1048
+ fi
1049
+ fi
1050
+ blocker_body="$(build_issue_publish_blocker_comment "${publish_blocker_reason:-}" "${publish_out}")"
1051
+ write_issue_comment_artifact "${blocker_body}" || true
1052
+ post_issue_comment_if_present
1053
+ if [[ -n "$publish_blocker_reason" ]]; then
1054
+ require_transition "issue_before_blocked" issue_before_blocked
1055
+ normalize_issue_runner_state "succeeded" "0" ""
1056
+ require_transition "issue_schedule_retry" issue_schedule_retry "$publish_blocker_reason"
1057
+ require_transition "issue_mark_blocked" issue_mark_blocked
1058
+ result_outcome="blocked"
1059
+ result_action="host-comment-blocker"
1060
+ failure_reason="$publish_blocker_reason"
1061
+ issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
1062
+ cleanup_issue_session
1063
+ notify_issue_reconciled
1064
+ mark_reconciled
1065
+ printf 'STATUS=%s\n' "$status"
1066
+ printf 'ISSUE_ID=%s\n' "$issue_id"
1067
+ printf 'PR_NUMBER=%s\n' "$pr_number"
1068
+ printf 'OUTCOME=%s\n' "$result_outcome"
1069
+ printf 'ACTION=%s\n' "$result_action"
1070
+ printf 'FAILURE_REASON=%s\n' "$failure_reason"
1071
+ printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1072
+ exit 0
1073
+ fi
1074
+ require_transition "issue_schedule_retry" issue_schedule_retry "host-publish-failed"
1075
+ require_transition "issue_mark_ready" issue_mark_ready
1076
+ issue_set_reconcile_summary "$status" "" "" "host-publish-failed"
1077
+ # Push branch to remote before cleanup to preserve commits for next retry
1078
+ if [[ -n "${BRANCH:-}" && -d "${WORKTREE:-}" && ( -f "${WORKTREE:-}/.git" || -d "${WORKTREE:-}/.git" ) ]]; then
1079
+ if ! git -C "$WORKTREE" push -u origin "$BRANCH" 2>/dev/null; then
1080
+ printf 'WORKTREE_PUSH_BEFORE_CLEANUP=failed\n' >&2
1081
+ else
1082
+ printf 'WORKTREE_PUSH_BEFORE_CLEANUP=ok\n' >&2
1083
+ fi
1084
+ fi
1085
+ cleanup_issue_session
1086
+ notify_issue_reconciled
1087
+ mark_reconciled
1088
+ printf 'STATUS=%s\n' "$status"
1089
+ printf 'ISSUE_ID=%s\n' "$issue_id"
1090
+ printf 'PR_NUMBER=%s\n' "$pr_number"
1091
+ printf 'PUBLISH_ERROR=%s\n' "$(printf '%s' "$publish_out" | tr '\n' ' ' | sed 's/ */ /g')"
1092
+ exit 0
1093
+ fi
1094
+
1095
+ pr_number="$(awk -F= '/^PR_NUMBER=/{print $2}' <<<"$publish_out")"
1096
+ normalize_issue_runner_state "succeeded" "0" ""
1097
+ require_transition "issue_clear_retry" issue_clear_retry
1098
+ require_transition "issue_remove_running" issue_remove_running
1099
+ if [[ -n "$pr_number" ]]; then
1100
+ require_transition "issue_after_pr_created" issue_after_pr_created "$pr_number"
1101
+ fi
1102
+ cleanup_issue_session
1103
+ notify_issue_reconciled
1104
+ ;;
1105
+ FAILED)
1106
+ failure_reason="${failure_reason:-worker-exit-failed}"
1107
+ schedule_provider_quota_cooldown "${failure_reason}"
1108
+ normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
1109
+ require_transition "issue_schedule_retry" issue_schedule_retry "${failure_reason}"
1110
+ require_transition "issue_mark_ready" issue_mark_ready
1111
+ issue_set_reconcile_summary "$status" "" "" "$failure_reason"
1112
+ cleanup_issue_session
1113
+ notify_issue_reconciled
1114
+ ;;
1115
+ *)
1116
+ ;;
1117
+ esac
1118
+
1119
+ mark_reconciled
1120
+ printf 'STATUS=%s\n' "$status"
1121
+ printf 'ISSUE_ID=%s\n' "$issue_id"
1122
+ printf 'PR_NUMBER=%s\n' "$pr_number"
1123
+ if [[ -n "$failure_reason" ]]; then
1124
+ printf 'FAILURE_REASON=%s\n' "$failure_reason"
1125
+ fi
1126
+ if [[ -n "$issue_result_contract_note" ]]; then
1127
+ printf 'RESULT_CONTRACT_NOTE=%s\n' "$issue_result_contract_note"
1128
+ fi