@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,195 @@
1
+ import { ResultCache } from './result-cache';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ describe('ResultCache', () => {
6
+ let cache: ResultCache;
7
+ let testFile: string;
8
+ let testDir: string;
9
+
10
+ beforeEach(() => {
11
+ cache = new ResultCache(3, 1000); // 3 entries, 1 sec TTL for testing
12
+ testDir = fs.mkdtempSync(path.join('/tmp', 'kaseki-cache-test-'));
13
+ testFile = path.join(testDir, 'test.txt');
14
+ fs.writeFileSync(testFile, 'test content');
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(testDir, { recursive: true, force: true });
19
+ });
20
+
21
+ test('API contract: cache miss returns null, increments miss counter, and does not crash', () => {
22
+ const statsBefore = cache.getStats();
23
+
24
+ let content: string | null = null;
25
+ expect(() => {
26
+ content = cache.getOrLoad('/non/existent/file');
27
+ }).not.toThrow();
28
+
29
+ expect(content).toBeNull();
30
+
31
+ const statsAfter = cache.getStats();
32
+ expect(statsAfter.entries).toBe(statsBefore.entries);
33
+ expect(statsAfter.bytes).toBe(statsBefore.bytes);
34
+ expect(statsAfter.misses).toBe(statsBefore.misses + 1);
35
+ });
36
+
37
+ test('reloads content when file is updated within TTL', () => {
38
+ jest.useFakeTimers();
39
+ const baseTime = new Date('2026-01-01T00:00:00.000Z');
40
+ jest.setSystemTime(baseTime);
41
+
42
+ const initialContent = cache.getOrLoad(testFile);
43
+ expect(initialContent).toBe('test content');
44
+
45
+ jest.setSystemTime(new Date(baseTime.getTime() + 100));
46
+ fs.writeFileSync(testFile, 'modified content');
47
+
48
+ const reloadedContent = cache.getOrLoad(testFile);
49
+ expect(reloadedContent).toBe('modified content');
50
+
51
+ jest.useRealTimers();
52
+ });
53
+
54
+ test('returns cached content when file is unchanged within TTL', () => {
55
+ jest.useFakeTimers();
56
+ const baseTime = new Date('2026-01-01T00:00:00.000Z');
57
+ jest.setSystemTime(baseTime);
58
+
59
+ const initialContent = cache.getOrLoad(testFile);
60
+ expect(initialContent).toBe('test content');
61
+
62
+ jest.setSystemTime(new Date(baseTime.getTime() + 999));
63
+ const cachedContent = cache.getOrLoad(testFile);
64
+ expect(cachedContent).toBe('test content');
65
+
66
+ jest.useRealTimers();
67
+ });
68
+
69
+ test('expires cached entries after TTL using deterministic time control', () => {
70
+ jest.useFakeTimers();
71
+ const baseTime = new Date('2026-01-01T00:00:00.000Z');
72
+ jest.setSystemTime(baseTime);
73
+
74
+ const initialContent = cache.getOrLoad(testFile);
75
+ expect(initialContent).toBe('test content');
76
+
77
+ jest.setSystemTime(new Date(baseTime.getTime() + 1001));
78
+ fs.writeFileSync(testFile, 'modified content');
79
+
80
+ const reloadedContent = cache.getOrLoad(testFile);
81
+ expect(reloadedContent).toBe('modified content');
82
+
83
+ jest.useRealTimers();
84
+ });
85
+
86
+ test('evicts oldest entry when cache is full', () => {
87
+ const files = [];
88
+ for (let i = 0; i < 4; i++) {
89
+ const file = path.join(testDir, `file-${i}.txt`);
90
+ fs.writeFileSync(file, `content-${i}`);
91
+ const initialContent = cache.getOrLoad(file);
92
+ expect(initialContent).toBe(`content-${i}`);
93
+ files.push(file);
94
+ }
95
+
96
+ // First file should be evicted after initial load of all entries.
97
+ fs.writeFileSync(files[0], 'evicted content');
98
+ const content = cache.getOrLoad(files[0]);
99
+ expect(content).toBe('evicted content');
100
+ });
101
+
102
+ test('clears cache for a job', () => {
103
+ const file = path.join(testDir, 'kaseki-1/data.txt');
104
+ fs.mkdirSync(path.dirname(file), { recursive: true });
105
+ fs.writeFileSync(file, 'job data');
106
+
107
+ cache.getOrLoad(file);
108
+ cache.clearForJob('kaseki-1');
109
+
110
+ // Modify file and reload
111
+ fs.writeFileSync(file, 'modified job data');
112
+ const content = cache.getOrLoad(file);
113
+ expect(content).toBe('modified job data');
114
+ });
115
+
116
+ test('clears cache entries for POSIX-style path strings by job segment', () => {
117
+ const internalCache = (cache as unknown as { cache: Map<string, { content: string; timestamp: number; size: number; mtimeMs?: number; inode?: number }> }).cache;
118
+ internalCache.set('/tmp/jobs/kaseki-1/result.txt', { content: 'a', timestamp: Date.now(), size: 1 });
119
+ internalCache.set('/tmp/jobs/kaseki-10/result.txt', { content: 'b', timestamp: Date.now(), size: 1 });
120
+
121
+ cache.clearForJob('kaseki-1');
122
+
123
+ expect(internalCache.has('/tmp/jobs/kaseki-1/result.txt')).toBe(false);
124
+ expect(internalCache.has('/tmp/jobs/kaseki-10/result.txt')).toBe(true);
125
+ });
126
+
127
+ test('clears cache entries for Windows-style path strings by job segment', () => {
128
+ const internalCache = (cache as unknown as { cache: Map<string, { content: string; timestamp: number; size: number; mtimeMs?: number; inode?: number }> }).cache;
129
+ internalCache.set('C:\\kaseki\\runs\\kaseki-1\\result.txt', {
130
+ content: 'a',
131
+ timestamp: Date.now(),
132
+ size: 1,
133
+ });
134
+ internalCache.set('C:\\kaseki\\runs\\kaseki-10\\result.txt', {
135
+ content: 'b',
136
+ timestamp: Date.now(),
137
+ size: 1,
138
+ });
139
+
140
+ cache.clearForJob('kaseki-1');
141
+
142
+ expect(internalCache.has('C:\\kaseki\\runs\\kaseki-1\\result.txt')).toBe(false);
143
+ expect(internalCache.has('C:\\kaseki\\runs\\kaseki-10\\result.txt')).toBe(true);
144
+ });
145
+
146
+ test('provides cache statistics', () => {
147
+ cache.getOrLoad(testFile);
148
+
149
+ const stats = cache.getStats();
150
+ expect(stats.entries).toBe(1);
151
+ expect(stats.bytes).toBeGreaterThan(0);
152
+ expect(stats.hits).toBe(0);
153
+ expect(stats.misses).toBe(1);
154
+ expect(stats.maxEntries).toBe(3);
155
+ expect(stats.ttlMs).toBe(1000);
156
+ expect(stats.maxFileBytes).toBe(10 * 1024 * 1024);
157
+ });
158
+
159
+ test('tracks cache hits and misses', () => {
160
+ expect(cache.getOrLoad(testFile)).toBe('test content');
161
+ expect(cache.getOrLoad(testFile)).toBe('test content');
162
+
163
+ expect(cache.getStats()).toMatchObject({ hits: 1, misses: 1 });
164
+ });
165
+
166
+ test('honors configured max file bytes by not caching oversized files', () => {
167
+ const smallCache = new ResultCache({ maxEntries: 3, ttlMs: 1000, maxFileBytes: 4 });
168
+ const content = smallCache.getOrLoad(testFile);
169
+ expect(content).toBe('test content');
170
+ expect(smallCache.getStats()).toMatchObject({ entries: 0, bytes: 0, hits: 0, misses: 1 });
171
+ });
172
+
173
+ test('honors zero max entries by disabling caching', () => {
174
+ const disabledCache = new ResultCache({ maxEntries: 0, ttlMs: 1000, maxFileBytes: 100 });
175
+ expect(disabledCache.getOrLoad(testFile)).toBe('test content');
176
+ expect(disabledCache.getOrLoad(testFile)).toBe('test content');
177
+ expect(disabledCache.getStats()).toMatchObject({ entries: 0, hits: 0, misses: 2, maxEntries: 0 });
178
+ });
179
+
180
+ test('clears all cache', () => {
181
+ const initialContent = cache.getOrLoad(testFile);
182
+ expect(initialContent).toBe('test content');
183
+
184
+ cache.clearAll();
185
+
186
+ const stats = cache.getStats();
187
+ expect(stats.entries).toBe(0);
188
+ expect(stats.bytes).toBe(0);
189
+
190
+ // Mutate file after clearAll to prove subsequent load comes from disk, not stale cache.
191
+ fs.writeFileSync(testFile, 'content after clear');
192
+ const reloadedContent = cache.getOrLoad(testFile);
193
+ expect(reloadedContent).toBe('content after clear');
194
+ });
195
+ });
@@ -0,0 +1,181 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * Cached artifact entry.
6
+ */
7
+ export interface ResultCacheOptions {
8
+ maxEntries?: number;
9
+ ttlMs?: number;
10
+ maxFileBytes?: number;
11
+ }
12
+
13
+ export interface ResultCacheStats {
14
+ entries: number;
15
+ bytes: number;
16
+ hits: number;
17
+ misses: number;
18
+ maxEntries: number;
19
+ ttlMs: number;
20
+ maxFileBytes: number;
21
+ }
22
+
23
+ interface CacheEntry {
24
+ content: string;
25
+ timestamp: number;
26
+ size: number;
27
+ mtimeMs: number;
28
+ inode?: number;
29
+ }
30
+
31
+ /**
32
+ * Result cache for lazily loading and caching kaseki artifacts.
33
+ * Reduces filesystem reads for frequently accessed files.
34
+ */
35
+ export class ResultCache {
36
+ private cache = new Map<string, CacheEntry>();
37
+ private readonly maxEntries: number;
38
+ private readonly ttlMs: number;
39
+ private readonly maxFileBytes: number;
40
+ private hits = 0;
41
+ private misses = 0;
42
+
43
+ constructor(maxEntries?: number, ttlMs?: number, maxFileBytes?: number);
44
+ constructor(options?: ResultCacheOptions);
45
+ constructor(
46
+ maxEntriesOrOptions: number | ResultCacheOptions = 20,
47
+ ttlMs = 5 * 60 * 1000,
48
+ maxFileBytes = 10 * 1024 * 1024
49
+ ) {
50
+ if (typeof maxEntriesOrOptions === 'object') {
51
+ this.maxEntries = maxEntriesOrOptions.maxEntries ?? 20;
52
+ this.ttlMs = maxEntriesOrOptions.ttlMs ?? 5 * 60 * 1000;
53
+ this.maxFileBytes = maxEntriesOrOptions.maxFileBytes ?? 10 * 1024 * 1024;
54
+ } else {
55
+ this.maxEntries = maxEntriesOrOptions;
56
+ this.ttlMs = ttlMs;
57
+ this.maxFileBytes = maxFileBytes;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get or load a file from disk.
63
+ */
64
+ getOrLoad(filePath: string): string | null {
65
+ let fileStat: fs.Stats | undefined;
66
+
67
+ // Check cache
68
+ const cached = this.cache.get(filePath);
69
+ if (cached) {
70
+ const age = Date.now() - cached.timestamp;
71
+ if (age < this.ttlMs) {
72
+ try {
73
+ fileStat = fs.statSync(filePath);
74
+ const inode = typeof fileStat.ino === 'number' ? fileStat.ino : undefined;
75
+ const unchanged =
76
+ fileStat.mtimeMs === cached.mtimeMs &&
77
+ fileStat.size === cached.size &&
78
+ (cached.inode === undefined || inode === cached.inode);
79
+
80
+ if (unchanged) {
81
+ this.hits += 1;
82
+ return cached.content;
83
+ }
84
+ } catch {
85
+ // Fall through to reload from disk
86
+ }
87
+ } else {
88
+ // Expired, remove from cache
89
+ this.cache.delete(filePath);
90
+ }
91
+ }
92
+
93
+ this.misses += 1;
94
+
95
+ // Load from disk
96
+ if (!fs.existsSync(filePath)) {
97
+ return null;
98
+ }
99
+
100
+ try {
101
+ const stat = fileStat ?? fs.statSync(filePath);
102
+ const content = fs.readFileSync(filePath, 'utf-8');
103
+
104
+ if (this.maxEntries > 0 && stat.size <= this.maxFileBytes) {
105
+ this.set(filePath, content, stat.size, stat.mtimeMs, typeof stat.ino === 'number' ? stat.ino : undefined);
106
+ }
107
+
108
+ return content;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Set a cache entry.
116
+ */
117
+ private set(filePath: string, content: string, size: number, mtimeMs: number, inode?: number): void {
118
+ if (this.maxEntries <= 0) {
119
+ return;
120
+ }
121
+
122
+ // Evict oldest entry if cache is full
123
+ if (this.cache.size >= this.maxEntries) {
124
+ const oldest = Array.from(this.cache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
125
+ if (oldest) {
126
+ this.cache.delete(oldest[0]);
127
+ }
128
+ }
129
+
130
+ this.cache.set(filePath, {
131
+ content,
132
+ timestamp: Date.now(),
133
+ size,
134
+ mtimeMs,
135
+ inode,
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Clear cache for a job (e.g., when cleaning up after completion).
141
+ */
142
+ clearForJob(jobId: string): void {
143
+ const normalizedJobId = path.basename(path.normalize(jobId));
144
+
145
+ for (const key of this.cache.keys()) {
146
+ const normalizedKey = path.normalize(key);
147
+ const keySegments = normalizedKey.split(/[\\/]+/).filter(Boolean);
148
+ const hasJobSegment = keySegments.includes(normalizedJobId);
149
+
150
+ if (hasJobSegment) {
151
+ this.cache.delete(key);
152
+ }
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Clear all cache.
158
+ */
159
+ clearAll(): void {
160
+ this.cache.clear();
161
+ }
162
+
163
+ /**
164
+ * Get cache statistics.
165
+ */
166
+ getStats(): ResultCacheStats {
167
+ let bytes = 0;
168
+ for (const entry of this.cache.values()) {
169
+ bytes += entry.size;
170
+ }
171
+ return {
172
+ entries: this.cache.size,
173
+ bytes,
174
+ hits: this.hits,
175
+ misses: this.misses,
176
+ maxEntries: this.maxEntries,
177
+ ttlMs: this.ttlMs,
178
+ maxFileBytes: this.maxFileBytes,
179
+ };
180
+ }
181
+ }
@@ -0,0 +1,169 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { JobScheduler } from '../job-scheduler';
5
+ import { ResultCache } from '../result-cache';
6
+ import { KasekiApiConfig } from '../kaseki-api-config';
7
+ import { ArtifactResponse, RunArtifactsResponse } from '../kaseki-api-types';
8
+ import { sendErrorResponse } from '../utils/response-helpers';
9
+ import { getJobOrRespond } from '../utils/route-helpers';
10
+ import { getRunArtifactMetadata } from '../run-artifact-metadata-cache';
11
+
12
+ const ALWAYS_SAFE_SUMMARY_ARTIFACTS = [
13
+ 'git.diff',
14
+ 'metadata.json',
15
+ 'analysis.md',
16
+ 'result-summary.md',
17
+ 'pi-events.jsonl',
18
+ 'pi-summary.json',
19
+ 'progress.log',
20
+ ] as const;
21
+
22
+ const FAILURE_ONLY_DIAGNOSTICS_ARTIFACTS = [
23
+ 'failure.json',
24
+ 'stderr.log',
25
+ 'stdout.log',
26
+ 'validation.log',
27
+ 'quality.log',
28
+ ] as const;
29
+
30
+ function isTerminalJobStatus(status: 'queued' | 'running' | 'completed' | 'failed'): boolean {
31
+ return status === 'completed' || status === 'failed';
32
+ }
33
+
34
+ function artifactContentType(fileName: string): string {
35
+ if (fileName.endsWith('.json')) return 'application/json';
36
+ if (fileName.endsWith('.md')) return 'text/markdown';
37
+ return 'text/plain';
38
+ }
39
+
40
+ export function readArtifactContent(
41
+ filePath: string,
42
+ jobStatus: 'queued' | 'running' | 'completed' | 'failed',
43
+ cache: ResultCache
44
+ ): string | null {
45
+ if (!isTerminalJobStatus(jobStatus)) {
46
+ try {
47
+ return fs.readFileSync(filePath, 'utf-8');
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ return cache.getOrLoad(filePath);
53
+ }
54
+
55
+ /**
56
+ * Create artifact-related routes (list artifacts, download artifacts).
57
+ */
58
+ export function createArtifactRoutes(scheduler: JobScheduler, config: KasekiApiConfig, cache: ResultCache): Router {
59
+ const router = Router();
60
+
61
+ /**
62
+ * GET /api/results/:id/:file - Download artifact.
63
+ */
64
+ router.get('/results/:id/:file', (req: Request, res: Response) => {
65
+ const job = getJobOrRespond(scheduler, req.params.id, res);
66
+ if (!job) {
67
+ return;
68
+ }
69
+
70
+ const fileName = req.params.file;
71
+ const allowedFiles = [...ALWAYS_SAFE_SUMMARY_ARTIFACTS, ...FAILURE_ONLY_DIAGNOSTICS_ARTIFACTS];
72
+
73
+ if (!allowedFiles.some((allowedFile) => allowedFile === fileName)) {
74
+ return sendErrorResponse(
75
+ res,
76
+ 400,
77
+ 'Bad Request',
78
+ `Artifact not allowed: ${fileName}. Allowed: ${allowedFiles.join(', ')}`
79
+ );
80
+ }
81
+
82
+ if (FAILURE_ONLY_DIAGNOSTICS_ARTIFACTS.some((artifact) => artifact === fileName) && job.status !== 'failed') {
83
+ return sendErrorResponse(
84
+ res,
85
+ 400,
86
+ 'Bad Request',
87
+ `Artifact only available for failed runs: ${fileName}`
88
+ );
89
+ }
90
+
91
+ try {
92
+ const filePath = path.join(config.resultsDir, job.id, fileName);
93
+
94
+ if (!fs.existsSync(filePath)) {
95
+ return sendErrorResponse(res, 404, 'Not Found', `Artifact not found: ${fileName}`);
96
+ }
97
+
98
+ const contentType = artifactContentType(fileName);
99
+
100
+ // Read from disk for non-terminal jobs; cache only terminal artifacts.
101
+ const content = readArtifactContent(filePath, job.status, cache);
102
+ if (content === null) {
103
+ return sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read artifact: ${fileName}`);
104
+ }
105
+
106
+ const stat = fs.statSync(filePath);
107
+
108
+ const response: ArtifactResponse = {
109
+ file: fileName,
110
+ contentType,
111
+ size: stat.size,
112
+ content,
113
+ };
114
+
115
+ res.setHeader('Content-Type', contentType);
116
+ res.json(response);
117
+ } catch (err) {
118
+ sendErrorResponse(
119
+ res,
120
+ 500,
121
+ 'Internal Server Error',
122
+ `Failed to read artifact: ${(err as Error).message}`
123
+ );
124
+ }
125
+ });
126
+
127
+ /**
128
+ * GET /api/runs/:id/artifacts - Enumerate allowlisted artifacts and availability.
129
+ */
130
+ router.get('/runs/:id/artifacts', (req: Request, res: Response) => {
131
+ const job = getJobOrRespond(scheduler, req.params.id, res);
132
+ if (!job) {
133
+ return;
134
+ }
135
+
136
+ const runDir = job.resultDir || path.join(config.resultsDir, job.id);
137
+ const allowedFiles = [...ALWAYS_SAFE_SUMMARY_ARTIFACTS, ...FAILURE_ONLY_DIAGNOSTICS_ARTIFACTS];
138
+
139
+ const metadata = getRunArtifactMetadata(job.id, runDir, allowedFiles, isTerminalJobStatus(job.status));
140
+ const artifacts = allowedFiles.map((fileName) => {
141
+ const artifactMetadata = metadata[fileName] ?? { exists: false, size: 0 };
142
+ const isFailureOnly = FAILURE_ONLY_DIAGNOSTICS_ARTIFACTS.some((artifact) => artifact === fileName);
143
+ const hasContent = artifactMetadata.exists && artifactMetadata.size > 0;
144
+ const available = hasContent && (!isFailureOnly || job.status === 'failed');
145
+
146
+ return {
147
+ name: fileName,
148
+ size: artifactMetadata.size,
149
+ contentType: artifactContentType(fileName),
150
+ available,
151
+ };
152
+ });
153
+
154
+ const response: RunArtifactsResponse = {
155
+ id: job.id,
156
+ runStatus: job.status,
157
+ exitCode: job.exitCode,
158
+ artifacts,
159
+ recommended:
160
+ job.status === 'failed'
161
+ ? ['failure.json', 'stderr.log', 'stdout.log', 'validation.log', 'quality.log']
162
+ : ['result-summary.md', 'metadata.json', 'pi-summary.json', 'git.diff'],
163
+ };
164
+
165
+ res.json(response);
166
+ });
167
+
168
+ return router;
169
+ }