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,718 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=/dev/null
6
+ source "${SCRIPT_DIR}/flow-config-lib.sh"
7
+
8
+ usage() {
9
+ cat <<'EOF'
10
+ Usage:
11
+ agent-project-run-codex-resilient --mode safe|bypass --worktree <path> --prompt-file <path> --output-file <path> --host-run-dir <path> --sandbox-run-dir <path> --codex-bin <path> [options]
12
+
13
+ Run Codex with persisted thread recovery for quota/auth interruptions.
14
+
15
+ Options:
16
+ --safe-profile <name> Codex profile for safe mode
17
+ --bypass-profile <name> Codex profile for bypass mode
18
+ --max-resume-attempts <count> Maximum resume attempts after interruption
19
+ --auth-refresh-timeout-seconds <secs> How long to wait for refreshed auth before failing
20
+ --auth-refresh-poll-seconds <secs> Poll interval while waiting for refreshed auth
21
+ --help Show this help
22
+ EOF
23
+ }
24
+
25
+ mode=""
26
+ worktree=""
27
+ prompt_file=""
28
+ output_file=""
29
+ host_run_dir=""
30
+ sandbox_run_dir=""
31
+ safe_profile="default"
32
+ bypass_profile="default"
33
+ codex_bin=""
34
+ max_resume_attempts="${ACP_CODEX_MAX_RESUME_ATTEMPTS:-${F_LOSNING_CODEX_MAX_RESUME_ATTEMPTS:-6}}"
35
+ auth_refresh_timeout_seconds="${ACP_CODEX_AUTH_REFRESH_TIMEOUT_SECONDS:-${F_LOSNING_CODEX_AUTH_REFRESH_TIMEOUT_SECONDS:-900}}"
36
+ auth_refresh_poll_seconds="${ACP_CODEX_AUTH_REFRESH_POLL_SECONDS:-${F_LOSNING_CODEX_AUTH_REFRESH_POLL_SECONDS:-10}}"
37
+ max_quota_autoswitch_attempts="${ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-${F_LOSNING_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-1}}"
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --mode) mode="${2:-}"; shift 2 ;;
42
+ --worktree) worktree="${2:-}"; shift 2 ;;
43
+ --prompt-file) prompt_file="${2:-}"; shift 2 ;;
44
+ --output-file) output_file="${2:-}"; shift 2 ;;
45
+ --host-run-dir) host_run_dir="${2:-}"; shift 2 ;;
46
+ --sandbox-run-dir) sandbox_run_dir="${2:-}"; shift 2 ;;
47
+ --safe-profile) safe_profile="${2:-}"; shift 2 ;;
48
+ --bypass-profile) bypass_profile="${2:-}"; shift 2 ;;
49
+ --codex-bin) codex_bin="${2:-}"; shift 2 ;;
50
+ --max-resume-attempts) max_resume_attempts="${2:-}"; shift 2 ;;
51
+ --auth-refresh-timeout-seconds) auth_refresh_timeout_seconds="${2:-}"; shift 2 ;;
52
+ --auth-refresh-poll-seconds) auth_refresh_poll_seconds="${2:-}"; shift 2 ;;
53
+ --help|-h) usage; exit 0 ;;
54
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
55
+ esac
56
+ done
57
+
58
+ if [[ -z "$mode" || -z "$worktree" || -z "$prompt_file" || -z "$output_file" || -z "$host_run_dir" || -z "$sandbox_run_dir" || -z "$codex_bin" ]]; then
59
+ usage >&2
60
+ exit 1
61
+ fi
62
+
63
+ case "$mode" in
64
+ safe|bypass) ;;
65
+ *)
66
+ echo "--mode must be safe or bypass" >&2
67
+ exit 1
68
+ ;;
69
+ esac
70
+
71
+ case "$max_resume_attempts" in
72
+ ''|*[!0-9]*) echo "--max-resume-attempts must be numeric" >&2; exit 1 ;;
73
+ esac
74
+ case "$auth_refresh_timeout_seconds" in
75
+ ''|*[!0-9]*) echo "--auth-refresh-timeout-seconds must be numeric" >&2; exit 1 ;;
76
+ esac
77
+ case "$auth_refresh_poll_seconds" in
78
+ ''|*[!0-9]*) echo "--auth-refresh-poll-seconds must be numeric" >&2; exit 1 ;;
79
+ esac
80
+ case "$max_quota_autoswitch_attempts" in
81
+ ''|*[!0-9]*) echo "ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS must be numeric" >&2; exit 1 ;;
82
+ esac
83
+
84
+ FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
85
+ state_file="${host_run_dir}/runner.env"
86
+ auth_file="${HOME}/.codex/auth.json"
87
+ shared_agent_home="${SHARED_AGENT_HOME:-$(resolve_shared_agent_home "${FLOW_SKILL_DIR}")}"
88
+ quota_tool_bin="$(flow_resolve_codex_quota_bin "${FLOW_SKILL_DIR}")"
89
+ quota_manager_script="$(flow_resolve_codex_quota_manager_script "${FLOW_SKILL_DIR}")"
90
+ quota_autoswitch_enabled="${ACP_CODEX_QUOTA_AUTOSWITCH_ENABLED:-${F_LOSNING_CODEX_QUOTA_AUTOSWITCH_ENABLED:-1}}"
91
+ quota_threshold="${ACP_CODEX_QUOTA_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_THRESHOLD:-70}}"
92
+ quota_weekly_threshold="${ACP_CODEX_QUOTA_WEEKLY_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_WEEKLY_THRESHOLD:-90}}"
93
+ quota_soft_threshold="${ACP_CODEX_QUOTA_SOFT_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_SOFT_THRESHOLD:-55}}"
94
+ quota_soft_worker_threshold="${ACP_CODEX_QUOTA_SOFT_WORKER_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_SOFT_WORKER_THRESHOLD:-8}}"
95
+ quota_emergency_threshold="${ACP_CODEX_QUOTA_EMERGENCY_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_EMERGENCY_THRESHOLD:-65}}"
96
+ quota_emergency_worker_threshold="${ACP_CODEX_QUOTA_EMERGENCY_WORKER_THRESHOLD:-${F_LOSNING_CODEX_QUOTA_EMERGENCY_WORKER_THRESHOLD:-12}}"
97
+ quota_switch_cooldown_seconds="${ACP_CODEX_QUOTA_SWITCH_COOLDOWN_SECONDS:-${F_LOSNING_CODEX_QUOTA_SWITCH_COOLDOWN_SECONDS:-600}}"
98
+ quota_timeout_seconds="${ACP_CODEX_QUOTA_TIMEOUT_SECONDS:-${F_LOSNING_CODEX_QUOTA_TIMEOUT_SECONDS:-45}}"
99
+ quota_prefer_label="${ACP_CODEX_QUOTA_PREFER_LABEL:-${F_LOSNING_CODEX_QUOTA_PREFER_LABEL:-}}"
100
+ quota_switch_state_file="${CODEX_QUOTA_MANAGER_SWITCH_STATE_FILE:-${XDG_CACHE_HOME:-$HOME/.cache}/codex-quota-manager/last-switch.env}"
101
+ config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
102
+ issue_session_prefix="$(flow_resolve_issue_session_prefix "${config_yaml}")"
103
+ pr_session_prefix="$(flow_resolve_pr_session_prefix "${config_yaml}")"
104
+
105
+ thread_id=""
106
+ attempt=0
107
+ resume_count=0
108
+ last_exit_code=""
109
+ last_failure_reason=""
110
+ last_trigger_reason=""
111
+ auth_wait_started_at=""
112
+ last_auth_fingerprint=""
113
+ last_attempt_start_size=0
114
+ last_attempt_start_quota_label=""
115
+ last_quota_switch_status=""
116
+ last_quota_next_retry_at=""
117
+ last_quota_selected_label=""
118
+ quota_autoswitch_attempt_count=0
119
+
120
+ mkdir -p "$host_run_dir"
121
+ touch "$output_file"
122
+
123
+ log_runner() {
124
+ local message="${1:-}"
125
+ [[ -n "$message" ]] || return 0
126
+ printf '[%s] %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$message" | tee -a "$output_file"
127
+ }
128
+
129
+ write_state() {
130
+ local runner_state="${1:?runner state required}"
131
+ local failure_reason="${2:-$last_failure_reason}"
132
+ local updated_at
133
+ local tmp_file="${state_file}.tmp.$$"
134
+
135
+ updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
136
+ {
137
+ printf 'RUNNER_STATE=%q\n' "$runner_state"
138
+ printf 'THREAD_ID=%q\n' "$thread_id"
139
+ printf 'ATTEMPT=%s\n' "$attempt"
140
+ printf 'RESUME_COUNT=%s\n' "$resume_count"
141
+ printf 'LAST_EXIT_CODE=%q\n' "$last_exit_code"
142
+ printf 'LAST_FAILURE_REASON=%q\n' "$failure_reason"
143
+ printf 'LAST_TRIGGER_REASON=%q\n' "$last_trigger_reason"
144
+ printf 'AUTH_WAIT_STARTED_AT=%q\n' "$auth_wait_started_at"
145
+ printf 'LAST_AUTH_FINGERPRINT=%q\n' "$last_auth_fingerprint"
146
+ printf 'UPDATED_AT=%q\n' "$updated_at"
147
+ } >"$tmp_file"
148
+ mv "$tmp_file" "$state_file"
149
+ }
150
+
151
+ run_with_timeout() {
152
+ local timeout_seconds="${1:?timeout seconds required}"
153
+ shift
154
+
155
+ /opt/homebrew/bin/python3 - "$timeout_seconds" "$@" <<'PY'
156
+ import os
157
+ import signal
158
+ import subprocess
159
+ import sys
160
+
161
+ timeout_seconds = float(sys.argv[1])
162
+ argv = sys.argv[2:]
163
+
164
+ if not argv:
165
+ sys.exit(64)
166
+
167
+ proc = subprocess.Popen(argv, start_new_session=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
168
+
169
+ try:
170
+ stdout, stderr = proc.communicate(timeout=timeout_seconds)
171
+ except subprocess.TimeoutExpired:
172
+ try:
173
+ os.killpg(proc.pid, signal.SIGTERM)
174
+ except ProcessLookupError:
175
+ pass
176
+ try:
177
+ stdout, stderr = proc.communicate(timeout=2)
178
+ except subprocess.TimeoutExpired:
179
+ try:
180
+ os.killpg(proc.pid, signal.SIGKILL)
181
+ except ProcessLookupError:
182
+ pass
183
+ stdout, stderr = proc.communicate()
184
+ if stdout:
185
+ sys.stdout.buffer.write(stdout)
186
+ if stderr:
187
+ sys.stderr.buffer.write(stderr)
188
+ sys.exit(124)
189
+
190
+ if stdout:
191
+ sys.stdout.buffer.write(stdout)
192
+ if stderr:
193
+ sys.stderr.buffer.write(stderr)
194
+ sys.exit(proc.returncode)
195
+ PY
196
+ }
197
+
198
+ auth_fingerprint() {
199
+ if [[ ! -f "$auth_file" ]]; then
200
+ printf 'missing\n'
201
+ return 0
202
+ fi
203
+
204
+ local mtime size sha
205
+ mtime="$(stat -f %m "$auth_file" 2>/dev/null || printf '0')"
206
+ size="$(stat -f %z "$auth_file" 2>/dev/null || printf '0')"
207
+ sha="$(shasum -a 256 "$auth_file" | awk '{print $1}')"
208
+ printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
209
+ }
210
+
211
+ quota_active_label() {
212
+ if [[ ! -x "$quota_tool_bin" ]] || ! command -v jq >/dev/null 2>&1; then
213
+ printf '\n'
214
+ return 0
215
+ fi
216
+
217
+ "$quota_tool_bin" codex list --json 2>/dev/null \
218
+ | jq -r '
219
+ .activeInfo.trackedLabel
220
+ // .activeInfo.activeLabel
221
+ // ([.accounts[]? | select(.isActive == true or .isNativeActive == true)][0].label)
222
+ // empty
223
+ ' 2>/dev/null \
224
+ || printf '\n'
225
+ }
226
+
227
+ quota_switch_signature() {
228
+ if [[ ! -f "$quota_switch_state_file" ]]; then
229
+ printf 'missing\n'
230
+ return 0
231
+ fi
232
+
233
+ local mtime size sha
234
+ mtime="$(stat -f %m "$quota_switch_state_file" 2>/dev/null || printf '0')"
235
+ size="$(stat -f %z "$quota_switch_state_file" 2>/dev/null || printf '0')"
236
+ sha="$(shasum -a 256 "$quota_switch_state_file" | awk '{print $1}')"
237
+ printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
238
+ }
239
+
240
+ running_workers() {
241
+ if ! command -v tmux >/dev/null 2>&1; then
242
+ printf '0\n'
243
+ return 0
244
+ fi
245
+
246
+ { tmux list-sessions -F '#S' 2>/dev/null || true; } \
247
+ | awk -v issue_prefix="$issue_session_prefix" -v pr_prefix="$pr_session_prefix" '
248
+ index($0, issue_prefix) == 1 || index($0, pr_prefix) == 1 { count++ }
249
+ END { print count + 0 }
250
+ '
251
+ }
252
+
253
+ extract_kv_value() {
254
+ local key="${1:?key required}"
255
+ local payload="${2:-}"
256
+ sed -nE "s/^${key}=(.*)$/\\1/p" <<<"$payload" | tail -n 1
257
+ }
258
+
259
+ run_quota_autoswitch() {
260
+ local quota_output quota_status shell_flags_before_quota_exec
261
+ local -a quota_cmd
262
+ local worker_count
263
+
264
+ quota_autoswitch_unavailable_reason() {
265
+ if [[ "$quota_autoswitch_enabled" == "0" ]]; then
266
+ printf 'disabled\n'
267
+ return 0
268
+ fi
269
+ if [[ ! -x "$quota_manager_script" ]]; then
270
+ printf 'missing-script\n'
271
+ return 0
272
+ fi
273
+ if [[ ! -x "$quota_tool_bin" ]]; then
274
+ printf 'missing-codex-quota\n'
275
+ return 0
276
+ fi
277
+ if ! command -v jq >/dev/null 2>&1; then
278
+ printf 'missing-jq\n'
279
+ return 0
280
+ fi
281
+ printf 'ok\n'
282
+ }
283
+
284
+ local unavailable_reason=""
285
+ last_quota_next_retry_at=""
286
+ last_quota_selected_label=""
287
+ if [[ "$quota_autoswitch_enabled" == "0" ]]; then
288
+ log_runner "quota auto-switch disabled; waiting for external Codex auth refresh"
289
+ printf 'CODEX_QUOTA_AUTOSWITCH_ENABLED=0\n' | tee -a "$output_file"
290
+ last_quota_switch_status="disabled"
291
+ return 1
292
+ fi
293
+
294
+ unavailable_reason="$(quota_autoswitch_unavailable_reason)"
295
+ if [[ "$unavailable_reason" != "ok" ]]; then
296
+ log_runner "quota auto-switch unavailable (${unavailable_reason}); waiting for external Codex auth refresh"
297
+ printf 'CODEX_QUOTA_MANAGER_UNAVAILABLE=yes\n' | tee -a "$output_file"
298
+ printf 'CODEX_QUOTA_MANAGER_REASON=%s\n' "$unavailable_reason" | tee -a "$output_file"
299
+ last_quota_switch_status="unavailable"
300
+ return 1
301
+ fi
302
+
303
+ quota_autoswitch_attempt_count=$((quota_autoswitch_attempt_count + 1))
304
+ worker_count="$(running_workers)"
305
+ quota_cmd=(
306
+ env
307
+ "CODEX_QUOTA_BIN=${quota_tool_bin}"
308
+ bash "$quota_manager_script"
309
+ --trigger-reason "$last_failure_reason"
310
+ --current-label "${last_attempt_start_quota_label}"
311
+ --five-hour-threshold "$quota_threshold"
312
+ --weekly-threshold "$quota_weekly_threshold"
313
+ --running-workers "$worker_count"
314
+ )
315
+ if [[ -n "$quota_prefer_label" ]]; then
316
+ quota_cmd+=(--prefer-label "$quota_prefer_label")
317
+ fi
318
+
319
+ log_runner "${last_failure_reason} detected; attempting failure-driven Codex account switch"
320
+ shell_flags_before_quota_exec="$-"
321
+ set +e
322
+ quota_output="$(run_with_timeout "$quota_timeout_seconds" "${quota_cmd[@]}" 2>&1)"
323
+ quota_status=$?
324
+ case "$shell_flags_before_quota_exec" in
325
+ *e*) set -e ;;
326
+ *) set +e ;;
327
+ esac
328
+
329
+ if [[ "$quota_status" == "0" ]]; then
330
+ last_quota_switch_status="$(extract_kv_value "SWITCH_DECISION" "$quota_output")"
331
+ last_quota_next_retry_at="$(extract_kv_value "NEXT_RETRY_AT" "$quota_output")"
332
+ last_quota_selected_label="$(extract_kv_value "SELECTED_LABEL" "$quota_output")"
333
+ [[ -n "$quota_output" ]] && printf '%s\n' "$quota_output" | tee -a "$output_file"
334
+ case "$last_quota_switch_status" in
335
+ switched|current-ok)
336
+ return 0
337
+ ;;
338
+ deferred)
339
+ return 10
340
+ ;;
341
+ *)
342
+ last_quota_switch_status="failed"
343
+ return 1
344
+ ;;
345
+ esac
346
+ fi
347
+
348
+ last_quota_next_retry_at="$(extract_kv_value "NEXT_RETRY_AT" "${quota_output:-}")"
349
+ last_quota_selected_label="$(extract_kv_value "SELECTED_LABEL" "${quota_output:-}")"
350
+ [[ -n "${quota_output:-}" ]] && printf '%s\n' "$quota_output" | tee -a "$output_file"
351
+ if [[ "$quota_status" == "10" ]]; then
352
+ last_quota_switch_status="deferred"
353
+ log_runner "no eligible Codex account is ready yet; waiting for the next reset window"
354
+ return 10
355
+ fi
356
+ last_quota_switch_status="failed"
357
+ if [[ "$quota_status" == "124" ]]; then
358
+ log_runner "quota auto-switch timed out after ${quota_timeout_seconds}s"
359
+ else
360
+ log_runner "quota auto-switch exited with status ${quota_status}"
361
+ fi
362
+ return "$quota_status"
363
+ }
364
+
365
+ new_output_since() {
366
+ local start_size="${1:?start size required}"
367
+ local file_size
368
+ file_size="$(stat -f %z "$output_file" 2>/dev/null || printf '0')"
369
+ if (( file_size <= start_size )); then
370
+ return 0
371
+ fi
372
+ tail -c "+$((start_size + 1))" "$output_file"
373
+ }
374
+
375
+ update_thread_id_from_output() {
376
+ local start_size="${1:?start size required}"
377
+ local new_thread_id
378
+
379
+ new_thread_id="$(new_output_since "$start_size" | extract_thread_id || true)"
380
+ if [[ -n "$new_thread_id" ]]; then
381
+ thread_id="$new_thread_id"
382
+ fi
383
+ }
384
+
385
+ extract_thread_id_from_line() {
386
+ local line="${1:-}"
387
+ sed -nE 's/.*"type"[[:space:]]*:[[:space:]]*"thread\.started".*"thread_id"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' <<<"$line"
388
+ }
389
+
390
+ persist_thread_id_from_line() {
391
+ local line="${1:-}"
392
+ local new_thread_id=""
393
+
394
+ new_thread_id="$(extract_thread_id_from_line "$line")"
395
+ if [[ -n "$new_thread_id" && "$new_thread_id" != "$thread_id" ]]; then
396
+ thread_id="$new_thread_id"
397
+ write_state "running" ""
398
+ fi
399
+ }
400
+
401
+ stream_codex_exec() {
402
+ local phase="${1:?phase required}"
403
+ local stream_fifo=""
404
+ local producer_pid=""
405
+ local line=""
406
+
407
+ last_attempt_start_size="$(stat -f %z "$output_file" 2>/dev/null || printf '0')"
408
+ stream_fifo="$(mktemp -u "${TMPDIR:-/tmp}/codex-stream.XXXXXX")"
409
+ mkfifo "$stream_fifo"
410
+
411
+ case "$phase" in
412
+ initial)
413
+ (
414
+ case "$mode" in
415
+ safe)
416
+ "$codex_bin" exec --json --profile "$safe_profile" --full-auto <"$prompt_file"
417
+ ;;
418
+ bypass)
419
+ "$codex_bin" exec --json --profile "$bypass_profile" --dangerously-bypass-approvals-and-sandbox <"$prompt_file"
420
+ ;;
421
+ esac
422
+ ) >"$stream_fifo" 2>&1 &
423
+ ;;
424
+ resume)
425
+ (
426
+ case "$mode" in
427
+ safe)
428
+ resume_prompt | "$codex_bin" exec resume --json --full-auto "$thread_id" -
429
+ ;;
430
+ bypass)
431
+ resume_prompt | "$codex_bin" exec resume --json --dangerously-bypass-approvals-and-sandbox "$thread_id" -
432
+ ;;
433
+ esac
434
+ ) >"$stream_fifo" 2>&1 &
435
+ ;;
436
+ *)
437
+ rm -f "$stream_fifo"
438
+ echo "unknown codex exec phase: $phase" >&2
439
+ exit 1
440
+ ;;
441
+ esac
442
+
443
+ producer_pid="$!"
444
+ while IFS= read -r line || [[ -n "$line" ]]; do
445
+ printf '%s\n' "$line" | tee -a "$output_file"
446
+ persist_thread_id_from_line "$line"
447
+ done <"$stream_fifo"
448
+
449
+ rm -f "$stream_fifo"
450
+
451
+ if wait "$producer_pid"; then
452
+ last_exit_code="0"
453
+ else
454
+ last_exit_code="$?"
455
+ fi
456
+
457
+ update_thread_id_from_output "$last_attempt_start_size"
458
+ }
459
+
460
+ extract_thread_id() {
461
+ /opt/homebrew/bin/python3 -c '
462
+ import json
463
+ import sys
464
+
465
+ thread_id = ""
466
+ for raw in sys.stdin:
467
+ line = raw.strip()
468
+ if not line.startswith("{"):
469
+ continue
470
+ try:
471
+ payload = json.loads(line)
472
+ except Exception:
473
+ continue
474
+ if payload.get("type") == "thread.started" and payload.get("thread_id"):
475
+ thread_id = str(payload["thread_id"])
476
+
477
+ if thread_id:
478
+ sys.stdout.write(thread_id)
479
+ '
480
+ }
481
+
482
+ classify_failure_reason() {
483
+ local chunk="${1:-}"
484
+ local recent_chunk
485
+
486
+ recent_chunk="$(tail -n 120 <<<"$chunk")"
487
+
488
+ if grep -Eiq "You've hit your usage limit|You have reached your Codex usage limits|visit https://chatgpt.com/codex/settings/usage|Upgrade to Pro|rate limit exceeded|quota exceeded|usage cap (reached|exceeded)|usage quota (reached|exceeded)" <<<"$recent_chunk"; then
489
+ printf 'usage-limit\n'
490
+ return 0
491
+ fi
492
+
493
+ if grep -Eiq "(HTTP[^0-9]*)?401([^0-9]|$)|unauthorized|invalid credentials|invalid api key|authentication failed with status 401|received 401" <<<"$recent_chunk"; then
494
+ printf 'auth-401\n'
495
+ return 0
496
+ fi
497
+
498
+ if grep -Eiq "account (is )?(banned|suspended|disabled)|access revoked|account revoked|forbidden due to policy|account blocked|policy violation" <<<"$recent_chunk"; then
499
+ printf 'account-banned\n'
500
+ return 0
501
+ fi
502
+
503
+ if grep -Eiq "Authentication required|Please log in|Please login|Please authenticate|login required|run codex login|codex login required|logged out|not logged in|expired session|session expired|token expired|reauthenticate|unauthenticated|auth(entication)? failed|credentials expired" <<<"$recent_chunk"; then
504
+ printf 'auth-failure\n'
505
+ return 0
506
+ fi
507
+
508
+ if [[ -n "$last_exit_code" && "$last_exit_code" != "0" ]]; then
509
+ printf 'worker-exit-failed\n'
510
+ fi
511
+ }
512
+
513
+ resume_prompt() {
514
+ cat <<EOF
515
+ The previous Codex exec turn in this same thread was interrupted because the host refreshed Codex authentication after a quota or auth failure.
516
+
517
+ Continue the same task from the next unfinished step only. Do not restart completed work unless you need to verify or repair it.
518
+
519
+ If you need to reorient, inspect the current git status plus the existing run artifacts in:
520
+ - Host run dir: ${host_run_dir}
521
+ - Sandbox run dir: ${sandbox_run_dir}
522
+ EOF
523
+ }
524
+
525
+ codex_login_healthy() {
526
+ "$codex_bin" login status >/dev/null 2>&1
527
+ }
528
+
529
+ wait_for_auth_refresh() {
530
+ local baseline_fingerprint="${1:?baseline fingerprint required}"
531
+ local trigger_reason="${2:?trigger reason required}"
532
+ local baseline_quota_label="${3:-}"
533
+ local baseline_switch_signature="${4:-}"
534
+ local deadline now current_fingerprint current_quota_label current_switch_signature
535
+ local sleep_seconds
536
+
537
+ auth_wait_started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
538
+ last_trigger_reason="$trigger_reason"
539
+ write_state "waiting-auth-refresh" "$trigger_reason"
540
+
541
+ deadline=$(( $(date +%s) + auth_refresh_timeout_seconds ))
542
+ while :; do
543
+ current_fingerprint="$(auth_fingerprint)"
544
+ last_auth_fingerprint="$current_fingerprint"
545
+ case "$trigger_reason" in
546
+ usage-limit|auth-401|account-banned)
547
+ current_quota_label="$(quota_active_label)"
548
+ current_switch_signature="$(quota_switch_signature)"
549
+ if codex_login_healthy; then
550
+ if [[ "$current_fingerprint" != "$baseline_fingerprint" ]]; then
551
+ log_runner "detected refreshed Codex auth after quota interruption; resuming thread ${thread_id}"
552
+ auth_wait_started_at=""
553
+ write_state "running" ""
554
+ return 0
555
+ fi
556
+
557
+ if [[ -n "$baseline_quota_label" && -n "$current_quota_label" && "$current_quota_label" != "$baseline_quota_label" ]]; then
558
+ log_runner "detected rotated Codex quota account (${baseline_quota_label} -> ${current_quota_label}); resuming thread ${thread_id}"
559
+ auth_wait_started_at=""
560
+ write_state "running" ""
561
+ return 0
562
+ fi
563
+
564
+ if [[ -n "$baseline_switch_signature" && -n "$current_switch_signature" && "$current_switch_signature" != "$baseline_switch_signature" ]]; then
565
+ log_runner "detected quota switch state refresh; resuming thread ${thread_id}"
566
+ auth_wait_started_at=""
567
+ write_state "running" ""
568
+ return 0
569
+ fi
570
+
571
+ if [[ "$last_quota_switch_status" == "switched" && -n "$current_quota_label" ]]; then
572
+ log_runner "quota manager reports healthy Codex account ${current_quota_label}; resuming thread ${thread_id}"
573
+ auth_wait_started_at=""
574
+ write_state "running" ""
575
+ return 0
576
+ fi
577
+ fi
578
+
579
+ ;;
580
+ *)
581
+ if codex_login_healthy; then
582
+ if [[ "$current_fingerprint" != "$baseline_fingerprint" ]]; then
583
+ log_runner "detected refreshed Codex auth; resuming thread ${thread_id}"
584
+ else
585
+ log_runner "Codex auth is healthy again; resuming thread ${thread_id}"
586
+ fi
587
+ auth_wait_started_at=""
588
+ write_state "running" ""
589
+ return 0
590
+ fi
591
+ ;;
592
+ esac
593
+
594
+ now="$(date +%s)"
595
+ if (( now >= deadline )); then
596
+ last_failure_reason="auth-refresh-timeout"
597
+ write_state "failed" "$last_failure_reason"
598
+ return 1
599
+ fi
600
+
601
+ sleep_seconds="$auth_refresh_poll_seconds"
602
+ if (( sleep_seconds > deadline - now )); then
603
+ sleep_seconds=$(( deadline - now ))
604
+ fi
605
+
606
+ if (( sleep_seconds < 1 )); then
607
+ sleep_seconds=1
608
+ fi
609
+ sleep "$sleep_seconds"
610
+ done
611
+ }
612
+
613
+ run_initial_exec() {
614
+ stream_codex_exec initial
615
+ }
616
+
617
+ run_resume_exec() {
618
+ stream_codex_exec resume
619
+ }
620
+
621
+ attempt_run() {
622
+ local reason auth_before_switch quota_label_before_switch quota_switch_signature_before_switch quota_switch_result shell_flags_before_quota_switch
623
+
624
+ attempt=$((attempt + 1))
625
+ last_quota_switch_status=""
626
+ last_attempt_start_quota_label="$(quota_active_label)"
627
+ write_state "running" ""
628
+
629
+ if [[ -z "$thread_id" ]]; then
630
+ log_runner "starting Codex exec attempt ${attempt}"
631
+ run_initial_exec
632
+ else
633
+ log_runner "resuming Codex thread ${thread_id} (resume ${resume_count}/${max_resume_attempts})"
634
+ run_resume_exec
635
+ fi
636
+
637
+ if [[ "${last_exit_code}" == "0" ]]; then
638
+ last_failure_reason=""
639
+ write_state "succeeded" ""
640
+ return 0
641
+ fi
642
+
643
+ reason="$(classify_failure_reason "$(new_output_since "$last_attempt_start_size")")"
644
+ last_failure_reason="${reason:-worker-exit-failed}"
645
+
646
+ if [[ -z "$thread_id" ]]; then
647
+ write_state "failed" "$last_failure_reason"
648
+ return 1
649
+ fi
650
+
651
+ case "$last_failure_reason" in
652
+ usage-limit|auth-failure|auth-401|account-banned)
653
+ if (( resume_count >= max_resume_attempts )); then
654
+ last_failure_reason="resume-attempts-exhausted"
655
+ write_state "failed" "$last_failure_reason"
656
+ return 1
657
+ fi
658
+
659
+ auth_before_switch="$(auth_fingerprint)"
660
+ quota_label_before_switch="$last_attempt_start_quota_label"
661
+ quota_switch_signature_before_switch="$(quota_switch_signature)"
662
+ last_auth_fingerprint="$auth_before_switch"
663
+ if [[ "$last_failure_reason" == "usage-limit" || "$last_failure_reason" == "auth-401" || "$last_failure_reason" == "account-banned" ]]; then
664
+ if (( quota_autoswitch_attempt_count >= max_quota_autoswitch_attempts )); then
665
+ log_runner "automatic Codex quota switching already ran ${quota_autoswitch_attempt_count} time(s) in this worker; refusing another rotation"
666
+ last_failure_reason="quota-switch-attempt-limit"
667
+ write_state "failed" "$last_failure_reason"
668
+ return 1
669
+ fi
670
+ write_state "switching-account" "$last_failure_reason"
671
+ shell_flags_before_quota_switch="$-"
672
+ set +e
673
+ run_quota_autoswitch
674
+ quota_switch_result=$?
675
+ case "$shell_flags_before_quota_switch" in
676
+ *e*) set -e ;;
677
+ *) set +e ;;
678
+ esac
679
+ if [[ "$quota_switch_result" == "10" ]]; then
680
+ log_runner "quota manager deferred rotation until ${last_quota_next_retry_at:-unknown}; automatic timed re-tries are disabled for safety"
681
+ last_failure_reason="quota-switch-deferred"
682
+ write_state "failed" "$last_failure_reason"
683
+ return 1
684
+ fi
685
+ fi
686
+
687
+ if ! wait_for_auth_refresh "$auth_before_switch" "$last_failure_reason" "$quota_label_before_switch" "$quota_switch_signature_before_switch"; then
688
+ return 1
689
+ fi
690
+
691
+ resume_count=$((resume_count + 1))
692
+ return 2
693
+ ;;
694
+ *)
695
+ write_state "failed" "$last_failure_reason"
696
+ return 1
697
+ ;;
698
+ esac
699
+ }
700
+
701
+ write_state "running" ""
702
+
703
+ while :; do
704
+ set +e
705
+ attempt_run
706
+ attempt_status=$?
707
+ set -e
708
+
709
+ if [[ "$attempt_status" == "0" ]]; then
710
+ exit 0
711
+ fi
712
+
713
+ if [[ "$attempt_status" == "2" ]]; then
714
+ continue
715
+ fi
716
+
717
+ exit "${last_exit_code:-1}"
718
+ done