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