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,984 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage:
7
+ agent-project-run-openclaw-session --mode safe|bypass --session <id> --worktree <path> --prompt-file <path> --runs-root <path> --adapter-id <id> --task-kind <kind> --task-id <id> [options]
8
+
9
+ Launch an OpenClaw local agent worker inside tmux for a project adapter and
10
+ persist the standard run artifacts.
11
+
12
+ Options:
13
+ --env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
14
+ --context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
15
+ --collect-file <name> Copy worker artifact file into the host run dir after execution
16
+ --reconcile-command <cmd> Host-side command queued after the worker exits
17
+ --sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .openclaw-artifacts)
18
+ --openclaw-model <id> Model id for the isolated OpenClaw agent
19
+ --openclaw-thinking <level> OpenClaw thinking level
20
+ --openclaw-timeout-seconds <secs> OpenClaw local-agent timeout
21
+ --help Show this help
22
+ EOF
23
+ }
24
+
25
+ mode=""
26
+ session=""
27
+ worktree=""
28
+ prompt_file=""
29
+ runs_root=""
30
+ adapter_id=""
31
+ task_kind=""
32
+ task_id=""
33
+ env_prefix=""
34
+ sandbox_subdir=".openclaw-artifacts"
35
+ reconcile_command=""
36
+ keep_agent="false"
37
+ openclaw_model="${ACP_OPENCLAW_MODEL:-${F_LOSNING_OPENCLAW_MODEL:-openrouter/stepfun/step-3.5-flash:free}}"
38
+ openclaw_thinking="${ACP_OPENCLAW_THINKING:-${F_LOSNING_OPENCLAW_THINKING:-adaptive}}"
39
+ openclaw_timeout_seconds="${ACP_OPENCLAW_TIMEOUT_SECONDS:-${F_LOSNING_OPENCLAW_TIMEOUT_SECONDS:-900}}"
40
+ provided_openclaw_agent_id=""
41
+ provided_openclaw_session_id=""
42
+ provided_openclaw_agent_dir=""
43
+ provided_openclaw_state_dir=""
44
+ provided_openclaw_config_path=""
45
+ declare -a context_items=()
46
+ declare -a collect_files=()
47
+
48
+ while [[ $# -gt 0 ]]; do
49
+ case "$1" in
50
+ --mode) mode="${2:-}"; shift 2 ;;
51
+ --session) session="${2:-}"; shift 2 ;;
52
+ --worktree) worktree="${2:-}"; shift 2 ;;
53
+ --prompt-file) prompt_file="${2:-}"; shift 2 ;;
54
+ --runs-root) runs_root="${2:-}"; shift 2 ;;
55
+ --adapter-id) adapter_id="${2:-}"; shift 2 ;;
56
+ --task-kind) task_kind="${2:-}"; shift 2 ;;
57
+ --task-id) task_id="${2:-}"; shift 2 ;;
58
+ --env-prefix) env_prefix="${2:-}"; shift 2 ;;
59
+ --context) context_items+=("${2:-}"); shift 2 ;;
60
+ --collect-file) collect_files+=("${2:-}"); shift 2 ;;
61
+ --reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
62
+ --sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
63
+ --keep-agent) keep_agent="true"; shift ;;
64
+ --openclaw-model) openclaw_model="${2:-}"; shift 2 ;;
65
+ --openclaw-thinking) openclaw_thinking="${2:-}"; shift 2 ;;
66
+ --openclaw-timeout-seconds) openclaw_timeout_seconds="${2:-}"; shift 2 ;;
67
+ --openclaw-agent-id) provided_openclaw_agent_id="${2:-}"; shift 2 ;;
68
+ --openclaw-session-id) provided_openclaw_session_id="${2:-}"; shift 2 ;;
69
+ --openclaw-agent-dir) provided_openclaw_agent_dir="${2:-}"; shift 2 ;;
70
+ --openclaw-state-dir) provided_openclaw_state_dir="${2:-}"; shift 2 ;;
71
+ --openclaw-config-path) provided_openclaw_config_path="${2:-}"; shift 2 ;;
72
+ --help|-h) usage; exit 0 ;;
73
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
74
+ esac
75
+ done
76
+
77
+ if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
78
+ usage >&2
79
+ exit 1
80
+ fi
81
+
82
+ case "$mode" in
83
+ safe|bypass) ;;
84
+ *)
85
+ echo "--mode must be safe or bypass" >&2
86
+ exit 1
87
+ ;;
88
+ esac
89
+
90
+ case "$openclaw_timeout_seconds" in
91
+ ''|*[!0-9]*) echo "--openclaw-timeout-seconds must be numeric" >&2; exit 1 ;;
92
+ esac
93
+
94
+ if ! command -v openclaw >/dev/null 2>&1; then
95
+ echo "unable to resolve a runnable openclaw binary" >&2
96
+ exit 1
97
+ fi
98
+
99
+ artifact_dir="${runs_root}/${session}"
100
+ output_file="${artifact_dir}/${session}.log"
101
+ inner_script="${artifact_dir}/${session}.sh"
102
+ meta_file="${artifact_dir}/run.env"
103
+ result_file="${artifact_dir}/result.env"
104
+ runner_state_file="${artifact_dir}/runner.env"
105
+ sandbox_artifact_dir="${worktree%/}/${sandbox_subdir}"
106
+ sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
107
+ started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
108
+ openclaw_bin="$(command -v openclaw)"
109
+ default_openclaw_agent_id="$(
110
+ printf '%s-%s' "$adapter_id" "$session" \
111
+ | tr '[:upper:]' '[:lower:]' \
112
+ | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' \
113
+ | cut -c1-63
114
+ )"
115
+ openclaw_agent_id="${provided_openclaw_agent_id:-${default_openclaw_agent_id}}"
116
+ openclaw_session_id="${provided_openclaw_session_id:-${openclaw_agent_id}}"
117
+ openclaw_state_dir="${provided_openclaw_state_dir:-${artifact_dir}/openclaw-state}"
118
+ openclaw_config_path="${provided_openclaw_config_path:-${artifact_dir}/openclaw-config/openclaw.json}"
119
+ openclaw_agent_dir="${provided_openclaw_agent_dir:-${artifact_dir}/openclaw-agent}"
120
+
121
+ mkdir -p "$artifact_dir"
122
+ mkdir -p "$sandbox_run_dir"
123
+
124
+ if tmux has-session -t "$session" 2>/dev/null; then
125
+ echo "tmux session already exists: $session" >&2
126
+ exit 1
127
+ fi
128
+
129
+ branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
130
+
131
+ printf -v session_q '%q' "$session"
132
+ printf -v task_kind_q '%q' "$task_kind"
133
+ printf -v task_id_q '%q' "$task_id"
134
+ printf -v mode_q '%q' "$mode"
135
+ printf -v worktree_q '%q' "$worktree"
136
+ printf -v prompt_q '%q' "$prompt_file"
137
+ printf -v output_q '%q' "$output_file"
138
+ printf -v artifact_dir_q '%q' "$artifact_dir"
139
+ printf -v script_q '%q' "$inner_script"
140
+ printf -v result_q '%q' "$result_file"
141
+ printf -v meta_file_q '%q' "$meta_file"
142
+ printf -v runner_state_q '%q' "$runner_state_file"
143
+ printf -v branch_q '%q' "$branch_name"
144
+ printf -v sandbox_artifact_dir_q '%q' "$sandbox_artifact_dir"
145
+ printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
146
+ printf -v adapter_id_q '%q' "$adapter_id"
147
+ printf -v started_at_q '%q' "$started_at"
148
+ printf -v openclaw_bin_q '%q' "$openclaw_bin"
149
+ printf -v openclaw_state_dir_q '%q' "$openclaw_state_dir"
150
+ printf -v openclaw_config_path_q '%q' "$openclaw_config_path"
151
+ printf -v openclaw_agent_dir_q '%q' "$openclaw_agent_dir"
152
+ printf -v openclaw_agent_id_q '%q' "$openclaw_agent_id"
153
+ printf -v openclaw_session_id_q '%q' "$openclaw_session_id"
154
+ printf -v openclaw_model_q '%q' "$openclaw_model"
155
+ printf -v openclaw_thinking_q '%q' "$openclaw_thinking"
156
+ printf -v openclaw_timeout_q '%q' "$openclaw_timeout_seconds"
157
+ printf -v keep_agent_q '%q' "$keep_agent"
158
+
159
+ {
160
+ printf 'TASK_KIND=%s\n' "$task_kind_q"
161
+ printf 'TASK_ID=%s\n' "$task_id_q"
162
+ printf 'SESSION=%s\n' "$session_q"
163
+ printf 'MODE=%s\n' "$mode_q"
164
+ printf 'WORKTREE=%s\n' "$worktree_q"
165
+ printf 'PROMPT_FILE=%s\n' "$prompt_q"
166
+ printf 'OUTPUT_FILE=%s\n' "$output_q"
167
+ printf 'SCRIPT=%s\n' "$script_q"
168
+ printf 'BRANCH=%s\n' "$branch_q"
169
+ printf 'RESULT_FILE=%s\n' "$result_q"
170
+ printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
171
+ printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
172
+ printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
173
+ printf 'STARTED_AT=%s\n' "$started_at_q"
174
+ printf 'OPENCLAW_BIN=%s\n' "$openclaw_bin_q"
175
+ printf 'OPENCLAW_STATE_DIR=%s\n' "$openclaw_state_dir_q"
176
+ printf 'OPENCLAW_CONFIG_PATH=%s\n' "$openclaw_config_path_q"
177
+ printf 'OPENCLAW_AGENT_DIR=%s\n' "$openclaw_agent_dir_q"
178
+ printf 'OPENCLAW_AGENT_ID=%s\n' "$openclaw_agent_id_q"
179
+ printf 'OPENCLAW_SESSION_ID=%s\n' "$openclaw_session_id_q"
180
+ printf 'OPENCLAW_MODEL=%s\n' "$openclaw_model_q"
181
+ printf 'OPENCLAW_THINKING=%s\n' "$openclaw_thinking_q"
182
+ printf 'OPENCLAW_TIMEOUT_SECONDS=%s\n' "$openclaw_timeout_q"
183
+ printf 'OPENCLAW_KEEP_AGENT=%s\n' "$keep_agent_q"
184
+ } >"$meta_file"
185
+
186
+ context_exports=""
187
+ if ((${#context_items[@]} > 0)); then
188
+ for item in "${context_items[@]}"; do
189
+ if [[ "$item" != *=* ]]; then
190
+ echo "--context must use KEY=VALUE syntax: $item" >&2
191
+ exit 1
192
+ fi
193
+ key="${item%%=*}"
194
+ value="${item#*=}"
195
+ if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
196
+ echo "Invalid context key: $key" >&2
197
+ exit 1
198
+ fi
199
+ printf -v value_q '%q' "$value"
200
+ printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
201
+ if [[ -n "$env_prefix" ]]; then
202
+ context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
203
+ fi
204
+ context_exports+="export ACP_${key}=${value_q}"$'\n'
205
+ if [[ "$env_prefix" != "F_LOSNING_" ]]; then
206
+ context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
207
+ fi
208
+ done
209
+ fi
210
+
211
+ runtime_exports=$(
212
+ cat <<EOF
213
+ export AGENT_PROJECT_SESSION=${session_q}
214
+ export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
215
+ export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
216
+ export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
217
+ export AGENT_PROJECT_OPENCLAW_BIN=${openclaw_bin_q}
218
+ export ACP_SESSION=${session_q}
219
+ export ACP_RUN_DIR=${sandbox_run_dir_q}
220
+ export ACP_HOST_RUN_DIR=${artifact_dir_q}
221
+ export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
222
+ export ACP_OPENCLAW_BIN=${openclaw_bin_q}
223
+ export ACP_OPENCLAW_SESSION_ID=${openclaw_session_id_q}
224
+ export F_LOSNING_SESSION=${session_q}
225
+ export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
226
+ export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
227
+ export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
228
+ export F_LOSNING_OPENCLAW_BIN=${openclaw_bin_q}
229
+ export F_LOSNING_OPENCLAW_SESSION_ID=${openclaw_session_id_q}
230
+ export OPENCLAW_STATE_DIR=${openclaw_state_dir_q}
231
+ export OPENCLAW_CONFIG_PATH=${openclaw_config_path_q}
232
+ EOF
233
+ )
234
+
235
+ if [[ -n "$env_prefix" ]]; then
236
+ runtime_exports+=$'\n'
237
+ runtime_exports+=$(cat <<EOF
238
+ export ${env_prefix}SESSION=${session_q}
239
+ export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
240
+ export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
241
+ export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
242
+ export ${env_prefix}OPENCLAW_BIN=${openclaw_bin_q}
243
+ export ${env_prefix}OPENCLAW_SESSION_ID=${openclaw_session_id_q}
244
+ EOF
245
+ )
246
+ fi
247
+
248
+ collect_copy_snippet=""
249
+ if ((${#collect_files[@]} > 0)); then
250
+ for artifact_name in "${collect_files[@]}"; do
251
+ if [[ -z "$artifact_name" ]]; then
252
+ continue
253
+ fi
254
+ printf -v artifact_q '%q' "$artifact_name"
255
+ collect_copy_snippet+=$(
256
+ cat <<EOF
257
+ if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
258
+ cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
259
+ fi
260
+ EOF
261
+ )
262
+ collect_copy_snippet+=$'\n'
263
+ done
264
+ fi
265
+
266
+ reconcile_snippet=""
267
+ if [[ -n "$reconcile_command" ]]; then
268
+ printf -v delayed_reconcile_q '%q' "sleep 2; $reconcile_command"
269
+ reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
270
+ fi
271
+
272
+ cat >"$inner_script" <<EOF
273
+ #!/usr/bin/env bash
274
+ set -euo pipefail
275
+ ${runtime_exports}
276
+ ${context_exports}cd ${worktree_q}
277
+
278
+ runner_state_file=${runner_state_q}
279
+ output_file=${output_q}
280
+ sandbox_artifact_dir=${sandbox_artifact_dir_q}
281
+ sandbox_run_dir=${sandbox_run_dir_q}
282
+ artifact_dir=${artifact_dir_q}
283
+ run_dir=${artifact_dir_q}
284
+ worktree=${worktree_q}
285
+ prompt_file_path=${prompt_q}
286
+ openclaw_state_dir=${openclaw_state_dir_q}
287
+ openclaw_config_path=${openclaw_config_path_q}
288
+ openclaw_agent_dir=${openclaw_agent_dir_q}
289
+ openclaw_agent_id=${openclaw_agent_id_q}
290
+ openclaw_session_id=${openclaw_session_id_q}
291
+ openclaw_model=${openclaw_model_q}
292
+ openclaw_bin=${openclaw_bin_q}
293
+ openclaw_timeout=${openclaw_timeout_q}
294
+ openclaw_thinking=${openclaw_thinking_q}
295
+ keep_agent=${keep_agent_q}
296
+ openclaw_add_log="\${sandbox_run_dir}/openclaw-agents-add.log"
297
+
298
+ write_state() {
299
+ local runner_state="\${1:?runner state required}"
300
+ local last_exit_code="\${2:-}"
301
+ local failure_reason="\${3:-}"
302
+ local updated_at tmp_file
303
+
304
+ updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
305
+ tmp_file="\${runner_state_file}.tmp.\$\$"
306
+ {
307
+ printf 'RUNNER_STATE=%q\n' "\${runner_state}"
308
+ printf 'THREAD_ID=%q\n' "\${openclaw_session_id}"
309
+ printf 'ATTEMPT=1\n'
310
+ printf 'RESUME_COUNT=0\n'
311
+ printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
312
+ printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
313
+ printf 'LAST_TRIGGER_REASON=%q\n' ''
314
+ printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
315
+ printf 'LAST_AUTH_FINGERPRINT=%q\n' ''
316
+ printf 'UPDATED_AT=%q\n' "\${updated_at}"
317
+ } >"\${tmp_file}"
318
+ mv "\${tmp_file}" "\${runner_state_file}"
319
+ }
320
+
321
+ record_final_git_state() {
322
+ local final_head final_branch tmp_file
323
+
324
+ final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
325
+ final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
326
+ tmp_file=${meta_file_q}.tmp.final.$$
327
+ grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
328
+ {
329
+ printf 'FINAL_HEAD=%q\n' "\${final_head}"
330
+ printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
331
+ } >>"\${tmp_file}"
332
+ mv "\${tmp_file}" ${meta_file_q}
333
+ }
334
+
335
+ ensure_openclaw_workspace_excludes() {
336
+ local exclude_file line
337
+ exclude_file="\$(git -C ${worktree_q} config --worktree --get core.excludesFile 2>/dev/null || true)"
338
+ if [[ -z "\${exclude_file}" ]]; then
339
+ exclude_file="\${sandbox_artifact_dir}/git-exclude"
340
+ git -C ${worktree_q} config extensions.worktreeConfig true >/dev/null 2>&1 || true
341
+ git -C ${worktree_q} config --worktree core.excludesFile "\${exclude_file}"
342
+ fi
343
+
344
+ mkdir -p "\$(dirname "\${exclude_file}")"
345
+ touch "\${exclude_file}"
346
+ while IFS= read -r line; do
347
+ [[ -n "\${line}" ]] || continue
348
+ if ! grep -Fqx "\${line}" "\${exclude_file}" 2>/dev/null; then
349
+ printf '%s\n' "\${line}" >>"\${exclude_file}"
350
+ fi
351
+ done <<'PATTERNS'
352
+ .openclaw-artifacts
353
+ .openclaw
354
+ SOUL.md
355
+ TOOLS.md
356
+ IDENTITY.md
357
+ USER.md
358
+ HEARTBEAT.md
359
+ BOOTSTRAP.md
360
+ .agent-session.env
361
+ \$ACP_RUN_DIR
362
+ \$AGENT_PROJECT_RUN_DIR
363
+ \$F_LOSNING_RUN_DIR
364
+ \$ACP_HOST_RUN_DIR
365
+ \$AGENT_PROJECT_HOST_RUN_DIR
366
+ \$F_LOSNING_HOST_RUN_DIR
367
+ \$ACP_RESULT_FILE
368
+ \$AGENT_PROJECT_RESULT_FILE
369
+ \$F_LOSNING_RESULT_FILE
370
+ PATTERNS
371
+ }
372
+
373
+ install_pre_commit_scope_hook() {
374
+ local hooks_dir="\$(git -C ${worktree_q} rev-parse --git-path hooks 2>/dev/null || true)"
375
+ if [[ -z "\${hooks_dir}" ]]; then
376
+ hooks_dir="\$(git -C ${worktree_q} config --get core.hooksPath 2>/dev/null || true)"
377
+ fi
378
+ if [[ -z "\${hooks_dir}" ]]; then
379
+ hooks_dir="${worktree_q}/.git-hooks"
380
+ fi
381
+ # Resolve relative path against worktree root
382
+ if [[ "\${hooks_dir}" != /* ]]; then
383
+ hooks_dir="${worktree_q}/\${hooks_dir}"
384
+ fi
385
+ mkdir -p "\${hooks_dir}"
386
+ cat > "\${hooks_dir}/pre-commit" <<'HOOK_EOF'
387
+ #!/usr/bin/env bash
388
+ # Pre-commit scope guard: reject commits that touch too many product surfaces
389
+ set -euo pipefail
390
+
391
+ changed_files="\$(
392
+ git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true
393
+ )"
394
+
395
+ if [[ -z "\${changed_files}" ]]; then
396
+ exit 0
397
+ fi
398
+
399
+ # Count non-test product files by surface
400
+ api_count=0
401
+ web_count=0
402
+ mobile_count=0
403
+ package_count=0
404
+ doc_count=0
405
+ other_count=0
406
+
407
+ while IFS= read -r file; do
408
+ [[ -n "\${file}" ]] || continue
409
+ case "\${file}" in
410
+ apps/api/*) api_count=\$((api_count + 1)) ;;
411
+ apps/web/*) web_count=\$((web_count + 1)) ;;
412
+ apps/mobile/*) mobile_count=\$((mobile_count + 1)) ;;
413
+ packages/*) package_count=\$((package_count + 1)) ;;
414
+ openspec/*|docs/*|*.md) doc_count=\$((doc_count + 1)) ;;
415
+ *) other_count=\$((other_count + 1)) ;;
416
+ esac
417
+ done <<< "\${changed_files}"
418
+
419
+ # Count how many product surfaces are touched (excluding docs-only)
420
+ surfaces=0
421
+ [[ \$api_count -gt 0 ]] && surfaces=\$((surfaces + 1))
422
+ [[ \$web_count -gt 0 ]] && surfaces=\$((surfaces + 1))
423
+ [[ \$mobile_count -gt 0 ]] && surfaces=\$((surfaces + 1))
424
+
425
+ # If touching 3+ product surfaces, warn but allow (scope guard at publish is stricter)
426
+ if [[ \$surfaces -ge 3 ]]; then
427
+ echo "[pre-commit scope warning] This commit touches \$surfaces product surfaces (api=\$api_count web=\$web_count mobile=\$mobile_count). Consider splitting into focused commits." >&2
428
+ fi
429
+
430
+ # Hard block: more than 20 non-doc product files in one commit
431
+ total_product=\$((api_count + web_count + mobile_count + package_count + other_count))
432
+ if [[ \$total_product -gt 20 ]]; then
433
+ echo "[pre-commit scope BLOCK] This commit touches \$total_product product files across \$surfaces surfaces. Split into smaller focused commits." >&2
434
+ exit 1
435
+ fi
436
+
437
+ # Hard block: more than 8 mobile product files in one commit
438
+ if [[ \$mobile_count -gt 8 ]]; then
439
+ echo "[pre-commit scope BLOCK] This commit touches \$mobile_product mobile product files. Keep mobile changes to one focused route family (max 8 files)." >&2
440
+ exit 1
441
+ fi
442
+
443
+ exit 0
444
+ HOOK_EOF
445
+ chmod +x "\${hooks_dir}/pre-commit"
446
+ }
447
+
448
+ ensure_runtime_artifact_alias() {
449
+ local literal_name="\${1:?literal name required}"
450
+ local target_path="\${2:?target path required}"
451
+ local alias_path="\${worktree}/\${literal_name}"
452
+ local current_target=""
453
+
454
+ if [[ -L "\${alias_path}" ]]; then
455
+ current_target="\$(readlink "\${alias_path}" 2>/dev/null || true)"
456
+ if [[ "\${current_target}" == "\${target_path}" ]]; then
457
+ return 0
458
+ fi
459
+ rm -f "\${alias_path}"
460
+ elif [[ -e "\${alias_path}" ]]; then
461
+ printf '[openclaw] runtime alias path already exists, leaving untouched: %s\n' "\${alias_path}" >>"\${output_file}" 2>/dev/null || true
462
+ return 0
463
+ fi
464
+
465
+ ln -s "\${target_path}" "\${alias_path}"
466
+ }
467
+
468
+ ensure_runtime_artifact_aliases() {
469
+ ensure_runtime_artifact_alias '\$ACP_RUN_DIR' "\${sandbox_run_dir}"
470
+ ensure_runtime_artifact_alias '\$AGENT_PROJECT_RUN_DIR' "\${sandbox_run_dir}"
471
+ ensure_runtime_artifact_alias '\$F_LOSNING_RUN_DIR' "\${sandbox_run_dir}"
472
+ ensure_runtime_artifact_alias '\$ACP_HOST_RUN_DIR' "\${artifact_dir}"
473
+ ensure_runtime_artifact_alias '\$AGENT_PROJECT_HOST_RUN_DIR' "\${artifact_dir}"
474
+ ensure_runtime_artifact_alias '\$F_LOSNING_HOST_RUN_DIR' "\${artifact_dir}"
475
+ ensure_runtime_artifact_alias '\$ACP_RESULT_FILE' "\${sandbox_run_dir}/result.env"
476
+ ensure_runtime_artifact_alias '\$AGENT_PROJECT_RESULT_FILE' "\${sandbox_run_dir}/result.env"
477
+ ensure_runtime_artifact_alias '\$F_LOSNING_RESULT_FILE' "\${sandbox_run_dir}/result.env"
478
+ }
479
+
480
+ cleanup_runtime_artifact_aliases() {
481
+ local literal_name alias_path
482
+ for literal_name in \
483
+ '\$ACP_RUN_DIR' \
484
+ '\$AGENT_PROJECT_RUN_DIR' \
485
+ '\$F_LOSNING_RUN_DIR' \
486
+ '\$ACP_HOST_RUN_DIR' \
487
+ '\$AGENT_PROJECT_HOST_RUN_DIR' \
488
+ '\$F_LOSNING_HOST_RUN_DIR' \
489
+ '\$ACP_RESULT_FILE' \
490
+ '\$AGENT_PROJECT_RESULT_FILE' \
491
+ '\$F_LOSNING_RESULT_FILE'
492
+ do
493
+ alias_path="\${worktree}/\${literal_name}"
494
+ if [[ -L "\${alias_path}" ]]; then
495
+ rm -f "\${alias_path}"
496
+ fi
497
+ done
498
+ }
499
+
500
+ recover_literal_runtime_artifacts() {
501
+ local literal_name literal_path actual_result_file recovered="no"
502
+
503
+ actual_result_file="\${sandbox_run_dir}/result.env"
504
+ for literal_name in '\$ACP_RESULT_FILE' '\$AGENT_PROJECT_RESULT_FILE' '\$F_LOSNING_RESULT_FILE'; do
505
+ literal_path="\${worktree}/\${literal_name}"
506
+ if [[ -f "\${literal_path}" && ! -L "\${literal_path}" ]]; then
507
+ cp "\${literal_path}" "\${actual_result_file}" 2>/dev/null || true
508
+ cp "\${literal_path}" "\${run_dir}/result.env" 2>/dev/null || true
509
+ rm -f "\${literal_path}" 2>/dev/null || true
510
+ printf '[openclaw] recovered literal result artifact: %s\n' "\${literal_path}" >>"\${output_file}" 2>/dev/null || true
511
+ recovered="yes"
512
+ break
513
+ fi
514
+ done
515
+
516
+ for literal_name in '\$ACP_RUN_DIR' '\$AGENT_PROJECT_RUN_DIR' '\$F_LOSNING_RUN_DIR'; do
517
+ literal_path="\${worktree}/\${literal_name}"
518
+ if [[ -d "\${literal_path}" && ! -L "\${literal_path}" ]]; then
519
+ for artifact_name in result.env verification.jsonl issue-comment.md pr-comment.md; do
520
+ if [[ -f "\${literal_path}/\${artifact_name}" ]]; then
521
+ cp "\${literal_path}/\${artifact_name}" "\${sandbox_run_dir}/\${artifact_name}" 2>/dev/null || true
522
+ cp "\${literal_path}/\${artifact_name}" "\${artifact_dir}/\${artifact_name}" 2>/dev/null || true
523
+ recovered="yes"
524
+ fi
525
+ done
526
+ rm -rf "\${literal_path}" 2>/dev/null || true
527
+ printf '[openclaw] recovered literal run-dir artifact tree: %s\n' "\${literal_path}" >>"\${output_file}" 2>/dev/null || true
528
+ fi
529
+ done
530
+
531
+ [[ "\${recovered}" == "yes" ]] && return 0
532
+ return 0
533
+ }
534
+
535
+ reset_sandbox_run_dir() {
536
+ mkdir -p "\${sandbox_run_dir}"
537
+ find "\${sandbox_run_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
538
+ }
539
+
540
+ classify_failure_reason() {
541
+ if grep -Eiq 'Config was last written by a newer OpenClaw' "\${output_file}" 2>/dev/null; then
542
+ printf 'openclaw-version-mismatch\n'
543
+ return 0
544
+ fi
545
+ if grep -Eiq 'invalid api key|authentication failed|unauthorized|provider api key|login required|please authenticate|api_key_invalid' "\${output_file}" 2>/dev/null; then
546
+ printf 'auth-failure\n'
547
+ return 0
548
+ fi
549
+ if grep -Eiq 'rate limit exceeded|quota exceeded|usage limit|insufficient credits|payment required|too many requests|429' "\${output_file}" 2>/dev/null; then
550
+ printf 'provider-quota-limit\n'
551
+ return 0
552
+ fi
553
+ if grep -Eiq 'model not found|model .* not available|unsupported model|invalid model' "\${output_file}" 2>/dev/null; then
554
+ printf 'model-unavailable\n'
555
+ return 0
556
+ fi
557
+ if grep -Eiq 'context length exceeded|token limit|maximum context|too many tokens' "\${output_file}" 2>/dev/null; then
558
+ printf 'context-length-exceeded\n'
559
+ return 0
560
+ fi
561
+ if grep -Eiq 'timeout|timed out|ETIMEDOUT|ECONNREFUSED' "\${output_file}" 2>/dev/null; then
562
+ printf 'timeout\n'
563
+ return 0
564
+ fi
565
+ printf 'openclaw-exit-failed\n'
566
+ }
567
+
568
+ infer_result_from_output() {
569
+ local result_file_path="\${sandbox_run_dir}/result.env"
570
+ local verification_file="\${sandbox_run_dir}/verification.jsonl"
571
+ # Host-side result file (always writable, never inside worktree)
572
+ local host_result_file="\${run_dir}/result.env"
573
+ local write_result=''
574
+
575
+ write_result() {
576
+ printf '%b' "\$1" > "\${result_file_path}" 2>/dev/null || true
577
+ printf '%b' "\$1" > "\${host_result_file}" 2>/dev/null || true
578
+ }
579
+
580
+ # If sandbox result.env exists with implemented, validate it has verification
581
+ if [[ -f "\${result_file_path}" ]]; then
582
+ if grep -q 'OUTCOME=implemented' "\${result_file_path}" 2>/dev/null; then
583
+ if [[ ! -f "\${verification_file}" ]]; then
584
+ write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
585
+ printf '[infer] BLOCKED: agent wrote OUTCOME=implemented but verification.jsonl is missing\n' >> "\${output_file}" 2>/dev/null || true
586
+ return 0
587
+ fi
588
+ fi
589
+ return 0
590
+ fi
591
+
592
+ # Check if there are actual code changes (not just artifact files)
593
+ local has_product_changes="no"
594
+ if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.openclaw-artifacts/|\.md$' 2>/dev/null; then
595
+ has_product_changes="yes"
596
+ fi
597
+ if git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.openclaw-artifacts/|\.md$' 2>/dev/null; then
598
+ has_product_changes="yes"
599
+ fi
600
+ if git -C ${worktree_q} log --oneline origin/main..HEAD 2>/dev/null | grep -q . 2>/dev/null; then
601
+ has_product_changes="yes"
602
+ fi
603
+
604
+ # If no product changes and output suggests nothing to do, report no-changes
605
+ if [[ "\${has_product_changes}" == "no" ]] && grep -Eiq 'already done|no changes needed|up to date|nothing to do|no changes' "\${output_file}" 2>/dev/null; then
606
+ write_result 'OUTCOME=reported\nACTION=host-comment-scheduled-report\nDETAIL=no-changes-needed\n'
607
+ return 0
608
+ fi
609
+
610
+ # If there are product changes but NO verification.jsonl, this is BLOCKED
611
+ # The worker skipped verification — do not allow publish
612
+ if [[ "\${has_product_changes}" == "yes" && ! -f "\${verification_file}" ]]; then
613
+ write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
614
+ return 0
615
+ fi
616
+
617
+ # If there are product changes AND verification.jsonl exists with entries, allow implemented
618
+ if [[ "\${has_product_changes}" == "yes" && -f "\${verification_file}" ]] && grep -q '"status":"pass"' "\${verification_file}" 2>/dev/null; then
619
+ write_result 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n'
620
+ return 0
621
+ fi
622
+
623
+ # If output explicitly says blocked, trust that
624
+ if grep -Eiq 'blocked|cannot proceed|unable to complete|not possible|missing spec' "\${output_file}" 2>/dev/null; then
625
+ write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
626
+ return 0
627
+ fi
628
+
629
+ # If output suggests implemented but we couldn't verify, still block
630
+ if grep -Eiq 'created PR|opened PR|PR #|pull request|implemented' "\${output_file}" 2>/dev/null; then
631
+ if [[ -f "\${verification_file}" ]]; then
632
+ write_result 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n'
633
+ else
634
+ write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
635
+ fi
636
+ return 0
637
+ fi
638
+
639
+ # Default fallback: block (safe default)
640
+ write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
641
+ }
642
+
643
+ cleanup_agent() {
644
+ # --force required for non-interactive (tmux) sessions, otherwise delete waits for confirmation
645
+ "\${openclaw_bin}" agents delete "\${openclaw_agent_id}" --json --force >/dev/null 2>&1 || true
646
+ }
647
+
648
+ openclaw_agent_exists() {
649
+ local agents_json=""
650
+
651
+ agents_json="\$("\${openclaw_bin}" agents list --json 2>/dev/null || true)"
652
+ [[ -n "\${agents_json}" ]] || return 1
653
+
654
+ OPENCLAW_AGENTS_JSON="\${agents_json}" python3 - "\${openclaw_agent_id}" <<'PY'
655
+ import json
656
+ import os
657
+ import sys
658
+
659
+ agent_id = sys.argv[1]
660
+
661
+ try:
662
+ payload = json.loads(os.environ.get("OPENCLAW_AGENTS_JSON", ""))
663
+ except Exception:
664
+ raise SystemExit(1)
665
+
666
+ agents = payload if isinstance(payload, list) else payload.get("agents", [])
667
+ for agent in agents:
668
+ if not isinstance(agent, dict):
669
+ continue
670
+ if str(agent.get("id", "")) == agent_id or str(agent.get("name", "")) == agent_id:
671
+ raise SystemExit(0)
672
+
673
+ raise SystemExit(1)
674
+ PY
675
+ }
676
+
677
+ sync_openclaw_agent_config() {
678
+ mkdir -p "\$(dirname "\${openclaw_config_path}")"
679
+
680
+ python3 - "\${openclaw_config_path}" "\${openclaw_agent_id}" "\${worktree}" "\${openclaw_agent_dir}" "\${openclaw_model}" <<'PY'
681
+ import json
682
+ import os
683
+ import sys
684
+
685
+ config_path, agent_id, workspace, agent_dir, model = sys.argv[1:6]
686
+
687
+ payload = {}
688
+ if os.path.exists(config_path):
689
+ try:
690
+ with open(config_path, "r", encoding="utf-8") as handle:
691
+ loaded = json.load(handle)
692
+ if isinstance(loaded, dict):
693
+ payload = loaded
694
+ except Exception:
695
+ payload = {}
696
+
697
+ agents = payload.get("agents")
698
+ if not isinstance(agents, dict):
699
+ agents = {}
700
+ payload["agents"] = agents
701
+
702
+ agent_list = agents.get("list")
703
+ if not isinstance(agent_list, list):
704
+ agent_list = []
705
+ agents["list"] = agent_list
706
+
707
+ entry = None
708
+ for candidate in agent_list:
709
+ if not isinstance(candidate, dict):
710
+ continue
711
+ candidate_id = str(candidate.get("id", ""))
712
+ candidate_name = str(candidate.get("name", ""))
713
+ if candidate_id == agent_id or candidate_name == agent_id:
714
+ entry = candidate
715
+ break
716
+
717
+ if entry is None:
718
+ entry = {"id": agent_id}
719
+ agent_list.append(entry)
720
+
721
+ entry["id"] = agent_id
722
+ entry["name"] = agent_id
723
+ entry["workspace"] = workspace
724
+ entry["agentDir"] = agent_dir
725
+ if model:
726
+ entry["model"] = model
727
+
728
+ tmp_path = f"{config_path}.tmp.{os.getpid()}"
729
+ with open(tmp_path, "w", encoding="utf-8") as handle:
730
+ json.dump(payload, handle, indent=2)
731
+ handle.write("\n")
732
+ os.replace(tmp_path, config_path)
733
+ PY
734
+ }
735
+
736
+ run_openclaw_agent_command() {
737
+ python3 - "\${output_file}" "\${openclaw_timeout}" "\${openclaw_bin}" "\${openclaw_agent_id}" "\${openclaw_session_id}" "\${openclaw_thinking}" "\${prompt_file_path}" <<'PY'
738
+ import os
739
+ import re
740
+ import selectors
741
+ import signal
742
+ import subprocess
743
+ import sys
744
+ import time
745
+
746
+ output_path, timeout_seconds_raw, openclaw_bin, agent_id, session_id, thinking, prompt_path = sys.argv[1:8]
747
+
748
+ with open(prompt_path, "r", encoding="utf-8") as handle:
749
+ prompt = handle.read()
750
+
751
+ cmd = [
752
+ openclaw_bin,
753
+ "agent",
754
+ "--agent",
755
+ agent_id,
756
+ "--session-id",
757
+ session_id,
758
+ "--local",
759
+ "--json",
760
+ "--timeout",
761
+ timeout_seconds_raw,
762
+ "--thinking",
763
+ thinking,
764
+ "--message",
765
+ prompt,
766
+ ]
767
+
768
+ timeout_seconds = float(timeout_seconds_raw)
769
+ hard_deadline = time.monotonic() + timeout_seconds + 15.0
770
+ terminal_patterns = [
771
+ re.compile(r"Config was last written by a newer OpenClaw", re.I),
772
+ re.compile(r"invalid api key|authentication failed|unauthorized|provider api key|login required|please authenticate|api_key_invalid", re.I),
773
+ re.compile(r"rate limit exceeded|quota exceeded|usage limit|insufficient credits|payment required|too many requests|429", re.I),
774
+ re.compile(r"model not found|model .* not available|unsupported model|invalid model", re.I),
775
+ re.compile(r"context length exceeded|token limit|maximum context|too many tokens", re.I),
776
+ ]
777
+
778
+ proc = None
779
+ sel = selectors.DefaultSelector()
780
+ matched_terminal_error = False
781
+ tail = ""
782
+
783
+ def terminate_process_group(process: subprocess.Popen) -> None:
784
+ try:
785
+ os.killpg(process.pid, signal.SIGTERM)
786
+ except ProcessLookupError:
787
+ return
788
+ deadline = time.monotonic() + 2.0
789
+ while time.monotonic() < deadline:
790
+ if process.poll() is not None:
791
+ return
792
+ time.sleep(0.1)
793
+ try:
794
+ os.killpg(process.pid, signal.SIGKILL)
795
+ except ProcessLookupError:
796
+ return
797
+
798
+ with open(output_path, "ab", buffering=0) as log_handle:
799
+ proc = subprocess.Popen(
800
+ cmd,
801
+ stdin=subprocess.PIPE,
802
+ stdout=subprocess.PIPE,
803
+ stderr=subprocess.STDOUT,
804
+ start_new_session=True,
805
+ )
806
+ if proc.stdin is not None:
807
+ proc.stdin.write(prompt.encode("utf-8"))
808
+ proc.stdin.close()
809
+
810
+ if proc.stdout is None:
811
+ raise SystemExit(1)
812
+
813
+ sel.register(proc.stdout, selectors.EVENT_READ)
814
+
815
+ while True:
816
+ if time.monotonic() >= hard_deadline:
817
+ terminate_process_group(proc)
818
+ break
819
+
820
+ events = sel.select(timeout=0.2)
821
+ if not events:
822
+ if proc.poll() is not None:
823
+ break
824
+ continue
825
+
826
+ for key, _ in events:
827
+ chunk = os.read(key.fd, 4096)
828
+ if not chunk:
829
+ continue
830
+
831
+ log_handle.write(chunk)
832
+ text = chunk.decode("utf-8", errors="replace")
833
+ tail = (tail + text)[-8192:]
834
+
835
+ if not matched_terminal_error and any(pattern.search(tail) for pattern in terminal_patterns):
836
+ matched_terminal_error = True
837
+ terminate_process_group(proc)
838
+
839
+ if proc.poll() is not None:
840
+ break
841
+
842
+ while True:
843
+ if proc.stdout is None:
844
+ break
845
+ chunk = os.read(proc.stdout.fileno(), 4096)
846
+ if not chunk:
847
+ break
848
+ log_handle.write(chunk)
849
+
850
+ return_code = proc.wait()
851
+ if matched_terminal_error and return_code == 0:
852
+ raise SystemExit(1)
853
+ raise SystemExit(return_code)
854
+ PY
855
+ }
856
+
857
+ ensure_openclaw_auth_profiles() {
858
+ if [[ ! -s "\${openclaw_agent_dir}/auth-profiles.json" && -n "\${OPENROUTER_API_KEY:-}" ]]; then
859
+ mkdir -p "\$(dirname "\${openclaw_agent_dir}/auth-profiles.json" 2>/dev/null || true)"
860
+ cat > "\${openclaw_agent_dir}/auth-profiles.json" <<AUTH_EOF
861
+ {
862
+ "version": 1,
863
+ "profiles": {
864
+ "openrouter:default": {
865
+ "type": "api_key",
866
+ "provider": "openrouter",
867
+ "keyRef": {
868
+ "source": "direct",
869
+ "provider": "default",
870
+ "id": "OPENROUTER_API_KEY",
871
+ "secret": "\${OPENROUTER_API_KEY}"
872
+ }
873
+ }
874
+ }
875
+ }
876
+ AUTH_EOF
877
+ fi
878
+
879
+ if [[ ! -s "\${openclaw_agent_dir}/auth-profiles.json" && -z "\${OPENROUTER_API_KEY:-}" && -f "\${openclaw_agent_dir}/../auth-profiles.json" ]]; then
880
+ cp "\${openclaw_agent_dir}/../auth-profiles.json" "\${openclaw_agent_dir}/auth-profiles.json" 2>/dev/null || true
881
+ fi
882
+ }
883
+
884
+ reset_openclaw_resident_state() {
885
+ rm -rf "\${openclaw_state_dir}" "\${openclaw_agent_dir}" "\$(dirname "\${openclaw_config_path}")"
886
+ mkdir -p "\${openclaw_state_dir}" "\$(dirname "\${openclaw_config_path}")" "\${openclaw_agent_dir}"
887
+ ensure_openclaw_auth_profiles
888
+ }
889
+
890
+ mkdir -p "\${sandbox_run_dir}" "\${artifact_dir}" "\${openclaw_state_dir}" "\$(dirname "\${openclaw_config_path}")" "\${openclaw_agent_dir}"
891
+ reset_sandbox_run_dir
892
+ ensure_openclaw_workspace_excludes
893
+ install_pre_commit_scope_hook
894
+ ensure_runtime_artifact_aliases
895
+ trap cleanup_runtime_artifact_aliases EXIT
896
+ write_state running "" ""
897
+
898
+ ensure_openclaw_auth_profiles
899
+
900
+ # Export API key as env var for --local mode (required alongside auth-profiles.json)
901
+ export OPENROUTER_API_KEY="\${OPENROUTER_API_KEY:-}"
902
+
903
+ prompt_content="\$(cat ${prompt_q})"
904
+ set +e
905
+ status=0
906
+ failure_reason=""
907
+ resident_reset_attempted="no"
908
+ while true; do
909
+ status=0
910
+ if [[ "\${keep_agent}" != "true" ]] || ! openclaw_agent_exists; then
911
+ : >"\${openclaw_add_log}"
912
+ if "\${openclaw_bin}" agents add "\${openclaw_agent_id}" \
913
+ --model "\${openclaw_model}" \\
914
+ --workspace ${worktree_q} \\
915
+ --agent-dir "\${openclaw_agent_dir}" \\
916
+ --non-interactive --json >"\${openclaw_add_log}" 2>&1; then
917
+ status=0
918
+ else
919
+ status=\$?
920
+ if grep -Eiq 'already exists' "\${openclaw_add_log}" 2>/dev/null; then
921
+ printf '[openclaw] reusing existing agent after add race: %s\n' "\${openclaw_agent_id}" >>"\${output_file}" 2>/dev/null || true
922
+ status=0
923
+ fi
924
+ fi
925
+ cat "\${openclaw_add_log}" >>"\${output_file}" 2>/dev/null || true
926
+ fi
927
+ if [[ "\${status}" -eq 0 ]]; then
928
+ if sync_openclaw_agent_config; then
929
+ :
930
+ else
931
+ status=1
932
+ failure_reason="openclaw-config-sync-failed"
933
+ printf '[openclaw] failed to sync agent config for %s\n' "\${openclaw_agent_id}" >>"\${output_file}" 2>/dev/null || true
934
+ fi
935
+ fi
936
+ if [[ "\${status}" -eq 0 ]]; then
937
+ run_openclaw_agent_command
938
+ status=\$?
939
+ fi
940
+ if [[ "\${status}" -eq 0 ]]; then
941
+ break
942
+ fi
943
+
944
+ failure_reason="\$(classify_failure_reason)"
945
+ if [[ "\${keep_agent}" == "true" && "\${resident_reset_attempted}" != "yes" && "\${failure_reason}" == "openclaw-version-mismatch" ]]; then
946
+ resident_reset_attempted="yes"
947
+ reset_openclaw_resident_state
948
+ continue
949
+ fi
950
+ break
951
+ done
952
+ recover_literal_runtime_artifacts
953
+ if [[ "\${status}" -eq 0 ]]; then
954
+ # Infer result.env from output if agent didn't create one
955
+ infer_result_from_output
956
+ write_state succeeded "0" ""
957
+ else
958
+ if [[ -z "\${failure_reason}" ]]; then
959
+ failure_reason="\$(classify_failure_reason)"
960
+ fi
961
+ write_state failed "\${status}" "\${failure_reason}"
962
+ fi
963
+ record_final_git_state
964
+ if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
965
+ cp ${sandbox_run_dir_q}/result.env ${result_q}
966
+ fi
967
+ if [[ "\${keep_agent}" != "true" ]]; then
968
+ cleanup_agent
969
+ fi
970
+ ${collect_copy_snippet}${reconcile_snippet}
971
+ printf '\n__CODEX_EXIT__:%s\n' "\${status}" | tee -a ${output_q}
972
+ exit "\${status}"
973
+ EOF
974
+
975
+ chmod +x "$inner_script"
976
+ tmux new-session -d -s "$session" "$inner_script"
977
+
978
+ printf 'SESSION=%s\n' "$session"
979
+ printf 'TASK_KIND=%s\n' "$task_kind"
980
+ printf 'TASK_ID=%s\n' "$task_id"
981
+ printf 'WORKTREE=%s\n' "$worktree"
982
+ printf 'OUTPUT=%s\n' "$output_file"
983
+ printf 'SCRIPT=%s\n' "$inner_script"
984
+ printf 'META=%s\n' "$meta_file"