charness 3.6.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (578) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/DEVELOPMENT.md +31 -5
  3. package/MIGRATION_RENAME_TO_CHARNESS.md +3 -2
  4. package/README.md +84 -782
  5. package/SETUP.md +37 -10
  6. package/agent-plugin/.claude-plugin/plugin.json +7 -0
  7. package/agent-plugin/README.md +35 -0
  8. package/agent-plugin/skills/charness-delegate-claude/SKILL.md +85 -0
  9. package/agent-plugin/skills/charness-delegate-claude/summarize-events.py +90 -0
  10. package/agent-plugin/skills/charness-delegate-opencode/SKILL.md +68 -0
  11. package/agent-plugin/skills/charness-manager/SKILL.md +152 -0
  12. package/dist/adapters/adapterNames.d.ts +6 -0
  13. package/dist/adapters/adapterNames.d.ts.map +1 -1
  14. package/dist/adapters/adapterNames.js +8 -2
  15. package/dist/adapters/adapterNames.js.map +1 -1
  16. package/dist/adapters/claudeCliAdapter.d.ts +8 -0
  17. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  18. package/dist/adapters/claudeCliAdapter.js +42 -5
  19. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  20. package/dist/adapters/claudeJsonStreamAdapter.d.ts +10 -9
  21. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  22. package/dist/adapters/claudeJsonStreamAdapter.js +53 -55
  23. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  24. package/dist/adapters/createAdapter.d.ts +8 -0
  25. package/dist/adapters/createAdapter.d.ts.map +1 -1
  26. package/dist/adapters/createAdapter.js +14 -2
  27. package/dist/adapters/createAdapter.js.map +1 -1
  28. package/dist/adapters/openCodeAdapter.d.ts +43 -6
  29. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  30. package/dist/adapters/openCodeAdapter.js +95 -6
  31. package/dist/adapters/openCodeAdapter.js.map +1 -1
  32. package/dist/agentLogin/agentLogin.d.ts +1 -1
  33. package/dist/agentLogin/agentLogin.d.ts.map +1 -1
  34. package/dist/apiRetryKick.d.ts +4 -3
  35. package/dist/apiRetryKick.d.ts.map +1 -1
  36. package/dist/apiRetryKick.js +16 -9
  37. package/dist/apiRetryKick.js.map +1 -1
  38. package/dist/bot.d.ts.map +1 -1
  39. package/dist/bot.js +1313 -381
  40. package/dist/bot.js.map +1 -1
  41. package/dist/cli/claudeProfiles.d.ts +11 -6
  42. package/dist/cli/claudeProfiles.d.ts.map +1 -1
  43. package/dist/cli/claudeProfiles.js +50 -22
  44. package/dist/cli/claudeProfiles.js.map +1 -1
  45. package/dist/cli/hot.d.ts +5 -0
  46. package/dist/cli/hot.d.ts.map +1 -1
  47. package/dist/cli/hot.js +71 -25
  48. package/dist/cli/hot.js.map +1 -1
  49. package/dist/cli/tscWatchGuard.d.ts +95 -0
  50. package/dist/cli/tscWatchGuard.d.ts.map +1 -0
  51. package/dist/cli/tscWatchGuard.js +295 -0
  52. package/dist/cli/tscWatchGuard.js.map +1 -0
  53. package/dist/cli.js +12 -1
  54. package/dist/cli.js.map +1 -1
  55. package/dist/connectors/jira/answerSink.d.ts +10 -0
  56. package/dist/connectors/jira/answerSink.d.ts.map +1 -1
  57. package/dist/connectors/jira/answerSink.js +13 -0
  58. package/dist/connectors/jira/answerSink.js.map +1 -1
  59. package/dist/connectors/jira/config.d.ts +2 -0
  60. package/dist/connectors/jira/config.d.ts.map +1 -1
  61. package/dist/connectors/jira/config.js +9 -1
  62. package/dist/connectors/jira/config.js.map +1 -1
  63. package/dist/connectors/jira/connector.d.ts +11 -1
  64. package/dist/connectors/jira/connector.d.ts.map +1 -1
  65. package/dist/connectors/jira/connector.js +7 -0
  66. package/dist/connectors/jira/connector.js.map +1 -1
  67. package/dist/connectors/jira/inbound.d.ts +27 -4
  68. package/dist/connectors/jira/inbound.d.ts.map +1 -1
  69. package/dist/connectors/jira/inbound.js +69 -8
  70. package/dist/connectors/jira/inbound.js.map +1 -1
  71. package/dist/connectors/jira/issueBlocks.d.ts +5 -0
  72. package/dist/connectors/jira/issueBlocks.d.ts.map +1 -1
  73. package/dist/connectors/jira/issueBlocks.js +11 -0
  74. package/dist/connectors/jira/issueBlocks.js.map +1 -1
  75. package/dist/connectors/telegram/bindPicker.d.ts +5 -0
  76. package/dist/connectors/telegram/bindPicker.d.ts.map +1 -1
  77. package/dist/connectors/telegram/bindPicker.js +18 -1
  78. package/dist/connectors/telegram/bindPicker.js.map +1 -1
  79. package/dist/connectors/telegram/bindPickerClaims.d.ts +46 -0
  80. package/dist/connectors/telegram/bindPickerClaims.d.ts.map +1 -0
  81. package/dist/connectors/telegram/bindPickerClaims.js +83 -0
  82. package/dist/connectors/telegram/bindPickerClaims.js.map +1 -0
  83. package/dist/connectors/telegram/bindSweep.d.ts +32 -0
  84. package/dist/connectors/telegram/bindSweep.d.ts.map +1 -0
  85. package/dist/connectors/telegram/bindSweep.js +47 -0
  86. package/dist/connectors/telegram/bindSweep.js.map +1 -0
  87. package/dist/connectors/telegram/bindingWelcome.d.ts +23 -0
  88. package/dist/connectors/telegram/bindingWelcome.d.ts.map +1 -0
  89. package/dist/connectors/telegram/bindingWelcome.js +26 -0
  90. package/dist/connectors/telegram/bindingWelcome.js.map +1 -0
  91. package/dist/connectors/telegram/botApiConnection.d.ts +74 -0
  92. package/dist/connectors/telegram/botApiConnection.d.ts.map +1 -0
  93. package/dist/connectors/telegram/botApiConnection.js +201 -0
  94. package/dist/connectors/telegram/botApiConnection.js.map +1 -0
  95. package/dist/connectors/telegram/commands/botCore.d.ts +8 -2
  96. package/dist/connectors/telegram/commands/botCore.d.ts.map +1 -1
  97. package/dist/connectors/telegram/commands/botSkills.d.ts +21 -0
  98. package/dist/connectors/telegram/commands/botSkills.d.ts.map +1 -0
  99. package/dist/connectors/telegram/commands/botSkills.js +133 -0
  100. package/dist/connectors/telegram/commands/botSkills.js.map +1 -0
  101. package/dist/connectors/telegram/commands/callbackMessage.d.ts +5 -0
  102. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  103. package/dist/connectors/telegram/commands/callbackMessage.js +9 -0
  104. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  105. package/dist/connectors/telegram/commands/compaction.d.ts +65 -3
  106. package/dist/connectors/telegram/commands/compaction.d.ts.map +1 -1
  107. package/dist/connectors/telegram/commands/compaction.js +512 -94
  108. package/dist/connectors/telegram/commands/compaction.js.map +1 -1
  109. package/dist/connectors/telegram/commands/displayModes.d.ts +1 -1
  110. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  111. package/dist/connectors/telegram/commands/displayModes.js +6 -18
  112. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  113. package/dist/connectors/telegram/commands/fallbackChain.js +5 -5
  114. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  115. package/dist/connectors/telegram/commands/reminders.d.ts +1 -1
  116. package/dist/connectors/telegram/commands/reminders.d.ts.map +1 -1
  117. package/dist/connectors/telegram/commands/reminders.js +4 -15
  118. package/dist/connectors/telegram/commands/reminders.js.map +1 -1
  119. package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -1
  120. package/dist/connectors/telegram/commands/selfUpdate.js +1 -11
  121. package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -1
  122. package/dist/connectors/telegram/commands/taskAgents.d.ts +22 -0
  123. package/dist/connectors/telegram/commands/taskAgents.d.ts.map +1 -0
  124. package/dist/connectors/telegram/commands/taskAgents.js +104 -0
  125. package/dist/connectors/telegram/commands/taskAgents.js.map +1 -0
  126. package/dist/connectors/telegram/commands/todo.d.ts +82 -0
  127. package/dist/connectors/telegram/commands/todo.d.ts.map +1 -0
  128. package/dist/connectors/telegram/commands/todo.js +384 -0
  129. package/dist/connectors/telegram/commands/todo.js.map +1 -0
  130. package/dist/connectors/telegram/commands/topicCommand.d.ts +6 -2
  131. package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -1
  132. package/dist/connectors/telegram/commands/topicCommand.js +6 -9
  133. package/dist/connectors/telegram/commands/topicCommand.js.map +1 -1
  134. package/dist/connectors/telegram/openBindMessages.d.ts +45 -0
  135. package/dist/connectors/telegram/openBindMessages.d.ts.map +1 -0
  136. package/dist/connectors/telegram/openBindMessages.js +67 -0
  137. package/dist/connectors/telegram/openBindMessages.js.map +1 -0
  138. package/dist/connectors/telegram/outputFlushPlan.d.ts +49 -0
  139. package/dist/connectors/telegram/outputFlushPlan.d.ts.map +1 -1
  140. package/dist/connectors/telegram/outputFlushPlan.js +46 -0
  141. package/dist/connectors/telegram/outputFlushPlan.js.map +1 -1
  142. package/dist/connectors/telegram/pendingTopicName.d.ts +26 -0
  143. package/dist/connectors/telegram/pendingTopicName.d.ts.map +1 -0
  144. package/dist/connectors/telegram/pendingTopicName.js +27 -0
  145. package/dist/connectors/telegram/pendingTopicName.js.map +1 -0
  146. package/dist/connectors/test/connector.d.ts.map +1 -1
  147. package/dist/connectors/test/connector.js +6 -1
  148. package/dist/connectors/test/connector.js.map +1 -1
  149. package/dist/i18n/de.d.ts.map +1 -1
  150. package/dist/i18n/de.js +90 -7
  151. package/dist/i18n/de.js.map +1 -1
  152. package/dist/i18n/en.d.ts.map +1 -1
  153. package/dist/i18n/en.js +90 -7
  154. package/dist/i18n/en.js.map +1 -1
  155. package/dist/i18n/es.d.ts.map +1 -1
  156. package/dist/i18n/es.js +90 -7
  157. package/dist/i18n/es.js.map +1 -1
  158. package/dist/i18n/fr.d.ts.map +1 -1
  159. package/dist/i18n/fr.js +90 -7
  160. package/dist/i18n/fr.js.map +1 -1
  161. package/dist/i18n/hi.d.ts.map +1 -1
  162. package/dist/i18n/hi.js +90 -7
  163. package/dist/i18n/hi.js.map +1 -1
  164. package/dist/i18n/ja.d.ts.map +1 -1
  165. package/dist/i18n/ja.js +90 -7
  166. package/dist/i18n/ja.js.map +1 -1
  167. package/dist/i18n/ka.d.ts.map +1 -1
  168. package/dist/i18n/ka.js +90 -7
  169. package/dist/i18n/ka.js.map +1 -1
  170. package/dist/i18n/pt.d.ts.map +1 -1
  171. package/dist/i18n/pt.js +90 -7
  172. package/dist/i18n/pt.js.map +1 -1
  173. package/dist/i18n/ru.d.ts.map +1 -1
  174. package/dist/i18n/ru.js +90 -7
  175. package/dist/i18n/ru.js.map +1 -1
  176. package/dist/i18n/uk.d.ts.map +1 -1
  177. package/dist/i18n/uk.js +90 -7
  178. package/dist/i18n/uk.js.map +1 -1
  179. package/dist/i18n/uz.d.ts.map +1 -1
  180. package/dist/i18n/uz.js +90 -7
  181. package/dist/i18n/uz.js.map +1 -1
  182. package/dist/i18n/zh.d.ts.map +1 -1
  183. package/dist/i18n/zh.js +90 -7
  184. package/dist/i18n/zh.js.map +1 -1
  185. package/dist/i18n.d.ts +1 -1
  186. package/dist/i18n.js +1 -1
  187. package/dist/installManager.d.ts +13 -0
  188. package/dist/installManager.d.ts.map +1 -1
  189. package/dist/installManager.js +38 -4
  190. package/dist/installManager.js.map +1 -1
  191. package/dist/instanceSettings.d.ts +40 -1
  192. package/dist/instanceSettings.d.ts.map +1 -1
  193. package/dist/instanceSettings.js +105 -26
  194. package/dist/instanceSettings.js.map +1 -1
  195. package/dist/openCodeQuestionFlow.d.ts +30 -0
  196. package/dist/openCodeQuestionFlow.d.ts.map +1 -1
  197. package/dist/openCodeQuestionFlow.js +43 -0
  198. package/dist/openCodeQuestionFlow.js.map +1 -1
  199. package/dist/postToSession.d.ts +20 -5
  200. package/dist/postToSession.d.ts.map +1 -1
  201. package/dist/postToSession.js +38 -7
  202. package/dist/postToSession.js.map +1 -1
  203. package/dist/remoteRun/cliArgs.d.ts +73 -0
  204. package/dist/remoteRun/cliArgs.d.ts.map +1 -0
  205. package/dist/remoteRun/cliArgs.js +197 -0
  206. package/dist/remoteRun/cliArgs.js.map +1 -0
  207. package/dist/remoteRun/commands.d.ts +56 -0
  208. package/dist/remoteRun/commands.d.ts.map +1 -0
  209. package/dist/remoteRun/commands.js +306 -0
  210. package/dist/remoteRun/commands.js.map +1 -0
  211. package/dist/remoteRun/depsPlan.d.ts +16 -0
  212. package/dist/remoteRun/depsPlan.d.ts.map +1 -0
  213. package/dist/remoteRun/depsPlan.js +82 -0
  214. package/dist/remoteRun/depsPlan.js.map +1 -0
  215. package/dist/remoteRun/exitCodes.d.ts +34 -0
  216. package/dist/remoteRun/exitCodes.d.ts.map +1 -0
  217. package/dist/remoteRun/exitCodes.js +38 -0
  218. package/dist/remoteRun/exitCodes.js.map +1 -0
  219. package/dist/remoteRun/foreground.d.ts +29 -0
  220. package/dist/remoteRun/foreground.d.ts.map +1 -0
  221. package/dist/remoteRun/foreground.js +159 -0
  222. package/dist/remoteRun/foreground.js.map +1 -0
  223. package/dist/remoteRun/interrupts.d.ts +17 -0
  224. package/dist/remoteRun/interrupts.d.ts.map +1 -0
  225. package/dist/remoteRun/interrupts.js +41 -0
  226. package/dist/remoteRun/interrupts.js.map +1 -0
  227. package/dist/remoteRun/jobFiles.d.ts +31 -0
  228. package/dist/remoteRun/jobFiles.d.ts.map +1 -0
  229. package/dist/remoteRun/jobFiles.js +84 -0
  230. package/dist/remoteRun/jobFiles.js.map +1 -0
  231. package/dist/remoteRun/jobFormat.d.ts +16 -0
  232. package/dist/remoteRun/jobFormat.d.ts.map +1 -0
  233. package/dist/remoteRun/jobFormat.js +130 -0
  234. package/dist/remoteRun/jobFormat.js.map +1 -0
  235. package/dist/remoteRun/jobId.d.ts +15 -0
  236. package/dist/remoteRun/jobId.d.ts.map +1 -0
  237. package/dist/remoteRun/jobId.js +85 -0
  238. package/dist/remoteRun/jobId.js.map +1 -0
  239. package/dist/remoteRun/jobMeta.d.ts +18 -0
  240. package/dist/remoteRun/jobMeta.d.ts.map +1 -0
  241. package/dist/remoteRun/jobMeta.js +33 -0
  242. package/dist/remoteRun/jobMeta.js.map +1 -0
  243. package/dist/remoteRun/jobQuery.d.ts +12 -0
  244. package/dist/remoteRun/jobQuery.d.ts.map +1 -0
  245. package/dist/remoteRun/jobQuery.js +22 -0
  246. package/dist/remoteRun/jobQuery.js.map +1 -0
  247. package/dist/remoteRun/jobRecords.d.ts +22 -0
  248. package/dist/remoteRun/jobRecords.d.ts.map +1 -0
  249. package/dist/remoteRun/jobRecords.js +145 -0
  250. package/dist/remoteRun/jobRecords.js.map +1 -0
  251. package/dist/remoteRun/jobState.d.ts +62 -0
  252. package/dist/remoteRun/jobState.d.ts.map +1 -0
  253. package/dist/remoteRun/jobState.js +132 -0
  254. package/dist/remoteRun/jobState.js.map +1 -0
  255. package/dist/remoteRun/leaseTiming.d.ts +18 -0
  256. package/dist/remoteRun/leaseTiming.d.ts.map +1 -0
  257. package/dist/remoteRun/leaseTiming.js +43 -0
  258. package/dist/remoteRun/leaseTiming.js.map +1 -0
  259. package/dist/remoteRun/output.d.ts +35 -0
  260. package/dist/remoteRun/output.d.ts.map +1 -0
  261. package/dist/remoteRun/output.js +82 -0
  262. package/dist/remoteRun/output.js.map +1 -0
  263. package/dist/remoteRun/pathPatterns.d.ts +33 -0
  264. package/dist/remoteRun/pathPatterns.d.ts.map +1 -0
  265. package/dist/remoteRun/pathPatterns.js +115 -0
  266. package/dist/remoteRun/pathPatterns.js.map +1 -0
  267. package/dist/remoteRun/probe.d.ts +56 -0
  268. package/dist/remoteRun/probe.d.ts.map +1 -0
  269. package/dist/remoteRun/probe.js +123 -0
  270. package/dist/remoteRun/probe.js.map +1 -0
  271. package/dist/remoteRun/project.d.ts +26 -0
  272. package/dist/remoteRun/project.d.ts.map +1 -0
  273. package/dist/remoteRun/project.js +159 -0
  274. package/dist/remoteRun/project.js.map +1 -0
  275. package/dist/remoteRun/projectConfig.d.ts +38 -0
  276. package/dist/remoteRun/projectConfig.d.ts.map +1 -0
  277. package/dist/remoteRun/projectConfig.js +87 -0
  278. package/dist/remoteRun/projectConfig.js.map +1 -0
  279. package/dist/remoteRun/remoteRunCli.d.ts +15 -0
  280. package/dist/remoteRun/remoteRunCli.d.ts.map +1 -0
  281. package/dist/remoteRun/remoteRunCli.js +144 -0
  282. package/dist/remoteRun/remoteRunCli.js.map +1 -0
  283. package/dist/remoteRun/remoteRunError.d.ts +14 -0
  284. package/dist/remoteRun/remoteRunError.d.ts.map +1 -0
  285. package/dist/remoteRun/remoteRunError.js +28 -0
  286. package/dist/remoteRun/remoteRunError.js.map +1 -0
  287. package/dist/remoteRun/rsync.d.ts +17 -0
  288. package/dist/remoteRun/rsync.d.ts.map +1 -0
  289. package/dist/remoteRun/rsync.js +39 -0
  290. package/dist/remoteRun/rsync.js.map +1 -0
  291. package/dist/remoteRun/runnerClient.d.ts +50 -0
  292. package/dist/remoteRun/runnerClient.d.ts.map +1 -0
  293. package/dist/remoteRun/runnerClient.js +139 -0
  294. package/dist/remoteRun/runnerClient.js.map +1 -0
  295. package/dist/remoteRun/runners.d.ts +57 -0
  296. package/dist/remoteRun/runners.d.ts.map +1 -0
  297. package/dist/remoteRun/runners.js +181 -0
  298. package/dist/remoteRun/runners.js.map +1 -0
  299. package/dist/remoteRun/runnersConfig.d.ts +44 -0
  300. package/dist/remoteRun/runnersConfig.d.ts.map +1 -0
  301. package/dist/remoteRun/runnersConfig.js +107 -0
  302. package/dist/remoteRun/runnersConfig.js.map +1 -0
  303. package/dist/remoteRun/safeCopy.d.ts +31 -0
  304. package/dist/remoteRun/safeCopy.d.ts.map +1 -0
  305. package/dist/remoteRun/safeCopy.js +137 -0
  306. package/dist/remoteRun/safeCopy.js.map +1 -0
  307. package/dist/remoteRun/sendPlan.d.ts +67 -0
  308. package/dist/remoteRun/sendPlan.d.ts.map +1 -0
  309. package/dist/remoteRun/sendPlan.js +139 -0
  310. package/dist/remoteRun/sendPlan.js.map +1 -0
  311. package/dist/remoteRun/ssh.d.ts +62 -0
  312. package/dist/remoteRun/ssh.d.ts.map +1 -0
  313. package/dist/remoteRun/ssh.js +202 -0
  314. package/dist/remoteRun/ssh.js.map +1 -0
  315. package/dist/remoteRun/submit.d.ts +29 -0
  316. package/dist/remoteRun/submit.d.ts.map +1 -0
  317. package/dist/remoteRun/submit.js +142 -0
  318. package/dist/remoteRun/submit.js.map +1 -0
  319. package/dist/requests/answerRequest.d.ts +5 -0
  320. package/dist/requests/answerRequest.d.ts.map +1 -1
  321. package/dist/requests/answerRequest.js +16 -1
  322. package/dist/requests/answerRequest.js.map +1 -1
  323. package/dist/requests/requestLedger.d.ts +13 -0
  324. package/dist/requests/requestLedger.d.ts.map +1 -1
  325. package/dist/requests/requestLedger.js +9 -0
  326. package/dist/requests/requestLedger.js.map +1 -1
  327. package/dist/requests/topicRequestSettle.d.ts +30 -0
  328. package/dist/requests/topicRequestSettle.d.ts.map +1 -0
  329. package/dist/requests/topicRequestSettle.js +23 -0
  330. package/dist/requests/topicRequestSettle.js.map +1 -0
  331. package/dist/requests/wakeUpEngine.d.ts +4 -0
  332. package/dist/requests/wakeUpEngine.d.ts.map +1 -1
  333. package/dist/requests/wakeUpEngine.js +5 -1
  334. package/dist/requests/wakeUpEngine.js.map +1 -1
  335. package/dist/scheduler/delivery.d.ts +2 -0
  336. package/dist/scheduler/delivery.d.ts.map +1 -1
  337. package/dist/scheduler/delivery.js +8 -4
  338. package/dist/scheduler/delivery.js.map +1 -1
  339. package/dist/scheduler/mcpSurface.d.ts +59 -10
  340. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  341. package/dist/scheduler/mcpSurface.js +276 -79
  342. package/dist/scheduler/mcpSurface.js.map +1 -1
  343. package/dist/scheduler/recurrence.d.ts +5 -0
  344. package/dist/scheduler/recurrence.d.ts.map +1 -1
  345. package/dist/scheduler/recurrence.js +12 -2
  346. package/dist/scheduler/recurrence.js.map +1 -1
  347. package/dist/scheduler/strictToolInput.d.ts +45 -0
  348. package/dist/scheduler/strictToolInput.d.ts.map +1 -0
  349. package/dist/scheduler/strictToolInput.js +155 -0
  350. package/dist/scheduler/strictToolInput.js.map +1 -0
  351. package/dist/scheduler/types.d.ts +5 -0
  352. package/dist/scheduler/types.d.ts.map +1 -1
  353. package/dist/scheduler/workerWatch.d.ts +23 -0
  354. package/dist/scheduler/workerWatch.d.ts.map +1 -0
  355. package/dist/scheduler/workerWatch.js +26 -0
  356. package/dist/scheduler/workerWatch.js.map +1 -0
  357. package/dist/selfUpdate/emittedSource.d.ts +11 -0
  358. package/dist/selfUpdate/emittedSource.d.ts.map +1 -0
  359. package/dist/selfUpdate/emittedSource.js +24 -0
  360. package/dist/selfUpdate/emittedSource.js.map +1 -0
  361. package/dist/selfUpdate/gitSource.d.ts +0 -6
  362. package/dist/selfUpdate/gitSource.d.ts.map +1 -1
  363. package/dist/selfUpdate/gitSource.js +2 -18
  364. package/dist/selfUpdate/gitSource.js.map +1 -1
  365. package/dist/selfUpdate/updatePending.d.ts +1 -1
  366. package/dist/sendErrorClassifier.d.ts +10 -0
  367. package/dist/sendErrorClassifier.d.ts.map +1 -1
  368. package/dist/sendErrorClassifier.js +22 -0
  369. package/dist/sendErrorClassifier.js.map +1 -1
  370. package/dist/state.d.ts +78 -7
  371. package/dist/state.d.ts.map +1 -1
  372. package/dist/state.js +125 -8
  373. package/dist/state.js.map +1 -1
  374. package/dist/stateFile.d.ts +9 -0
  375. package/dist/stateFile.d.ts.map +1 -0
  376. package/dist/stateFile.js +57 -0
  377. package/dist/stateFile.js.map +1 -0
  378. package/dist/stt/sttInstall.d.ts +7 -3
  379. package/dist/stt/sttInstall.d.ts.map +1 -1
  380. package/dist/stt/sttInstall.js +8 -4
  381. package/dist/stt/sttInstall.js.map +1 -1
  382. package/dist/stt/sttSettings.d.ts +2 -1
  383. package/dist/stt/sttSettings.d.ts.map +1 -1
  384. package/dist/stt/sttSettings.js +3 -2
  385. package/dist/stt/sttSettings.js.map +1 -1
  386. package/dist/stt/transcribeChain.js +2 -0
  387. package/dist/stt/transcribeChain.js.map +1 -1
  388. package/dist/testTimings.d.ts +13 -3
  389. package/dist/testTimings.d.ts.map +1 -1
  390. package/dist/testTimings.js +26 -2
  391. package/dist/testTimings.js.map +1 -1
  392. package/dist/todos/board.d.ts +83 -0
  393. package/dist/todos/board.d.ts.map +1 -0
  394. package/dist/todos/board.js +227 -0
  395. package/dist/todos/board.js.map +1 -0
  396. package/dist/todos/format.d.ts +33 -0
  397. package/dist/todos/format.d.ts.map +1 -0
  398. package/dist/todos/format.js +90 -0
  399. package/dist/todos/format.js.map +1 -0
  400. package/dist/todos/merge.d.ts +46 -0
  401. package/dist/todos/merge.d.ts.map +1 -0
  402. package/dist/todos/merge.js +137 -0
  403. package/dist/todos/merge.js.map +1 -0
  404. package/dist/todos/midTurn.d.ts +75 -0
  405. package/dist/todos/midTurn.d.ts.map +1 -0
  406. package/dist/todos/midTurn.js +214 -0
  407. package/dist/todos/midTurn.js.map +1 -0
  408. package/dist/todos/nag.d.ts +85 -0
  409. package/dist/todos/nag.d.ts.map +1 -0
  410. package/dist/todos/nag.js +103 -0
  411. package/dist/todos/nag.js.map +1 -0
  412. package/dist/todos/order.d.ts +25 -0
  413. package/dist/todos/order.d.ts.map +1 -0
  414. package/dist/todos/order.js +61 -0
  415. package/dist/todos/order.js.map +1 -0
  416. package/dist/todos/projects.d.ts +102 -0
  417. package/dist/todos/projects.d.ts.map +1 -0
  418. package/dist/todos/projects.js +342 -0
  419. package/dist/todos/projects.js.map +1 -0
  420. package/dist/todos/registry.d.ts +64 -0
  421. package/dist/todos/registry.d.ts.map +1 -0
  422. package/dist/todos/registry.js +116 -0
  423. package/dist/todos/registry.js.map +1 -0
  424. package/dist/todos/reminderEngine.d.ts +78 -0
  425. package/dist/todos/reminderEngine.d.ts.map +1 -0
  426. package/dist/todos/reminderEngine.js +162 -0
  427. package/dist/todos/reminderEngine.js.map +1 -0
  428. package/dist/todos/store.d.ts +34 -0
  429. package/dist/todos/store.d.ts.map +1 -0
  430. package/dist/todos/store.js +208 -0
  431. package/dist/todos/store.js.map +1 -0
  432. package/dist/todos/tools.d.ts +26 -0
  433. package/dist/todos/tools.d.ts.map +1 -0
  434. package/dist/todos/tools.js +25 -0
  435. package/dist/todos/tools.js.map +1 -0
  436. package/dist/todos/types.d.ts +85 -0
  437. package/dist/todos/types.d.ts.map +1 -0
  438. package/dist/todos/types.js +30 -0
  439. package/dist/todos/types.js.map +1 -0
  440. package/dist/trackers/taskAgentLimit.d.ts +58 -0
  441. package/dist/trackers/taskAgentLimit.d.ts.map +1 -0
  442. package/dist/trackers/taskAgentLimit.js +62 -0
  443. package/dist/trackers/taskAgentLimit.js.map +1 -0
  444. package/dist/trackers/taskAgentSlots.d.ts +109 -0
  445. package/dist/trackers/taskAgentSlots.d.ts.map +1 -0
  446. package/dist/trackers/taskAgentSlots.js +227 -0
  447. package/dist/trackers/taskAgentSlots.js.map +1 -0
  448. package/dist/trackers/types.d.ts +14 -0
  449. package/dist/trackers/types.d.ts.map +1 -0
  450. package/dist/trackers/types.js +3 -0
  451. package/dist/trackers/types.js.map +1 -0
  452. package/dist/types.d.ts +80 -4
  453. package/dist/types.d.ts.map +1 -1
  454. package/dist/utils/adoptReplayGate.d.ts +16 -0
  455. package/dist/utils/adoptReplayGate.d.ts.map +1 -0
  456. package/dist/utils/adoptReplayGate.js +35 -0
  457. package/dist/utils/adoptReplayGate.js.map +1 -0
  458. package/dist/utils/agentPlugin.d.ts +113 -0
  459. package/dist/utils/agentPlugin.d.ts.map +1 -0
  460. package/dist/utils/agentPlugin.js +192 -0
  461. package/dist/utils/agentPlugin.js.map +1 -0
  462. package/dist/utils/autoContinueOnLimit.d.ts +7 -4
  463. package/dist/utils/autoContinueOnLimit.d.ts.map +1 -1
  464. package/dist/utils/autoContinueOnLimit.js +8 -5
  465. package/dist/utils/autoContinueOnLimit.js.map +1 -1
  466. package/dist/utils/botSkills.d.ts +62 -0
  467. package/dist/utils/botSkills.d.ts.map +1 -0
  468. package/dist/utils/botSkills.js +53 -0
  469. package/dist/utils/botSkills.js.map +1 -0
  470. package/dist/utils/botSkillsSetting.d.ts +7 -0
  471. package/dist/utils/botSkillsSetting.d.ts.map +1 -0
  472. package/dist/utils/botSkillsSetting.js +9 -0
  473. package/dist/utils/botSkillsSetting.js.map +1 -0
  474. package/dist/utils/claudeCodeVersion.d.ts +7 -0
  475. package/dist/utils/claudeCodeVersion.d.ts.map +1 -1
  476. package/dist/utils/claudeCodeVersion.js +14 -1
  477. package/dist/utils/claudeCodeVersion.js.map +1 -1
  478. package/dist/utils/claudeCompactHook.d.ts +33 -15
  479. package/dist/utils/claudeCompactHook.d.ts.map +1 -1
  480. package/dist/utils/claudeCompactHook.js +36 -17
  481. package/dist/utils/claudeCompactHook.js.map +1 -1
  482. package/dist/utils/claudeCompactSummary.d.ts +15 -0
  483. package/dist/utils/claudeCompactSummary.d.ts.map +1 -0
  484. package/dist/utils/claudeCompactSummary.js +96 -0
  485. package/dist/utils/claudeCompactSummary.js.map +1 -0
  486. package/dist/utils/claudeLimitSwitch.d.ts +11 -9
  487. package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
  488. package/dist/utils/claudeLimitSwitch.js +18 -16
  489. package/dist/utils/claudeLimitSwitch.js.map +1 -1
  490. package/dist/utils/claudeProfiles.d.ts +5 -0
  491. package/dist/utils/claudeProfiles.d.ts.map +1 -1
  492. package/dist/utils/claudeProfiles.js +14 -0
  493. package/dist/utils/claudeProfiles.js.map +1 -1
  494. package/dist/utils/claudeSessionTitle.d.ts +2 -1
  495. package/dist/utils/claudeSessionTitle.d.ts.map +1 -1
  496. package/dist/utils/claudeSessionTitle.js +14 -14
  497. package/dist/utils/claudeSessionTitle.js.map +1 -1
  498. package/dist/utils/claudeUsage.d.ts +95 -15
  499. package/dist/utils/claudeUsage.d.ts.map +1 -1
  500. package/dist/utils/claudeUsage.js +241 -41
  501. package/dist/utils/claudeUsage.js.map +1 -1
  502. package/dist/utils/claudeUsageRefresher.d.ts +10 -0
  503. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  504. package/dist/utils/claudeUsageRefresher.js +11 -0
  505. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  506. package/dist/utils/claudeUserEntry.d.ts +50 -0
  507. package/dist/utils/claudeUserEntry.d.ts.map +1 -0
  508. package/dist/utils/claudeUserEntry.js +120 -0
  509. package/dist/utils/claudeUserEntry.js.map +1 -0
  510. package/dist/utils/clearThreadOutputQueues.d.ts +4 -3
  511. package/dist/utils/clearThreadOutputQueues.d.ts.map +1 -1
  512. package/dist/utils/clearThreadOutputQueues.js +1 -1
  513. package/dist/utils/clearThreadOutputQueues.js.map +1 -1
  514. package/dist/utils/compactOnIdle.d.ts +33 -13
  515. package/dist/utils/compactOnIdle.d.ts.map +1 -1
  516. package/dist/utils/compactOnIdle.js +35 -15
  517. package/dist/utils/compactOnIdle.js.map +1 -1
  518. package/dist/utils/fallbackBridge.d.ts +5 -0
  519. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  520. package/dist/utils/fallbackBridge.js +19 -3
  521. package/dist/utils/fallbackBridge.js.map +1 -1
  522. package/dist/utils/fallbackChain.d.ts +9 -3
  523. package/dist/utils/fallbackChain.d.ts.map +1 -1
  524. package/dist/utils/fallbackChain.js +21 -3
  525. package/dist/utils/fallbackChain.js.map +1 -1
  526. package/dist/utils/fileSendPlan.d.ts +6 -0
  527. package/dist/utils/fileSendPlan.d.ts.map +1 -1
  528. package/dist/utils/fileSendPlan.js +11 -0
  529. package/dist/utils/fileSendPlan.js.map +1 -1
  530. package/dist/utils/fileSendService.d.ts +7 -1
  531. package/dist/utils/fileSendService.d.ts.map +1 -1
  532. package/dist/utils/fileSendService.js +6 -2
  533. package/dist/utils/fileSendService.js.map +1 -1
  534. package/dist/utils/groupFinalizePlan.d.ts +8 -10
  535. package/dist/utils/groupFinalizePlan.d.ts.map +1 -1
  536. package/dist/utils/groupFinalizePlan.js +8 -12
  537. package/dist/utils/groupFinalizePlan.js.map +1 -1
  538. package/dist/utils/jsonStreamHost.d.ts +13 -0
  539. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  540. package/dist/utils/jsonStreamHost.js +13 -0
  541. package/dist/utils/jsonStreamHost.js.map +1 -1
  542. package/dist/utils/messageSendService.d.ts +5 -3
  543. package/dist/utils/messageSendService.d.ts.map +1 -1
  544. package/dist/utils/messageSendService.js +12 -5
  545. package/dist/utils/messageSendService.js.map +1 -1
  546. package/dist/utils/openCodeCompactPlugin.d.ts +39 -14
  547. package/dist/utils/openCodeCompactPlugin.d.ts.map +1 -1
  548. package/dist/utils/openCodeCompactPlugin.js +106 -38
  549. package/dist/utils/openCodeCompactPlugin.js.map +1 -1
  550. package/dist/utils/promptDelivery.d.ts +29 -12
  551. package/dist/utils/promptDelivery.d.ts.map +1 -1
  552. package/dist/utils/promptDelivery.js +13 -5
  553. package/dist/utils/promptDelivery.js.map +1 -1
  554. package/dist/utils/sentKindsSummary.d.ts +10 -0
  555. package/dist/utils/sentKindsSummary.d.ts.map +1 -0
  556. package/dist/utils/sentKindsSummary.js +18 -0
  557. package/dist/utils/sentKindsSummary.js.map +1 -0
  558. package/dist/utils/telegramError.d.ts +6 -0
  559. package/dist/utils/telegramError.d.ts.map +1 -1
  560. package/dist/utils/telegramError.js +12 -0
  561. package/dist/utils/telegramError.js.map +1 -1
  562. package/dist/utils/topicView.d.ts +1 -1
  563. package/dist/utils/topicView.js +1 -1
  564. package/dist/utils/transcribeAudio.d.ts +10 -0
  565. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  566. package/dist/utils/transcribeAudio.js +22 -3
  567. package/dist/utils/transcribeAudio.js.map +1 -1
  568. package/dist/utils/transcriptErrorTime.d.ts +8 -0
  569. package/dist/utils/transcriptErrorTime.d.ts.map +1 -0
  570. package/dist/utils/transcriptErrorTime.js +56 -0
  571. package/dist/utils/transcriptErrorTime.js.map +1 -0
  572. package/dist/utils/wedgeRecovery.d.ts +2 -0
  573. package/dist/utils/wedgeRecovery.d.ts.map +1 -1
  574. package/dist/utils/wedgeRecovery.js +1 -1
  575. package/dist/utils/wedgeRecovery.js.map +1 -1
  576. package/package.json +11 -3
  577. package/remoteRun/runner.sh +1242 -0
  578. package/stt/gigaam_sidecar.py +555 -50
@@ -0,0 +1,1242 @@
1
+ #!/usr/bin/env bash
2
+ # charness remote-run — the script that runs ON a runner (never on the main box).
3
+ #
4
+ # `charness remote-run` uploads this file to <workDir>/bin/runner-<sha8>.sh and calls it over ssh. Every subcommand takes
5
+ # the runner's work dir first: runner.sh <subcommand> <workDir> [arguments…]
6
+ # Pueue is reached through PUEUE_BIN / PUEUE_CONFIG (both optional), set by the caller on every call.
7
+ #
8
+ # bash 3.2-compatible on purpose (the stock bash of macOS): no associative arrays, no `wait -n`, no `mapfile`, no
9
+ # `timeout(1)`, no `read -t` (its timeout status differs between 3.2 and 4+), no GNU-only flags. Time limits are done by background watchdogs.
10
+ #
11
+ # Subcommands (the contract is in src/remoteRun/README.md):
12
+ # probe key=value lines: OS, CPU, RAM, disk, jobs, capabilities
13
+ # prepare layout for a job, Pueue daemon + groups, prune
14
+ # lease add the job to Pueue, stream its log, kill it when the caller's heartbeat stops (foreground run)
15
+ # submit add the job to Pueue and return (--bg)
16
+ # job the Pueue task itself: lock → apply the staged tree → deps → command → result files
17
+ # follow stream a job's log until it ends (logs -f)
18
+ # wait block until the job ends
19
+ # result key=value facts of a job (exit code, phase, markers)
20
+ # meta the job's meta.json
21
+ # tail the last N lines of a job's log
22
+ # kill stop a job: SIGTERM, SIGKILL after 10 s
23
+ # exec one-off command in the project's copy, leased like a foreground job
24
+ # workdir the work dir as an absolute path
25
+ # pueue run Pueue itself with the configured binary and config
26
+ # jobs the meta.json of every job of a project (or of all), as a JSON array
27
+ # prune remove old jobs, stale stage dirs and unused project copies
28
+ # sync-tree the apply step alone (tests)
29
+
30
+ LC_ALL=C
31
+ export LC_ALL
32
+ set -u
33
+
34
+ # ─────────────────────────── constants ───────────────────────────
35
+
36
+ kill_grace_sec=10
37
+ pueue_start_wait_sec=8
38
+ default_timeout_sec=10800
39
+ job_keep_days=3
40
+ copy_keep_days=30
41
+ stage_keep_days=1
42
+ drain_interval_sec=0.2
43
+ probe_step_sec=4
44
+ exit_file_wait_sec=5
45
+ task_id_wait_sec=5
46
+ heartbeat_poll_sec=1
47
+ # How long a job waiting for its project's lock sleeps between two attempts (a test shortens it to race many waiters quickly).
48
+ lock_poll_sec=1
49
+
50
+ # ─────────────────────────── helpers ───────────────────────────
51
+
52
+ die() {
53
+ printf 'remote-run-runner: %s\n' "$*" >&2
54
+ exit 125
55
+ }
56
+
57
+ # Own progress lines of a job, written into its log; the caller shows lines with this prefix on stderr.
58
+ say() {
59
+ printf 'remote-run: %s\n' "$*"
60
+ }
61
+
62
+ pq() {
63
+ "${PUEUE_BIN:-pueue}" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} "$@"
64
+ }
65
+
66
+ pueued_bin() {
67
+ if [ -n "${PUEUE_BIN:-}" ] && [ -x "$(dirname "$PUEUE_BIN")/pueued" ]; then
68
+ printf '%s\n' "$(dirname "$PUEUE_BIN")/pueued"
69
+ else
70
+ printf 'pueued\n'
71
+ fi
72
+ }
73
+
74
+ # A name that is safe to use as one path component.
75
+ check_name() {
76
+ case $1 in
77
+ '' | *[!A-Za-z0-9._-]* | .*) die "bad name: $1" ;;
78
+ esac
79
+ }
80
+
81
+ # A relative path inside a project: no leading slash, no ".." component.
82
+ check_relative_path() {
83
+ case $1 in
84
+ /*) return 1 ;;
85
+ esac
86
+ case "/$1/" in
87
+ */../*) return 1 ;;
88
+ esac
89
+ return 0
90
+ }
91
+
92
+ # Sets work, slug, job and every directory derived from them. A slug of "-" means "the project this job id belongs to" (the commands
93
+ # that take only a job id): found by looking for the job's folder.
94
+ set_paths() {
95
+ work=$1
96
+ slug=${2:-}
97
+ job=${3:-}
98
+ if [ "$slug" = "-" ] && [ -n "$job" ]; then
99
+ slug=
100
+ local found
101
+ for found in "$work"/p/*/jobs/"$job"; do
102
+ [ -d "$found" ] && slug=$(basename "$(dirname "$(dirname "$found")")") && break
103
+ done
104
+ [ -n "$slug" ] || die "no such job: $job"
105
+ fi
106
+ check_name "$slug"
107
+ proj="$work/p/$slug"
108
+ tree="$proj/tree"
109
+ jobs_dir="$proj/jobs"
110
+ lock="$proj/lock"
111
+ if [ -n "$job" ]; then
112
+ case $job in *[!a-z0-9-]* | -*) die "bad job id: $job" ;; esac
113
+ jd="$jobs_dir/$job"
114
+ stage="$proj/stage/$job"
115
+ fi
116
+ }
117
+
118
+ set_phase() {
119
+ printf '%s\n' "$1" > "$jd/phase"
120
+ }
121
+
122
+ read_file() {
123
+ cat "$1" 2>/dev/null
124
+ }
125
+
126
+ # ─────────────────────────── probe ───────────────────────────
127
+
128
+ kb_to_mb() {
129
+ awk -v kb="$1" 'BEGIN { printf "%d", kb / 1024 }'
130
+ }
131
+
132
+ probe_linux() {
133
+ local os_name os_version
134
+ os_name=Linux
135
+ os_version=
136
+ if [ -r /etc/os-release ]; then
137
+ os_name=$(. /etc/os-release; printf '%s' "${NAME:-Linux}")
138
+ os_version=$(. /etc/os-release; printf '%s' "${VERSION_ID:-}")
139
+ fi
140
+ echo "os_name=$os_name"
141
+ echo "os_version=$os_version"
142
+ echo "cpu_model=$(sed -n 's/^model name[[:space:]]*:[[:space:]]*//p' /proc/cpuinfo | head -n 1)"
143
+ local threads cores
144
+ threads=$(grep -c '^processor' /proc/cpuinfo)
145
+ cores=$(awk -F: '/^physical id/ { p = $2 } /^core id/ { print p ":" $2 }' /proc/cpuinfo | sort -u | wc -l | tr -d ' ')
146
+ [ "$cores" -gt 0 ] 2> /dev/null || cores=$threads
147
+ echo "cpu_cores=$cores"
148
+ echo "cpu_threads=$threads"
149
+ echo "load=$(cut -d' ' -f1 /proc/loadavg)"
150
+ echo "ram_total_mb=$(kb_to_mb "$(sed -n 's/^MemTotal:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo)")"
151
+ echo "ram_free_mb=$(kb_to_mb "$(sed -n 's/^MemAvailable:[[:space:]]*\([0-9]*\).*/\1/p' /proc/meminfo)")"
152
+ }
153
+
154
+ probe_darwin() {
155
+ echo "os_name=$(sw_vers -productName 2> /dev/null || echo macOS)"
156
+ echo "os_version=$(sw_vers -productVersion 2> /dev/null)"
157
+ echo "cpu_model=$(sysctl -n machdep.cpu.brand_string 2> /dev/null)"
158
+ echo "cpu_cores=$(sysctl -n hw.physicalcpu 2> /dev/null)"
159
+ echo "cpu_threads=$(sysctl -n hw.logicalcpu 2> /dev/null)"
160
+ echo "load=$(sysctl -n vm.loadavg 2> /dev/null | tr -d '{}' | awk '{ print $1 }')"
161
+ local mem_bytes page_size free_pages
162
+ mem_bytes=$(sysctl -n hw.memsize 2> /dev/null)
163
+ page_size=$(sysctl -n hw.pagesize 2> /dev/null)
164
+ free_pages=$(vm_stat 2> /dev/null | awk '/^Pages (free|inactive|speculative):/ { gsub("\\.", "", $NF); sum += $NF } END { print sum + 0 }')
165
+ echo "ram_total_mb=$(awk -v b="$mem_bytes" 'BEGIN { printf "%d", b / 1048576 }')"
166
+ echo "ram_free_mb=$(awk -v p="$free_pages" -v s="${page_size:-4096}" 'BEGIN { printf "%d", p * s / 1048576 }')"
167
+ }
168
+
169
+ # capability lines: cap_<name>=yes|no and cap_<name>_detail=<words>
170
+ probe_android() {
171
+ local acceleration=
172
+ case $(uname -s) in
173
+ Linux) [ -r /dev/kvm ] && [ -w /dev/kvm ] && acceleration=KVM ;;
174
+ Darwin) [ "$(sysctl -n kern.hv_support 2> /dev/null)" = 1 ] && acceleration=HVF ;;
175
+ esac
176
+ local emulator_path=
177
+ local sdk_root
178
+ for sdk_root in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}"; do
179
+ [ -n "$sdk_root" ] && [ -x "$sdk_root/emulator/emulator" ] && emulator_path="$sdk_root/emulator/emulator" && break
180
+ done
181
+ [ -z "$emulator_path" ] && emulator_path=$(command -v emulator 2> /dev/null || true)
182
+ if [ -n "$acceleration" ] && [ -n "$emulator_path" ]; then
183
+ echo "cap_android=yes"
184
+ echo "cap_android_detail=$acceleration, emulator"
185
+ else
186
+ echo "cap_android=no"
187
+ if [ -z "$acceleration" ] && [ -z "$emulator_path" ]; then
188
+ echo "cap_android_detail=no hardware acceleration, no emulator binary"
189
+ elif [ -z "$acceleration" ]; then
190
+ echo "cap_android_detail=no hardware acceleration (KVM / HVF)"
191
+ else
192
+ echo "cap_android_detail=no emulator binary"
193
+ fi
194
+ fi
195
+ }
196
+
197
+ probe_playwright() {
198
+ local browsers_dir=${PLAYWRIGHT_BROWSERS_PATH:-}
199
+ if [ -z "$browsers_dir" ]; then
200
+ case $(uname -s) in
201
+ Darwin) browsers_dir="$HOME/Library/Caches/ms-playwright" ;;
202
+ *) browsers_dir="$HOME/.cache/ms-playwright" ;;
203
+ esac
204
+ fi
205
+ local found=
206
+ local kind
207
+ for kind in chromium firefox webkit; do
208
+ if ls -d "$browsers_dir/$kind-"* > /dev/null 2>&1; then
209
+ found="${found:+$found, }$kind"
210
+ fi
211
+ done
212
+ if [ -n "$found" ]; then
213
+ echo "cap_playwright=yes"
214
+ echo "cap_playwright_detail=$found"
215
+ else
216
+ echo "cap_playwright=no"
217
+ echo "cap_playwright_detail=no browsers installed"
218
+ fi
219
+ }
220
+
221
+ # Jobs by their files: running = a live job process, queued = submitted and not started, finished = has an exit file.
222
+ probe_jobs() {
223
+ local running=0 queued=0
224
+ local active_slugs=
225
+ local job_dir slug pid
226
+ for job_dir in "$work"/p/*/jobs/*/; do
227
+ [ -d "$job_dir" ] || continue
228
+ [ -f "$job_dir/exit" ] && continue
229
+ [ -f "$job_dir/task-id" ] || continue
230
+ slug=$(basename "$(dirname "$(dirname "$job_dir")")")
231
+ pid=$(read_file "$job_dir/pid")
232
+ if [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null; then
233
+ running=$((running + 1))
234
+ elif [ -z "$pid" ]; then
235
+ queued=$((queued + 1))
236
+ else
237
+ continue
238
+ fi
239
+ case " $active_slugs " in
240
+ *" $slug "*) ;;
241
+ *) active_slugs="$active_slugs $slug" ;;
242
+ esac
243
+ done
244
+ local project_count=0
245
+ local counted
246
+ for counted in $active_slugs; do project_count=$((project_count + 1)); done
247
+ echo "jobs_running=$running"
248
+ echo "jobs_queued=$queued"
249
+ echo "jobs_projects=$project_count"
250
+ }
251
+
252
+ # The probe's slow questions — Pueue, the disk, Xcode and its simulators, Docker — are asked at once, each bounded by probe_step_sec, so
253
+ # a hung one costs that much in all, not that much each: the caller gives the whole probe 20 s, and a Mac with a slow simctl, a stalled
254
+ # mount and a sleeping Docker would otherwise read as offline. One step: output in $probe_dir/<name>.out, its status in <name>.status
255
+ # (137: it was cut off; no file: it never finished).
256
+ start_probe_step() {
257
+ local name=$1
258
+ local limit=$2
259
+ shift 2
260
+ (
261
+ "$@" > "$probe_dir/$name.out" 2> /dev/null < /dev/null &
262
+ local command_pid=$!
263
+ ( sleep "$limit"; kill -9 "$command_pid" ) > /dev/null 2>&1 &
264
+ local watchdog_pid=$!
265
+ wait "$command_pid" 2> /dev/null
266
+ echo "$?" > "$probe_dir/$name.status"
267
+ kill "$watchdog_pid" > /dev/null 2>&1
268
+ ) &
269
+ }
270
+
271
+ probe_step_status() {
272
+ read_file "$probe_dir/$1.status"
273
+ }
274
+
275
+ probe_step_output() {
276
+ read_file "$probe_dir/$1.out"
277
+ }
278
+
279
+ probe_slow() {
280
+ mkdir -p "$work/tmp"
281
+ probe_dir="$work/tmp/probe.$$.$RANDOM"
282
+ mkdir -p "$probe_dir" || return 1
283
+ local is_darwin=0
284
+ [ "$(uname -s)" = Darwin ] && is_darwin=1
285
+ local has_docker=0
286
+ command -v docker > /dev/null 2>&1 && has_docker=1
287
+ start_probe_step pueue "$probe_step_sec" "${PUEUE_BIN:-pueue}" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} --version
288
+ start_probe_step disk "$probe_step_sec" df -Pk "$work"
289
+ if [ "$is_darwin" -eq 1 ]; then
290
+ start_probe_step xcode "$probe_step_sec" xcodebuild -version
291
+ start_probe_step simctl "$probe_step_sec" xcrun simctl list devices available
292
+ fi
293
+ [ "$has_docker" -eq 1 ] && start_probe_step docker "$probe_step_sec" docker info
294
+ wait
295
+
296
+ echo "disk_free_mb=$(kb_to_mb "$(probe_step_output disk | awk 'NR == 2 { print $4 }')")"
297
+ if [ "$(probe_step_status pueue)" = 0 ]; then
298
+ echo "pueue=$(probe_step_output pueue | awk '{ print $NF }')"
299
+ else
300
+ echo "pueue=missing"
301
+ fi
302
+ if [ "$is_darwin" -eq 0 ]; then
303
+ echo "cap_ios=no"
304
+ echo "cap_ios_detail=$(uname -s)"
305
+ elif [ "$(probe_step_status xcode)" = 0 ]; then
306
+ local xcode_version
307
+ xcode_version=$(probe_step_output xcode | head -n 1 | sed 's/^Xcode //')
308
+ echo "cap_ios=yes"
309
+ if [ "$(probe_step_status simctl)" = 0 ]; then
310
+ echo "cap_ios_detail=Xcode $xcode_version, $(probe_step_output simctl | grep -c '(Shutdown)\|(Booted)') simulators"
311
+ else
312
+ # A slow simctl is no reason to say there is no Xcode, nor to invent a simulator count.
313
+ echo "cap_ios_detail=Xcode $xcode_version"
314
+ fi
315
+ else
316
+ echo "cap_ios=no"
317
+ if [ "$(probe_step_status xcode)" = 137 ] || [ -z "$(probe_step_status xcode)" ]; then
318
+ echo "cap_ios_detail=Xcode did not answer in time"
319
+ else
320
+ echo "cap_ios_detail=Xcode not found"
321
+ fi
322
+ fi
323
+ if [ "$has_docker" -eq 0 ]; then
324
+ echo "cap_docker=no"
325
+ echo "cap_docker_detail=docker not installed"
326
+ elif [ "$(probe_step_status docker)" = 0 ]; then
327
+ echo "cap_docker=yes"
328
+ echo "cap_docker_detail="
329
+ else
330
+ echo "cap_docker=no"
331
+ echo "cap_docker_detail=daemon not reachable"
332
+ fi
333
+ rm -rf "$probe_dir"
334
+ }
335
+
336
+ cmd_probe() {
337
+ work=$1
338
+ mkdir -p "$work" 2> /dev/null
339
+ echo "os=$(uname -s)"
340
+ echo "arch=$(uname -m)"
341
+ echo "kernel=$(uname -r)"
342
+ case $(uname -s) in
343
+ Linux) probe_linux ;;
344
+ Darwin) probe_darwin ;;
345
+ esac
346
+ probe_slow
347
+ probe_jobs
348
+ probe_android
349
+ probe_playwright
350
+ }
351
+
352
+ # ─────────────────────────── pueue ───────────────────────────
353
+
354
+ # Start the daemon once when it does not answer, with its output away from the ssh channel (it would keep the call open).
355
+ ensure_pueued() {
356
+ if pq status > /dev/null 2>&1; then
357
+ return 0
358
+ fi
359
+ "$(pueued_bin)" ${PUEUE_CONFIG:+-c "$PUEUE_CONFIG"} -d > /dev/null 2>&1 < /dev/null
360
+ local started=$SECONDS
361
+ while [ $((SECONDS - started)) -lt "$pueue_start_wait_sec" ]; do
362
+ pq status > /dev/null 2>&1 && return 0
363
+ sleep 0.2
364
+ done
365
+ return 1
366
+ }
367
+
368
+ # groups = "name=parallel,name=parallel"
369
+ apply_groups() {
370
+ printf '%s\n' "$1" | tr ',' '\n' | while IFS= read -r pair; do
371
+ name=${pair%%=*}
372
+ parallel=${pair#*=}
373
+ [ -n "$name" ] || continue
374
+ [ "$name" = default ] || pq group add "$name" > /dev/null 2>&1
375
+ pq parallel -g "$name" "$parallel" > /dev/null 2>&1
376
+ done
377
+ }
378
+
379
+ # ─────────────────────────── prepare / prune ───────────────────────────
380
+
381
+ cmd_prepare() {
382
+ set_paths "$1" "$2" "$3"
383
+ local groups=${4:-}
384
+ # Ids are 30 random bits, made on the caller's side: a clash would make two jobs share one folder.
385
+ if [ -e "$jd/meta.json" ] || [ -e "$jd/task-id" ]; then
386
+ die "job id already in use: $job"
387
+ fi
388
+ mkdir -p "$work/bin" "$tree" "$proj/stage" "$jobs_dir" "$stage" "$jd" || die "cannot create the layout under $work"
389
+ : > "$jd/log"
390
+ touch "$proj/used"
391
+ if ! ensure_pueued; then
392
+ die "Pueue daemon is not reachable and could not be started (check PUEUE_BIN / the pueue settings of this runner)"
393
+ fi
394
+ apply_groups "$groups"
395
+ prune_work "$work" "$slug"
396
+ echo "work=$(cd "$work" && pwd -P)"
397
+ }
398
+
399
+ # Has the job in folder $1 ended? It wrote its exit file; or it never got a Pueue task (its submit failed half-way); or its process is
400
+ # gone (a runner script of an earlier version SIGKILLed it without recording that). One that waits in the queue, or runs, has not.
401
+ check_job_dir_ended() {
402
+ [ -f "$1/exit" ] && return 0
403
+ [ -f "$1/task-id" ] || return 0
404
+ local recorded_pid
405
+ recorded_pid=$(read_file "$1/pid")
406
+ [ -n "$recorded_pid" ] && ! kill -0 "$recorded_pid" 2> /dev/null
407
+ }
408
+
409
+ # Is Pueue's task number $1 the task of job $2? Pueue hands a number it has freed (a task that was removed, or cleaned) to the next task
410
+ # added, and `pueue wait` / `pueue remove` of a number act on whoever holds it — so a job's recorded number is only good while the task
411
+ # under it carries the job's own label. The label filter is Pueue's (`status --json label=…`: an exact match), the answer the task's id.
412
+ check_task_belongs_to_job() {
413
+ pq status --json "label=rr-$2" 2> /dev/null | grep -Eq "\"$1\": ?\{ ?\"id\": ?$1[,}]"
414
+ }
415
+
416
+ # Remove: jobs older than the keep time (and their Pueue tasks), leftover stage dirs, project copies unused for long.
417
+ prune_work() {
418
+ local work_dir=$1
419
+ local keep_slug=${2:-}
420
+ [ -d "$work_dir/p" ] || return 0
421
+ find "$work_dir/p" -mindepth 3 -maxdepth 3 -path '*/jobs/*' -type d -mtime +"$job_keep_days" 2> /dev/null | while IFS= read -r old_job; do
422
+ check_job_dir_ended "$old_job" || continue
423
+ task=$(read_file "$old_job/task-id")
424
+ if [ -n "$task" ] && [ ! -f "$old_job/task-removed" ] && check_task_belongs_to_job "$task" "$(basename "$old_job")"; then
425
+ pq remove "$task" > /dev/null 2>&1
426
+ fi
427
+ rm -rf "$old_job"
428
+ done
429
+ find "$work_dir/p" -mindepth 3 -maxdepth 3 -path '*/stage/*' -type d -mtime +"$stage_keep_days" 2> /dev/null | while IFS= read -r old_stage; do
430
+ # A job that is queued in Pueue (a task, no exit yet) has not applied its stage: behind a busy group it can wait for days.
431
+ check_job_dir_ended "$(dirname "$(dirname "$old_stage")")/jobs/$(basename "$old_stage")" || continue
432
+ rm -rf "$old_stage"
433
+ done
434
+ # Lease state of a watcher that was killed hard (a live one rewrites its beat every few seconds, so its folder is never this old).
435
+ find "$work_dir/tmp" -mindepth 1 -maxdepth 1 -mtime +"$stage_keep_days" 2> /dev/null | while IFS= read -r old_lease; do
436
+ rm -rf "$old_lease"
437
+ done
438
+ find "$work_dir/p" -mindepth 2 -maxdepth 2 -name used -type f -mtime +"$copy_keep_days" 2> /dev/null | while IFS= read -r used_file; do
439
+ old_copy=$(dirname "$used_file")
440
+ [ "$(basename "$old_copy")" = "$keep_slug" ] && continue
441
+ [ -e "$old_copy/lock" ] && continue
442
+ rm -rf "$old_copy"
443
+ done
444
+ }
445
+
446
+ cmd_prune() {
447
+ prune_work "$1" ""
448
+ }
449
+
450
+ # ─────────────────────────── adding the job to Pueue ───────────────────────────
451
+
452
+ # Quote one word for a shell (single quotes).
453
+ quote_word() {
454
+ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
455
+ }
456
+
457
+ add_job() {
458
+ local group=$1
459
+ local script_path
460
+ script_path=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
461
+ local command_line
462
+ command_line="exec bash $(quote_word "$script_path") job $(quote_word "$work") $(quote_word "$slug") $(quote_word "$job")"
463
+ local task
464
+ # The job's time limit counts from here (a foreground run's own timer does too): a job that waited its whole limit in the queue is
465
+ # over when it starts (cmd_job), not given the limit again.
466
+ date +%s > "$jd/queued-at"
467
+ task=$(pq add --print-task-id -g "$group" -l "rr-$job" -w "$proj" -- "$command_line") || return 1
468
+ printf '%s\n' "$task" > "$jd/task-id"
469
+ }
470
+
471
+ cmd_submit() {
472
+ set_paths "$1" "$2" "$3"
473
+ add_job "${4:-default}" || die "pueue add failed (does the group \"${4:-default}\" exist on this runner?)"
474
+ echo queued
475
+ }
476
+
477
+ # ─────────────────────────── killing ───────────────────────────
478
+
479
+ # Signal the whole process group of the job (the script, its command and everything the command started). Pueue runs a task as
480
+ # `sh -c <command>` in a process group of its own, and the command line of add_job starts with `exec`, so the job script IS that
481
+ # group's leader: a group-wide signal has no wrapper to end first (which made Pueue call the task done while the job still ran).
482
+ # The group is read from the LIVE job process at the moment of use and signalled only while the job leads its own group: a stale id,
483
+ # or a Pueue that stopped isolating its tasks, can never make this hit another group (pueued's, the caller's).
484
+ signal_job_group() {
485
+ local job_pid group_id
486
+ job_pid=$(read_file "$jd/pid")
487
+ [ -n "$job_pid" ] && kill -0 "$job_pid" 2> /dev/null || return 1
488
+ group_id=$(ps -o pgid= -p "$job_pid" 2> /dev/null | tr -d ' ')
489
+ [ -n "$group_id" ] && [ "$group_id" = "$job_pid" ] || return 1
490
+ kill "-$1" -- "-$group_id" 2> /dev/null
491
+ }
492
+
493
+ # Stop a job: a queued one is removed, a running one gets SIGTERM (its whole group) and, after the grace time, Pueue's own kill of the
494
+ # whole task tree. $1 says why: `timeout` when the caller's own time limit ran out (recorded like the job's own watchdog does it — 124,
495
+ # "timed out" — whichever of the two gets there first), anything else is a kill (137).
496
+ kill_job() {
497
+ local task
498
+ task=$(read_file "$jd/task-id")
499
+ [ -n "$task" ] || return 1
500
+ [ -f "$jd/exit" ] && return 0
501
+ if [ "${1:-}" = timeout ]; then
502
+ touch "$jd/timeout"
503
+ else
504
+ touch "$jd/killed"
505
+ fi
506
+ if [ ! -f "$jd/pid" ] && pq remove "$task" > /dev/null 2>&1; then
507
+ # Pueue gives a number it has freed to the next task it is asked to add: from here on `task-id` is a number that may belong to another
508
+ # job, so nothing may act on it any more (`prune_work` skips such a job's task; waiting is decided by the exit file below).
509
+ touch "$jd/task-removed"
510
+ finish_killed_job
511
+ return 0
512
+ fi
513
+ signal_job_group TERM || pq kill "$task" > /dev/null 2>&1
514
+ local started=$SECONDS
515
+ while [ $((SECONDS - started)) -lt "$kill_grace_sec" ]; do
516
+ check_job_ended && return 0
517
+ sleep 0.2
518
+ done
519
+ hard_kill_job "$task"
520
+ sleep 1
521
+ return 0
522
+ }
523
+
524
+ # SIGKILL of the whole group; Pueue's own kill follows only so that its task list agrees. Without a verifiable group, Pueue's kill
525
+ # is all there is. A job that is SIGKILLed cannot write its own result, so the result is recorded FIRST: when the caller is the job's own
526
+ # watchdog the group-wide SIGKILL takes the watchdog down with the job.
527
+ hard_kill_job() {
528
+ finish_killed_job
529
+ signal_job_group KILL || pq kill "$1" > /dev/null 2>&1
530
+ pq kill "$1" > /dev/null 2>&1
531
+ return 0
532
+ }
533
+
534
+ # A job that was killed (never started and removed from the queue, or SIGKILLed after it ignored SIGTERM): its result is written the way a
535
+ # finished job writes it — 124 when its time limit did it, 137 otherwise — so it is an ENDED job: `wait` and `ls` read a code, the next job
536
+ # of the project takes over its lock at once, and prune removes it after 3 days like any other.
537
+ finish_killed_job() {
538
+ [ -f "$jd/exit" ] && return 0
539
+ local code=137
540
+ [ -f "$jd/timeout" ] && code=124
541
+ rm -rf "$stage"
542
+ printf '%s\n' done > "$jd/phase"
543
+ date +%s > "$jd/finished-at"
544
+ printf '%s\n' "$code" > "$jd/exit.tmp" && mv "$jd/exit.tmp" "$jd/exit"
545
+ }
546
+
547
+ # Ended = it wrote its exit file, or its process is gone.
548
+ check_job_ended() {
549
+ [ -f "$jd/exit" ] && return 0
550
+ local pid
551
+ pid=$(read_file "$jd/pid")
552
+ [ -n "$pid" ] && ! kill -0 "$pid" 2> /dev/null && return 0
553
+ return 1
554
+ }
555
+
556
+ # Prints `killed`, or `already-ended <code>` for a job that had finished before the kill came (nothing is killed then, and the caller
557
+ # must not be told it was).
558
+ cmd_kill() {
559
+ set_paths "$1" "$2" "$3"
560
+ if [ -n "$(read_file "$jd/task-id")" ] && [ -f "$jd/exit" ]; then
561
+ echo "already-ended $(read_file "$jd/exit")"
562
+ return 0
563
+ fi
564
+ kill_job "${4:-}" || die "no such job: $job"
565
+ echo killed
566
+ }
567
+
568
+ # ─────────────────────────── log streaming ───────────────────────────
569
+
570
+ # Print what the log gained since $log_offset, and move the offset.
571
+ drain_log() {
572
+ local size
573
+ size=$(wc -c < "$jd/log" 2> /dev/null | tr -d ' ')
574
+ [ -n "$size" ] || return 0
575
+ if [ "$size" -gt "$log_offset" ]; then
576
+ tail -c +"$((log_offset + 1))" "$jd/log" | head -c "$((size - log_offset))"
577
+ log_offset=$size
578
+ fi
579
+ }
580
+
581
+ # Stream the log until the file $jd/drain-stop appears, then print the rest. Runs as a background job.
582
+ stream_log() {
583
+ log_offset=${1:-0}
584
+ while [ ! -f "$jd/drain-stop" ]; do
585
+ drain_log
586
+ sleep "$drain_interval_sec"
587
+ done
588
+ drain_log
589
+ }
590
+
591
+ # Return when the job is over: Pueue says its task is done, or the job's exit file is there — whichever comes first, and at once if it
592
+ # already is. The exit file is the job's own record. The number of a task that was removed (a queued job that was killed) can be given to
593
+ # another job, and `pueue wait` on it would then follow THAT job for as long as it runs (hours, behind a busy group), or return at once
594
+ # for a number nobody holds: so the wait is cut as soon as the exit file appears, and the number is never trusted alone.
595
+ #
596
+ # The Pueue wait runs in a process group of its own (`set -m`, as `exec` does) and the cut signals the GROUP. `pq` is a function, so the
597
+ # background job is a subshell and `pueue wait` its child — unless the bash happens to run it in place of the subshell (5.3 can; 5.2, the
598
+ # bash of Ubuntu 24.04, does not). A kill of the subshell alone left `pueue wait` running for as long as the task, however many times
599
+ # the exit file won the race. Under job control bash no longer gives a background job /dev/null as stdin, so it is given here: in a
600
+ # lease the caller's stdin is the heartbeat stream, which only the lease watcher (fd 3) may read.
601
+ wait_for_task() {
602
+ [ -f "$jd/exit" ] && return 0
603
+ set -m
604
+ pq wait "$1" < /dev/null > /dev/null 2>&1 &
605
+ local waiter_pid=$!
606
+ set +m
607
+ while kill -0 "$waiter_pid" 2> /dev/null; do
608
+ if [ -f "$jd/exit" ]; then
609
+ # The group, or the process alone when it did not get a group of its own: a group id that is not a live group is harmless.
610
+ kill -- "-$waiter_pid" > /dev/null 2>&1 || kill "$waiter_pid" > /dev/null 2>&1
611
+ break
612
+ fi
613
+ sleep 0.2
614
+ done
615
+ { wait "$waiter_pid"; } 2> /dev/null
616
+ return 0
617
+ }
618
+
619
+ # Stream the log of the task until Pueue reports it done.
620
+ follow_until_done() {
621
+ rm -f "$jd/drain-stop"
622
+ stream_log "$1" &
623
+ local streamer_pid=$!
624
+ local task
625
+ task=$(read_file "$jd/task-id")
626
+ [ -n "$task" ] && wait_for_task "$task"
627
+ touch "$jd/drain-stop"
628
+ wait "$streamer_pid" 2> /dev/null
629
+ }
630
+
631
+ # Where `follow` starts in the log: the beginning, the end, or the last N lines.
632
+ resolve_follow_offset() {
633
+ local size
634
+ size=$(wc -c < "$jd/log" 2> /dev/null | tr -d ' ')
635
+ size=${size:-0}
636
+ case ${1:-start} in
637
+ end) echo "$size" ;;
638
+ tail:*)
639
+ local tail_bytes
640
+ tail_bytes=$(tail -n "${1#tail:}" "$jd/log" 2> /dev/null | wc -c | tr -d ' ')
641
+ echo $((size - tail_bytes))
642
+ ;;
643
+ *) echo 0 ;;
644
+ esac
645
+ }
646
+
647
+ cmd_follow() {
648
+ set_paths "$1" "$2" "$3"
649
+ local from_offset
650
+ from_offset=$(resolve_follow_offset "${4:-start}")
651
+ follow_until_done "$from_offset"
652
+ }
653
+
654
+ # After Pueue says a task is done, its own last file may still be a moment away: wait for the exit file, or for the job process to
655
+ # be gone (killed with SIGKILL, it never writes one), at most five seconds.
656
+ wait_for_exit_file() {
657
+ local started=$SECONDS
658
+ while [ ! -f "$jd/exit" ] && [ $((SECONDS - started)) -lt "$exit_file_wait_sec" ]; do
659
+ check_job_ended && return 0
660
+ sleep 0.2
661
+ done
662
+ }
663
+
664
+ cmd_wait() {
665
+ set_paths "$1" "$2" "$3"
666
+ local task
667
+ task=$(read_file "$jd/task-id")
668
+ [ -n "$task" ] || die "no such job: $job"
669
+ wait_for_task "$task"
670
+ wait_for_exit_file
671
+ echo done
672
+ }
673
+
674
+ # The facts of a job, one key=value per line (parsed by jobState.ts).
675
+ print_job_facts() {
676
+ echo "slug=$slug"
677
+ echo "exit=$(read_file "$jd/exit")"
678
+ echo "phase=$(read_file "$jd/phase")"
679
+ echo "task=$(read_file "$jd/task-id")"
680
+ echo "started=$(read_file "$jd/started-at")"
681
+ echo "finished=$(read_file "$jd/finished-at")"
682
+ [ -f "$jd/timeout" ] && echo "timeout=1" || echo "timeout=0"
683
+ [ -f "$jd/killed" ] && echo "killed=1" || echo "killed=0"
684
+ [ -f "$jd/pid" ] && echo "has_pid=1" || echo "has_pid=0"
685
+ [ -d "$jd/artifacts" ] && echo "artifacts=1" || echo "artifacts=0"
686
+ echo "failed_step=$(read_file "$jd/failed-step")"
687
+ }
688
+
689
+ cmd_result() {
690
+ set_paths "$1" "$2" "$3"
691
+ [ -d "$jd" ] || die "no such job: $job"
692
+ print_job_facts
693
+ }
694
+
695
+ cmd_meta() {
696
+ set_paths "$1" "$2" "$3"
697
+ cat "$jd/meta.json" 2> /dev/null || die "no such job: $job"
698
+ }
699
+
700
+ # The last N lines of a job's log (all of it when N is "all").
701
+ cmd_tail() {
702
+ set_paths "$1" "$2" "$3"
703
+ if [ "${4:-all}" = all ]; then cat "$jd/log" 2> /dev/null; else tail -n "$4" "$jd/log" 2> /dev/null; fi
704
+ }
705
+
706
+ # ─────────────────────────── the lease ───────────────────────────
707
+
708
+ # The caller writes one byte every few seconds. No byte for the lease time, or the end of its stream, means the caller is gone: the job
709
+ # is killed. A caller that finishes normally has no use for the lease any more because the job it guarded is over (the lease script
710
+ # stops its watcher when the job ends), and one that wants the job stopped says so with `kill` (an interrupted `exec` ends its stream on purpose, to have its command stopped).
711
+ # A stock sshd never notices a half-open connection, so this is the only thing that stops an orphaned job. The stream is the caller's,
712
+ # on fd 3 (a background job gets /dev/null as stdin, so the callers keep theirs there). A reader process does plain blocking reads and
713
+ # records each byte's arrival time; this loop compares the clock with it once a second — no `read -t`, whose exit status for a timeout
714
+ # is not the same in bash 3.2 (macOS) and bash 4+, and which would make a quiet second look like a closed connection.
715
+ watch_lease() {
716
+ local lease_sec=$1
717
+ lease_state_dir=
718
+ lease_reader_pid=
719
+ # The traps first: a command that ends at once gets the watcher a SIGTERM before it has done anything, and whatever it created by
720
+ # then must go with it. The name is made here, not by a command substitution, so there is no instant with a dir nobody knows of.
721
+ trap 'end_lease_watch' EXIT
722
+ trap 'exit 143' TERM
723
+ lease_state_dir="$work/tmp/lease.$$.$RANDOM"
724
+ mkdir -p "$lease_state_dir" || return 1
725
+ write_lease_beat
726
+ (
727
+ byte=
728
+ while IFS= read -r -n 1 byte; do
729
+ write_lease_beat
730
+ done
731
+ : > "$lease_state_dir/eof"
732
+ ) <&3 &
733
+ lease_reader_pid=$!
734
+ local verdict=0
735
+ local last_beat
736
+ while :; do
737
+ if [ -f "$lease_state_dir/eof" ]; then
738
+ verdict=1
739
+ break
740
+ fi
741
+ last_beat=$(cat "$lease_state_dir/beat" 2> /dev/null)
742
+ case $last_beat in
743
+ '' | *[!0-9]*) ;;
744
+ *) [ $(($(date +%s) - last_beat)) -ge "$lease_sec" ] && { verdict=1; break; } ;;
745
+ esac
746
+ sleep "$heartbeat_poll_sec"
747
+ done
748
+ end_lease_watch
749
+ return "$verdict"
750
+ }
751
+
752
+ # The time of the latest heartbeat, replaced atomically: a plain `> beat` truncates the file first, and a watcher that read it in
753
+ # that instant got an empty string, whose arithmetic is a syntax error that ended the watcher without a verdict.
754
+ write_lease_beat() {
755
+ date +%s > "$lease_state_dir/beat.new" && mv -f "$lease_state_dir/beat.new" "$lease_state_dir/beat"
756
+ }
757
+
758
+ end_lease_watch() {
759
+ [ -n "${lease_reader_pid:-}" ] && kill "$lease_reader_pid" > /dev/null 2>&1
760
+ [ -n "${lease_state_dir:-}" ] && rm -rf "$lease_state_dir"
761
+ }
762
+
763
+ ms_to_sec() {
764
+ awk -v ms="$1" 'BEGIN { s = int((ms + 999) / 1000); if (s < 2) s = 2; print s }'
765
+ }
766
+
767
+ cmd_lease() {
768
+ set_paths "$1" "$2" "$3"
769
+ local group=$4
770
+ local lease_sec
771
+ lease_sec=$(ms_to_sec "$5")
772
+ # A background job gets /dev/null as stdin unless job control is on: keep the caller's stream on fd 3 for the watcher.
773
+ exec 3<&0
774
+ add_job "$group" || die "pueue add failed (does the group \"$group\" exist on this runner?)"
775
+ rm -f "$jd/drain-stop"
776
+ stream_log 0 &
777
+ local streamer_pid=$!
778
+ (
779
+ watch_lease "$lease_sec" || kill_job
780
+ ) &
781
+ local watcher_pid=$!
782
+ local task
783
+ task=$(read_file "$jd/task-id")
784
+ wait_for_task "$task"
785
+ kill "$watcher_pid" > /dev/null 2>&1
786
+ wait_for_exit_file
787
+ touch "$jd/drain-stop"
788
+ wait "$streamer_pid" 2> /dev/null
789
+ }
790
+
791
+ # ─────────────────────────── the job (the Pueue task) ───────────────────────────
792
+
793
+ lock_held=0
794
+
795
+ # The project lock is a FILE made with the shell's own exclusive create (`set -C`, noclobber: open(O_EXCL)) — never with `mkdir`. The uutils
796
+ # coreutils that current Ubuntu ships as /usr/bin/mkdir let several simultaneous `mkdir` of one path ALL succeed (measured here: 265 races
797
+ # of 1200 with five contenders), which gave two jobs of one project the lock together. The file holds the holder's pid and job, two lines,
798
+ # written by the very redirection that creates it. Succeeds only for the one that created the file.
799
+ create_lock_file() {
800
+ ( set -C; printf '%s\n%s\n' "$$" "$job" > "$1" ) 2> /dev/null
801
+ }
802
+
803
+ # Read the lock file $1 into holder_pid and holder_job (empty when it is missing or has no holder written yet).
804
+ read_lock_holder() {
805
+ holder_pid=
806
+ holder_job=
807
+ { IFS= read -r holder_pid; IFS= read -r holder_job; } 2> /dev/null < "$1"
808
+ return 0
809
+ }
810
+
811
+ # Is the lock held by nobody any more? Its holder's process is gone, or its job has an exit record, or no holder was ever written and it
812
+ # is over a minute old (its creator died between creating the file and writing it).
813
+ check_lock_is_stale() {
814
+ local holder_pid holder_job
815
+ read_lock_holder "$lock"
816
+ if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2> /dev/null; then
817
+ return 0
818
+ fi
819
+ if [ -n "$holder_job" ] && [ -f "$jobs_dir/$holder_job/exit" ]; then
820
+ return 0
821
+ fi
822
+ [ -z "$holder_pid" ] && [ -n "$(find "$lock" -maxdepth 0 -mmin +1 2> /dev/null)" ]
823
+ }
824
+
825
+ # Break a stale lock — by ONE waiter at a time, and only the lock of the very holder that was seen dead. Several waiters can see the same
826
+ # dead holder at once; if each simply removed the lock, a slow one could remove the lock a faster one had already re-created, and two jobs
827
+ # of a project would run on one tree. So the RIGHT to break the lock of holder (pid, job) is a file named for that holder, made with the
828
+ # exclusive create: of the waiters that saw it, one wins and the others yield. That holder can never hold the lock again (its process is
829
+ # gone, its job id is unique), so once the winner has seen the lock still carries that holder, nothing but the winner can remove it, and a
830
+ # lock some other job has taken since is not that holder's and is left alone. Returns 1 when another waiter is at it.
831
+ break_stale_lock() {
832
+ local holder_pid holder_job
833
+ read_lock_holder "$lock"
834
+ check_lock_is_stale || return 0
835
+ break_lock_of_holder "$holder_pid" "$holder_job"
836
+ }
837
+
838
+ # The right to break the lock of holder $1/$2 (pid, job): the file `$lock.breaking.<pid>.<job>.<generation>`. A winner that died leaves its
839
+ # file behind, held by a process that is gone: the next waiter takes the next generation of the name — and again only one of them wins it.
840
+ # A name is made of the holder's own words only (a lock file with something else in it must not name a path).
841
+ break_lock_of_holder() {
842
+ local stale_pid=$1 stale_job=$2
843
+ case $stale_pid in *[!0-9]*) stale_pid= ;; esac
844
+ case $stale_job in *[!A-Za-z0-9._-]*) stale_job= ;; esac
845
+ local generation=0 right
846
+ while :; do
847
+ right="$lock.breaking.${stale_pid:-none}.${stale_job:-none}.$generation"
848
+ create_lock_file "$right" && break
849
+ check_breaker_is_alive "$right" && return 1
850
+ generation=$((generation + 1))
851
+ done
852
+ # The lock must carry that holder AND still be stale. For a named holder the second test adds nothing (it is dead for good). For a lock
853
+ # with no holder written it is what matters: every fresh lock is empty for a moment between its creation and the write of its pid, so
854
+ # "no holder" names no one in particular — only the over-a-minute test tells a creator that died from one that is about to write.
855
+ local holder_pid holder_job
856
+ read_lock_holder "$lock"
857
+ if [ "$holder_pid" = "$1" ] && [ "$holder_job" = "$2" ] && check_lock_is_stale; then
858
+ rm -f "$lock"
859
+ fi
860
+ return 0
861
+ }
862
+
863
+ # Is the breaker that made the right file $1 still at it? Its process is alive — the test the lock itself uses, so a breaker that is merely
864
+ # slow (a busy disk) keeps its right however long it takes — and its job has no exit record (the lock's second test: a breaker that died
865
+ # and whose pid went to some long-lived process would otherwise hold its right for ever, and the project's queue would never drain). A file
866
+ # with no process written yet is a breaker between creating it and writing it: it counts until it is over a minute old.
867
+ check_breaker_is_alive() {
868
+ local holder_pid holder_job
869
+ read_lock_holder "$1"
870
+ if [ -n "$holder_job" ] && [ -f "$jobs_dir/$holder_job/exit" ]; then
871
+ return 1
872
+ fi
873
+ if [ -n "$holder_pid" ]; then
874
+ kill -0 "$holder_pid" 2> /dev/null
875
+ return
876
+ fi
877
+ [ -z "$(find "$1" -maxdepth 0 -mmin +1 2> /dev/null)" ]
878
+ }
879
+
880
+ acquire_lock() {
881
+ local noticed=0
882
+ local holder_pid holder_job
883
+ while ! create_lock_file "$lock"; do
884
+ [ "$killed" -eq 1 ] && return 1
885
+ if check_lock_is_stale && break_stale_lock; then
886
+ continue
887
+ fi
888
+ if [ "$noticed" -eq 0 ]; then
889
+ read_lock_holder "$lock"
890
+ say "waiting for job ${holder_job:-another job} of this project"
891
+ noticed=1
892
+ fi
893
+ nap "$lock_poll_sec"
894
+ done
895
+ lock_held=1
896
+ # The rights to break the lock name holders that are gone and can never hold it again: nothing needs them any more.
897
+ rm -f "$lock".breaking.* 2> /dev/null
898
+ return 0
899
+ }
900
+
901
+ # Give the lock back — only if it is still ours (a lock that was broken as stale and taken by another job is not ours to remove).
902
+ release_lock() {
903
+ if [ "$lock_held" -eq 1 ]; then
904
+ local holder_pid holder_job
905
+ read_lock_holder "$lock"
906
+ if [ "$holder_pid" = "$$" ] && [ "$holder_job" = "$job" ]; then
907
+ rm -f "$lock"
908
+ fi
909
+ fi
910
+ lock_held=0
911
+ }
912
+
913
+ remove_tree_file() {
914
+ check_relative_path "$1" || return 0
915
+ rm -f "$tree/$1"
916
+ local dir
917
+ dir=$(dirname "$1")
918
+ while [ "$dir" != "." ] && rmdir "$tree/$dir" 2> /dev/null; do
919
+ dir=$(dirname "$dir")
920
+ done
921
+ }
922
+
923
+ # The apply step: delete what the previous submit had and this one has not, copy the staged files over, keep this submit's list.
924
+ sync_tree() {
925
+ mkdir -p "$tree"
926
+ if [ -f "$proj/manifest" ]; then
927
+ sort "$proj/manifest" > "$jd/manifest.old.sorted"
928
+ sort "$jd/manifest" > "$jd/manifest.new.sorted"
929
+ comm -23 "$jd/manifest.old.sorted" "$jd/manifest.new.sorted" | while IFS= read -r removed_file; do
930
+ remove_tree_file "$removed_file"
931
+ done
932
+ fi
933
+ # --modify-window=-1: compare the mtimes to the nanosecond too — a same-length edit within one second of the previous version is
934
+ # otherwise "unchanged" (rsync's quick check) and the copy would keep the old file.
935
+ rsync -a --modify-window=-1 --exclude=/.git "$stage/" "$tree/" || return 1
936
+ if [ -d "$stage/.git" ]; then
937
+ mkdir -p "$tree/.git"
938
+ rsync -a --modify-window=-1 --delete "$stage/.git/" "$tree/.git/" || return 1
939
+ else
940
+ # `.git` only with --with-git: a copy that got one from an earlier run must not keep serving its stale history.
941
+ rm -rf "$tree/.git"
942
+ fi
943
+ cp "$jd/manifest" "$proj/manifest"
944
+ rm -rf "$stage"
945
+ }
946
+
947
+ cmd_sync_tree() {
948
+ set_paths "$1" "$2" "$3"
949
+ sync_tree || die "apply failed"
950
+ }
951
+
952
+ # bash -c <command> in <directory>, with the job's -e variables, output into the log; sets step_code. Runs as a child so a
953
+ # SIGTERM to the job (the `killed` flag) does not abandon it half-way.
954
+ run_step() {
955
+ local directory=$1
956
+ local command_text=$2
957
+ (
958
+ cd "$directory" || exit 126
959
+ if [ -s "$jd/env" ]; then
960
+ while IFS= read -r env_line; do export "$env_line"; done < "$jd/env"
961
+ fi
962
+ exec bash -c "$command_text"
963
+ ) < /dev/null &
964
+ local step_pid=$!
965
+ # The notices bash prints when it reaps a signalled child ("Terminated") are not the job's output.
966
+ { wait "$step_pid"; } 2> /dev/null
967
+ step_code=$?
968
+ while kill -0 "$step_pid" 2> /dev/null; do
969
+ { wait "$step_pid"; } 2> /dev/null
970
+ step_code=$?
971
+ done
972
+ }
973
+
974
+ # A sleep that a group-wide SIGTERM does not end (and bash does not report as "Terminated").
975
+ nap() {
976
+ ( trap '' TERM; exec sleep "$1" )
977
+ }
978
+
979
+ # The timeout: from the job's start, covering the lock wait, the install and the command. SIGTERM to the job's group, SIGKILL after the
980
+ # grace time (a process that ignores SIGTERM). It polls once a second for the cancel file instead of sleeping the whole limit: a
981
+ # sleeper that a group-wide SIGTERM (a kill) ends would look like an expired limit, and a finished job needs no process to reap.
982
+ start_watchdog() {
983
+ local limit=$1
984
+ local task
985
+ task=$(read_file "$jd/task-id")
986
+ (
987
+ trap '' TERM
988
+ local started=$SECONDS
989
+ while [ $((SECONDS - started)) -lt "$limit" ]; do
990
+ [ -f "$jd/watchdog-cancel" ] && exit 0
991
+ nap 1
992
+ done
993
+ [ -f "$jd/watchdog-cancel" ] && exit 0
994
+ touch "$jd/timeout"
995
+ signal_job_group TERM || pq kill "$task" > /dev/null 2>&1
996
+ local grace_started=$SECONDS
997
+ while [ $((SECONDS - grace_started)) -lt "$kill_grace_sec" ]; do
998
+ [ -f "$jd/watchdog-cancel" ] && exit 0
999
+ nap 1
1000
+ done
1001
+ hard_kill_job "$task"
1002
+ ) > /dev/null 2>&1 &
1003
+ }
1004
+
1005
+ stop_watchdog() {
1006
+ touch "$jd/watchdog-cancel"
1007
+ }
1008
+
1009
+ # True when "$tree/$1" has no symlink in it, so a walk that starts there stays inside the copy (the walk of the whole copy never followed a
1010
+ # link either).
1011
+ check_has_no_symlink() {
1012
+ local parent physical
1013
+ [ ! -L "$tree/$1" ] || return 1
1014
+ parent=$(dirname "$1")
1015
+ [ "$parent" = . ] && return 0
1016
+ physical=$(cd "$tree/$parent" 2> /dev/null && pwd -P) || return 1
1017
+ [ "$physical" = "$(cd "$tree" && pwd -P)/$parent" ]
1018
+ }
1019
+
1020
+ # Files the command created or changed that match pull.ere (one extended regular expression per line), kept per job. Only the folders in
1021
+ # pull.roots are walked (the fixed prefixes of the patterns, `e2e/artifacts`): the copy also holds node_modules and every build folder,
1022
+ # which a Gradle or Metro build fills with hundreds of thousands of files. `.` there (a pattern that starts with a wildcard) or no
1023
+ # pull.roots at all (an older main box) walks the whole copy.
1024
+ collect_artifacts() {
1025
+ [ -s "$jd/pull.ere" ] || return 0
1026
+ [ -f "$jd/start-marker" ] || return 0
1027
+ local roots=()
1028
+ local root
1029
+ if [ -s "$jd/pull.roots" ]; then
1030
+ while IFS= read -r root; do
1031
+ [ -n "$root" ] || continue
1032
+ if [ "$root" = . ]; then
1033
+ roots=(.)
1034
+ break
1035
+ fi
1036
+ check_relative_path "$root" && [ -e "$tree/$root" ] && check_has_no_symlink "$root" && roots+=("$root")
1037
+ done < "$jd/pull.roots"
1038
+ [ "${#roots[@]}" -gt 0 ] || return 0
1039
+ else
1040
+ roots=(.)
1041
+ fi
1042
+ local patterns=()
1043
+ local pattern
1044
+ while IFS= read -r pattern; do
1045
+ [ -n "$pattern" ] && patterns+=("$pattern")
1046
+ done < "$jd/pull.ere"
1047
+ [ "${#patterns[@]}" -gt 0 ] || return 0
1048
+ local candidate relative
1049
+ while IFS= read -r -d '' candidate; do
1050
+ relative=${candidate#./}
1051
+ for pattern in "${patterns[@]}"; do
1052
+ if [[ $relative =~ $pattern ]]; then
1053
+ mkdir -p "$jd/artifacts/$(dirname "$relative")" && cp -p "$tree/$relative" "$jd/artifacts/$relative"
1054
+ break
1055
+ fi
1056
+ done
1057
+ done < <(cd "$tree" && find "${roots[@]}" -name .git -prune -o -type f -newer "$jd/start-marker" -print0 2> /dev/null)
1058
+ }
1059
+
1060
+ finish_job() {
1061
+ local code=$1
1062
+ if [ -f "$jd/timeout" ]; then
1063
+ code=124
1064
+ say "timed out after ${timeout_sec}s"
1065
+ elif [ "$killed" -eq 1 ]; then
1066
+ code=137
1067
+ say "job killed"
1068
+ fi
1069
+ stop_watchdog
1070
+ set_phase collecting
1071
+ collect_artifacts
1072
+ release_lock
1073
+ rm -rf "$stage"
1074
+ set_phase done
1075
+ date +%s > "$jd/finished-at"
1076
+ printf '%s\n' "$code" > "$jd/exit.tmp" && mv "$jd/exit.tmp" "$jd/exit"
1077
+ exit "$code"
1078
+ }
1079
+
1080
+ cmd_job() {
1081
+ set_paths "$1" "$2" "$3"
1082
+ exec >> "$jd/log" 2>&1
1083
+ printf '%s\n' "$$" > "$jd/pid"
1084
+ date +%s > "$jd/started-at"
1085
+ set_phase starting
1086
+ killed=0
1087
+ trap 'killed=1' TERM INT HUP
1088
+ # Pueue starts the task before the adder has written its id down.
1089
+ local id_wait_started=$SECONDS
1090
+ while [ ! -s "$jd/task-id" ] && [ $((SECONDS - id_wait_started)) -lt "$task_id_wait_sec" ]; do
1091
+ sleep 0.1
1092
+ done
1093
+ timeout_sec=$(read_file "$jd/timeout-sec")
1094
+ [ -n "$timeout_sec" ] || timeout_sec=$default_timeout_sec
1095
+ local limit_sec=$timeout_sec
1096
+ local queued_at
1097
+ queued_at=$(read_file "$jd/queued-at")
1098
+ case $queued_at in
1099
+ '' | *[!0-9]*) ;;
1100
+ *) limit_sec=$((timeout_sec - ($(date +%s) - queued_at))) ;;
1101
+ esac
1102
+ if [ "$limit_sec" -le 0 ]; then
1103
+ touch "$jd/timeout"
1104
+ say "its time limit ran out while it was queued"
1105
+ finish_job 124
1106
+ fi
1107
+ start_watchdog "$limit_sec"
1108
+
1109
+ set_phase waiting-lock
1110
+ acquire_lock || finish_job 137
1111
+
1112
+ set_phase syncing
1113
+ sync_tree || { say "applying the files failed"; finish_job 125; }
1114
+
1115
+ local subfolder
1116
+ subfolder=$(read_file "$jd/subfolder")
1117
+ check_relative_path "$subfolder" || { say "bad subfolder"; finish_job 125; }
1118
+ [ "$killed" -eq 1 ] && finish_job 137
1119
+
1120
+ local install_command install_key
1121
+ install_command=$(read_file "$jd/install-command")
1122
+ install_key=$(read_file "$jd/install-key")
1123
+ if [ -n "$install_command" ] && [ "$(read_file "$proj/deps.hash")" != "$install_key" ]; then
1124
+ set_phase deps
1125
+ say "installing dependencies: $install_command"
1126
+ run_step "$tree" "$install_command"
1127
+ if [ "$step_code" -ne 0 ]; then
1128
+ rm -f "$proj/deps.hash"
1129
+ echo deps > "$jd/failed-step"
1130
+ say "the dependencies step failed with exit code $step_code"
1131
+ finish_job 125
1132
+ fi
1133
+ printf '%s\n' "$install_key" > "$proj/deps.hash"
1134
+ fi
1135
+ [ "$killed" -eq 1 ] && finish_job 137
1136
+
1137
+ touch "$jd/start-marker"
1138
+ set_phase running
1139
+ run_step "$tree/$subfolder" "$(read_file "$jd/cmd")"
1140
+ finish_job "$step_code"
1141
+ }
1142
+
1143
+ # ─────────────────────────── exec ───────────────────────────
1144
+
1145
+ cmd_exec() {
1146
+ set_paths "$1" "$2" ""
1147
+ local subfolder=$3
1148
+ local lease_sec
1149
+ lease_sec=$(ms_to_sec "$4")
1150
+ shift 4
1151
+ check_relative_path "$subfolder" || die "bad subfolder"
1152
+ [ -d "$tree/$subfolder" ] || die "the runner has no copy of this project yet (run a command first)"
1153
+ exec 3<&0
1154
+ # Own process group, so the lease can stop the command and everything it started.
1155
+ set -m
1156
+ ( cd "$tree/$subfolder" && exec bash -c "$*" ) < /dev/null &
1157
+ local exec_pid=$!
1158
+ set +m
1159
+ (
1160
+ if ! watch_lease "$lease_sec"; then
1161
+ kill -TERM -- "-$exec_pid" 2> /dev/null
1162
+ local grace_started=$SECONDS
1163
+ while kill -0 "$exec_pid" 2> /dev/null && [ $((SECONDS - grace_started)) -lt "$kill_grace_sec" ]; do
1164
+ sleep 0.2
1165
+ done
1166
+ kill -KILL -- "-$exec_pid" 2> /dev/null
1167
+ fi
1168
+ ) &
1169
+ local watcher_pid=$!
1170
+ wait "$exec_pid"
1171
+ local exec_code=$?
1172
+ kill "$watcher_pid" > /dev/null 2>&1
1173
+ # The command's own code, said out loud: ssh reports 255 for ITS failures, and a command that really exits 255 (ssh and adb do) would
1174
+ # look like a lost connection. The marker (RS, the name, the code, a newline) is the last thing on stderr; the CLI takes it out of what it
1175
+ # shows. A session that dies before it has none, which is how a lost connection is told from a command that exited 255.
1176
+ printf '\036remote-run-exit:%s\n' "$exec_code" >&2
1177
+ exit "$exec_code"
1178
+ }
1179
+
1180
+ # ─────────────────────────── jobs / pueue passthrough ───────────────────────────
1181
+
1182
+ # One JSON object per job: {"id", "facts": "k=v;k=v", "meta": <meta.json>} — everything `ls` and `status` show, in one call.
1183
+ cmd_jobs() {
1184
+ local work_dir=$1
1185
+ local only_slug=${2:-}
1186
+ local first=1
1187
+ local meta job_dir
1188
+ printf '['
1189
+ for meta in "$work_dir"/p/${only_slug:-*}/jobs/*/meta.json; do
1190
+ [ -f "$meta" ] || continue
1191
+ job_dir=$(dirname "$meta")
1192
+ set_paths "$work_dir" "$(basename "$(dirname "$(dirname "$job_dir")")")" "$(basename "$job_dir")"
1193
+ [ "$first" -eq 1 ] || printf ','
1194
+ first=0
1195
+ printf '{"id":"%s","facts":"%s","meta":' "$job" "$(print_job_facts | tr '\n' ';')"
1196
+ cat "$meta"
1197
+ printf '}'
1198
+ done
1199
+ printf ']\n'
1200
+ }
1201
+
1202
+ # The work dir as an absolute path (a `~/…` the caller configured is expanded by the shell before it gets here).
1203
+ cmd_workdir() {
1204
+ mkdir -p "$1" && (cd "$1" && pwd -P)
1205
+ }
1206
+
1207
+ cmd_pueue() {
1208
+ shift
1209
+ pq "$@"
1210
+ }
1211
+
1212
+ # ─────────────────────────── dispatch ───────────────────────────
1213
+
1214
+ main() {
1215
+ local subcommand=${1:-}
1216
+ [ $# -gt 0 ] && shift
1217
+ case $subcommand in
1218
+ probe) cmd_probe "$@" ;;
1219
+ prepare) cmd_prepare "$@" ;;
1220
+ lease) cmd_lease "$@" ;;
1221
+ submit) cmd_submit "$@" ;;
1222
+ job) cmd_job "$@" ;;
1223
+ follow) cmd_follow "$@" ;;
1224
+ wait) cmd_wait "$@" ;;
1225
+ result) cmd_result "$@" ;;
1226
+ meta) cmd_meta "$@" ;;
1227
+ tail) cmd_tail "$@" ;;
1228
+ kill) cmd_kill "$@" ;;
1229
+ exec) cmd_exec "$@" ;;
1230
+ pueue) cmd_pueue "$@" ;;
1231
+ workdir) cmd_workdir "$@" ;;
1232
+ jobs) cmd_jobs "$@" ;;
1233
+ prune) cmd_prune "$@" ;;
1234
+ sync-tree) cmd_sync_tree "$@" ;;
1235
+ *) die "unknown subcommand: $subcommand" ;;
1236
+ esac
1237
+ }
1238
+
1239
+ # Run as a script it dispatches; sourced (the unit tests call the functions one by one) it only defines them.
1240
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
1241
+ main "$@"
1242
+ fi