@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,643 @@
1
+ import { Router, Request, Response, NextFunction } from 'express';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as crypto from 'crypto';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { JobScheduler } from './job-scheduler';
7
+ import { IdempotencyStore } from './idempotency-store';
8
+ import { PreFlightValidator } from './pre-flight-validator';
9
+ import { execDockerCommand } from './lib/subprocess-helpers';
10
+ import {
11
+ RunRequestSchema,
12
+ RunResponse,
13
+ ValidationResponse,
14
+ PreflightCheck,
15
+ PreflightResponse,
16
+ Job,
17
+ } from './kaseki-api-types';
18
+ import { KasekiApiConfig, validateApiKey } from './kaseki-api-config';
19
+ import { createEventLogger } from './logger';
20
+ import { sendErrorResponse } from './utils/response-helpers';
21
+ import { readFirstLine, commandOutput } from './utils/file-helpers';
22
+ import { createStatusRoutes } from './routes/status-routes';
23
+ import { createLogRoutes } from './routes/log-routes';
24
+ import { createArtifactRoutes } from './routes/artifact-routes';
25
+ import { createWebhookRoutes } from './routes/webhook-routes';
26
+ import { metricsRegistry } from './metrics';
27
+ import { ResultCache } from './result-cache';
28
+
29
+ // Re-export UTF-8 helpers for backward compatibility
30
+ export { decodeUtf8TailSafely, tailLogByLines, readTailBytes } from './utils/utf8-helpers';
31
+
32
+ function stableStringify(value: unknown): string {
33
+ if (Array.isArray(value)) {
34
+ return `[${value.map((item) => stableStringify(item)).join(',')}]`;
35
+ }
36
+ if (value && typeof value === 'object') {
37
+ const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b));
38
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;
39
+ }
40
+ return JSON.stringify(value);
41
+ }
42
+
43
+ function buildRequestFingerprint(runRequest: Record<string, unknown>): string {
44
+ const requestForFingerprint = { ...runRequest };
45
+ delete requestForFingerprint.idempotencyKey;
46
+ return crypto.createHash('sha256').update(stableStringify(requestForFingerprint)).digest('hex');
47
+ }
48
+
49
+ function readKasekiImage(templateDir = '/agents/kaseki-template'): string {
50
+ if (process.env.KASEKI_IMAGE) {
51
+ return process.env.KASEKI_IMAGE;
52
+ }
53
+ const imageFile = path.join(templateDir, '.kaseki-image');
54
+ try {
55
+ const value = fs.readFileSync(imageFile, 'utf-8').trim();
56
+ if (value) {
57
+ return value;
58
+ }
59
+ } catch {
60
+ // Fall through to the registry default.
61
+ }
62
+ return 'docker.io/cyanautomation/kaseki-agent:latest';
63
+ }
64
+
65
+ function inspectImageDigest(image: string): string | undefined {
66
+ return commandOutput('docker', ['image', 'inspect', image, '--format', '{{range .RepoDigests}}{{println .}}{{end}}'])
67
+ ?.split(/\r?\n/)
68
+ .find((line) => line.trim().length > 0);
69
+ }
70
+
71
+ // Re-export from subprocess-helpers for backward compatibility with tests
72
+ export { classifyDockerFailure } from './lib/subprocess-helpers';
73
+
74
+ function checkOpenRouterKey(): PreflightCheck {
75
+ if (process.env.OPENROUTER_API_KEY && process.env.OPENROUTER_API_KEY.length > 0) {
76
+ return { name: 'openrouter-key', ok: true, detail: 'OPENROUTER_API_KEY is present in the API environment.' };
77
+ }
78
+
79
+ const keyFile = process.env.OPENROUTER_API_KEY_FILE || '/run/secrets/openrouter_api_key';
80
+ try {
81
+ const stat = fs.statSync(keyFile);
82
+ if (stat.isFile() && stat.size > 0) {
83
+ return { name: 'openrouter-key', ok: true, detail: `Readable key file: ${keyFile}` };
84
+ }
85
+ } catch {
86
+ // Handled below.
87
+ }
88
+
89
+ return {
90
+ name: 'openrouter-key',
91
+ ok: false,
92
+ detail: 'No OpenRouter API key was found in env or the configured key file.',
93
+ remediation: 'Set OPENROUTER_API_KEY for API-triggered runs or mount OPENROUTER_API_KEY_FILE.',
94
+ };
95
+ }
96
+
97
+ function readSecretValue(inlineValue?: string, filePath?: string): string | undefined {
98
+ const trimmedInline = inlineValue?.trim();
99
+ if (trimmedInline) {
100
+ return trimmedInline;
101
+ }
102
+ if (!filePath) {
103
+ return undefined;
104
+ }
105
+ try {
106
+ const stat = fs.statSync(filePath);
107
+ if (!stat.isFile() || stat.size === 0) {
108
+ return undefined;
109
+ }
110
+ const value = fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '').trim();
111
+ return value || undefined;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ function checkGitHubAppCredentials(): PreflightCheck {
118
+ const configured =
119
+ Boolean(process.env.GITHUB_APP_ID || process.env.GITHUB_APP_ID_FILE) ||
120
+ Boolean(process.env.GITHUB_APP_CLIENT_ID || process.env.GITHUB_APP_CLIENT_ID_FILE) ||
121
+ Boolean(process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_FILE);
122
+
123
+ if (!configured) {
124
+ return {
125
+ name: 'github-app',
126
+ ok: true,
127
+ detail: 'GitHub App credentials are not configured; PR creation will be disabled.',
128
+ };
129
+ }
130
+
131
+ const appId = readSecretValue(process.env.GITHUB_APP_ID, process.env.GITHUB_APP_ID_FILE);
132
+ const clientId = readSecretValue(process.env.GITHUB_APP_CLIENT_ID, process.env.GITHUB_APP_CLIENT_ID_FILE);
133
+ const privateKey =
134
+ process.env.GITHUB_APP_PRIVATE_KEY?.trim() ||
135
+ readSecretValue(undefined, process.env.GITHUB_APP_PRIVATE_KEY_FILE);
136
+
137
+ const missing: string[] = [];
138
+ if (!appId) {
139
+ missing.push('GITHUB_APP_ID or GITHUB_APP_ID_FILE');
140
+ }
141
+ if (!clientId) {
142
+ missing.push('GITHUB_APP_CLIENT_ID or GITHUB_APP_CLIENT_ID_FILE');
143
+ }
144
+ if (!privateKey) {
145
+ missing.push('GITHUB_APP_PRIVATE_KEY or GITHUB_APP_PRIVATE_KEY_FILE');
146
+ }
147
+
148
+ if (missing.length > 0) {
149
+ return {
150
+ name: 'github-app',
151
+ ok: false,
152
+ detail: `GitHub App credentials are incomplete: missing ${missing.join(', ')}.`,
153
+ remediation: 'Mount readable GitHub App secret files or set the corresponding environment variables.',
154
+ };
155
+ }
156
+ const keyLooksLikePem = /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(privateKey as string);
157
+ if (!/^\d+$/.test(appId as string)) {
158
+ return {
159
+ name: 'github-app',
160
+ ok: false,
161
+ detail: 'GitHub App ID is present but is not numeric.',
162
+ remediation: 'Set GITHUB_APP_ID or GITHUB_APP_ID_FILE to the numeric GitHub App ID.',
163
+ };
164
+ }
165
+ if (!keyLooksLikePem) {
166
+ return {
167
+ name: 'github-app',
168
+ ok: false,
169
+ detail: 'GitHub App private key is present but does not look like a PEM private key.',
170
+ remediation: 'Mount the GitHub App private key PEM file and point GITHUB_APP_PRIVATE_KEY_FILE at it.',
171
+ };
172
+ }
173
+
174
+ return {
175
+ name: 'github-app',
176
+ ok: true,
177
+ detail: 'GitHub App credentials are readable and structurally valid for PR creation.',
178
+ };
179
+ }
180
+
181
+ function isGitHubAppReady(): boolean {
182
+ return checkGitHubAppCredentials().ok &&
183
+ (Boolean(process.env.GITHUB_APP_ID || process.env.GITHUB_APP_ID_FILE) ||
184
+ Boolean(process.env.GITHUB_APP_CLIENT_ID || process.env.GITHUB_APP_CLIENT_ID_FILE) ||
185
+ Boolean(process.env.GITHUB_APP_PRIVATE_KEY || process.env.GITHUB_APP_PRIVATE_KEY_FILE));
186
+ }
187
+
188
+ function buildPreflightResponse(config: KasekiApiConfig): PreflightResponse {
189
+ const templateDir = process.env.KASEKI_TEMPLATE_DIR || '/agents/kaseki-template';
190
+ const image = readKasekiImage(templateDir);
191
+ const templateImageDigest = readFirstLine(path.join(templateDir, '.kaseki-image-digest')) || inspectImageDigest(image);
192
+ const checkoutDir = process.env.KASEKI_CHECKOUT_DIR || '/agents/kaseki-agent';
193
+ const templateRef = fs.existsSync(path.join(checkoutDir, '.git'))
194
+ ? commandOutput('git', ['rev-parse', '--short', 'HEAD'], checkoutDir)
195
+ : undefined;
196
+ const checks: PreflightCheck[] = [];
197
+
198
+ try {
199
+ fs.accessSync(config.resultsDir, fs.constants.R_OK | fs.constants.W_OK);
200
+ checks.push({ name: 'results-dir', ok: true, detail: `${config.resultsDir} is readable and writable.` });
201
+ } catch (err) {
202
+ checks.push({
203
+ name: 'results-dir',
204
+ ok: false,
205
+ detail: `${config.resultsDir} is not readable and writable: ${(err as Error).message}`,
206
+ remediation: 'Create the results directory and make it writable by the API container user.',
207
+ });
208
+ }
209
+
210
+ checks.push(checkOpenRouterKey());
211
+ checks.push(checkGitHubAppCredentials());
212
+
213
+ const dockerVersion = execDockerCommand(['version', '--format', '{{.Client.Version}} -> {{.Server.Version}}']);
214
+ if (dockerVersion.ok) {
215
+ checks.push({ name: 'docker-daemon', ok: true, detail: dockerVersion.stdout });
216
+ } else {
217
+ const classified = dockerVersion.classification || { detail: 'Docker command failed', remediation: 'Check Docker daemon' };
218
+ checks.push({ name: 'docker-daemon', ok: false, ...classified });
219
+ }
220
+
221
+ const imageInspect = execDockerCommand(['image', 'inspect', image]);
222
+ if (imageInspect.ok) {
223
+ checks.push({ name: 'docker-image', ok: true, detail: `Image is present: ${image}` });
224
+ } else {
225
+ const classified = imageInspect.classification || { detail: 'Docker command failed', remediation: 'Check Docker daemon' };
226
+ const daemonFailed = checks.some((check) => check.name === 'docker-daemon' && !check.ok);
227
+ checks.push({
228
+ name: 'docker-image',
229
+ ok: false,
230
+ detail: daemonFailed ? classified.detail : `Docker image is not present locally: ${image}`,
231
+ remediation: daemonFailed ? classified.remediation : `Pull ${image} or set KASEKI_IMAGE to an available image.`,
232
+ });
233
+ }
234
+
235
+ const runScript = path.join(templateDir, 'run-kaseki.sh');
236
+ checks.push({
237
+ name: 'template',
238
+ ok: fs.existsSync(runScript),
239
+ detail: fs.existsSync(runScript) ? `Template runner exists: ${runScript}` : `Missing template runner: ${runScript}`,
240
+ remediation: fs.existsSync(runScript) ? undefined : 'Run scripts/kaseki-activate.sh --controller bootstrap.',
241
+ });
242
+
243
+ const status = checks.every((check) => check.ok)
244
+ ? 'ok'
245
+ : checks.some((check) => check.name === 'docker-daemon' && !check.ok)
246
+ ? 'error'
247
+ : 'degraded';
248
+ return {
249
+ status,
250
+ timestamp: new Date().toISOString(),
251
+ checks,
252
+ image,
253
+ imageDigest: templateImageDigest,
254
+ templateImage: image,
255
+ templateImageDigest,
256
+ templateDir,
257
+ templateRef,
258
+ resultsDir: config.resultsDir,
259
+ runtime: {
260
+ nodeVersion: process.version,
261
+ uid: process.getuid?.(),
262
+ gid: process.getgid?.(),
263
+ groups: process.getgroups?.(),
264
+ },
265
+ docker: {
266
+ version: dockerVersion.stdout,
267
+ clientVersion: dockerVersion.stdout?.split(' -> ')[0],
268
+ serverVersion: dockerVersion.stdout?.split(' -> ')[1],
269
+ },
270
+ };
271
+ }
272
+
273
+ function buildRunResponse(job: Job, cached = false): RunResponse {
274
+ return {
275
+ id: job.id,
276
+ status: job.status,
277
+ createdAt: job.createdAt.toISOString(),
278
+ correlationId: job.correlationId,
279
+ requestId: job.requestId,
280
+ cached: cached || undefined,
281
+ completedAt: job.completedAt?.toISOString(),
282
+ exitCode: job.exitCode,
283
+ failureClass: job.failureClass,
284
+ error: job.error,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Create the API routes.
290
+ */
291
+ export function createApiRouter(
292
+ scheduler: JobScheduler,
293
+ config: KasekiApiConfig,
294
+ idempotencyStore: IdempotencyStore,
295
+ preFlightValidator: PreFlightValidator,
296
+ artifactCache = new ResultCache({
297
+ maxEntries: config.artifactCacheMaxEntries,
298
+ ttlMs: config.artifactCacheTtlMs,
299
+ maxFileBytes: config.artifactCacheMaxFileBytes,
300
+ }),
301
+ ): Router {
302
+ const router = Router();
303
+ const logger = createEventLogger('api');
304
+
305
+ /**
306
+ * Middleware: Request/Response logging.
307
+ */
308
+ router.use((req: Request, res: Response, next: NextFunction) => {
309
+ const startTime = Date.now();
310
+ const originalSend = res.send;
311
+
312
+ res.send = function (data: any) {
313
+ const duration = Date.now() - startTime;
314
+ const statusCode = res.statusCode;
315
+
316
+ // Log request/response event
317
+ logger.event('api_request_complete', {
318
+ method: req.method,
319
+ path: req.path,
320
+ statusCode,
321
+ durationMs: duration,
322
+ query: Object.keys(req.query).length > 0 ? req.query : undefined,
323
+ });
324
+
325
+ return originalSend.call(this, data);
326
+ };
327
+
328
+ next();
329
+ });
330
+
331
+ /**
332
+ * Middleware: API key validation.
333
+ */
334
+ router.use((req: Request, res: Response, next: NextFunction) => {
335
+ // Skip auth for health check
336
+ if (req.path === '/health' || req.path === '/ready') {
337
+ return next();
338
+ }
339
+
340
+ const authHeader = req.get('Authorization');
341
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
342
+ logger.event('api_auth_failed', {
343
+ path: req.path,
344
+ reason: 'missing_or_invalid_header',
345
+ });
346
+ return sendErrorResponse(res, 401, 'Unauthorized', 'Missing or invalid Authorization header');
347
+ }
348
+
349
+ const token = authHeader.slice(7);
350
+ if (!validateApiKey(config, token)) {
351
+ logger.event('api_auth_failed', {
352
+ path: req.path,
353
+ reason: 'invalid_api_key',
354
+ });
355
+ return sendErrorResponse(res, 401, 'Unauthorized', 'Invalid API key');
356
+ }
357
+
358
+ next();
359
+ });
360
+
361
+ /**
362
+ * Health check endpoint.
363
+ */
364
+ router.get('/health', (_req: Request, res: Response) => {
365
+ const queueStatus = scheduler.getQueueStatus();
366
+ const errors: string[] = [];
367
+
368
+ // Check if results directory is accessible
369
+ if (!fs.existsSync(config.resultsDir)) {
370
+ errors.push(`Results directory not accessible: ${config.resultsDir}`);
371
+ }
372
+
373
+ const status = errors.length === 0 ? 'healthy' : 'degraded';
374
+
375
+ res.json({
376
+ status,
377
+ timestamp: new Date().toISOString(),
378
+ queue: queueStatus,
379
+ errors: errors.length > 0 ? errors : undefined,
380
+ });
381
+ });
382
+
383
+ router.get('/ready', (_req: Request, res: Response) => {
384
+ const readiness = scheduler.getReadiness();
385
+ if (readiness.ready) {
386
+ return res.status(200).json({ status: 'ready', timestamp: new Date().toISOString() });
387
+ }
388
+ return res.status(503).json({
389
+ status: 'not_ready',
390
+ timestamp: new Date().toISOString(),
391
+ reasons: readiness.reasons,
392
+ });
393
+ });
394
+
395
+ router.get('/metrics', (_req: Request, res: Response) => {
396
+ const cacheStats = artifactCache.getStats();
397
+ const artifactCacheMetrics = [
398
+ '# HELP kaseki_artifact_cache_entries Number of artifact content cache entries currently held in memory.',
399
+ '# TYPE kaseki_artifact_cache_entries gauge',
400
+ `kaseki_artifact_cache_entries ${cacheStats.entries}`,
401
+ '# HELP kaseki_artifact_cache_bytes Bytes of artifact content currently held in memory.',
402
+ '# TYPE kaseki_artifact_cache_bytes gauge',
403
+ `kaseki_artifact_cache_bytes ${cacheStats.bytes}`,
404
+ '# HELP kaseki_artifact_cache_hits_total Total artifact content cache hits.',
405
+ '# TYPE kaseki_artifact_cache_hits_total counter',
406
+ `kaseki_artifact_cache_hits_total ${cacheStats.hits}`,
407
+ '# HELP kaseki_artifact_cache_misses_total Total artifact content cache misses.',
408
+ '# TYPE kaseki_artifact_cache_misses_total counter',
409
+ `kaseki_artifact_cache_misses_total ${cacheStats.misses}`,
410
+ '# HELP kaseki_artifact_cache_max_entries Configured maximum artifact content cache entries.',
411
+ '# TYPE kaseki_artifact_cache_max_entries gauge',
412
+ `kaseki_artifact_cache_max_entries ${cacheStats.maxEntries}`,
413
+ '# HELP kaseki_artifact_cache_max_file_bytes Configured maximum file size eligible for artifact content caching.',
414
+ '# TYPE kaseki_artifact_cache_max_file_bytes gauge',
415
+ `kaseki_artifact_cache_max_file_bytes ${cacheStats.maxFileBytes}`,
416
+ ].join('\n');
417
+
418
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
419
+ res.status(200).send(`${metricsRegistry.renderPrometheus()}${artifactCacheMetrics}\n`);
420
+ });
421
+
422
+ /**
423
+ * GET /api/preflight - Controller-oriented readiness diagnostics.
424
+ */
425
+ router.get('/preflight', (_req: Request, res: Response) => {
426
+ const response = buildPreflightResponse(config);
427
+ res.status(response.status === 'error' ? 503 : 200).json(response);
428
+ });
429
+
430
+ /**
431
+ * POST /api/runs - Trigger a new kaseki run.
432
+ */
433
+ router.post('/runs', async (req: Request, res: Response) => {
434
+ try {
435
+ // Validate request body
436
+ const runRequest = RunRequestSchema.parse({
437
+ ...req.body,
438
+ startupCheck:
439
+ req.query.dryRun === 'true' || req.query.startupCheck === 'true'
440
+ ? true
441
+ : req.body?.startupCheck,
442
+ });
443
+
444
+ if ((runRequest.publishMode === 'branch' || runRequest.publishMode === 'draft_pr') && !isGitHubAppReady()) {
445
+ return sendErrorResponse(
446
+ res,
447
+ 400,
448
+ 'Bad Request',
449
+ `publishMode=${runRequest.publishMode} requires readable GitHub App credentials. Check /api/preflight before submitting publishable runs.`,
450
+ );
451
+ }
452
+
453
+ // Auto-generate idempotency key if not provided
454
+ const idempotencyKey = runRequest.idempotencyKey || randomUUID();
455
+ const requestFingerprint = buildRequestFingerprint(runRequest as Record<string, unknown>);
456
+
457
+ const claimResult = idempotencyStore.claimOrGet(idempotencyKey, requestFingerprint);
458
+ if (claimResult.kind === 'fulfilled') {
459
+ const currentJob = scheduler.getJob(claimResult.response.id);
460
+ const response = currentJob
461
+ ? buildRunResponse(currentJob, true)
462
+ : {
463
+ ...claimResult.response,
464
+ cached: true,
465
+ };
466
+ logger.event('api_idempotent_resubmission', {
467
+ jobId: response.id,
468
+ idempotencyKey,
469
+ currentStatus: currentJob?.status,
470
+ });
471
+ return res.status(200).json(response); // 200 OK, not 202
472
+ }
473
+ if (claimResult.kind === 'pending') {
474
+ return sendErrorResponse(res, 409, 'Conflict', 'Request with this idempotency key is already being processed');
475
+ }
476
+
477
+ // Log request
478
+ logger.event('api_run_request', {
479
+ repoUrl: runRequest.repoUrl,
480
+ ref: runRequest.ref,
481
+ taskMode: runRequest.taskMode,
482
+ publishMode: runRequest.publishMode,
483
+ startupCheck: runRequest.startupCheck,
484
+ idempotencyKey,
485
+ });
486
+
487
+ // Submit to scheduler
488
+ const job = await scheduler.submitJob(runRequest);
489
+
490
+ // Store idempotency key on job
491
+ job.idempotencyKey = idempotencyKey;
492
+
493
+ const response = buildRunResponse(job);
494
+
495
+ // Store in idempotency cache
496
+ idempotencyStore.storeResponse(idempotencyKey, response, requestFingerprint);
497
+
498
+ res.status(202).json(response); // 202 Accepted
499
+ } catch (err: unknown) {
500
+ if (err instanceof Error && 'errors' in err) {
501
+ // Zod validation error
502
+ const details = (err as any).errors.map((e: any) => `${(e.path as string[]).join('.')}: ${e.message}`).join('; ');
503
+ logger.event('api_validation_error', {
504
+ path: '/runs',
505
+ details,
506
+ });
507
+ return sendErrorResponse(res, 400, 'Bad Request', details);
508
+ }
509
+ logger.event('api_error', {
510
+ path: '/runs',
511
+ error: (err as Error).message,
512
+ });
513
+ return sendErrorResponse(res, 400, 'Bad Request', (err as Error).message);
514
+ }
515
+ });
516
+
517
+ /**
518
+ * POST /api/webhooks/test - Test webhook configuration.
519
+ */
520
+ router.post('/webhooks/test', async (req: Request, res: Response) => {
521
+ try {
522
+ const { url, secret } = req.body;
523
+
524
+ if (!url || typeof url !== 'string') {
525
+ return sendErrorResponse(res, 400, 'Bad Request', 'Webhook URL is required');
526
+ }
527
+
528
+ // Validate URL format
529
+ try {
530
+ new URL(url);
531
+ } catch {
532
+ return sendErrorResponse(res, 400, 'Bad Request', 'Invalid webhook URL format');
533
+ }
534
+
535
+ // Send test webhook
536
+ let statusCode: number | undefined;
537
+ let error: string | undefined;
538
+ let durationMs = 0;
539
+ const startTime = Date.now();
540
+
541
+ try {
542
+ const testPayload = {
543
+ eventType: 'webhook.test',
544
+ jobId: 'test',
545
+ timestamp: new Date().toISOString(),
546
+ data: { message: 'This is a test webhook from kaseki-agent API' },
547
+ };
548
+
549
+ // Generate HMAC signature if secret provided
550
+ let signature: string | null = null;
551
+ if (secret && typeof secret === 'string') {
552
+ const body = JSON.stringify(testPayload);
553
+ signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
554
+ }
555
+
556
+ const response = await fetch(url, {
557
+ method: 'POST',
558
+ headers: {
559
+ 'Content-Type': 'application/json',
560
+ 'X-Kaseki-Event': 'webhook.test',
561
+ 'X-Kaseki-Job-Id': 'test',
562
+ ...(signature && { 'X-Kaseki-Signature': `sha256=${signature}` }),
563
+ },
564
+ body: JSON.stringify(testPayload),
565
+ signal: AbortSignal.timeout(10000),
566
+ });
567
+
568
+ durationMs = Date.now() - startTime;
569
+ statusCode = response.status;
570
+
571
+ if (!response.ok) {
572
+ error = `HTTP ${response.status} ${response.statusText}`;
573
+ }
574
+ } catch (err) {
575
+ durationMs = Date.now() - startTime;
576
+ error = err instanceof Error ? err.message : String(err);
577
+ }
578
+
579
+ const result = {
580
+ url,
581
+ statusCode,
582
+ durationMs,
583
+ success: !error,
584
+ error,
585
+ };
586
+
587
+ logger.event('webhook_test', result);
588
+
589
+ res.json(result);
590
+ } catch (err) {
591
+ logger.event('api_error', {
592
+ path: '/webhooks/test',
593
+ error: (err as Error).message,
594
+ });
595
+ return sendErrorResponse(res, 400, 'Bad Request', (err as Error).message);
596
+ }
597
+ });
598
+
599
+ /**
600
+ * POST /api/validate - Pre-flight validation of job request (dry-run).
601
+ */
602
+ router.post('/validate', async (req: Request, res: Response) => {
603
+ try {
604
+ // Validate request body
605
+ const runRequest = RunRequestSchema.parse(req.body);
606
+
607
+ logger.event('api_validation_request', {
608
+ repoUrl: runRequest.repoUrl,
609
+ ref: runRequest.ref,
610
+ });
611
+
612
+ // Run pre-flight validation
613
+ const validationResult = await preFlightValidator.validate(runRequest);
614
+
615
+ const response: ValidationResponse = validationResult;
616
+
617
+ res.json(response);
618
+ } catch (err: unknown) {
619
+ if (err instanceof Error && 'errors' in err) {
620
+ // Zod validation error
621
+ const details = (err as any).errors.map((e: any) => `${(e.path as string[]).join('.')}: ${e.message}`).join('; ');
622
+ logger.event('api_validation_error', {
623
+ path: '/validate',
624
+ details,
625
+ });
626
+ return sendErrorResponse(res, 400, 'Bad Request', details);
627
+ }
628
+ logger.event('api_error', {
629
+ path: '/validate',
630
+ error: (err as Error).message,
631
+ });
632
+ return sendErrorResponse(res, 400, 'Bad Request', (err as Error).message);
633
+ }
634
+ });
635
+
636
+ // Register domain-focused route modules
637
+ router.use(createStatusRoutes(scheduler, config));
638
+ router.use(createLogRoutes(scheduler, config));
639
+ router.use(createArtifactRoutes(scheduler, config, artifactCache));
640
+ router.use(createWebhookRoutes());
641
+
642
+ return router;
643
+ }