@cyanautomation/kaseki-agent 1.4.1

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 (459) hide show
  1. package/.dockerignore +54 -0
  2. package/.eslintignore +11 -0
  3. package/.eslintrc.json +95 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +53 -0
  6. package/.github/ISSUE_TEMPLATE/security.md +51 -0
  7. package/.github/PULL_REQUEST_TEMPLATE/default.md +71 -0
  8. package/.github/dependabot.yml +38 -0
  9. package/.github/skills/dependency-cache-optimization/SKILL.md +526 -0
  10. package/.github/skills/docker-image-management/SKILL.md +532 -0
  11. package/.github/skills/frontend-design/SKILL.md +782 -0
  12. package/.github/skills/prompt-engineering/SKILL.md +360 -0
  13. package/.github/skills/quality-gate-config/SKILL.md +591 -0
  14. package/.github/skills/result-report-analysis/SKILL.md +576 -0
  15. package/.github/skills/test-automation/SKILL.md +593 -0
  16. package/.github/skills/workflow-diagnosis/SKILL.md +468 -0
  17. package/.github/workflows/build-docker-image.yml +453 -0
  18. package/.github/workflows/release.yml +68 -0
  19. package/.releaserc.json +135 -0
  20. package/CHANGELOG.md +117 -0
  21. package/CLAUDE.md +336 -0
  22. package/CONTRIBUTING.md +339 -0
  23. package/Dockerfile +217 -0
  24. package/README.md +1527 -0
  25. package/STYLE.md +521 -0
  26. package/add-js-extensions.d.ts +9 -0
  27. package/add-js-extensions.d.ts.map +1 -0
  28. package/add-js-extensions.js.map +1 -0
  29. package/dist/add-js-extensions.d.ts +9 -0
  30. package/dist/add-js-extensions.d.ts.map +1 -0
  31. package/dist/add-js-extensions.js +52 -0
  32. package/dist/add-js-extensions.js.map +1 -0
  33. package/dist/ansi-colors.d.ts +26 -0
  34. package/dist/ansi-colors.d.ts.map +1 -0
  35. package/dist/ansi-colors.js +51 -0
  36. package/dist/ansi-colors.js.map +1 -0
  37. package/dist/cli/BaseCommand.d.ts +18 -0
  38. package/dist/cli/BaseCommand.d.ts.map +1 -0
  39. package/dist/cli/BaseCommand.js +31 -0
  40. package/dist/cli/BaseCommand.js.map +1 -0
  41. package/dist/cli/KasekiCLI.d.ts +30 -0
  42. package/dist/cli/KasekiCLI.d.ts.map +1 -0
  43. package/dist/cli/KasekiCLI.js +134 -0
  44. package/dist/cli/KasekiCLI.js.map +1 -0
  45. package/dist/cli/commands/ConfigCommand.d.ts +13 -0
  46. package/dist/cli/commands/ConfigCommand.d.ts.map +1 -0
  47. package/dist/cli/commands/ConfigCommand.js +131 -0
  48. package/dist/cli/commands/ConfigCommand.js.map +1 -0
  49. package/dist/cli/commands/DoctorCommand.d.ts +45 -0
  50. package/dist/cli/commands/DoctorCommand.d.ts.map +1 -0
  51. package/dist/cli/commands/DoctorCommand.js +309 -0
  52. package/dist/cli/commands/DoctorCommand.js.map +1 -0
  53. package/dist/cli/commands/ListCommand.d.ts +9 -0
  54. package/dist/cli/commands/ListCommand.d.ts.map +1 -0
  55. package/dist/cli/commands/ListCommand.js +81 -0
  56. package/dist/cli/commands/ListCommand.js.map +1 -0
  57. package/dist/cli/commands/ReportCommand.d.ts +9 -0
  58. package/dist/cli/commands/ReportCommand.d.ts.map +1 -0
  59. package/dist/cli/commands/ReportCommand.js +98 -0
  60. package/dist/cli/commands/ReportCommand.js.map +1 -0
  61. package/dist/cli/commands/RunCommand.d.ts +13 -0
  62. package/dist/cli/commands/RunCommand.d.ts.map +1 -0
  63. package/dist/cli/commands/RunCommand.js +191 -0
  64. package/dist/cli/commands/RunCommand.js.map +1 -0
  65. package/dist/cli/commands/SecretsCommand.d.ts +9 -0
  66. package/dist/cli/commands/SecretsCommand.d.ts.map +1 -0
  67. package/dist/cli/commands/SecretsCommand.js +109 -0
  68. package/dist/cli/commands/SecretsCommand.js.map +1 -0
  69. package/dist/cli/commands/ServeCommand.d.ts +9 -0
  70. package/dist/cli/commands/ServeCommand.d.ts.map +1 -0
  71. package/dist/cli/commands/ServeCommand.js +50 -0
  72. package/dist/cli/commands/ServeCommand.js.map +1 -0
  73. package/dist/cli/commands/SetupCommand.d.ts +42 -0
  74. package/dist/cli/commands/SetupCommand.d.ts.map +1 -0
  75. package/dist/cli/commands/SetupCommand.js +249 -0
  76. package/dist/cli/commands/SetupCommand.js.map +1 -0
  77. package/dist/cli.d.ts +9 -0
  78. package/dist/cli.d.ts.map +1 -0
  79. package/dist/cli.js +130 -0
  80. package/dist/cli.js.map +1 -0
  81. package/dist/config/ConfigManager.d.ts +395 -0
  82. package/dist/config/ConfigManager.d.ts.map +1 -0
  83. package/dist/config/ConfigManager.js +446 -0
  84. package/dist/config/ConfigManager.js.map +1 -0
  85. package/dist/docker/DockerManager.d.ts +69 -0
  86. package/dist/docker/DockerManager.d.ts.map +1 -0
  87. package/dist/docker/DockerManager.js +266 -0
  88. package/dist/docker/DockerManager.js.map +1 -0
  89. package/dist/event-aggregator.d.ts +71 -0
  90. package/dist/event-aggregator.d.ts.map +1 -0
  91. package/dist/event-aggregator.js +95 -0
  92. package/dist/event-aggregator.js.map +1 -0
  93. package/dist/github-app-token.d.ts +16 -0
  94. package/dist/github-app-token.d.ts.map +1 -0
  95. package/dist/github-app-token.js +148 -0
  96. package/dist/github-app-token.js.map +1 -0
  97. package/dist/idempotency-store.d.ts +61 -0
  98. package/dist/idempotency-store.d.ts.map +1 -0
  99. package/dist/idempotency-store.js +321 -0
  100. package/dist/idempotency-store.js.map +1 -0
  101. package/dist/index.d.ts +25 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +31 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/instance/InstanceManager.d.ts +81 -0
  106. package/dist/instance/InstanceManager.d.ts.map +1 -0
  107. package/dist/instance/InstanceManager.js +220 -0
  108. package/dist/instance/InstanceManager.js.map +1 -0
  109. package/dist/instance-metadata-reader.d.ts +48 -0
  110. package/dist/instance-metadata-reader.d.ts.map +1 -0
  111. package/dist/instance-metadata-reader.js +94 -0
  112. package/dist/instance-metadata-reader.js.map +1 -0
  113. package/dist/instance-state-derivation.d.ts +42 -0
  114. package/dist/instance-state-derivation.d.ts.map +1 -0
  115. package/dist/instance-state-derivation.js +133 -0
  116. package/dist/instance-state-derivation.js.map +1 -0
  117. package/dist/job-scheduler.d.ts +124 -0
  118. package/dist/job-scheduler.d.ts.map +1 -0
  119. package/dist/job-scheduler.js +992 -0
  120. package/dist/job-scheduler.js.map +1 -0
  121. package/dist/kaseki-api-client.d.ts +89 -0
  122. package/dist/kaseki-api-client.d.ts.map +1 -0
  123. package/dist/kaseki-api-client.js +405 -0
  124. package/dist/kaseki-api-client.js.map +1 -0
  125. package/dist/kaseki-api-config.d.ts +34 -0
  126. package/dist/kaseki-api-config.d.ts.map +1 -0
  127. package/dist/kaseki-api-config.js +113 -0
  128. package/dist/kaseki-api-config.js.map +1 -0
  129. package/dist/kaseki-api-routes.d.ts +13 -0
  130. package/dist/kaseki-api-routes.d.ts.map +1 -0
  131. package/dist/kaseki-api-routes.js +559 -0
  132. package/dist/kaseki-api-routes.js.map +1 -0
  133. package/dist/kaseki-api-service-wrapper.d.ts +43 -0
  134. package/dist/kaseki-api-service-wrapper.d.ts.map +1 -0
  135. package/dist/kaseki-api-service-wrapper.js +150 -0
  136. package/dist/kaseki-api-service-wrapper.js.map +1 -0
  137. package/dist/kaseki-api-service.d.ts +16 -0
  138. package/dist/kaseki-api-service.d.ts.map +1 -0
  139. package/dist/kaseki-api-service.js +143 -0
  140. package/dist/kaseki-api-service.js.map +1 -0
  141. package/dist/kaseki-api-types.d.ts +440 -0
  142. package/dist/kaseki-api-types.d.ts.map +1 -0
  143. package/dist/kaseki-api-types.js +64 -0
  144. package/dist/kaseki-api-types.js.map +1 -0
  145. package/dist/kaseki-cli-lib.d.ts +219 -0
  146. package/dist/kaseki-cli-lib.d.ts.map +1 -0
  147. package/dist/kaseki-cli-lib.js +523 -0
  148. package/dist/kaseki-cli-lib.js.map +1 -0
  149. package/dist/kaseki-cli.d.ts +38 -0
  150. package/dist/kaseki-cli.d.ts.map +1 -0
  151. package/dist/kaseki-cli.js +559 -0
  152. package/dist/kaseki-cli.js.map +1 -0
  153. package/dist/kaseki-report.d.ts +3 -0
  154. package/dist/kaseki-report.d.ts.map +1 -0
  155. package/dist/kaseki-report.js +140 -0
  156. package/dist/kaseki-report.js.map +1 -0
  157. package/dist/lib/subprocess-helpers.d.ts +98 -0
  158. package/dist/lib/subprocess-helpers.d.ts.map +1 -0
  159. package/dist/lib/subprocess-helpers.js +136 -0
  160. package/dist/lib/subprocess-helpers.js.map +1 -0
  161. package/dist/logger.d.ts +39 -0
  162. package/dist/logger.d.ts.map +1 -0
  163. package/dist/logger.js +79 -0
  164. package/dist/logger.js.map +1 -0
  165. package/dist/metrics.d.ts +19 -0
  166. package/dist/metrics.d.ts.map +1 -0
  167. package/dist/metrics.js +59 -0
  168. package/dist/metrics.js.map +1 -0
  169. package/dist/middleware/job-lookup.d.ts +27 -0
  170. package/dist/middleware/job-lookup.d.ts.map +1 -0
  171. package/dist/middleware/job-lookup.js +28 -0
  172. package/dist/middleware/job-lookup.js.map +1 -0
  173. package/dist/pi-event-filter.d.ts +3 -0
  174. package/dist/pi-event-filter.d.ts.map +1 -0
  175. package/dist/pi-event-filter.js +126 -0
  176. package/dist/pi-event-filter.js.map +1 -0
  177. package/dist/pi-progress-stream.d.ts +3 -0
  178. package/dist/pi-progress-stream.d.ts.map +1 -0
  179. package/dist/pi-progress-stream.js +205 -0
  180. package/dist/pi-progress-stream.js.map +1 -0
  181. package/dist/pi-progress-summarizer.d.ts +61 -0
  182. package/dist/pi-progress-summarizer.d.ts.map +1 -0
  183. package/dist/pi-progress-summarizer.js +246 -0
  184. package/dist/pi-progress-summarizer.js.map +1 -0
  185. package/dist/pre-flight-validator.d.ts +72 -0
  186. package/dist/pre-flight-validator.d.ts.map +1 -0
  187. package/dist/pre-flight-validator.js +513 -0
  188. package/dist/pre-flight-validator.js.map +1 -0
  189. package/dist/progress-stream-utils.d.ts +3 -0
  190. package/dist/progress-stream-utils.d.ts.map +1 -0
  191. package/dist/progress-stream-utils.js +15 -0
  192. package/dist/progress-stream-utils.js.map +1 -0
  193. package/dist/result-cache.d.ts +52 -0
  194. package/dist/result-cache.d.ts.map +1 -0
  195. package/dist/result-cache.js +134 -0
  196. package/dist/result-cache.js.map +1 -0
  197. package/dist/routes/artifact-routes.d.ts +10 -0
  198. package/dist/routes/artifact-routes.d.ts.map +1 -0
  199. package/dist/routes/artifact-routes.js +126 -0
  200. package/dist/routes/artifact-routes.js.map +1 -0
  201. package/dist/routes/log-routes.d.ts +8 -0
  202. package/dist/routes/log-routes.d.ts.map +1 -0
  203. package/dist/routes/log-routes.js +345 -0
  204. package/dist/routes/log-routes.js.map +1 -0
  205. package/dist/routes/status-routes.d.ts +8 -0
  206. package/dist/routes/status-routes.d.ts.map +1 -0
  207. package/dist/routes/status-routes.js +82 -0
  208. package/dist/routes/status-routes.js.map +1 -0
  209. package/dist/routes/webhook-routes.d.ts +6 -0
  210. package/dist/routes/webhook-routes.d.ts.map +1 -0
  211. package/dist/routes/webhook-routes.js +86 -0
  212. package/dist/routes/webhook-routes.js.map +1 -0
  213. package/dist/run-artifact-metadata-cache.d.ts +42 -0
  214. package/dist/run-artifact-metadata-cache.d.ts.map +1 -0
  215. package/dist/run-artifact-metadata-cache.js +139 -0
  216. package/dist/run-artifact-metadata-cache.js.map +1 -0
  217. package/dist/secret-value-cache.d.ts +13 -0
  218. package/dist/secret-value-cache.d.ts.map +1 -0
  219. package/dist/secret-value-cache.js +44 -0
  220. package/dist/secret-value-cache.js.map +1 -0
  221. package/dist/secrets/SecretsManager.d.ts +80 -0
  222. package/dist/secrets/SecretsManager.d.ts.map +1 -0
  223. package/dist/secrets/SecretsManager.js +306 -0
  224. package/dist/secrets/SecretsManager.js.map +1 -0
  225. package/dist/test-utils.d.ts +55 -0
  226. package/dist/test-utils.d.ts.map +1 -0
  227. package/dist/test-utils.js +48 -0
  228. package/dist/test-utils.js.map +1 -0
  229. package/dist/timestamp-tracker.d.ts +75 -0
  230. package/dist/timestamp-tracker.d.ts.map +1 -0
  231. package/dist/timestamp-tracker.js +121 -0
  232. package/dist/timestamp-tracker.js.map +1 -0
  233. package/dist/utils/failure-artifact-writer.d.ts +29 -0
  234. package/dist/utils/failure-artifact-writer.d.ts.map +1 -0
  235. package/dist/utils/failure-artifact-writer.js +157 -0
  236. package/dist/utils/failure-artifact-writer.js.map +1 -0
  237. package/dist/utils/file-helpers.d.ts +41 -0
  238. package/dist/utils/file-helpers.d.ts.map +1 -0
  239. package/dist/utils/file-helpers.js +143 -0
  240. package/dist/utils/file-helpers.js.map +1 -0
  241. package/dist/utils/http-client-factory.d.ts +46 -0
  242. package/dist/utils/http-client-factory.d.ts.map +1 -0
  243. package/dist/utils/http-client-factory.js +114 -0
  244. package/dist/utils/http-client-factory.js.map +1 -0
  245. package/dist/utils/progress-normalizer.d.ts +13 -0
  246. package/dist/utils/progress-normalizer.d.ts.map +1 -0
  247. package/dist/utils/progress-normalizer.js +57 -0
  248. package/dist/utils/progress-normalizer.js.map +1 -0
  249. package/dist/utils/response-helpers.d.ts +34 -0
  250. package/dist/utils/response-helpers.d.ts.map +1 -0
  251. package/dist/utils/response-helpers.js +78 -0
  252. package/dist/utils/response-helpers.js.map +1 -0
  253. package/dist/utils/route-helpers.d.ts +17 -0
  254. package/dist/utils/route-helpers.d.ts.map +1 -0
  255. package/dist/utils/route-helpers.js +22 -0
  256. package/dist/utils/route-helpers.js.map +1 -0
  257. package/dist/utils/status-response-builder.d.ts +23 -0
  258. package/dist/utils/status-response-builder.d.ts.map +1 -0
  259. package/dist/utils/status-response-builder.js +144 -0
  260. package/dist/utils/status-response-builder.js.map +1 -0
  261. package/dist/utils/type-guards.d.ts +37 -0
  262. package/dist/utils/type-guards.d.ts.map +1 -0
  263. package/dist/utils/type-guards.js +45 -0
  264. package/dist/utils/type-guards.js.map +1 -0
  265. package/dist/utils/utf8-helpers.d.ts +32 -0
  266. package/dist/utils/utf8-helpers.d.ts.map +1 -0
  267. package/dist/utils/utf8-helpers.js +97 -0
  268. package/dist/utils/utf8-helpers.js.map +1 -0
  269. package/dist/utils/webhook-event-builder.d.ts +26 -0
  270. package/dist/utils/webhook-event-builder.d.ts.map +1 -0
  271. package/dist/utils/webhook-event-builder.js +77 -0
  272. package/dist/utils/webhook-event-builder.js.map +1 -0
  273. package/dist/webhook-manager.d.ts +56 -0
  274. package/dist/webhook-manager.d.ts.map +1 -0
  275. package/dist/webhook-manager.js +359 -0
  276. package/dist/webhook-manager.js.map +1 -0
  277. package/docker/workspace-cache/package-lock.json +13 -0
  278. package/docker/workspace-cache/package.json +7 -0
  279. package/docker-compose.yml +53 -0
  280. package/docs/API.md +708 -0
  281. package/docs/BACKLOG.md +19 -0
  282. package/docs/BUILD_STRATEGY.md +404 -0
  283. package/docs/CLI.md +569 -0
  284. package/docs/DEPLOYMENT.md +521 -0
  285. package/docs/DEVELOPMENT.md +459 -0
  286. package/docs/DOCKER_SETUP.md +522 -0
  287. package/docs/ENHANCED_PROGRESS_LOGS.md +264 -0
  288. package/docs/IMPLEMENTATION_SUMMARY.md +549 -0
  289. package/docs/INTEGRATION_EXAMPLE.md +217 -0
  290. package/docs/NPM_SETUP.md +468 -0
  291. package/docs/PHASE1-4_IMPLEMENTATION.md +302 -0
  292. package/docs/PHASE1_COMPLETION.md +192 -0
  293. package/docs/PHASE2_COMPLETION.md +134 -0
  294. package/docs/PHASE6_MIGRATION.md +392 -0
  295. package/docs/PRINTF_SAFETY_FIX.md +282 -0
  296. package/docs/QUALITY_GATES.md +369 -0
  297. package/docs/SETUP_GUIDE.md +482 -0
  298. package/docs/TASK_PROMPT_TEMPLATES.md +533 -0
  299. package/docs/VALIDATION_FIX.md +139 -0
  300. package/docs/VERIFICATION_CHECKLIST.md +335 -0
  301. package/docs/repo-maturity.md +760 -0
  302. package/fix-tests.d.ts +9 -0
  303. package/fix-tests.d.ts.map +1 -0
  304. package/fix-tests.js.map +1 -0
  305. package/fix-tests.ts +53 -0
  306. package/jest.config.ts +31 -0
  307. package/kaseki +183 -0
  308. package/kaseki-agent.sh +1961 -0
  309. package/ops/logrotate/kaseki +10 -0
  310. package/package.json +83 -0
  311. package/perf/README.md +54 -0
  312. package/perf/pi-event-filter.benchmark.test.ts +98 -0
  313. package/run-kaseki-json.test.sh +106 -0
  314. package/run-kaseki.sh +990 -0
  315. package/scripts/allowlist-helper.sh +56 -0
  316. package/scripts/cleanup-kaseki.sh +168 -0
  317. package/scripts/deploy-pi-template.sh +293 -0
  318. package/scripts/docker-entrypoint.sh +71 -0
  319. package/scripts/dry-run-allowlist.sh +161 -0
  320. package/scripts/kaseki-activate.sh +396 -0
  321. package/scripts/kaseki-api.service +62 -0
  322. package/scripts/kaseki-container-entrypoint-wrapper.sh +119 -0
  323. package/scripts/kaseki-container-setup-remote.sh +172 -0
  324. package/scripts/kaseki-container-setup.sh +193 -0
  325. package/scripts/kaseki-healthcheck.sh +95 -0
  326. package/scripts/kaseki-install.sh +50 -0
  327. package/scripts/kaseki-maturity-score.sh +291 -0
  328. package/scripts/kaseki-performance-metrics.sh +122 -0
  329. package/scripts/kaseki-preflight.sh +270 -0
  330. package/scripts/kaseki-setup.sh +265 -0
  331. package/scripts/pi-setup-remote.sh +213 -0
  332. package/scripts/setup-github-labels.sh +42 -0
  333. package/scripts/suggest-allowlist.sh +68 -0
  334. package/scripts/templates/MULTI_HOST_DISTRIBUTED.md +337 -0
  335. package/scripts/templates/REST_API_SERVICE.md +490 -0
  336. package/scripts/templates/SINGLE_HOST_CLI.md +194 -0
  337. package/scripts/test-github-app.sh +248 -0
  338. package/src/add-js-extensions.ts +61 -0
  339. package/src/ansi-colors.test.ts +62 -0
  340. package/src/ansi-colors.ts +67 -0
  341. package/src/cli/BaseCommand.ts +40 -0
  342. package/src/cli/KasekiCLI.ts +154 -0
  343. package/src/cli/commands/ConfigCommand.ts +145 -0
  344. package/src/cli/commands/DoctorCommand.ts +329 -0
  345. package/src/cli/commands/ListCommand.ts +105 -0
  346. package/src/cli/commands/ReportCommand.ts +110 -0
  347. package/src/cli/commands/RunCommand.ts +218 -0
  348. package/src/cli/commands/SecretsCommand.ts +120 -0
  349. package/src/cli/commands/ServeCommand.ts +62 -0
  350. package/src/cli/commands/SetupCommand.ts +301 -0
  351. package/src/cli.ts +138 -0
  352. package/src/config/ConfigManager.ts +476 -0
  353. package/src/docker/DockerManager.ts +319 -0
  354. package/src/docker-entrypoint-packaging.test.ts +33 -0
  355. package/src/event-aggregator.test.ts +117 -0
  356. package/src/event-aggregator.ts +126 -0
  357. package/src/github-app-token.ts +215 -0
  358. package/src/idempotency-store.test.ts +117 -0
  359. package/src/idempotency-store.ts +385 -0
  360. package/src/index.ts +89 -0
  361. package/src/instance/InstanceManager.ts +285 -0
  362. package/src/instance-metadata-reader.test.ts +190 -0
  363. package/src/instance-metadata-reader.ts +129 -0
  364. package/src/instance-state-derivation.test.ts +263 -0
  365. package/src/instance-state-derivation.ts +148 -0
  366. package/src/job-scheduler.test.ts +1236 -0
  367. package/src/job-scheduler.ts +1117 -0
  368. package/src/kaseki-api-client.ts +488 -0
  369. package/src/kaseki-api-config.test.ts +315 -0
  370. package/src/kaseki-api-config.ts +175 -0
  371. package/src/kaseki-api-routes.test.ts +1615 -0
  372. package/src/kaseki-api-routes.ts +643 -0
  373. package/src/kaseki-api-service-wrapper.ts +188 -0
  374. package/src/kaseki-api-service.test.ts +418 -0
  375. package/src/kaseki-api-service.ts +192 -0
  376. package/src/kaseki-api-types.ts +320 -0
  377. package/src/kaseki-cli-lib.test.ts +552 -0
  378. package/src/kaseki-cli-lib.ts +760 -0
  379. package/src/kaseki-cli.ts +682 -0
  380. package/src/kaseki-report.test.ts +118 -0
  381. package/src/kaseki-report.ts +192 -0
  382. package/src/lib/subprocess-helpers.ts +177 -0
  383. package/src/logger.ts +114 -0
  384. package/src/metrics.ts +66 -0
  385. package/src/middleware/job-lookup.test.ts +113 -0
  386. package/src/middleware/job-lookup.ts +45 -0
  387. package/src/pi-event-filter.test.ts +183 -0
  388. package/src/pi-event-filter.ts +183 -0
  389. package/src/pi-progress-stream.ts +287 -0
  390. package/src/pi-progress-summarizer.test.ts +302 -0
  391. package/src/pi-progress-summarizer.ts +287 -0
  392. package/src/pre-flight-validator.test.ts +512 -0
  393. package/src/pre-flight-validator.ts +618 -0
  394. package/src/progress-stream-utils.test.ts +35 -0
  395. package/src/progress-stream-utils.ts +14 -0
  396. package/src/result-cache.test.ts +195 -0
  397. package/src/result-cache.ts +181 -0
  398. package/src/routes/artifact-routes.ts +169 -0
  399. package/src/routes/log-routes.ts +391 -0
  400. package/src/routes/status-routes.ts +92 -0
  401. package/src/routes/webhook-routes.ts +97 -0
  402. package/src/run-artifact-metadata-cache.test.ts +80 -0
  403. package/src/run-artifact-metadata-cache.ts +184 -0
  404. package/src/secret-value-cache.test.ts +66 -0
  405. package/src/secret-value-cache.ts +55 -0
  406. package/src/secrets/SecretsManager.ts +343 -0
  407. package/src/test-utils.ts +81 -0
  408. package/src/timestamp-tracker.test.ts +134 -0
  409. package/src/timestamp-tracker.ts +132 -0
  410. package/src/utils/failure-artifact-writer.ts +187 -0
  411. package/src/utils/file-helpers.test.ts +235 -0
  412. package/src/utils/file-helpers.ts +150 -0
  413. package/src/utils/http-client-factory.test.ts +245 -0
  414. package/src/utils/http-client-factory.ts +157 -0
  415. package/src/utils/progress-normalizer.test.ts +442 -0
  416. package/src/utils/progress-normalizer.ts +68 -0
  417. package/src/utils/response-helpers.test.ts +122 -0
  418. package/src/utils/response-helpers.ts +101 -0
  419. package/src/utils/route-helpers.ts +30 -0
  420. package/src/utils/status-response-builder.ts +159 -0
  421. package/src/utils/type-guards.ts +52 -0
  422. package/src/utils/utf8-helpers.ts +102 -0
  423. package/src/utils/webhook-event-builder.test.ts +143 -0
  424. package/src/utils/webhook-event-builder.ts +87 -0
  425. package/src/webhook-manager.test.ts +152 -0
  426. package/src/webhook-manager.ts +445 -0
  427. package/templates/allowlist-api-route.txt +7 -0
  428. package/templates/allowlist-comprehensive.txt +8 -0
  429. package/templates/allowlist-parser-fix.txt +6 -0
  430. package/templates/allowlist-ui-component.txt +9 -0
  431. package/templates/allowlist-utility.txt +9 -0
  432. package/test/actual-model-metadata.test.sh +102 -0
  433. package/test/dry-run.test.sh +131 -0
  434. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-0.json +1 -0
  435. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-1.json +1 -0
  436. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-invalid.json +1 -0
  437. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-0.json +1 -0
  438. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-1.json +1 -0
  439. package/test/kaseki-api.integration.test.sh +165 -0
  440. package/test/pi-event-filter-failure.test.sh +83 -0
  441. package/test/printf-safety-focused.test.sh +99 -0
  442. package/test/printf-safety-results/results/restoration.jsonl +10 -0
  443. package/test/printf-safety-results/results/test.jsonl +0 -0
  444. package/test/printf-safety.test.sh +297 -0
  445. package/test/validation-fix.test.sh +79 -0
  446. package/test/validation-integration.test.sh +109 -0
  447. package/tests/allowlist-glob.test.sh +61 -0
  448. package/tests/dependency-cache-key.test.sh +48 -0
  449. package/tests/dependency-restore-mode.test.sh +48 -0
  450. package/tests/doctor-template-parity.test.sh +95 -0
  451. package/tests/github-operations.test.sh +142 -0
  452. package/tests/npm-install-flags.test.sh +58 -0
  453. package/tests/quality-gates.test.sh +178 -0
  454. package/tests/repo-memory.test.sh +103 -0
  455. package/tests/restore-disallowed-changes.test.sh +80 -0
  456. package/tests/validation-missing-npm-scripts.test.sh +93 -0
  457. package/tests/validation-strict-mode.test.sh +118 -0
  458. package/tsconfig.changed.json +7 -0
  459. package/tsconfig.json +39 -0
@@ -0,0 +1,1117 @@
1
+ import { ChildProcess, spawn } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { StringDecoder } from 'node:string_decoder';
6
+ import { Job, RunRequest, WebhookEventType, WebhookPayload } from './kaseki-api-types';
7
+ import { DEFAULT_JOB_INDEX_MAX_ENTRIES, KasekiApiConfig } from './kaseki-api-config';
8
+ import { createEventLogger, EventLogger } from './logger';
9
+ import { WebhookManager } from './webhook-manager';
10
+ import { metricsRegistry } from './metrics';
11
+ import { execSubprocess } from './lib/subprocess-helpers';
12
+ import { FailureArtifactWriter } from './utils/failure-artifact-writer';
13
+ import { clearRunArtifactMetadataCache } from './run-artifact-metadata-cache';
14
+ import { secretValueCache } from './secret-value-cache';
15
+ import type { ResultCache } from './result-cache';
16
+
17
+ type PersistedJob = Omit<Job, 'createdAt' | 'startedAt' | 'completedAt' | 'timeout'> & {
18
+ createdAt: string;
19
+ startedAt?: string;
20
+ completedAt?: string;
21
+ };
22
+
23
+ type CleanupResult = {
24
+ attempted: boolean;
25
+ ok?: boolean;
26
+ detail?: string;
27
+ };
28
+
29
+ type LiveProgressCacheEntry = {
30
+ events: Array<Record<string, unknown>>;
31
+ expiresAt: number;
32
+ };
33
+
34
+ /**
35
+ * Job scheduler manages a FIFO queue of kaseki runs with concurrency control.
36
+ */
37
+ export class JobScheduler {
38
+ private static readonly STREAM_TAIL_LIMIT_BYTES = 64 * 1024;
39
+ private static readonly DEFAULT_LIVE_PROGRESS_CACHE_TTL_MS = 1500;
40
+ private jobs = new Map<string, Job>();
41
+ private queue: Job[] = [];
42
+ private running = new Set<string>();
43
+ private processes = new Map<string, ChildProcess>();
44
+ private processExited = new Map<string, boolean>();
45
+ private shutdownKillTimers = new Map<string, NodeJS.Timeout>();
46
+ private timeoutKillTimers = new Map<string, NodeJS.Timeout>();
47
+ private liveProgressCache = new Map<string, LiveProgressCacheEntry>();
48
+ private config: KasekiApiConfig;
49
+ private indexPath: string;
50
+ private nextIdPath: string;
51
+ private idLockPath: string;
52
+ private indexLockPath: string;
53
+ private logger: EventLogger;
54
+ private webhookManager: WebhookManager;
55
+ private failureArtifactWriter: FailureArtifactWriter;
56
+ private artifactCache?: Pick<ResultCache, 'clearForJob'>;
57
+ private static readonly SHUTDOWN_GRACE_MS = 5000;
58
+
59
+ constructor(config: KasekiApiConfig, webhookManager: WebhookManager, artifactCache?: Pick<ResultCache, 'clearForJob'>) {
60
+ this.config = config;
61
+ this.indexPath = path.join(config.resultsDir, '.kaseki-api-jobs.json');
62
+ this.nextIdPath = path.join(config.resultsDir, '.kaseki-api-next-id');
63
+ this.idLockPath = path.join(config.resultsDir, '.kaseki-api-id.lock');
64
+ this.indexLockPath = path.join(config.resultsDir, '.kaseki-api-jobs.lock');
65
+ this.logger = createEventLogger('job-scheduler');
66
+ this.webhookManager = webhookManager;
67
+ this.failureArtifactWriter = new FailureArtifactWriter(config.resultsDir);
68
+ this.artifactCache = artifactCache;
69
+ this.loadPersistedJobs();
70
+ this.persistJobs();
71
+ this.processQueue();
72
+ metricsRegistry.setQueuePending(this.queue.length);
73
+ metricsRegistry.setRunningJobs(this.running.size);
74
+ }
75
+
76
+ /**
77
+ * Submit a new job to the queue.
78
+ */
79
+ async submitJob(request: RunRequest): Promise<Job> {
80
+ const instanceId = await this.generateInstanceId();
81
+
82
+ // Generate tracing IDs if not provided
83
+ const correlationId = request.tracing?.correlationId || randomUUID();
84
+ const requestId = request.tracing?.requestId || randomUUID();
85
+
86
+ const job: Job = {
87
+ id: instanceId,
88
+ status: 'queued',
89
+ request,
90
+ createdAt: new Date(),
91
+ resultDir: this.getResultDir(instanceId),
92
+ webhookConfig: request.webhookConfig,
93
+ correlationId,
94
+ requestId,
95
+ };
96
+
97
+ this.jobs.set(instanceId, job);
98
+ this.queue.push(job);
99
+ this.persistJobs();
100
+
101
+ // Emit webhook event for job submission
102
+ if (job.webhookConfig) {
103
+ const payload: WebhookPayload = {
104
+ eventType: WebhookEventType.JOB_SUBMITTED,
105
+ jobId: instanceId,
106
+ timestamp: new Date().toISOString(),
107
+ data: {
108
+ status: 'queued',
109
+ },
110
+ };
111
+ this.webhookManager.enqueueWebhook(instanceId, payload, job.webhookConfig);
112
+ }
113
+
114
+ this.processQueue();
115
+ metricsRegistry.setQueuePending(this.queue.length);
116
+
117
+ // Log job submission
118
+ this.logger.event('job_submitted', {
119
+ jobId: instanceId,
120
+ correlationId,
121
+ requestId,
122
+ repoUrl: request.repoUrl,
123
+ ref: request.ref,
124
+ queueDepth: this.queue.length,
125
+ runningCount: this.running.size,
126
+ });
127
+
128
+ return job;
129
+ }
130
+
131
+ /**
132
+ * Get a job by ID.
133
+ */
134
+ getJob(id: string): Job | undefined {
135
+ return this.jobs.get(id);
136
+ }
137
+
138
+ /**
139
+ * List jobs retained in the API index. Active jobs are always retained;
140
+ * terminal jobs may be compacted out of this API index after the newest
141
+ * `jobIndexMaxEntries` terminal records, while their artifacts remain on disk.
142
+ */
143
+ listJobs(): Job[] {
144
+ return Array.from(this.jobs.values()).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
145
+ }
146
+
147
+ /**
148
+ * Cancel a queued or running job.
149
+ */
150
+ cancelJob(id: string): Job | undefined {
151
+ const job = this.jobs.get(id);
152
+ if (!job || job.status === 'completed' || job.status === 'failed') {
153
+ return job;
154
+ }
155
+
156
+ const completedAt = new Date();
157
+ if (job.status === 'queued') {
158
+ this.queue = this.queue.filter((queued) => queued.id !== id);
159
+ job.status = 'failed';
160
+ job.exitCode = 143;
161
+ job.failureClass = 'cancelled';
162
+ job.error = 'Job cancelled before execution';
163
+ job.completedAt = completedAt;
164
+ job.finalized = true;
165
+ this.failureArtifactWriter.writeFailureArtifacts(job, {
166
+ attempted: false,
167
+ detail: 'Job never started; no worker container was created.',
168
+ });
169
+ clearRunArtifactMetadataCache(job.id, job.resultDir);
170
+ this.clearArtifactContentCache(job.id);
171
+ this.clearLiveProgressCache(job.id);
172
+ this.persistJobs();
173
+
174
+ // Emit webhook event for cancellation
175
+ if (job.webhookConfig) {
176
+ const payload: WebhookPayload = {
177
+ eventType: WebhookEventType.JOB_CANCELLED,
178
+ jobId: id,
179
+ timestamp: new Date().toISOString(),
180
+ data: {
181
+ status: 'failed',
182
+ failureClass: 'cancelled',
183
+ error: job.error,
184
+ },
185
+ };
186
+ this.webhookManager.enqueueWebhook(id, payload, job.webhookConfig);
187
+ }
188
+
189
+ this.logger.event('job_cancelled', {
190
+ jobId: id,
191
+ reason: 'cancelled_before_execution',
192
+ });
193
+
194
+ return job;
195
+ }
196
+
197
+ const proc = this.processes.get(id);
198
+ if (proc) {
199
+ proc.kill('SIGTERM');
200
+ }
201
+ const cleanup = this.cleanupContainer(id);
202
+ const updates: Partial<Job> = {
203
+ status: 'failed',
204
+ exitCode: 143,
205
+ failureClass: 'cancelled',
206
+ error: 'Job cancelled by API request',
207
+ completedAt,
208
+ };
209
+ this.finalizeJobIfNeeded(job, updates);
210
+ this.failureArtifactWriter.writeFailureArtifacts(job, cleanup);
211
+ clearRunArtifactMetadataCache(job.id, job.resultDir);
212
+ this.clearArtifactContentCache(job.id);
213
+ this.clearLiveProgressCache(job.id);
214
+
215
+ this.logger.event('job_cancelled', {
216
+ jobId: id,
217
+ reason: 'cancelled_by_request',
218
+ processId: job.processId,
219
+ });
220
+
221
+ return job;
222
+ }
223
+
224
+ /**
225
+ * Process the queue, respecting max concurrent limit.
226
+ */
227
+ private processQueue(): void {
228
+ while (this.queue.length > 0 && this.running.size < this.config.maxConcurrentRuns) {
229
+ const job = this.queue.shift();
230
+ if (job) {
231
+ this.executeJob(job);
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Execute a single job.
238
+ */
239
+ private executeJob(job: Job): void {
240
+ const effectiveTimeoutSeconds = job.request.timeoutSeconds ?? this.config.agentTimeoutSeconds;
241
+ job.status = 'running';
242
+ job.startedAt = new Date();
243
+ job.effectiveTimeoutSeconds = effectiveTimeoutSeconds;
244
+ job.resultDir = this.getResultDir(job.id);
245
+ this.running.add(job.id);
246
+ metricsRegistry.setRunningJobs(this.running.size);
247
+
248
+ // Emit webhook event for job start
249
+ if (job.webhookConfig) {
250
+ const payload: WebhookPayload = {
251
+ eventType: WebhookEventType.JOB_STARTED,
252
+ jobId: job.id,
253
+ timestamp: new Date().toISOString(),
254
+ data: {
255
+ status: 'running',
256
+ },
257
+ };
258
+ this.webhookManager.enqueueWebhook(job.id, payload, job.webhookConfig);
259
+ }
260
+
261
+ // Log job start
262
+ this.logger.event('job_started', {
263
+ jobId: job.id,
264
+ repoUrl: job.request.repoUrl,
265
+ ref: job.request.ref,
266
+ processId: job.processId,
267
+ runningCount: this.running.size,
268
+ });
269
+ const env: NodeJS.ProcessEnv = {
270
+ ...process.env,
271
+ // run-kaseki.sh owns creation of the per-instance result directory and
272
+ // refuses to overwrite it. Keep host log mirroring at the parent results
273
+ // directory so the API does not accidentally reserve the final result path.
274
+ KASEKI_LOG_DIR: this.config.resultsDir,
275
+ KASEKI_TASK_MODE: job.request.taskMode || this.config.defaultTaskMode,
276
+ ...(job.request.publishMode ? { KASEKI_PUBLISH_MODE: job.request.publishMode } : {}),
277
+ KASEKI_MAX_DIFF_BYTES: String(job.request.maxDiffBytes || this.config.maxDiffBytes),
278
+ KASEKI_AGENT_TIMEOUT_SECONDS: String(effectiveTimeoutSeconds),
279
+ };
280
+ this.populateGitHubAppEnv(env);
281
+
282
+ if (job.request.startupCheck) {
283
+ env.KASEKI_DRY_RUN = '1';
284
+ env.KASEKI_TASK_MODE = 'inspect';
285
+ env.KASEKI_VALIDATION_COMMANDS = 'none';
286
+ env.TASK_PROMPT =
287
+ job.request.taskPrompt ||
288
+ 'Run Kaseki startup checks only. Verify container boot and dependencies, then exit without agent work.';
289
+ }
290
+
291
+ const changedFilesAllowlist = job.request.changedFilesAllowlist ?? job.request.allowlist?.include;
292
+ if (changedFilesAllowlist) {
293
+ env.KASEKI_CHANGED_FILES_ALLOWLIST = changedFilesAllowlist.join(' ');
294
+ }
295
+
296
+ const validationCommands = job.request.validationCommands ?? job.request.validation?.commands;
297
+ if (validationCommands) {
298
+ env.KASEKI_VALIDATION_COMMANDS = validationCommands.join(';');
299
+ }
300
+
301
+ if (job.request.taskPrompt) {
302
+ env.TASK_PROMPT = job.request.taskPrompt;
303
+ }
304
+
305
+ // Determine kaseki-activate.sh path
306
+ let activateScript = '/agents/kaseki-template/scripts/kaseki-activate.sh';
307
+ if (!fs.existsSync(activateScript)) {
308
+ // Fall back to development path
309
+ activateScript = `${process.env.PWD || '/workspaces/kaseki-agent'}/scripts/kaseki-activate.sh`;
310
+ }
311
+
312
+ // Invoke kaseki-activate.sh with --controller flag
313
+ const proc = spawn('bash', [activateScript, '--controller', 'run', job.request.repoUrl, job.request.ref, job.id], {
314
+ env,
315
+ stdio: 'pipe',
316
+ });
317
+ this.processes.set(job.id, proc);
318
+ this.processExited.set(job.id, false);
319
+ let stdoutTail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
320
+ let stderrTail: Buffer<ArrayBufferLike> = Buffer.alloc(0);
321
+
322
+ job.processId = proc.pid;
323
+ let timedOut = false;
324
+ this.persistJobs();
325
+
326
+ proc.stdout?.on('data', (chunk: Buffer | string) => {
327
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
328
+ stdoutTail = this.appendBoundedTail(stdoutTail, incoming, JobScheduler.STREAM_TAIL_LIMIT_BYTES);
329
+ });
330
+ proc.stderr?.on('data', (chunk: Buffer | string) => {
331
+ const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
332
+ stderrTail = this.appendBoundedTail(stderrTail, incoming, JobScheduler.STREAM_TAIL_LIMIT_BYTES);
333
+ });
334
+
335
+ // Set timeout
336
+ const timeout = setTimeout(() => {
337
+ if (job.finalized) {
338
+ return;
339
+ }
340
+ timedOut = true;
341
+ proc.kill('SIGTERM');
342
+ const timeoutKillTimer = setTimeout(() => {
343
+ if (!this.processExited.get(job.id) && !job.finalized) {
344
+ proc.kill('SIGKILL');
345
+ }
346
+ this.timeoutKillTimers.delete(job.id);
347
+ }, JobScheduler.SHUTDOWN_GRACE_MS);
348
+ this.unrefTimer(timeoutKillTimer);
349
+ this.timeoutKillTimers.set(job.id, timeoutKillTimer);
350
+ }, effectiveTimeoutSeconds * 1000);
351
+ this.unrefTimer(timeout);
352
+
353
+ job.timeout = timeout;
354
+
355
+ // Handle process exit
356
+ proc.on('exit', (code) => {
357
+ this.processExited.set(job.id, true);
358
+ if (job.finalized) {
359
+ return;
360
+ }
361
+ clearTimeout(timeout);
362
+ const timeoutKillTimer = this.timeoutKillTimers.get(job.id);
363
+ if (timeoutKillTimer) {
364
+ clearTimeout(timeoutKillTimer);
365
+ this.timeoutKillTimers.delete(job.id);
366
+ }
367
+ const updates: Partial<Job> = {
368
+ completedAt: new Date(),
369
+ exitCode: code ?? -1,
370
+ };
371
+ if (timedOut) {
372
+ metricsRegistry.incTimeout();
373
+ updates.status = 'failed';
374
+ updates.exitCode = 124;
375
+ updates.failureClass = 'timeout';
376
+ updates.error = `Agent timeout after ${effectiveTimeoutSeconds} seconds`;
377
+ this.logger.event('job_failed', {
378
+ jobId: job.id,
379
+ failureClass: 'timeout',
380
+ exitCode: 124,
381
+ durationSeconds: Math.round((updates.completedAt as Date).getTime() - (job.startedAt?.getTime() || 0)) / 1000,
382
+ });
383
+
384
+ } else if (code === 0) {
385
+ updates.status = 'completed';
386
+ this.logger.event('job_completed', {
387
+ jobId: job.id,
388
+ exitCode: code,
389
+ durationSeconds: Math.round((updates.completedAt as Date).getTime() - (job.startedAt?.getTime() || 0)) / 1000,
390
+ });
391
+
392
+ } else {
393
+ updates.status = 'failed';
394
+ this.parseFailureFromResults(job);
395
+ this.writeControllerBootstrapLogs(job, stdoutTail, stderrTail);
396
+ this.failureArtifactWriter.writeFailureArtifacts(job, { attempted: false, ok: false, detail: 'Worker failed before complete diagnostics.' }, {
397
+ stdoutTail,
398
+ stderrTail,
399
+ lastStage: 'worker_exit',
400
+ });
401
+ this.logger.event('job_failed', {
402
+ jobId: job.id,
403
+ exitCode: code,
404
+ failureClass: job.failureClass,
405
+ error: job.error,
406
+ durationSeconds: Math.round((updates.completedAt as Date).getTime() - (job.startedAt?.getTime() || 0)) / 1000,
407
+ });
408
+
409
+ }
410
+ this.finalizeJobIfNeeded(job, updates);
411
+ if (timedOut) {
412
+ const cleanup = this.cleanupContainer(job.id);
413
+ this.failureArtifactWriter.writeFailureArtifacts(job, cleanup, { stdoutTail, stderrTail, lastStage: 'timeout' });
414
+ }
415
+ });
416
+
417
+ // Handle process error
418
+ proc.on('error', (err) => {
419
+ this.processExited.set(job.id, true);
420
+ if (job.finalized) {
421
+ return;
422
+ }
423
+ clearTimeout(timeout);
424
+ const errorMsg = `Failed to spawn process: ${err.message}`;
425
+ this.logger.event('job_failed', {
426
+ jobId: job.id,
427
+ failureClass: 'spawn_error',
428
+ error: errorMsg,
429
+ });
430
+ this.finalizeJobIfNeeded(job, {
431
+ status: 'failed',
432
+ error: errorMsg,
433
+ completedAt: new Date(),
434
+ });
435
+ });
436
+ }
437
+
438
+ private unrefTimer(timer: NodeJS.Timeout): void {
439
+ timer.unref();
440
+ }
441
+
442
+ private populateGitHubAppEnv(env: NodeJS.ProcessEnv): void {
443
+ const githubAppId = secretValueCache.readSecretValue(env.GITHUB_APP_ID, env.GITHUB_APP_ID_FILE);
444
+ if (githubAppId) {
445
+ env.GITHUB_APP_ID = githubAppId;
446
+ }
447
+
448
+ const githubClientId = secretValueCache.readSecretValue(env.GITHUB_APP_CLIENT_ID, env.GITHUB_APP_CLIENT_ID_FILE);
449
+ if (githubClientId) {
450
+ env.GITHUB_APP_CLIENT_ID = githubClientId;
451
+ }
452
+
453
+ if (!env.GITHUB_APP_PRIVATE_KEY && env.GITHUB_APP_PRIVATE_KEY_FILE && !fs.existsSync(env.GITHUB_APP_PRIVATE_KEY_FILE)) {
454
+ this.logger.event('github_app_private_key_file_unreadable', {
455
+ path: env.GITHUB_APP_PRIVATE_KEY_FILE,
456
+ });
457
+ }
458
+ }
459
+
460
+ private finalizeJobIfNeeded(job: Job, updates: Partial<Job>): void {
461
+ if (job.finalized) {
462
+ return;
463
+ }
464
+
465
+ if (updates.status !== undefined) {
466
+ job.status = updates.status;
467
+ }
468
+ if (updates.exitCode !== undefined) {
469
+ job.exitCode = updates.exitCode;
470
+ }
471
+ if (updates.error !== undefined) {
472
+ job.error = updates.error;
473
+ }
474
+ if (updates.failureClass !== undefined) {
475
+ job.failureClass = updates.failureClass;
476
+ }
477
+ if (updates.completedAt !== undefined) {
478
+ job.completedAt = updates.completedAt;
479
+ }
480
+ if (updates.resultDir !== undefined) {
481
+ job.resultDir = updates.resultDir;
482
+ }
483
+
484
+ this.emitTerminalWebhook(job);
485
+ this.completeJob(job);
486
+ }
487
+
488
+ private emitTerminalWebhook(job: Job): void {
489
+ if (!job.webhookConfig) {
490
+ return;
491
+ }
492
+
493
+ const elapsed =
494
+ job.completedAt && job.startedAt ? Math.round((job.completedAt.getTime() - job.startedAt.getTime()) / 1000) : undefined;
495
+
496
+ if (job.failureClass === 'cancelled') {
497
+ const payload: WebhookPayload = {
498
+ eventType: WebhookEventType.JOB_CANCELLED,
499
+ jobId: job.id,
500
+ timestamp: new Date().toISOString(),
501
+ data: {
502
+ status: 'failed',
503
+ failureClass: 'cancelled',
504
+ error: job.error,
505
+ },
506
+ };
507
+ this.webhookManager.enqueueWebhook(job.id, payload, job.webhookConfig);
508
+ return;
509
+ }
510
+
511
+ if (job.status === 'completed') {
512
+ const payload: WebhookPayload = {
513
+ eventType: WebhookEventType.JOB_COMPLETED,
514
+ jobId: job.id,
515
+ timestamp: new Date().toISOString(),
516
+ data: {
517
+ status: 'completed',
518
+ exitCode: job.exitCode,
519
+ elapsed,
520
+ },
521
+ };
522
+ this.webhookManager.enqueueWebhook(job.id, payload, job.webhookConfig);
523
+ return;
524
+ }
525
+
526
+ if (job.status === 'failed') {
527
+ const payload: WebhookPayload = {
528
+ eventType: WebhookEventType.JOB_FAILED,
529
+ jobId: job.id,
530
+ timestamp: new Date().toISOString(),
531
+ data: {
532
+ status: 'failed',
533
+ exitCode: job.exitCode ?? undefined,
534
+ failureClass: job.failureClass,
535
+ error: job.error,
536
+ elapsed,
537
+ },
538
+ };
539
+ this.webhookManager.enqueueWebhook(job.id, payload, job.webhookConfig);
540
+ }
541
+ }
542
+
543
+ private appendBoundedTail(
544
+ currentTail: Buffer<ArrayBufferLike>,
545
+ incoming: Buffer<ArrayBufferLike>,
546
+ limitBytes: number
547
+ ): Buffer<ArrayBufferLike> {
548
+ if (incoming.length >= limitBytes) {
549
+ return incoming.subarray(incoming.length - limitBytes);
550
+ }
551
+ const combined = currentTail.length > 0 ? Buffer.concat([currentTail, incoming]) : incoming;
552
+ if (combined.length <= limitBytes) {
553
+ return combined;
554
+ }
555
+ return combined.subarray(combined.length - limitBytes);
556
+ }
557
+
558
+ private writeControllerBootstrapLogs(
559
+ job: Job,
560
+ stdoutTail: Buffer<ArrayBufferLike>,
561
+ stderrTail: Buffer<ArrayBufferLike>
562
+ ): void {
563
+ const resultDir = this.getResultDir(job.id);
564
+ const stderrPath = path.join(resultDir, 'stderr.log');
565
+ if (fs.existsSync(stderrPath)) {
566
+ return;
567
+ }
568
+
569
+ try {
570
+ fs.mkdirSync(resultDir, { recursive: true });
571
+ const stderrContent = `controller bootstrap stderr (captured by api wrapper)\n${this.decodeUtf8Tail(stderrTail)}`;
572
+ fs.writeFileSync(stderrPath, stderrContent, 'utf-8');
573
+
574
+ const stdoutPath = path.join(resultDir, 'stdout.log');
575
+ if (!fs.existsSync(stdoutPath)) {
576
+ const stdoutContent = `controller bootstrap stdout (captured by api wrapper)\n${this.decodeUtf8Tail(stdoutTail)}`;
577
+ fs.writeFileSync(stdoutPath, stdoutContent, 'utf-8');
578
+ }
579
+ } catch {
580
+ // Best effort fallback: avoid masking original run failure.
581
+ }
582
+ }
583
+
584
+ private decodeUtf8Tail(tail: Buffer<ArrayBufferLike>): string {
585
+ const decoder = new StringDecoder('utf8');
586
+ return decoder.end(tail);
587
+ }
588
+
589
+ /**
590
+ * Parse failure class from results metadata.
591
+ */
592
+ private parseFailureFromResults(job: Job): void {
593
+ try {
594
+ const metadataPath = path.join(this.getResultDir(job.id), 'metadata.json');
595
+ if (fs.existsSync(metadataPath)) {
596
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
597
+ if (metadata.failure) {
598
+ job.failureClass = metadata.failure.failureClass;
599
+ job.error = metadata.failure.message;
600
+ }
601
+ }
602
+ } catch {
603
+ // Ignore parsing errors
604
+ }
605
+ }
606
+
607
+ private clearArtifactContentCache(jobId: string): void {
608
+ this.artifactCache?.clearForJob(jobId);
609
+ }
610
+
611
+ /**
612
+ * Clean up after job completion.
613
+ */
614
+ private completeJob(job: Job): void {
615
+ if (job.finalized) {
616
+ return;
617
+ }
618
+ job.finalized = true;
619
+ if (!job.completedAt) {
620
+ job.completedAt = new Date();
621
+ }
622
+ if (job.startedAt && job.completedAt) {
623
+ metricsRegistry.observeRunDuration((job.completedAt.getTime() - job.startedAt.getTime()) / 1000);
624
+ }
625
+ if (job.status === 'completed') {
626
+ metricsRegistry.incRunSuccess();
627
+ } else if (job.status === 'failed') {
628
+ metricsRegistry.incRunFailure();
629
+ }
630
+ this.running.delete(job.id);
631
+ metricsRegistry.setRunningJobs(this.running.size);
632
+ this.processes.delete(job.id);
633
+ const shutdownKillTimer = this.shutdownKillTimers.get(job.id);
634
+ if (shutdownKillTimer) {
635
+ clearTimeout(shutdownKillTimer);
636
+ this.shutdownKillTimers.delete(job.id);
637
+ }
638
+ const timeoutKillTimer = this.timeoutKillTimers.get(job.id);
639
+ if (timeoutKillTimer) {
640
+ clearTimeout(timeoutKillTimer);
641
+ this.timeoutKillTimers.delete(job.id);
642
+ }
643
+ this.processExited.delete(job.id);
644
+ clearRunArtifactMetadataCache(job.id, job.resultDir);
645
+ this.clearArtifactContentCache(job.id);
646
+ this.clearLiveProgressCache(job.id);
647
+ this.persistJobs();
648
+ this.processQueue();
649
+ metricsRegistry.setQueuePending(this.queue.length);
650
+ }
651
+
652
+ /**
653
+ * Generate a unique, durable instance ID.
654
+ *
655
+ * Format: `kaseki-N`, matching run-kaseki.sh and result directory names.
656
+ */
657
+ private async generateInstanceId(): Promise<string> {
658
+ return this.withIdLock(async () => {
659
+ let nextId = this.readNextId();
660
+ const maxAttempts = 10000;
661
+
662
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
663
+ const id = `kaseki-${nextId}`;
664
+ if (!this.jobs.has(id) && !fs.existsSync(this.getResultDir(id))) {
665
+ fs.writeFileSync(this.nextIdPath, `${nextId + 1}\n`, { mode: 0o600 });
666
+ return id;
667
+ }
668
+ nextId += 1;
669
+ }
670
+
671
+ throw new Error(`Failed to allocate unique job ID after ${maxAttempts} attempts`);
672
+ });
673
+ }
674
+
675
+ private withIdLock<T>(callback: () => Promise<T>): Promise<T> {
676
+ return this.withLock(this.idLockPath, 'Kaseki instance ID', callback);
677
+ }
678
+
679
+ private async withLock<T>(lockPath: string, lockName: string, callback: () => Promise<T>): Promise<T> {
680
+ fs.mkdirSync(this.config.resultsDir, { recursive: true });
681
+ let acquired = false;
682
+ for (let attempt = 0; attempt < 100; attempt += 1) {
683
+ try {
684
+ fs.mkdirSync(lockPath, { mode: 0o700 });
685
+ acquired = true;
686
+ break;
687
+ } catch (err) {
688
+ const code = (err as NodeJS.ErrnoException).code;
689
+ if (code !== 'EEXIST') {
690
+ throw err;
691
+ }
692
+ await new Promise((resolve) => setTimeout(resolve, 25));
693
+ }
694
+ }
695
+
696
+ if (!acquired) {
697
+ throw new Error(`Failed to acquire ${lockName} lock: ${lockPath}`);
698
+ }
699
+
700
+ try {
701
+ return await callback();
702
+ } finally {
703
+ fs.rmSync(lockPath, { recursive: true, force: true });
704
+ }
705
+ }
706
+
707
+ private withSyncLock<T>(lockPath: string, lockName: string, callback: () => T): T {
708
+ fs.mkdirSync(this.config.resultsDir, { recursive: true });
709
+ try {
710
+ fs.mkdirSync(lockPath, { mode: 0o700 });
711
+ } catch (err) {
712
+ const code = (err as NodeJS.ErrnoException).code;
713
+ if (code === 'EEXIST') {
714
+ throw new Error(`Failed to acquire ${lockName} lock: ${lockPath}`);
715
+ }
716
+ throw err;
717
+ }
718
+
719
+ try {
720
+ return callback();
721
+ } finally {
722
+ fs.rmSync(lockPath, { recursive: true, force: true });
723
+ }
724
+ }
725
+
726
+ private readNextId(): number {
727
+ const persisted = this.readPositiveIntFile(this.nextIdPath);
728
+ const discovered = this.discoverNextId();
729
+ return Math.max(persisted ?? 1, discovered);
730
+ }
731
+
732
+ private readPositiveIntFile(filePath: string): number | undefined {
733
+ try {
734
+ const value = parseInt(fs.readFileSync(filePath, 'utf-8').trim(), 10);
735
+ return Number.isInteger(value) && value > 0 ? value : undefined;
736
+ } catch {
737
+ return undefined;
738
+ }
739
+ }
740
+
741
+ private discoverNextId(): number {
742
+ let maxId = 0;
743
+ for (const id of this.jobs.keys()) {
744
+ maxId = Math.max(maxId, this.parseInstanceNumber(id) ?? 0);
745
+ }
746
+ try {
747
+ for (const entry of fs.readdirSync(this.config.resultsDir, { withFileTypes: true })) {
748
+ if (entry.isDirectory()) {
749
+ maxId = Math.max(maxId, this.parseInstanceNumber(entry.name) ?? 0);
750
+ }
751
+ }
752
+ } catch {
753
+ // Missing/unreadable results dir is handled elsewhere; keep allocation best-effort.
754
+ }
755
+ return maxId + 1;
756
+ }
757
+
758
+ private cleanupContainer(id: string): CleanupResult {
759
+ if (!/^kaseki-\d+$/.test(id)) {
760
+ return { attempted: false, ok: false, detail: 'Invalid Kaseki container id.' };
761
+ }
762
+ const result = execSubprocess('docker', ['rm', '-f', id]);
763
+ return {
764
+ attempted: true,
765
+ ok: result.ok,
766
+ detail: result.detail || undefined,
767
+ };
768
+ }
769
+
770
+ getLiveDockerLogTail(id: string, lines = 200): string | null {
771
+ if (!/^kaseki-\d+$/.test(id)) {
772
+ return null;
773
+ }
774
+ const result = execSubprocess('docker', ['logs', '--tail', String(lines), id]);
775
+ const output = [result.stdout || '', result.stderr || ''].join('');
776
+ return output.trim().length > 0 ? output : null;
777
+ }
778
+
779
+ getLiveProgressEvents(id: string, tail = 25): Array<Record<string, unknown>> {
780
+ if (!/^kaseki-\d+$/.test(id)) {
781
+ return [];
782
+ }
783
+
784
+ const cachedEvents = this.getCachedLiveProgressEvents(id);
785
+ if (cachedEvents) {
786
+ return tail > 0 ? cachedEvents.slice(-tail) : [];
787
+ }
788
+
789
+ const output = this.getLiveDockerLogTail(id, Math.max(tail * 8, 80));
790
+ if (!output) {
791
+ this.cacheLiveProgressEvents(id, []);
792
+ return [];
793
+ }
794
+ const events = this.parseLiveProgressEvents(output);
795
+ this.cacheLiveProgressEvents(id, events);
796
+ return tail > 0 ? events.slice(-tail) : [];
797
+ }
798
+
799
+ private parseLiveProgressEvents(output: string): Array<Record<string, unknown>> {
800
+ const events: Array<Record<string, unknown>> = [];
801
+ for (const line of output.split(/\r?\n/)) {
802
+ const match = /^\[progress\]\s+([^:]+):\s*(.*)$/.exec(line);
803
+ if (match) {
804
+ events.push({
805
+ source: 'docker-logs',
806
+ stage: match[1].trim(),
807
+ message: match[2].trim(),
808
+ timestamp: new Date().toISOString(),
809
+ });
810
+ }
811
+ }
812
+ return events;
813
+ }
814
+
815
+ private getCachedLiveProgressEvents(id: string): Array<Record<string, unknown>> | undefined {
816
+ const cached = this.liveProgressCache.get(id);
817
+ if (!cached) {
818
+ return undefined;
819
+ }
820
+ if (Date.now() >= cached.expiresAt) {
821
+ this.liveProgressCache.delete(id);
822
+ return undefined;
823
+ }
824
+ return cached.events;
825
+ }
826
+
827
+ private cacheLiveProgressEvents(id: string, events: Array<Record<string, unknown>>): void {
828
+ this.liveProgressCache.set(id, {
829
+ events,
830
+ expiresAt: Date.now() + this.getLiveProgressCacheTtlMs(),
831
+ });
832
+ }
833
+
834
+ private clearLiveProgressCache(id: string): void {
835
+ this.liveProgressCache.delete(id);
836
+ }
837
+
838
+ private getLiveProgressCacheTtlMs(): number {
839
+ const rawTtl = process.env.KASEKI_LIVE_PROGRESS_CACHE_TTL_MS;
840
+ if (!rawTtl) {
841
+ return JobScheduler.DEFAULT_LIVE_PROGRESS_CACHE_TTL_MS;
842
+ }
843
+
844
+ const ttl = Number(rawTtl);
845
+ if (!Number.isFinite(ttl) || ttl < 0) {
846
+ return JobScheduler.DEFAULT_LIVE_PROGRESS_CACHE_TTL_MS;
847
+ }
848
+ return ttl;
849
+ }
850
+
851
+ private getResultDir(id: string): string {
852
+ return path.join(this.config.resultsDir, id);
853
+ }
854
+
855
+ private serializeJob(job: Job): PersistedJob {
856
+ const serializableJob = { ...job };
857
+ delete serializableJob.timeout;
858
+ return {
859
+ ...serializableJob,
860
+ createdAt: job.createdAt.toISOString(),
861
+ startedAt: job.startedAt?.toISOString(),
862
+ completedAt: job.completedAt?.toISOString(),
863
+ };
864
+ }
865
+
866
+ private deserializeJob(job: PersistedJob): Job {
867
+ return {
868
+ ...job,
869
+ createdAt: new Date(job.createdAt),
870
+ startedAt: job.startedAt ? new Date(job.startedAt) : undefined,
871
+ completedAt: job.completedAt ? new Date(job.completedAt) : undefined,
872
+ resultDir: job.resultDir || this.getResultDir(job.id),
873
+ finalized: job.status === 'completed' || job.status === 'failed' ? true : job.finalized,
874
+ };
875
+ }
876
+
877
+ private loadPersistedJobs(): void {
878
+ try {
879
+ this.withSyncLock(this.indexLockPath, 'Kaseki jobs index', () => {
880
+ if (!fs.existsSync(this.indexPath)) {
881
+ return;
882
+ }
883
+
884
+ try {
885
+ const parsed = JSON.parse(fs.readFileSync(this.indexPath, 'utf-8')) as { jobs?: PersistedJob[] };
886
+ for (const persisted of parsed.jobs || []) {
887
+ const job = this.deserializeJob(persisted);
888
+ if (job.status === 'running') {
889
+ job.status = 'failed';
890
+ job.exitCode = 143;
891
+ job.failureClass = 'api_restart';
892
+ job.error = 'API service restarted while job was running';
893
+ job.completedAt = job.completedAt || new Date();
894
+ job.finalized = true;
895
+ }
896
+ if (job.status === 'queued') {
897
+ this.queue.push(job);
898
+ }
899
+ this.jobs.set(job.id, job);
900
+ }
901
+ } catch {
902
+ // A corrupt index should not prevent the API from starting; existing
903
+ // artifacts remain available on disk for direct inspection.
904
+ }
905
+ });
906
+ } catch {
907
+ // Lock contention during startup is best-effort; a future persist/load cycle will reconcile state.
908
+ }
909
+ }
910
+
911
+ private parseInstanceNumber(id: string): number | null {
912
+ const match = /^kaseki-(\d+)$/.exec(id);
913
+ if (!match) {
914
+ return null;
915
+ }
916
+ const value = Number.parseInt(match[1], 10);
917
+ return Number.isFinite(value) && value > 0 ? value : null;
918
+ }
919
+
920
+ private persistJobs(): void {
921
+ try {
922
+ this.withSyncLock(this.indexLockPath, 'Kaseki jobs index', () => {
923
+ fs.mkdirSync(this.config.resultsDir, { recursive: true });
924
+ const current = this.readPersistedJobsIndex();
925
+ const merged = this.mergePersistedJobs(
926
+ current.jobs || [],
927
+ this.listJobs().map((job) => this.serializeJob(job)),
928
+ );
929
+ const payload = {
930
+ version: 1,
931
+ updatedAt: new Date().toISOString(),
932
+ jobs: merged,
933
+ };
934
+ const tmpPath = `${this.indexPath}.tmp`;
935
+ const json = this.shouldWriteCompactIndex(merged) ? JSON.stringify(payload) : JSON.stringify(payload, null, 2);
936
+ fs.writeFileSync(tmpPath, `${json}\n`, { mode: 0o600 });
937
+ fs.renameSync(tmpPath, this.indexPath);
938
+ });
939
+ } catch {
940
+ // Keep scheduler progress alive even if persistence is unavailable.
941
+ }
942
+ }
943
+
944
+ private readPersistedJobsIndex(): { jobs?: PersistedJob[] } {
945
+ if (!fs.existsSync(this.indexPath)) {
946
+ return {};
947
+ }
948
+ try {
949
+ return JSON.parse(fs.readFileSync(this.indexPath, 'utf-8')) as { jobs?: PersistedJob[] };
950
+ } catch {
951
+ return {};
952
+ }
953
+ }
954
+
955
+ private mergePersistedJobs(existing: PersistedJob[], incoming: PersistedJob[]): PersistedJob[] {
956
+ const byId = new Map<string, PersistedJob>();
957
+ for (const job of existing) {
958
+ byId.set(job.id, job);
959
+ }
960
+ for (const job of incoming) {
961
+ const prev = byId.get(job.id);
962
+ if (!prev) {
963
+ byId.set(job.id, job);
964
+ continue;
965
+ }
966
+ byId.set(job.id, this.selectMostRecentPersistedJob(prev, job));
967
+ }
968
+
969
+ const activeJobs: PersistedJob[] = [];
970
+ const terminalJobs: PersistedJob[] = [];
971
+ for (const job of byId.values()) {
972
+ if (this.isTerminalPersistedJob(job)) {
973
+ terminalJobs.push(job);
974
+ } else {
975
+ activeJobs.push(job);
976
+ }
977
+ }
978
+
979
+ const retainedTerminalJobs = terminalJobs
980
+ .sort((a, b) => this.comparePersistedJobsByTerminalRecency(a, b))
981
+ .slice(0, this.getJobIndexMaxEntries());
982
+
983
+ return [...activeJobs, ...retainedTerminalJobs].sort((a, b) => this.comparePersistedJobsByCreatedAt(a, b));
984
+ }
985
+
986
+ private shouldWriteCompactIndex(jobs: PersistedJob[]): boolean {
987
+ return jobs.length >= this.getJobIndexMaxEntries();
988
+ }
989
+
990
+ private getJobIndexMaxEntries(): number {
991
+ return this.config.jobIndexMaxEntries ?? DEFAULT_JOB_INDEX_MAX_ENTRIES;
992
+ }
993
+
994
+ private isTerminalPersistedJob(job: PersistedJob): boolean {
995
+ return job.status === 'completed' || job.status === 'failed';
996
+ }
997
+
998
+ private comparePersistedJobsByTerminalRecency(a: PersistedJob, b: PersistedJob): number {
999
+ const updatedDiff = this.persistedJobUpdatedAt(b) - this.persistedJobUpdatedAt(a);
1000
+ if (updatedDiff !== 0) {
1001
+ return updatedDiff;
1002
+ }
1003
+ return this.comparePersistedJobsByCreatedAt(a, b);
1004
+ }
1005
+
1006
+ private comparePersistedJobsByCreatedAt(a: PersistedJob, b: PersistedJob): number {
1007
+ const createdDiff = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
1008
+ if (createdDiff !== 0) {
1009
+ return createdDiff;
1010
+ }
1011
+ return b.id.localeCompare(a.id, undefined, { numeric: true, sensitivity: 'base' });
1012
+ }
1013
+
1014
+ private selectMostRecentPersistedJob(a: PersistedJob, b: PersistedJob): PersistedJob {
1015
+ const aUpdated = this.persistedJobUpdatedAt(a);
1016
+ const bUpdated = this.persistedJobUpdatedAt(b);
1017
+ if (aUpdated !== bUpdated) {
1018
+ return bUpdated > aUpdated ? b : a;
1019
+ }
1020
+ const statusPriority: Record<Job['status'], number> = { queued: 0, running: 1, failed: 2, completed: 2 };
1021
+ return (statusPriority[b.status] || 0) >= (statusPriority[a.status] || 0) ? b : a;
1022
+ }
1023
+
1024
+ private persistedJobUpdatedAt(job: PersistedJob): number {
1025
+ return Math.max(
1026
+ new Date(job.createdAt).getTime(),
1027
+ job.startedAt ? new Date(job.startedAt).getTime() : 0,
1028
+ job.completedAt ? new Date(job.completedAt).getTime() : 0,
1029
+ );
1030
+ }
1031
+
1032
+ /**
1033
+ * Get queue status.
1034
+ */
1035
+ getQueueStatus(): { pending: number; running: number; maxConcurrent: number } {
1036
+ return {
1037
+ pending: this.queue.length,
1038
+ running: this.running.size,
1039
+ maxConcurrent: this.config.maxConcurrentRuns,
1040
+ };
1041
+ }
1042
+
1043
+ getReadiness(): { ready: boolean; reasons: string[] } {
1044
+ const reasons: string[] = [];
1045
+ try {
1046
+ fs.accessSync(this.config.resultsDir, fs.constants.R_OK | fs.constants.W_OK);
1047
+ } catch (error) {
1048
+ reasons.push(`results_dir_unwritable:${(error as Error).message}`);
1049
+ }
1050
+ if (!this.webhookManager.isHealthy()) {
1051
+ reasons.push('webhook_manager_unhealthy');
1052
+ }
1053
+ try {
1054
+ const status = this.getQueueStatus();
1055
+ if (!Number.isFinite(status.pending) || !Number.isFinite(status.running)) {
1056
+ reasons.push('scheduler_status_invalid');
1057
+ }
1058
+ } catch {
1059
+ reasons.push('scheduler_unavailable');
1060
+ }
1061
+ return { ready: reasons.length === 0, reasons };
1062
+ }
1063
+
1064
+ /**
1065
+ * Shutdown the scheduler, aborting running jobs.
1066
+ */
1067
+ shutdown(): void {
1068
+ for (const jobId of this.running) {
1069
+ const j = this.jobs.get(jobId);
1070
+ if (j?.timeout) {
1071
+ clearTimeout(j.timeout);
1072
+ }
1073
+
1074
+ const proc = this.processes.get(jobId);
1075
+ if (proc) {
1076
+ proc.kill('SIGTERM');
1077
+
1078
+ const shutdownKillTimer = setTimeout(() => {
1079
+ if (!this.processExited.get(jobId)) {
1080
+ proc.kill('SIGKILL');
1081
+ }
1082
+ this.shutdownKillTimers.delete(jobId);
1083
+ }, JobScheduler.SHUTDOWN_GRACE_MS);
1084
+ this.unrefTimer(shutdownKillTimer);
1085
+ this.shutdownKillTimers.set(jobId, shutdownKillTimer);
1086
+ }
1087
+
1088
+ if (j && !j.finalized) {
1089
+ j.status = 'failed';
1090
+ j.failureClass = 'shutdown_aborted';
1091
+ j.error = 'Job aborted during scheduler shutdown';
1092
+ j.exitCode = 143;
1093
+ j.completedAt = new Date();
1094
+ this.completeJob(j);
1095
+ }
1096
+ }
1097
+
1098
+ const now = new Date();
1099
+ for (const queuedJob of this.queue) {
1100
+ if (queuedJob.finalized) {
1101
+ continue;
1102
+ }
1103
+ queuedJob.status = 'failed';
1104
+ queuedJob.failureClass = 'shutdown_aborted';
1105
+ queuedJob.error = 'Job dropped during scheduler shutdown before execution';
1106
+ queuedJob.exitCode = 143;
1107
+ queuedJob.completedAt = now;
1108
+ queuedJob.finalized = true;
1109
+ this.jobs.set(queuedJob.id, queuedJob);
1110
+ this.clearLiveProgressCache(queuedJob.id);
1111
+ }
1112
+
1113
+ this.queue = [];
1114
+ this.liveProgressCache.clear();
1115
+ this.persistJobs();
1116
+ }
1117
+ }