@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,1961 @@
1
+ #!/usr/bin/env bash
2
+ # NOTE: This script intentionally avoids global `set -e` so each stage can
3
+ # record status/timing artifacts before deciding whether to stop.
4
+ set -uo pipefail
5
+
6
+ INSTANCE_NAME="${KASEKI_INSTANCE:-kaseki-unknown}"
7
+ REPO_URL="${REPO_URL:-https://github.com/CyanAutomation/crudmapper}"
8
+ GIT_REF="${GIT_REF:-main}"
9
+ KASEKI_PROVIDER="${KASEKI_PROVIDER:-openrouter}"
10
+ KASEKI_MODEL="${KASEKI_MODEL:-openrouter/free}"
11
+ KASEKI_DRY_RUN="${KASEKI_DRY_RUN:-0}"
12
+ KASEKI_AGENT_TIMEOUT_SECONDS="${KASEKI_AGENT_TIMEOUT_SECONDS:-1200}"
13
+ KASEKI_VALIDATION_COMMANDS="${KASEKI_VALIDATION_COMMANDS-npm run check;npm run test;npm run build}"
14
+ KASEKI_SKIP_MISSING_NPM_SCRIPTS="${KASEKI_SKIP_MISSING_NPM_SCRIPTS:-1}"
15
+ KASEKI_DEBUG_RAW_EVENTS="${KASEKI_DEBUG_RAW_EVENTS:-0}"
16
+ KASEKI_STREAM_PROGRESS="${KASEKI_STREAM_PROGRESS:-1}"
17
+ KASEKI_VALIDATE_AFTER_AGENT_FAILURE="${KASEKI_VALIDATE_AFTER_AGENT_FAILURE:-0}"
18
+ KASEKI_TASK_MODE="${KASEKI_TASK_MODE:-patch}"
19
+ KASEKI_ALLOW_EMPTY_DIFF="${KASEKI_ALLOW_EMPTY_DIFF:-0}"
20
+ KASEKI_CHANGED_FILES_ALLOWLIST="${KASEKI_CHANGED_FILES_ALLOWLIST:-src/lib/parser.ts tests/parser.validation.ts}"
21
+ KASEKI_VALIDATION_ALLOWLIST="${KASEKI_VALIDATION_ALLOWLIST:-}"
22
+ KASEKI_MAX_DIFF_BYTES="${KASEKI_MAX_DIFF_BYTES:-200000}"
23
+ KASEKI_REPO_MEMORY_MODE="${KASEKI_REPO_MEMORY_MODE:-off}"
24
+ KASEKI_REPO_MEMORY_TTL_DAYS="${KASEKI_REPO_MEMORY_TTL_DAYS:-30}"
25
+ KASEKI_REPO_MEMORY_MAX_BYTES="${KASEKI_REPO_MEMORY_MAX_BYTES:-8000}"
26
+ TASK_PROMPT="${TASK_PROMPT:-Make normalizeRole treat a non-string Name fallback safely when FriendlyName is empty or missing. It should fall back to \"Unnamed Role\" instead of preserving arbitrary truthy non-string values. Add or update exactly one compact table-driven Vitest case in tests/parser.validation.ts, with a neutral static test title and no per-case assertion messages or explanatory comments. Do not add broad repeated test blocks. Do not print, inspect, or expose environment variables, secrets, credentials, or API keys. Keep changes limited to the source and test files needed for this fix.}"
27
+ KASEKI_AGENT_GUARDRAILS="${KASEKI_AGENT_GUARDRAILS:-1}"
28
+ KASEKI_RESTORE_DISALLOWED_CHANGES="${KASEKI_RESTORE_DISALLOWED_CHANGES:-1}"
29
+ KASEKI_VALIDATION_FAIL_FAST="${KASEKI_VALIDATION_FAIL_FAST:-1}"
30
+ KASEKI_STRICT_SCRIPT_CHECK="${KASEKI_STRICT_SCRIPT_CHECK:-0}"
31
+ GITHUB_APP_ENABLED="${GITHUB_APP_ENABLED:-0}"
32
+ KASEKI_PUBLISH_MODE="${KASEKI_PUBLISH_MODE:-auto}"
33
+ START_EPOCH="$(date +%s)"
34
+ START_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
35
+ CURRENT_STAGE="initializing"
36
+ PI_START_EPOCH=0
37
+ PI_DURATION_SECONDS=0
38
+ PI_VERSION=""
39
+ STATUS=0
40
+ FAILED_COMMAND=""
41
+ PI_EXIT=0
42
+ VALIDATION_EXIT=0
43
+ VALIDATION_FAILED_COMMAND_DETAIL=""
44
+ VALIDATION_FAILURE_REASON=""
45
+ VALIDATION_STOPPED_EARLY=false
46
+ VALIDATION_COMMANDS_ATTEMPTED=0
47
+ DIFF_NONEMPTY=false
48
+ QUALITY_EXIT=0
49
+ QUALITY_FAILURE_REASON=""
50
+ SECRET_SCAN_EXIT=0
51
+ GITHUB_PUSH_EXIT=0
52
+ GITHUB_PR_EXIT=0
53
+ ACTUAL_MODEL="unknown"
54
+ GITHUB_PR_URL=""
55
+ GITHUB_SKIP_REASONS=()
56
+ VALIDATION_TIMINGS_FILE="/results/validation-timings.tsv"
57
+ STAGE_TIMINGS_FILE="/results/stage-timings.tsv"
58
+ DEPENDENCY_CACHE_LOG="/results/dependency-cache.log"
59
+ RAW_EVENTS="/tmp/pi-events.raw.jsonl"
60
+ KASEKI_DEPENDENCY_CACHE_DIR="${KASEKI_DEPENDENCY_CACHE_DIR:-/workspace/.kaseki-cache}"
61
+ KASEKI_DEPENDENCY_RESTORE_MODE="${KASEKI_DEPENDENCY_RESTORE_MODE:-copy}"
62
+ KASEKI_INSTALL_IGNORE_SCRIPTS="${KASEKI_INSTALL_IGNORE_SCRIPTS:-1}"
63
+ KASEKI_NPM_OMIT_DEV="${KASEKI_NPM_OMIT_DEV:-0}"
64
+ KASEKI_IMAGE_DEPENDENCY_CACHE_DIR="${KASEKI_IMAGE_DEPENDENCY_CACHE_DIR:-/opt/kaseki/workspace-cache}"
65
+ KASEKI_LOG_DIR="${KASEKI_LOG_DIR:-/var/log/kaseki}"
66
+ KASEKI_STRICT_HOST_LOGGING="${KASEKI_STRICT_HOST_LOGGING:-0}"
67
+ KASEKI_GIT_CACHE_MODE="${KASEKI_GIT_CACHE_MODE:-mirror}"
68
+ KASEKI_GIT_CACHE_ROOT="${KASEKI_GIT_CACHE_ROOT:-/cache/git}"
69
+ KASEKI_GIT_CACHE_FETCH_TIMEOUT_SECONDS="${KASEKI_GIT_CACHE_FETCH_TIMEOUT_SECONDS:-120}"
70
+ GIT_CACHE_KEY=""
71
+ GIT_CACHE_MIRROR=""
72
+ GIT_CACHE_HIT="false"
73
+ GIT_CACHE_STATUS="not_started"
74
+ GIT_CACHE_MODE_USED="$KASEKI_GIT_CACHE_MODE"
75
+ GIT_CLONE_STRATEGY="not_started"
76
+ GIT_CLONE_DURATION_SECONDS=0
77
+ REPO_MEMORY_KEY=""
78
+ REPO_MEMORY_DIR=""
79
+ REPO_MEMORY_FILE=""
80
+ REPO_MEMORY_STATUS="disabled"
81
+ REPO_MEMORY_COMMIT_SHA="unknown"
82
+
83
+ setup_host_logging_mirror() {
84
+ local base_name="$1"
85
+ local stamp host_log_file
86
+ if mkdir -p "$KASEKI_LOG_DIR" 2>/dev/null && [ -w "$KASEKI_LOG_DIR" ]; then
87
+ stamp="$(date -u +%Y%m%dT%H%M%SZ)"
88
+ host_log_file="$KASEKI_LOG_DIR/${base_name}-${stamp}.log"
89
+ exec > >(tee -a /results/stdout.log | tee -a "$host_log_file") \
90
+ 2> >(tee -a /results/stderr.log | tee -a "$host_log_file" >&2)
91
+ printf 'Host log mirror: %s\n' "$host_log_file"
92
+ return 0
93
+ fi
94
+ if [ "$KASEKI_STRICT_HOST_LOGGING" = "1" ]; then
95
+ printf 'Error: strict host logging enabled, but KASEKI_LOG_DIR is not writable: %s\n' "$KASEKI_LOG_DIR" >&2
96
+ exit 1
97
+ fi
98
+ exec > >(tee -a /results/stdout.log) 2> >(tee -a /results/stderr.log >&2)
99
+ printf 'Warning: host log mirror disabled; KASEKI_LOG_DIR is unavailable: %s\n' "$KASEKI_LOG_DIR" >&2
100
+ }
101
+
102
+ mkdir_paths=(/results)
103
+ if [ -n "${HOME:-}" ]; then
104
+ mkdir_paths+=("${HOME}")
105
+ fi
106
+ if [ -n "${NPM_CONFIG_CACHE:-}" ]; then
107
+ mkdir_paths+=("${NPM_CONFIG_CACHE}")
108
+ fi
109
+ if [ -n "${TMPDIR:-}" ]; then
110
+ mkdir_paths+=("${TMPDIR}")
111
+ fi
112
+ if [ -n "${PI_CODING_AGENT_DIR:-}" ]; then
113
+ mkdir_paths+=("${PI_CODING_AGENT_DIR}")
114
+ fi
115
+ mkdir -p "${mkdir_paths[@]}"
116
+ PI_VERSION="$(pi --version 2>&1 | head -n 1 || true)"
117
+ : > /results/stdout.log
118
+ : > /results/stderr.log
119
+ : > /results/pi-events.jsonl
120
+ : > /results/pi-summary.json
121
+ : > /results/validation.log
122
+ : > /results/quality.log
123
+ : > /results/secret-scan.log
124
+ : > /results/git-push.log
125
+ : > /results/progress.log
126
+ : > /results/progress.jsonl
127
+ : > /results/format-check-command.txt
128
+ : > /results/failure.json
129
+ : > /results/result-summary.md
130
+ : > "$VALIDATION_TIMINGS_FILE"
131
+ : >> "$STAGE_TIMINGS_FILE"
132
+ : > "$DEPENDENCY_CACHE_LOG"
133
+ setup_host_logging_mirror "$INSTANCE_NAME"
134
+ case "$KASEKI_GIT_CACHE_MODE" in
135
+ off|mirror)
136
+ ;;
137
+ *)
138
+ printf 'Warning: unsupported KASEKI_GIT_CACHE_MODE=%s; falling back to off. Expected off or mirror.\n' "$KASEKI_GIT_CACHE_MODE" >&2
139
+ KASEKI_GIT_CACHE_MODE="off"
140
+ GIT_CACHE_MODE_USED="off"
141
+ ;;
142
+ esac
143
+
144
+ # Safely encode value as JSON string; fallback to empty string if node unavailable
145
+ json_encode() {
146
+ if ! command -v node &>/dev/null; then
147
+ printf '""' # Return empty JSON string if node is unavailable
148
+ return 1
149
+ fi
150
+ local output
151
+ output=$(node -e 'const chunks=[]; process.stdin.on("data", c => chunks.push(c)); process.stdin.on("end", () => process.stdout.write(JSON.stringify(Buffer.concat(chunks).toString().replace(/\n$/, ""))));' 2>&1)
152
+ local exit_code=$?
153
+ if [ $exit_code -eq 0 ] && [ -n "$output" ]; then
154
+ printf '%s' "$output"
155
+ else
156
+ # Log error and return empty JSON string as fallback
157
+ printf 'warning: json_encode failed (exit %d): %s\n' "$exit_code" "$output" >&2
158
+ printf '""'
159
+ return 1
160
+ fi
161
+ }
162
+
163
+ json_array() {
164
+ if ! command -v node &>/dev/null; then
165
+ printf '[]' # Return empty JSON array if node is unavailable
166
+ return 1
167
+ fi
168
+ node -e 'process.stdout.write(JSON.stringify(process.argv.slice(1)));' -- "$@" 2>&1 || printf '[]'
169
+ }
170
+
171
+ # Validate that a variable contains only numeric digits (for use before arithmetic)
172
+ validate_numeric() {
173
+ local var_name="$1"
174
+ local var_value="$2"
175
+ # Empty or missing value is treated as invalid
176
+ if [ -z "$var_value" ]; then
177
+ printf 'error: %s is not numeric (value="%s")\n' "$var_name" "$var_value" >&2
178
+ return 1
179
+ fi
180
+ # Reject any non-digit character, including embedded newlines.
181
+ case "$var_value" in
182
+ *[!0-9]*)
183
+ printf 'error: %s is not a valid integer (value="%s")\n' "$var_name" "$var_value" >&2
184
+ return 1
185
+ ;;
186
+ esac
187
+ return 0
188
+ }
189
+
190
+ emit_progress() {
191
+ local stage="$1"
192
+ local detail="$2"
193
+ local status="${3:-info}"
194
+ local now
195
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
196
+ printf '{"timestamp":%s,"component":%s,"stage":%s,"status":%s,"instance":%s,"detail":%s}\n' \
197
+ "$(printf '%s' "$now" | json_encode)" \
198
+ "$(printf '%s' "kaseki-agent" | json_encode)" \
199
+ "$(printf '%s' "$stage" | json_encode)" \
200
+ "$(printf '%s' "$status" | json_encode)" \
201
+ "$(printf '%s' "$INSTANCE_NAME" | json_encode)" \
202
+ "$(printf '%s' "$detail" | json_encode)" >> /results/progress.jsonl
203
+ printf '[progress] %s %s: %s\n' "$stage" "$status" "$detail" | tee -a /results/progress.log
204
+ }
205
+
206
+ emit_event() {
207
+ local event_type="$1"
208
+ shift
209
+ local detail_json="{}"
210
+ if [ $# -gt 0 ]; then
211
+ # Build detail object from key=value pairs
212
+ local -a pairs=("$@")
213
+ detail_json="{"
214
+ for i in "${!pairs[@]}"; do
215
+ local pair="${pairs[$i]}"
216
+ local key="${pair%%=*}"
217
+ local value="${pair#*=}"
218
+ if [ "$i" -gt 0 ]; then
219
+ detail_json="${detail_json},"
220
+ fi
221
+ detail_json="${detail_json}$(printf '%s' "$key" | json_encode):$(printf '%s' "$value" | json_encode)"
222
+ done
223
+ detail_json="${detail_json}}"
224
+ fi
225
+ local now
226
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
227
+ printf '{"timestamp":%s,"component":%s,"event_type":%s,"instance":%s,%s}\n' \
228
+ "$(printf '%s' "$now" | json_encode)" \
229
+ "$(printf '%s' "kaseki-agent" | json_encode)" \
230
+ "$(printf '%s' "$event_type" | json_encode)" \
231
+ "$(printf '%s' "$INSTANCE_NAME" | json_encode)" \
232
+ "$(printf '%s' "$detail_json" | sed 's/^{\(.*\)}$/\1/')" >> /results/progress.jsonl
233
+ }
234
+
235
+ emit_error_event() {
236
+ local error_type="$1"
237
+ local detail="$2"
238
+ local recovery="${3:-continue}"
239
+ emit_event "error" "error_type=$error_type" "detail=$detail" "recovery_action=$recovery"
240
+ printf '[error] %s: %s (recovery: %s)\n' "$error_type" "$detail" "$recovery" | tee -a /results/progress.log
241
+ }
242
+
243
+ write_metadata() {
244
+ local end_epoch end_iso duration exit_code
245
+ end_epoch="$(date +%s)"
246
+ end_iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
247
+ duration=$((end_epoch - START_EPOCH))
248
+ exit_code="${1:-$STATUS}"
249
+ cat > /results/metadata.json <<META
250
+ {
251
+ "instance": $(printf '%s' "$INSTANCE_NAME" | json_encode),
252
+ "repo_url": $(printf '%s' "$REPO_URL" | json_encode),
253
+ "git_ref": $(printf '%s' "$GIT_REF" | json_encode),
254
+ "provider": $(printf '%s' "$KASEKI_PROVIDER" | json_encode),
255
+ "model": $(printf '%s' "$KASEKI_MODEL" | json_encode),
256
+ "task_mode": $(printf '%s' "$KASEKI_TASK_MODE" | json_encode),
257
+ "allow_empty_diff": $(printf '%s' "$KASEKI_ALLOW_EMPTY_DIFF" | json_encode),
258
+ "started_at": $(printf '%s' "$START_ISO" | json_encode),
259
+ "current_stage": $(printf '%s' "$CURRENT_STAGE" | json_encode),
260
+ "ended_at": $(printf '%s' "$end_iso" | json_encode),
261
+ "duration_seconds": $duration,
262
+ "total_duration_seconds": $duration,
263
+ "pi_duration_seconds": $PI_DURATION_SECONDS,
264
+ "exit_code": $exit_code,
265
+ "failed_command": $(printf '%s' "$FAILED_COMMAND" | json_encode),
266
+ "validation_failed_command": $(printf '%s' "$VALIDATION_FAILED_COMMAND_DETAIL" | json_encode),
267
+ "validation_failure_reason": $(printf '%s' "$VALIDATION_FAILURE_REASON" | json_encode),
268
+ "quality_failure_reason": $(printf '%s' "$QUALITY_FAILURE_REASON" | json_encode),
269
+ "pi_exit_code": $PI_EXIT,
270
+ "validation_exit_code": $VALIDATION_EXIT,
271
+ "validation_fail_fast_mode": $([[ "$KASEKI_VALIDATION_FAIL_FAST" == "1" ]] && printf 'true' || printf 'false'),
272
+ "validation_stopped_early": $([[ "$VALIDATION_STOPPED_EARLY" == "true" ]] && printf 'true' || printf 'false'),
273
+ "validation_commands_attempted": $VALIDATION_COMMANDS_ATTEMPTED,
274
+ "quality_exit_code": $QUALITY_EXIT,
275
+ "secret_scan_exit_code": $SECRET_SCAN_EXIT,
276
+ "github_push_exit_code": $GITHUB_PUSH_EXIT,
277
+ "github_pr_exit_code": $GITHUB_PR_EXIT,
278
+ "diff_nonempty": $DIFF_NONEMPTY,
279
+ "actual_model": $(printf '%s' "$ACTUAL_MODEL" | json_encode),
280
+ "github_pr_url": $(printf '%s' "$GITHUB_PR_URL" | json_encode),
281
+ "publish_mode": $(printf '%s' "$KASEKI_PUBLISH_MODE" | json_encode),
282
+ "github_skip_reasons": $(json_array "${GITHUB_SKIP_REASONS[@]}"),
283
+ "git_cache_mode": $(printf '%s' "$GIT_CACHE_MODE_USED" | json_encode),
284
+ "git_cache_status": $(printf '%s' "$GIT_CACHE_STATUS" | json_encode),
285
+ "git_cache_hit": $GIT_CACHE_HIT,
286
+ "git_cache_key": $(printf '%s' "$GIT_CACHE_KEY" | json_encode),
287
+ "git_cache_mirror": $(printf '%s' "$GIT_CACHE_MIRROR" | json_encode),
288
+ "git_clone_strategy": $(printf '%s' "$GIT_CLONE_STRATEGY" | json_encode),
289
+ "git_clone_duration_seconds": $GIT_CLONE_DURATION_SECONDS,
290
+ "repo_memory_mode": $(printf '%s' "$KASEKI_REPO_MEMORY_MODE" | json_encode),
291
+ "repo_memory_status": $(printf '%s' "$REPO_MEMORY_STATUS" | json_encode),
292
+ "repo_memory_key": $(printf '%s' "$REPO_MEMORY_KEY" | json_encode),
293
+ "repo_memory_file": $(printf '%s' "$REPO_MEMORY_FILE" | json_encode),
294
+ "repo_memory_ttl_days": $KASEKI_REPO_MEMORY_TTL_DAYS,
295
+ "repo_memory_max_bytes": $KASEKI_REPO_MEMORY_MAX_BYTES,
296
+ "node_version": $(node --version 2>/dev/null | json_encode || printf 'null'),
297
+ "npm_version": $(npm --version 2>/dev/null | json_encode || printf 'null'),
298
+ "pi_version": $(printf '%s' "$PI_VERSION" | json_encode)
299
+ }
300
+ META
301
+ printf '%s\n' "$exit_code" > /results/exit_code
302
+ }
303
+
304
+ set_current_stage() {
305
+ CURRENT_STAGE="$1"
306
+ }
307
+
308
+ write_result_summary() {
309
+ local changed_files changed_files_markdown validation_status pr_status github_skip_reasons_summary
310
+ changed_files="$(cat /results/changed-files.txt 2>/dev/null || true)"
311
+ if [ -n "$changed_files" ]; then
312
+ changed_files_markdown="$(printf '%s\n' "$changed_files" | sed 's/^/ - /')"
313
+ else
314
+ changed_files_markdown=" - none"
315
+ fi
316
+ validation_status="passed"
317
+ [ "$VALIDATION_EXIT" -ne 0 ] && validation_status="failed"
318
+ if grep -q 'skipped_after_agent_failure' "$STAGE_TIMINGS_FILE" 2>/dev/null; then
319
+ validation_status="skipped"
320
+ fi
321
+ github_skip_reasons_summary="none"
322
+ if [ "${#GITHUB_SKIP_REASONS[@]}" -gt 0 ]; then
323
+ github_skip_reasons_summary="$(IFS=,; printf '%s' "${GITHUB_SKIP_REASONS[*]}")"
324
+ fi
325
+
326
+ pr_status="not attempted"
327
+ if [ "${#GITHUB_SKIP_REASONS[@]}" -gt 0 ]; then
328
+ pr_status="not attempted (reasons: $github_skip_reasons_summary)"
329
+ fi
330
+ if [ "$GITHUB_APP_ENABLED" = "1" ] && [ "${#GITHUB_SKIP_REASONS[@]}" -eq 0 ]; then
331
+ if [ "$GITHUB_PUSH_EXIT" -ne 0 ]; then
332
+ pr_status="push failed"
333
+ elif [ "$GITHUB_PR_EXIT" -eq 0 ] && [ -n "$GITHUB_PR_URL" ]; then
334
+ pr_status="created ($GITHUB_PR_URL)"
335
+ elif [ "$GITHUB_PR_EXIT" -ne 0 ]; then
336
+ pr_status="pr creation failed"
337
+ else
338
+ pr_status="push succeeded, pr not created"
339
+ fi
340
+ fi
341
+
342
+ cat > /results/result-summary.md <<SUMMARY
343
+ # Kaseki Result: $INSTANCE_NAME
344
+
345
+ - Status: $(if [ "$STATUS" -eq 0 ]; then printf 'passed'; else printf 'failed'; fi)
346
+ - Failed command: ${FAILED_COMMAND:-none}
347
+ - Requested model: $KASEKI_MODEL
348
+ - Actual model: ${ACTUAL_MODEL:-unknown}
349
+ - Pi exit code: $PI_EXIT
350
+ - Validation: $validation_status ($VALIDATION_EXIT)
351
+ $(if [ -n "$VALIDATION_FAILURE_REASON" ]; then printf ' - Reason: %s\n' "$VALIDATION_FAILURE_REASON"; fi)
352
+ - Validation failure detail: ${VALIDATION_FAILED_COMMAND_DETAIL:-none}
353
+ $(if [ "$VALIDATION_STOPPED_EARLY" = "true" ]; then printf '- **⚠️ Validation stopped early** (fail-fast mode): %s of %s commands ran\n' "$(printf '%s' "${VALIDATION_COMMANDS[@]}" | wc -w)" "$(echo "$KASEKI_VALIDATION_COMMANDS" | tr ';' '\n' | grep -c .)"; fi)
354
+ - Quality checks: $QUALITY_EXIT
355
+ - Secret scan: $SECRET_SCAN_EXIT
356
+ - GitHub PR: $pr_status
357
+ - GitHub skip reasons: $github_skip_reasons_summary
358
+ - Diff non-empty: $DIFF_NONEMPTY
359
+ - Changed files:
360
+ $changed_files_markdown
361
+
362
+ Artifacts:
363
+ - metadata.json
364
+ - pi-summary.json
365
+ - pi-events.jsonl
366
+ - validation.log
367
+ - validation-timings.tsv
368
+ - stage-timings.tsv
369
+ - dependency-cache.log
370
+ - git.diff
371
+ - git.status
372
+ - git-push.log (if GitHub App enabled)
373
+ - progress.log
374
+ - progress.jsonl
375
+ - cleanup.log (host artifact)
376
+ SUMMARY
377
+ }
378
+
379
+ write_failure_json() {
380
+ local exit_code="$1"
381
+ local stderr_tail
382
+ stderr_tail="$(tail -20 /results/stderr.log 2>/dev/null || true)"
383
+ if [ "$exit_code" -eq 0 ]; then
384
+ : > /results/failure.json
385
+ return 0
386
+ fi
387
+ cat > /results/failure.json <<FAILURE
388
+ {
389
+ "instance": $(printf '%s' "$INSTANCE_NAME" | json_encode),
390
+ "exit_code": $exit_code,
391
+ "failed_command": $(printf '%s' "$FAILED_COMMAND" | json_encode),
392
+ "validation_failed_command": $(printf '%s' "$VALIDATION_FAILED_COMMAND_DETAIL" | json_encode),
393
+ "validation_failure_reason": $(printf '%s' "$VALIDATION_FAILURE_REASON" | json_encode),
394
+ "quality_failure_reason": $(printf '%s' "$QUALITY_FAILURE_REASON" | json_encode),
395
+ "stage": $(printf '%s' "$CURRENT_STAGE" | json_encode),
396
+ "stderr_tail": $(printf '%s' "$stderr_tail" | json_encode),
397
+ "artifacts_dir": "/results",
398
+ "metadata": "metadata.json",
399
+ "stderr": "stderr.log",
400
+ "stdout": "stdout.log",
401
+ "progress": "progress.jsonl",
402
+ "summary": "result-summary.md"
403
+ }
404
+ FAILURE
405
+ }
406
+
407
+ collect_git_artifacts() {
408
+ DIFF_NONEMPTY=false
409
+ if [ -d /workspace/repo/.git ]; then
410
+ while IFS= read -r untracked_file || [ -n "$untracked_file" ]; do
411
+ [ -z "$untracked_file" ] && continue
412
+ git -C /workspace/repo add -N -- "$untracked_file" 2>/dev/null || true
413
+ done < <(git -C /workspace/repo ls-files --others --exclude-standard 2>/dev/null || true)
414
+ git -C /workspace/repo status --short > /results/git.status 2>/dev/null || true
415
+ git -C /workspace/repo diff -- . > /results/git.diff 2>/dev/null || true
416
+ git -C /workspace/repo diff --name-only -- . > /results/changed-files.txt 2>/dev/null || true
417
+ if [ -s /results/git.diff ]; then
418
+ DIFF_NONEMPTY=true
419
+ fi
420
+ else
421
+ : > /results/git.status
422
+ : > /results/git.diff
423
+ : > /results/changed-files.txt
424
+ fi
425
+ }
426
+
427
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
428
+ ALLOWLIST_HELPER="$SCRIPT_DIR/scripts/allowlist-helper.sh"
429
+ if [ ! -r "$ALLOWLIST_HELPER" ] && [ -r /app/scripts/allowlist-helper.sh ]; then
430
+ ALLOWLIST_HELPER="/app/scripts/allowlist-helper.sh"
431
+ fi
432
+ # shellcheck source=scripts/allowlist-helper.sh
433
+ . "$ALLOWLIST_HELPER"
434
+
435
+ restore_disallowed_changes() {
436
+ if [ "$KASEKI_RESTORE_DISALLOWED_CHANGES" != "1" ] || [ ! -d /workspace/repo/.git ]; then
437
+ return 0
438
+ fi
439
+
440
+ local allowlist_regex restored_any restored_count kept_count coverage
441
+ allowlist_regex="$(build_allowlist_regex)"
442
+ [ -z "$allowlist_regex" ] && return 0
443
+ restored_any=0
444
+ restored_count=0
445
+ kept_count=0
446
+ coverage=0
447
+
448
+ # Initialize restoration tracking file
449
+ : > /results/restoration.jsonl
450
+
451
+ while IFS= read -r changed_file || [ -n "$changed_file" ]; do
452
+ [ -z "$changed_file" ] && continue
453
+ if printf '%s\n' "$changed_file" | grep -Eq "^(${allowlist_regex})$"; then
454
+ # File matched allowlist - keep it
455
+ kept_count=$((kept_count + 1))
456
+ {
457
+ printf '{"timestamp":"%s","event":"file_evaluated","file":"%s","status":"kept","reason":"matched_allowlist"}\n' \
458
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(printf '%s' "$changed_file" | sed 's/"/\\"/g')"
459
+ } >> /results/restoration.jsonl
460
+ continue
461
+ fi
462
+ # File did not match allowlist - restore it
463
+ restored_count=$((restored_count + 1))
464
+ printf -- 'Restoring changed file outside allowlist before validation: %s\n' "$changed_file" | tee -a /results/quality.log
465
+ emit_event "quality_gate_rule_evaluated" "rule=allowlist_restore" "passed=true" "file=$changed_file"
466
+ {
467
+ printf '{"timestamp":"%s","event":"file_restored","file":"%s","status":"restored","reason":"not_in_allowlist"}\n' \
468
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(printf '%s' "$changed_file" | sed 's/"/\\"/g')"
469
+ } >> /results/restoration.jsonl
470
+ git -C /workspace/repo restore --staged --worktree -- "$changed_file" 2>/dev/null || true
471
+ git -C /workspace/repo clean -f -- "$changed_file" 2>/dev/null || true
472
+ restored_any=1
473
+ done < /results/changed-files.txt
474
+
475
+ # Emit restoration summary to quality.log with actionable guidance
476
+ if [ $((restored_count + kept_count)) -gt 0 ]; then
477
+ coverage=$((kept_count * 100 / (restored_count + kept_count)))
478
+ fi
479
+ if [ "$restored_count" -gt 0 ] || [ "$kept_count" -gt 0 ]; then
480
+ {
481
+ printf '\n[allowlist summary] Restored: %d files; Kept: %d files (coverage: %d%%)\n' "$restored_count" "$kept_count" "$coverage"
482
+ if [ "$restored_count" -gt 0 ] && [ "$coverage" -lt 50 ]; then
483
+ printf '[allowlist note] Low coverage detected. To improve:\n'
484
+ printf ' 1. Run: ./scripts/suggest-allowlist.sh /results (or /agents/kaseki-results/<instance>)\n'
485
+ printf ' 2. Review suggested patterns in allowlist-suggestions.md\n'
486
+ printf ' 3. Update KASEKI_CHANGED_FILES_ALLOWLIST and re-run\n'
487
+ printf 'See docs/QUALITY_GATES.md for more guidance.\n'
488
+ fi
489
+ } | tee -a /results/quality.log
490
+ emit_event "allowlist_restoration_complete" "restored=$restored_count" "kept=$kept_count" "coverage=$coverage"
491
+ fi
492
+
493
+ if [ "$restored_any" -eq 1 ]; then
494
+ collect_git_artifacts
495
+ fi
496
+ }
497
+
498
+ generate_restoration_report() {
499
+ if [ ! -f /results/restoration.jsonl ]; then
500
+ printf '[debug] restoration report: skipping - restoration.jsonl not found\n' >&2
501
+ return 0
502
+ fi
503
+
504
+ local restored_count kept_count total_count coverage_pct
505
+
506
+ # Safely extract counts from restoration.jsonl with validation
507
+ printf '[debug] restoration report: extracting counts from restoration.jsonl\n' >&2
508
+ restored_count=$(grep -c '"status":"restored"' /results/restoration.jsonl 2>/dev/null || true)
509
+ restored_count=${restored_count:-0}
510
+ printf '[debug] restoration report: restored_count="%s"\n' "$restored_count" >&2
511
+ if ! validate_numeric "restored_count" "$restored_count"; then
512
+ printf 'warning: restoration report generation failed - restored_count validation failed\n' >&2
513
+ return 1
514
+ fi
515
+
516
+ kept_count=$(grep -c '"status":"kept"' /results/restoration.jsonl 2>/dev/null || true)
517
+ kept_count=${kept_count:-0}
518
+ printf '[debug] restoration report: kept_count="%s"\n' "$kept_count" >&2
519
+ if ! validate_numeric "kept_count" "$kept_count"; then
520
+ printf 'warning: restoration report generation failed - kept_count validation failed\n' >&2
521
+ return 1
522
+ fi
523
+
524
+ # Arithmetic operation - now guaranteed to have valid numeric values
525
+ printf '[debug] restoration report: computing total_count from restored=%s and kept=%s\n' "$restored_count" "$kept_count" >&2
526
+ total_count=$((restored_count + kept_count))
527
+ printf '[debug] restoration report: total_count="%s"\n' "$total_count" >&2
528
+
529
+ if [ "$total_count" -eq 0 ]; then
530
+ printf '[debug] restoration report: no changes recorded, skipping report\n' >&2
531
+ return 0
532
+ fi
533
+
534
+ printf '[debug] restoration report: generating report with %d total changes\n' "$total_count" >&2
535
+
536
+ {
537
+ printf '# Allowlist Restoration Report\n\n'
538
+ printf 'Generated: %s\n\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
539
+ printf '## Summary\n\n'
540
+ # All variables are now validated as numeric by validate_numeric() above
541
+ printf -- '- **Total Files Changed:** %d\n' "$total_count" || { printf 'error: failed to write total count\n' >&2; return 1; }
542
+ printf -- '- **Files Kept (in allowlist):** %d\n' "$kept_count" || { printf 'error: failed to write kept count\n' >&2; return 1; }
543
+ printf -- '- **Files Restored (outside allowlist):** %d\n' "$restored_count" || { printf 'error: failed to write restored count\n' >&2; return 1; }
544
+ if [ "$total_count" -gt 0 ]; then
545
+ # Calculate coverage percentage - safe because total_count is validated as > 0
546
+ coverage_pct=$((kept_count * 100 / total_count))
547
+ printf '[debug] restoration report: coverage_pct=%d (kept=%s / total=%s)\n' "$coverage_pct" "$kept_count" "$total_count" >&2
548
+ printf -- '- **Allowlist Coverage:** %d%%\n\n' "$coverage_pct" || { printf 'error: failed to write coverage pct\n' >&2; return 1; }
549
+ fi
550
+
551
+ if [ "$restored_count" -gt 0 ]; then
552
+ printf '## Restored Files\n\n'
553
+ printf 'These files were modified by the agent but restored because they fall outside the allowlist:\n\n'
554
+ grep '"status":"restored"' /results/restoration.jsonl | \
555
+ sed "s/.*\"file\":\"\([^\"]*\)\".*/- \`\1\`/" | \
556
+ sort | uniq >> /results/restoration-report.md.tmp 2>/dev/null || true
557
+ if [ -f /results/restoration-report.md.tmp ]; then
558
+ cat /results/restoration-report.md.tmp
559
+ rm -f /results/restoration-report.md.tmp
560
+ fi
561
+ printf '\n'
562
+ fi
563
+
564
+ if [ "$kept_count" -gt 0 ]; then
565
+ printf '## Kept Files (Allowlist Matches)\n\n'
566
+ printf 'These files were in the allowlist and were kept:\n\n'
567
+ grep '"status":"kept"' /results/restoration.jsonl | \
568
+ sed "s/.*\"file\":\"\([^\"]*\)\".*/- \`\1\`/" | \
569
+ sort | uniq >> /results/restoration-report.md.tmp 2>/dev/null || true
570
+ if [ -f /results/restoration-report.md.tmp ]; then
571
+ cat /results/restoration-report.md.tmp
572
+ rm -f /results/restoration-report.md.tmp
573
+ fi
574
+ printf '\n'
575
+ fi
576
+
577
+ printf '## Recommendations\n\n'
578
+ if [ "$restored_count" -gt 0 ] && [ -n "$coverage_pct" ] && [ "$coverage_pct" -lt 50 ]; then
579
+ printf '**⚠️ Low Allowlist Coverage** — Only %d%% of changes were kept.\n' "$coverage_pct"
580
+ printf 'Consider:\n'
581
+ printf '1. Reviewing the TASK_PROMPT to be more specific about scope\n'
582
+ printf '2. Widening the allowlist to include related files\n'
583
+ printf "3. Running \`scripts/suggest-allowlist.sh\` to auto-generate a better allowlist\n\n"
584
+ fi
585
+ printf 'Run subsequent operations with an updated allowlist:\n'
586
+ printf '```bash\n'
587
+ printf 'KASEKI_CHANGED_FILES_ALLOWLIST="<your-pattern>" ./run-kaseki.sh\n'
588
+ printf '```\n\n'
589
+ printf "For help on allowlist patterns, see \`docs/QUALITY_GATES.md\`.\n"
590
+ } > /results/restoration-report.md
591
+ }
592
+
593
+ check_validation_allowlist() {
594
+ if [ -z "$KASEKI_VALIDATION_ALLOWLIST" ]; then
595
+ return 0
596
+ fi
597
+ if [ ! -d /workspace/repo/.git ]; then
598
+ return 0
599
+ fi
600
+
601
+ local allowlist_regex validation_violation_count
602
+ allowlist_regex="$(build_allowlist_regex "$KASEKI_VALIDATION_ALLOWLIST")"
603
+ [ -z "$allowlist_regex" ] && return 0
604
+ validation_violation_count=0
605
+
606
+ while IFS= read -r changed_file || [ -n "$changed_file" ]; do
607
+ [ -z "$changed_file" ] && continue
608
+ if ! printf '%s\n' "$changed_file" | grep -Eq "^(${allowlist_regex})$"; then
609
+ printf 'Validation-phase file outside allowlist: %s\n' "$changed_file" | tee -a /results/quality.log
610
+ validation_violation_count=$((validation_violation_count + 1))
611
+ emit_event "quality_gate_rule_evaluated" "rule=validation_allowlist" "passed=false" "file=$changed_file"
612
+ else
613
+ emit_event "quality_gate_rule_evaluated" "rule=validation_allowlist" "passed=true" "file=$changed_file"
614
+ fi
615
+ done < /results/changed-files.txt
616
+
617
+ if [ "$validation_violation_count" -gt 0 ]; then
618
+ QUALITY_EXIT=7
619
+ QUALITY_FAILURE_REASON="validation_allowlist_check: $validation_violation_count file(s) changed during validation outside KASEKI_VALIDATION_ALLOWLIST"
620
+ printf '\n[validation-allowlist] %d file(s) modified during validation outside allowlist\n' "$validation_violation_count" | tee -a /results/quality.log
621
+ return 1
622
+ fi
623
+ return 0
624
+ }
625
+
626
+
627
+ finish() {
628
+ local code=$?
629
+ if [ "$code" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
630
+ STATUS="$code"
631
+ FAILED_COMMAND="unexpected shell failure"
632
+ fi
633
+ # Authoritative call site: this runs at EXIT so artifacts reflect final repo state.
634
+ collect_git_artifacts
635
+
636
+ # Debug output for restoration report generation
637
+ if [ -f /results/restoration.jsonl ]; then
638
+ printf '[debug] restoration.jsonl exists (size=%d bytes)\n' "$(wc -c < /results/restoration.jsonl)" >&2
639
+ else
640
+ printf '[debug] restoration.jsonl does not exist\n' >&2
641
+ fi
642
+
643
+ if ! generate_restoration_report; then
644
+ printf 'warning: restoration report generation failed, but continuing with cleanup\n' >&2
645
+ fi
646
+
647
+ # Calculate and record maturity score
648
+ if [ -x /app/scripts/kaseki-maturity-score.sh ]; then
649
+ /app/scripts/kaseki-maturity-score.sh /workspace/repo /results/maturity-score.json 2>/dev/null || true
650
+ fi
651
+
652
+ # Calculate and record performance metrics
653
+ if [ -x /app/scripts/kaseki-performance-metrics.sh ] && [ -f /results/stage-timings.tsv ]; then
654
+ /app/scripts/kaseki-performance-metrics.sh /results/stage-timings.tsv /results/performance-metrics.json 2>/dev/null || true
655
+ fi
656
+
657
+ write_result_summary
658
+ write_failure_json "$STATUS"
659
+ write_repo_memory_summary
660
+ write_metadata "$STATUS"
661
+ exit "$STATUS"
662
+ }
663
+ trap finish EXIT
664
+
665
+ run_step() {
666
+ local label="$1"
667
+ shift
668
+ local step_start step_end code
669
+ step_start="$(date +%s)"
670
+ set_current_stage "$label"
671
+ printf '\n==> %s\n' "$label"
672
+ emit_progress "$label" "started"
673
+ # Keep this explicit branch (instead of relying on `set -e`) so we can
674
+ # always emit progress/timing and preserve FAILED_COMMAND deterministically.
675
+ if "$@"; then
676
+ code=0
677
+ else
678
+ code=$?
679
+ fi
680
+ step_end="$(date +%s)"
681
+ emit_progress "$label" "finished with exit $code"
682
+ record_stage_timing "$label" "$code" "$((step_end - step_start))" ""
683
+ if [ "$code" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
684
+ STATUS="$code"
685
+ FAILED_COMMAND="$label"
686
+ fi
687
+ return "$code"
688
+ }
689
+
690
+ run_step_dry() {
691
+ local label="$1"
692
+ shift
693
+ local step_start step_end
694
+ step_start="$(date +%s)"
695
+ set_current_stage "$label"
696
+ printf '\n==> %s (DRY-RUN: simulated)\n' "$label"
697
+ emit_progress "$label" "started (dry-run)"
698
+ # Show what commands would be run without executing them
699
+ printf '%s\n' "$@" >> /results/validation.log
700
+ step_end="$(date +%s)"
701
+ emit_progress "$label" "finished (dry-run, simulated exit 0)"
702
+ record_stage_timing "$label" "0" "$((step_end - step_start))" "dry-run"
703
+ return 0
704
+ }
705
+
706
+ record_stage_timing() {
707
+ local stage="$1"
708
+ local exit_code="$2"
709
+ local duration_seconds="$3"
710
+ local detail="${4:-}"
711
+ printf '%s\t%s\t%s\t%s\n' "$stage" "$exit_code" "$duration_seconds" "$detail" >> "$STAGE_TIMINGS_FILE"
712
+ }
713
+
714
+ set_dependency_cache_status() {
715
+ local status="$1"
716
+ local detail="${2:-}"
717
+ printf '%s\t%s\n' "$status" "$detail" >> "$DEPENDENCY_CACHE_LOG"
718
+ }
719
+
720
+ compute_git_cache_key() {
721
+ local hash
722
+ hash="$(printf '%s' "$REPO_URL" | sha256sum | awk '{print $1}')"
723
+ printf 'repo-%s' "$hash"
724
+ }
725
+
726
+ is_valid_git_mirror() {
727
+ local mirror="$1"
728
+ [ -d "$mirror" ] || return 1
729
+ [ "$(git -C "$mirror" rev-parse --is-bare-repository 2>/dev/null || true)" = "true" ] || return 1
730
+ git -C "$mirror" remote get-url origin >/dev/null 2>&1 || return 1
731
+ }
732
+
733
+ run_direct_clone() {
734
+ rm -rf /workspace/repo
735
+ GIT_CLONE_STRATEGY="direct_shallow"
736
+ git clone --depth 1 --branch "$GIT_REF" "$REPO_URL" /workspace/repo
737
+ }
738
+
739
+ clone_with_git_cache() {
740
+ local cache_root="$KASEKI_GIT_CACHE_ROOT"
741
+ local mirror lock_file tmp_mirror lock_rc fetch_rc mirror_rc clone_rc
742
+
743
+ if [ "$KASEKI_GIT_CACHE_MODE" != "mirror" ]; then
744
+ GIT_CACHE_STATUS="disabled"
745
+ GIT_CACHE_HIT="false"
746
+ emit_progress "clone repository" "git cache disabled mode=$KASEKI_GIT_CACHE_MODE"
747
+ run_direct_clone
748
+ return $?
749
+ fi
750
+
751
+ GIT_CACHE_KEY="$(compute_git_cache_key)"
752
+ mirror="$cache_root/${GIT_CACHE_KEY}.git"
753
+ lock_file="$cache_root/${GIT_CACHE_KEY}.lock"
754
+ GIT_CACHE_MIRROR="$mirror"
755
+
756
+ if ! mkdir -p "$cache_root" 2>/dev/null; then
757
+ GIT_CACHE_STATUS="unavailable"
758
+ GIT_CACHE_HIT="false"
759
+ emit_error_event "git_cache_unavailable" "Cannot create git cache directory $cache_root; using direct clone" "fallback_direct_clone"
760
+ run_direct_clone
761
+ return $?
762
+ fi
763
+
764
+ exec 9>"$lock_file"
765
+ flock 9
766
+ lock_rc=$?
767
+ if [ "$lock_rc" -ne 0 ]; then
768
+ GIT_CACHE_STATUS="lock_failed"
769
+ GIT_CACHE_HIT="false"
770
+ emit_error_event "git_cache_lock_failed" "Cannot lock $lock_file; using direct clone" "fallback_direct_clone"
771
+ run_direct_clone
772
+ return $?
773
+ fi
774
+
775
+ if is_valid_git_mirror "$mirror"; then
776
+ GIT_CACHE_STATUS="hit"
777
+ GIT_CACHE_HIT="true"
778
+ emit_progress "clone repository" "git cache hit key=$GIT_CACHE_KEY mirror=$mirror"
779
+ timeout "$KASEKI_GIT_CACHE_FETCH_TIMEOUT_SECONDS" git -C "$mirror" fetch --prune --tags origin
780
+ fetch_rc=$?
781
+ if [ "$fetch_rc" -ne 0 ]; then
782
+ flock -u 9 || true
783
+ GIT_CACHE_STATUS="fetch_failed"
784
+ GIT_CACHE_HIT="true"
785
+ emit_error_event "git_cache_fetch_failed" "Mirror fetch failed or timed out for key=$GIT_CACHE_KEY exit=$fetch_rc; using direct clone" "fallback_direct_clone"
786
+ run_direct_clone
787
+ return $?
788
+ fi
789
+ else
790
+ GIT_CACHE_STATUS="miss"
791
+ GIT_CACHE_HIT="false"
792
+ emit_progress "clone repository" "git cache miss key=$GIT_CACHE_KEY mirror=$mirror"
793
+ if [ -e "$mirror" ]; then
794
+ rm -rf "$mirror"
795
+ fi
796
+ tmp_mirror="${mirror}.tmp.$$"
797
+ rm -rf "$tmp_mirror"
798
+ timeout "$KASEKI_GIT_CACHE_FETCH_TIMEOUT_SECONDS" git clone --mirror "$REPO_URL" "$tmp_mirror"
799
+ mirror_rc=$?
800
+ if [ "$mirror_rc" -eq 0 ] && is_valid_git_mirror "$tmp_mirror"; then
801
+ mv "$tmp_mirror" "$mirror"
802
+ else
803
+ rm -rf "$tmp_mirror"
804
+ flock -u 9 || true
805
+ GIT_CACHE_STATUS="populate_failed"
806
+ emit_error_event "git_cache_populate_failed" "Mirror populate failed or timed out for key=$GIT_CACHE_KEY exit=$mirror_rc; using direct clone" "fallback_direct_clone"
807
+ run_direct_clone
808
+ return $?
809
+ fi
810
+ fi
811
+ flock -u 9 || true
812
+
813
+ rm -rf /workspace/repo
814
+ GIT_CLONE_STRATEGY="reference_shallow"
815
+ git clone --reference-if-able "$mirror" --depth 1 --branch "$GIT_REF" "$REPO_URL" /workspace/repo
816
+ clone_rc=$?
817
+ if [ "$clone_rc" -eq 0 ]; then
818
+ return 0
819
+ fi
820
+
821
+ rm -rf /workspace/repo
822
+ GIT_CLONE_STRATEGY="mirror_local"
823
+ emit_error_event "git_cache_reference_clone_failed" "Reference clone failed for key=$GIT_CACHE_KEY exit=$clone_rc; trying local mirror clone" "try_mirror_clone"
824
+ git clone --branch "$GIT_REF" "$mirror" /workspace/repo
825
+ clone_rc=$?
826
+ if [ "$clone_rc" -eq 0 ] && git -C /workspace/repo rev-parse --verify HEAD >/dev/null 2>&1; then
827
+ git -C /workspace/repo remote set-url origin "$REPO_URL" >/dev/null 2>&1 || true
828
+ return 0
829
+ fi
830
+
831
+ rm -rf /workspace/repo
832
+ GIT_CACHE_STATUS="mirror_clone_failed"
833
+ emit_error_event "git_cache_mirror_clone_failed" "Mirror clone failed for key=$GIT_CACHE_KEY exit=$clone_rc; using direct clone" "fallback_direct_clone"
834
+ run_direct_clone
835
+ }
836
+
837
+ run_clone_repository() {
838
+ local step_start step_end code detail
839
+ step_start="$(date +%s)"
840
+ set_current_stage "clone repository"
841
+ printf '\n==> clone repository\n'
842
+ emit_progress "clone repository" "started cache_mode=$KASEKI_GIT_CACHE_MODE"
843
+ if clone_with_git_cache; then
844
+ code=0
845
+ else
846
+ code=$?
847
+ fi
848
+ step_end="$(date +%s)"
849
+ GIT_CLONE_DURATION_SECONDS="$((step_end - step_start))"
850
+ detail="cache_mode=$GIT_CACHE_MODE_USED cache_status=$GIT_CACHE_STATUS cache_hit=$GIT_CACHE_HIT cache_key=$GIT_CACHE_KEY strategy=$GIT_CLONE_STRATEGY mirror=$GIT_CACHE_MIRROR"
851
+ emit_progress "clone repository" "finished with exit $code elapsed=${GIT_CLONE_DURATION_SECONDS}s $detail"
852
+ emit_event "git_clone_cache" \
853
+ "mode=$GIT_CACHE_MODE_USED" \
854
+ "status=$GIT_CACHE_STATUS" \
855
+ "cache_hit=$GIT_CACHE_HIT" \
856
+ "cache_key=$GIT_CACHE_KEY" \
857
+ "strategy=$GIT_CLONE_STRATEGY" \
858
+ "mirror=$GIT_CACHE_MIRROR" \
859
+ "duration_seconds=$GIT_CLONE_DURATION_SECONDS" \
860
+ "exit_code=$code"
861
+ record_stage_timing "clone repository" "$code" "$GIT_CLONE_DURATION_SECONDS" "$detail"
862
+ if [ "$code" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
863
+ STATUS="$code"
864
+ FAILED_COMMAND="clone repository"
865
+ fi
866
+ return "$code"
867
+ }
868
+
869
+
870
+ same_filesystem() {
871
+ local left="$1"
872
+ local right="$2"
873
+ local left_device right_device
874
+ left_device="$(stat -c %d "$left" 2>/dev/null || true)"
875
+ right_device="$(stat -c %d "$right" 2>/dev/null || true)"
876
+ [ -n "$left_device" ] && [ "$left_device" = "$right_device" ]
877
+ }
878
+
879
+ restore_node_modules_from_cache() {
880
+ local source_dir="$1"
881
+ local target_dir="$2"
882
+ local mode="${3:-copy}"
883
+ DEPENDENCY_RESTORE_METHOD="$mode"
884
+ case "$mode" in
885
+ copy)
886
+ cp -a "$source_dir" "$target_dir"
887
+ ;;
888
+ hardlink)
889
+ if same_filesystem "$source_dir" "$(dirname "$target_dir")"; then
890
+ if cp -al "$source_dir" "$target_dir"; then
891
+ DEPENDENCY_RESTORE_METHOD="hardlink"
892
+ return 0
893
+ fi
894
+ DEPENDENCY_RESTORE_METHOD="hardlink_fallback_copy"
895
+ printf 'Dependency cache status: hardlink restore failed; falling back to copy.\n' | tee -a "$DEPENDENCY_CACHE_LOG"
896
+ cp -a "$source_dir" "$target_dir"
897
+ else
898
+ DEPENDENCY_RESTORE_METHOD="hardlink_cross_fs_copy"
899
+ printf 'Dependency cache status: hardlink restore skipped because cache and workspace are on different filesystems; falling back to copy.\n' | tee -a "$DEPENDENCY_CACHE_LOG"
900
+ cp -a "$source_dir" "$target_dir"
901
+ fi
902
+ ;;
903
+ symlink)
904
+ # Experimental: only keep this restore if downstream validation confirms tooling
905
+ # tolerates a symlinked node_modules tree.
906
+ DEPENDENCY_RESTORE_METHOD="symlink_experimental"
907
+ ln -s "$source_dir" "$target_dir"
908
+ ;;
909
+ *)
910
+ printf 'Unsupported KASEKI_DEPENDENCY_RESTORE_MODE: %s (expected copy, hardlink, or symlink)\n' "$mode" >&2
911
+ return 2
912
+ ;;
913
+ esac
914
+ }
915
+
916
+ publish_node_modules_cache() {
917
+ local source_dir="$1"
918
+ local tmp_cache_dir="$2"
919
+ rm -rf "$tmp_cache_dir"
920
+ mkdir -p "$tmp_cache_dir" && cp -a "$source_dir/." "$tmp_cache_dir/"
921
+ }
922
+
923
+ dependency_cache_flags_identity() {
924
+ printf 'omit_dev=%s\nignore_scripts=%s\n' "${KASEKI_NPM_OMIT_DEV:-0}" "${KASEKI_INSTALL_IGNORE_SCRIPTS:-1}"
925
+ }
926
+
927
+ dependency_cache_flags_hash() {
928
+ dependency_cache_flags_identity | sha256sum | awk '{print $1}'
929
+ }
930
+
931
+ append_npm_install_flags() {
932
+ local -n flags_ref="$1"
933
+ flags_ref=()
934
+ if [ "${KASEKI_NPM_OMIT_DEV:-0}" = "1" ]; then
935
+ flags_ref+=("--omit=dev")
936
+ fi
937
+ if [ "${KASEKI_INSTALL_IGNORE_SCRIPTS:-1}" = "1" ]; then
938
+ flags_ref+=("--ignore-scripts")
939
+ fi
940
+ }
941
+
942
+ render_npm_install_flags() {
943
+ if [ "$#" -eq 0 ]; then
944
+ printf 'none'
945
+ return 0
946
+ fi
947
+
948
+ local rendered=""
949
+ local flag
950
+ for flag in "$@"; do
951
+ if [ -n "$rendered" ]; then
952
+ rendered+=" "
953
+ fi
954
+ rendered+="$(printf '%q' "$flag")"
955
+ done
956
+ printf '%s' "$rendered"
957
+ }
958
+
959
+ dependency_cache_key() {
960
+ local lock_hash="$1"
961
+ local node_major="$2"
962
+ local flags_hash="$3"
963
+ printf 'npm/%s/node-%s/flags-%s' "$lock_hash" "$node_major" "$flags_hash"
964
+ }
965
+
966
+ npm_run_script_name() {
967
+ local command="$1"
968
+ local npm_run_regex='^npm[[:space:]]+run[[:space:]]+([^[:space:]-][^[:space:]-]*)($|[[:space:]])'
969
+ if [[ "$command" =~ $npm_run_regex ]]; then
970
+ printf '%s' "${BASH_REMATCH[1]}"
971
+ return 0
972
+ fi
973
+ return 1
974
+ }
975
+
976
+ package_json_has_npm_script() {
977
+ local script_name="$1"
978
+ [ -f package.json ] || return 1
979
+ node - "$script_name" <<'NODE'
980
+ const fs = require('fs');
981
+ const scriptName = process.argv[2];
982
+ try {
983
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
984
+ const scripts = pkg && typeof pkg.scripts === 'object' && pkg.scripts ? pkg.scripts : {};
985
+ process.exit(Object.prototype.hasOwnProperty.call(scripts, scriptName) ? 0 : 1);
986
+ } catch {
987
+ process.exit(1);
988
+ }
989
+ NODE
990
+ }
991
+
992
+ missing_npm_script_for_validation_command() {
993
+ local command="$1"
994
+ local script_name
995
+ script_name="$(npm_run_script_name "$command")" || return 1
996
+ package_json_has_npm_script "$script_name" && return 1
997
+ printf '%s' "$script_name"
998
+ return 0
999
+ }
1000
+
1001
+ record_skipped_validation_command() {
1002
+ local command="$1"
1003
+ local script_name="$2"
1004
+ local duration_seconds="$3"
1005
+ {
1006
+ printf '\n==> %s\n' "$command"
1007
+ printf 'skipped: package.json does not define npm script "%s"\n' "$script_name"
1008
+ } 2>&1 | tee -a /results/validation.log
1009
+ printf '%s\tskipped\t%s\tmissing_npm_script=%s\n' "$command" "$duration_seconds" "$script_name" >> "$VALIDATION_TIMINGS_FILE"
1010
+ }
1011
+ compute_repo_memory_key() {
1012
+ printf '%s\n%s' "$REPO_URL" "$GIT_REF" | sha256sum | awk '{print $1}'
1013
+ }
1014
+
1015
+ init_repo_memory_paths() {
1016
+ if [ "$KASEKI_REPO_MEMORY_MODE" != "summary" ]; then
1017
+ REPO_MEMORY_STATUS="disabled"
1018
+ return 0
1019
+ fi
1020
+ REPO_MEMORY_KEY="$(compute_repo_memory_key)"
1021
+ REPO_MEMORY_DIR="/cache/repo-memory/$REPO_MEMORY_KEY"
1022
+ REPO_MEMORY_FILE="$REPO_MEMORY_DIR/summary.md"
1023
+ REPO_MEMORY_STATUS="enabled"
1024
+ }
1025
+
1026
+ repo_memory_is_fresh() {
1027
+ local memory_file="$1"
1028
+ local now modified ttl_seconds age_seconds size_bytes
1029
+ [ -f "$memory_file" ] || return 1
1030
+ size_bytes="$(wc -c < "$memory_file" 2>/dev/null | tr -d ' ' || printf '0')"
1031
+ [ "$size_bytes" -gt 0 ] || return 1
1032
+ [ "$size_bytes" -le "$KASEKI_REPO_MEMORY_MAX_BYTES" ] || return 1
1033
+ now="$(date +%s)"
1034
+ modified="$(stat -c %Y "$memory_file" 2>/dev/null || printf '0')"
1035
+ ttl_seconds=$((KASEKI_REPO_MEMORY_TTL_DAYS * 86400))
1036
+ age_seconds=$((now - modified))
1037
+ [ "$age_seconds" -ge 0 ] && [ "$age_seconds" -le "$ttl_seconds" ]
1038
+ }
1039
+
1040
+ read_repo_memory_section() {
1041
+ init_repo_memory_paths
1042
+ [ "$KASEKI_REPO_MEMORY_MODE" = "summary" ] || return 0
1043
+ if ! repo_memory_is_fresh "$REPO_MEMORY_FILE"; then
1044
+ REPO_MEMORY_STATUS="miss_or_expired"
1045
+ return 0
1046
+ fi
1047
+ REPO_MEMORY_STATUS="hit"
1048
+ {
1049
+ printf '\n\n---\nPrior repository context (opt-in cache; use only as efficiency hints, not authoritative source of truth):\n'
1050
+ head -c "$KASEKI_REPO_MEMORY_MAX_BYTES" "$REPO_MEMORY_FILE"
1051
+ printf '\n---\n'
1052
+ }
1053
+ }
1054
+
1055
+ write_repo_memory_summary() {
1056
+ [ "$KASEKI_REPO_MEMORY_MODE" = "summary" ] || return 0
1057
+ [ "$KASEKI_DRY_RUN" != "1" ] || return 0
1058
+ init_repo_memory_paths
1059
+ [ -n "$REPO_MEMORY_FILE" ] || return 0
1060
+ [ "$PI_EXIT" -eq 0 ] || return 0
1061
+ [ "$SECRET_SCAN_EXIT" -eq 0 ] || return 0
1062
+ if [ "$STATUS" -ne 0 ] && [ "$KASEKI_TASK_MODE" != "inspect" ]; then
1063
+ return 0
1064
+ fi
1065
+ if ! mkdir -p "$REPO_MEMORY_DIR" 2>/dev/null; then
1066
+ emit_error_event "repo_memory_unavailable" "Cannot create repository memory directory $REPO_MEMORY_DIR" "continue"
1067
+ return 0
1068
+ fi
1069
+ local updated_at
1070
+ REPO_MEMORY_COMMIT_SHA="$(git -C /workspace/repo rev-parse HEAD 2>/dev/null || printf 'unknown')"
1071
+ updated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
1072
+ node - "$KASEKI_REPO_MEMORY_MAX_BYTES" "$REPO_MEMORY_FILE" "$REPO_URL" "$GIT_REF" "$REPO_MEMORY_COMMIT_SHA" "$updated_at" "$KASEKI_TASK_MODE" "$STATUS" "$PI_EXIT" "$VALIDATION_EXIT" "$QUALITY_EXIT" "$SECRET_SCAN_EXIT" <<'NODE' || {
1073
+ const fs = require('fs');
1074
+ const path = require('path');
1075
+ const [maxBytesArg, outputFile, repoUrl, gitRef, commitSha, timestamp, taskMode, status, piExit, validationExit, qualityExit, secretScanExit] = process.argv.slice(2);
1076
+ const maxBytes = Math.max(1024, Number(maxBytesArg) || 8000);
1077
+
1078
+ function readFile(file, maxChars = 12000) {
1079
+ try {
1080
+ return fs.readFileSync(file, 'utf8').slice(0, maxChars);
1081
+ } catch {
1082
+ return '';
1083
+ }
1084
+ }
1085
+
1086
+ function sanitize(text) {
1087
+ return String(text || '')
1088
+ .split(/\r?\n/)
1089
+ .filter((line) => !/(secret|credential|password|api[_ -]?key|token|bearer|authorization|private[_ -]?key|openrouter|task prompt|user prompt|^Task:)/i.test(line))
1090
+ .map((line) => line.replace(/sk-[A-Za-z0-9_-]{12,}/g, '[REDACTED_SECRET]').replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, '[REDACTED_SECRET]'))
1091
+ .join('\n')
1092
+ .trim();
1093
+ }
1094
+
1095
+ function compactLines(text, limit = 16) {
1096
+ const lines = sanitize(text)
1097
+ .split(/\r?\n/)
1098
+ .map((line) => line.trim())
1099
+ .filter(Boolean)
1100
+ .filter((line) => !/^Artifacts:?$/i.test(line) && !/^[-*] .*\.log( |$)/i.test(line));
1101
+ return lines.slice(0, limit);
1102
+ }
1103
+
1104
+ function changedFiles() {
1105
+ return sanitize(readFile('/results/changed-files.txt', 4000))
1106
+ .split(/\r?\n/)
1107
+ .map((line) => line.trim())
1108
+ .filter(Boolean)
1109
+ .slice(0, 40);
1110
+ }
1111
+
1112
+ function validationOutcomes() {
1113
+ const rows = sanitize(readFile('/results/validation-timings.tsv', 8000))
1114
+ .split(/\r?\n/)
1115
+ .map((line) => line.split('\t'))
1116
+ .filter((parts) => parts.length >= 2 && parts[0]);
1117
+ if (!rows.length) return ['No per-command validation timings recorded.'];
1118
+ return rows.slice(0, 20).map(([command, exitCode, duration]) => `${command}: exit ${exitCode}${duration ? `, ${duration}s` : ''}`);
1119
+ }
1120
+
1121
+ const resultLines = compactLines(readFile('/results/result-summary.md'));
1122
+ const analysisLines = compactLines(readFile('/results/analysis.md'), 10);
1123
+ const files = changedFiles();
1124
+ const validations = validationOutcomes();
1125
+
1126
+ let output = `# Repository Memory Summary\n\n` +
1127
+ `> Opt-in efficiency cache only. Treat this as prior context hints, not authoritative source of truth; inspect the repository before relying on it.\n\n` +
1128
+ `- Repo URL: ${repoUrl}\n` +
1129
+ `- Default ref: ${gitRef}\n` +
1130
+ `- Commit SHA: ${commitSha}\n` +
1131
+ `- Updated at: ${timestamp}\n` +
1132
+ `- Last run mode: ${taskMode}\n` +
1133
+ `- Exit status: overall ${status}, agent ${piExit}, validation ${validationExit}, quality ${qualityExit}, secret scan ${secretScanExit}\n` +
1134
+ `\n## Last run summary\n` +
1135
+ (resultLines.length ? resultLines.map((line) => `- ${line.replace(/^[-*]\s*/, '')}`).join('\n') : '- No result summary available.') +
1136
+ `\n\n## Changed files\n` +
1137
+ (files.length ? files.map((file) => `- ${file}`).join('\n') : '- none') +
1138
+ `\n\n## Validation outcomes\n` +
1139
+ validations.map((line) => `- ${line}`).join('\n');
1140
+
1141
+ if (analysisLines.length) {
1142
+ output += `\n\n## Sanitized analysis notes\n` + analysisLines.map((line) => `- ${line.replace(/^[-*]\s*/, '')}`).join('\n');
1143
+ }
1144
+
1145
+ const marker = '\n\n<!-- repo-memory-truncated -->\n';
1146
+ let buffer = Buffer.from(output + '\n', 'utf8');
1147
+ if (buffer.length > maxBytes) {
1148
+ buffer = Buffer.from(output.slice(0, Math.max(0, maxBytes - Buffer.byteLength(marker))) + marker, 'utf8');
1149
+ }
1150
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
1151
+ fs.writeFileSync(outputFile, buffer);
1152
+ NODE
1153
+ emit_error_event "repo_memory_write_failed" "Failed to update repository memory summary" "continue"
1154
+ return 0
1155
+ }
1156
+ REPO_MEMORY_STATUS="updated"
1157
+ emit_event "repo_memory_updated" "mode=$KASEKI_REPO_MEMORY_MODE" "repo_key=$REPO_MEMORY_KEY" "summary=$REPO_MEMORY_FILE" "max_bytes=$KASEKI_REPO_MEMORY_MAX_BYTES"
1158
+ }
1159
+
1160
+ build_agent_prompt() {
1161
+ local memory_section
1162
+ memory_section="$(read_repo_memory_section)"
1163
+ if [ "$KASEKI_AGENT_GUARDRAILS" != "1" ]; then
1164
+ printf '%s' "$TASK_PROMPT"
1165
+ printf '%s' "$memory_section"
1166
+ return 0
1167
+ fi
1168
+
1169
+ cat <<EOF
1170
+ You are editing inside a Kaseki-managed ephemeral workspace.
1171
+
1172
+ Operational guardrails:
1173
+ - Do not run git add, git commit, git push, gh, hub, or create pull requests. Kaseki owns commit, push, and PR creation after validation passes.
1174
+ - Do not run npm install, npm ci, yarn install, pnpm install, or package-manager commands that modify lockfiles. Kaseki owns dependency setup and validation.
1175
+ - Keep edits limited to the requested source and test files. If a tool or command changes unrelated files, restore those unrelated files before finishing.
1176
+ - Do not print, inspect, or expose environment variables, secrets, credentials, API keys, or mounted secret files.
1177
+
1178
+ Task:
1179
+ $TASK_PROMPT
1180
+ $memory_section
1181
+ EOF
1182
+ }
1183
+
1184
+ run_github_operations() {
1185
+ local app_id private_key_file owner repo feature_branch token token_data
1186
+
1187
+ # Load GitHub App credentials
1188
+ app_id="$(cat /run/secrets/github_app_id)" || { printf 'Failed to read app ID\n' >&2; return 7; }
1189
+ cat /run/secrets/github_app_client_id >/dev/null || { printf 'Failed to read client ID\n' >&2; return 7; }
1190
+ private_key_file="/run/secrets/github_app_private_key"
1191
+
1192
+ # Parse repo URL to extract owner and repo
1193
+ if [[ "$REPO_URL" =~ ^https?://github\.com/([^/]+)/([^/]+)(/|\.git)?$ ]]; then
1194
+ owner="${BASH_REMATCH[1]}"
1195
+ repo="${BASH_REMATCH[2]}"
1196
+ else
1197
+ printf -- 'Cannot parse GitHub repo URL: %s\n' "$REPO_URL" | tee -a /results/git-push.log >&2
1198
+ return 7
1199
+ fi
1200
+
1201
+ printf -- 'GitHub operations: owner=%s, repo=%s\n' "$owner" "$repo" | tee -a /results/git-push.log
1202
+
1203
+ # Set git user for commits
1204
+ git config user.name "GitHub App [$app_id]" || { printf 'Failed to set git user name\n' >&2; return 7; }
1205
+ git config user.email "${app_id}+kaseki@users.noreply.github.com" || { printf 'Failed to set git email\n' >&2; return 7; }
1206
+
1207
+ # Generate GitHub App installation token
1208
+ printf 'Generating GitHub App installation token...\n' | tee -a /results/git-push.log
1209
+ token_data="$(node /usr/local/bin/github-app-token "$app_id" "$private_key_file" "$owner" "$repo")" || {
1210
+ printf 'Failed to generate token\n' | tee -a /results/git-push.log >&2
1211
+ GITHUB_PUSH_EXIT=7
1212
+ return 7
1213
+ }
1214
+
1215
+ token="$(printf '%s' "$token_data" | node -e "const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); process.stdout.write(d.token || '')" 2>/dev/null)"
1216
+ if [ -z "$token" ]; then
1217
+ printf -- 'Failed to extract token from response: %s\n' "$token_data" | tee -a /results/git-push.log >&2
1218
+ GITHUB_PUSH_EXIT=7
1219
+ return 7
1220
+ fi
1221
+
1222
+ printf 'Token generated successfully\n' | tee -a /results/git-push.log
1223
+
1224
+ # Create and push feature branch
1225
+ feature_branch="kaseki/$INSTANCE_NAME"
1226
+ printf -- 'Creating feature branch: %s\n' "$feature_branch" | tee -a /results/git-push.log
1227
+ git checkout -b "$feature_branch" || {
1228
+ printf 'Failed to create branch\n' | tee -a /results/git-push.log >&2
1229
+ GITHUB_PUSH_EXIT=7
1230
+ return 7
1231
+ }
1232
+
1233
+ # Commit changes (git should already have changes from pi agent)
1234
+ printf 'Committing changes...\n' | tee -a /results/git-push.log
1235
+ if [ ! -s /results/changed-files.txt ]; then
1236
+ printf 'No changed files to stage\n' | tee -a /results/git-push.log >&2
1237
+ GITHUB_PUSH_EXIT=7
1238
+ return 7
1239
+ fi
1240
+ while IFS= read -r changed_file || [ -n "$changed_file" ]; do
1241
+ [ -z "$changed_file" ] && continue
1242
+ git add -- "$changed_file" || {
1243
+ printf -- 'Failed to stage changed file: %s\n' "$changed_file" | tee -a /results/git-push.log >&2
1244
+ GITHUB_PUSH_EXIT=7
1245
+ return 7
1246
+ }
1247
+ done < /results/changed-files.txt
1248
+ if ! git commit -m "Kaseki: $INSTANCE_NAME"; then
1249
+ printf 'No changes to commit or commit failed\n' | tee -a /results/git-push.log >&2
1250
+ GITHUB_PUSH_EXIT=7
1251
+ return 7
1252
+ fi
1253
+
1254
+ # Push branch
1255
+ printf 'Pushing branch to GitHub...\n' | tee -a /results/git-push.log
1256
+ local askpass_file
1257
+ askpass_file="$(mktemp /tmp/kaseki-github-askpass.XXXXXX)" || {
1258
+ printf 'Failed to create GitHub credential helper\n' | tee -a /results/git-push.log >&2
1259
+ GITHUB_PUSH_EXIT=8
1260
+ return 8
1261
+ }
1262
+ cat > "$askpass_file" <<'EOF_ASKPASS'
1263
+ #!/usr/bin/env bash
1264
+ case "$1" in
1265
+ *Username*) printf '%s\n' x-access-token ;;
1266
+ *) printf '%s\n' "$KASEKI_GITHUB_TOKEN" ;;
1267
+ esac
1268
+ EOF_ASKPASS
1269
+ chmod 0700 "$askpass_file"
1270
+
1271
+ if KASEKI_GITHUB_TOKEN="$token" GIT_ASKPASS="$askpass_file" GIT_TERMINAL_PROMPT=0 \
1272
+ git push "https://github.com/$owner/$repo.git" "$feature_branch" --force-with-lease 2>&1 | tee -a /results/git-push.log; then
1273
+ printf 'Branch pushed successfully\n' | tee -a /results/git-push.log
1274
+ else
1275
+ rm -f "$askpass_file"
1276
+ printf 'Failed to push branch\n' | tee -a /results/git-push.log >&2
1277
+ GITHUB_PUSH_EXIT=8
1278
+ return 8
1279
+ fi
1280
+ rm -f "$askpass_file"
1281
+
1282
+ if [ "$KASEKI_PUBLISH_MODE" = "branch" ]; then
1283
+ printf 'Publish mode branch: skipping pull request creation.\n' | tee -a /results/git-push.log
1284
+ GITHUB_PR_EXIT=0
1285
+ unset token
1286
+ return 0
1287
+ fi
1288
+
1289
+ # Create pull request
1290
+ printf 'Creating pull request...\n' | tee -a /results/git-push.log
1291
+ local pr_title pr_body pr_response pr_url
1292
+ pr_title="Kaseki: $INSTANCE_NAME"
1293
+ pr_body=$(cat <<EOF
1294
+ Generated by Kaseki agent (instance: $INSTANCE_NAME)
1295
+
1296
+ **Model:** $KASEKI_MODEL
1297
+
1298
+ **Duration:** $(($(date +%s) - START_EPOCH)) seconds
1299
+
1300
+ **Validation:** $([ "$VALIDATION_EXIT" -eq 0 ] && printf 'passed' || printf 'failed (exit %s)' "$VALIDATION_EXIT")
1301
+
1302
+ **Quality Checks:** $([ "$QUALITY_EXIT" -eq 0 ] && printf 'passed' || printf 'failed (exit %s)' "$QUALITY_EXIT")
1303
+
1304
+ This PR is in draft status. Please review before merging.
1305
+ EOF
1306
+ )
1307
+
1308
+ pr_response=$(curl -s -X POST \
1309
+ -H "Authorization: token $token" \
1310
+ -H "Accept: application/vnd.github.v3+json" \
1311
+ "https://api.github.com/repos/$owner/$repo/pulls" \
1312
+ -d "{\"title\": $(printf '%s' "$pr_title" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"body\": $(printf '%s' "$pr_body" | node -e "console.log(JSON.stringify(require('fs').readFileSync(0, 'utf8')))"), \"head\": \"$feature_branch\", \"base\": \"$GIT_REF\", \"draft\": true}" 2>&1)
1313
+
1314
+ pr_url="$(printf '%s' "$pr_response" | node -e "const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); process.stdout.write(d.html_url || '')" 2>/dev/null || true)"
1315
+
1316
+ if [ -n "$pr_url" ]; then
1317
+ GITHUB_PR_URL="$pr_url"
1318
+ GITHUB_PR_EXIT=0
1319
+ printf 'Pull request created: %s\n' "$pr_url" | tee -a /results/git-push.log
1320
+ else
1321
+ printf 'Failed to create PR. Response: %s\n' "$pr_response" | tee -a /results/git-push.log >&2
1322
+ GITHUB_PR_EXIT=9
1323
+ fi
1324
+
1325
+ # Clean up token
1326
+ unset token
1327
+ }
1328
+
1329
+ printf 'Kaseki instance: %s\n' "$INSTANCE_NAME"
1330
+ printf 'Repository: %s\n' "$REPO_URL"
1331
+ printf 'Git ref: %s\n' "$GIT_REF"
1332
+ printf 'Provider: %s\n' "$KASEKI_PROVIDER"
1333
+ printf 'Model: %s\n' "$KASEKI_MODEL"
1334
+ printf 'Pi version: %s\n' "$PI_VERSION"
1335
+
1336
+ openrouter_api_key=""
1337
+ openrouter_api_key_source=""
1338
+ if [ -n "${OPENROUTER_API_KEY:-}" ]; then
1339
+ openrouter_api_key="$OPENROUTER_API_KEY"
1340
+ openrouter_api_key_source="env"
1341
+ elif [ -r /run/secrets/openrouter_api_key ]; then
1342
+ secret_content="$(cat /run/secrets/openrouter_api_key)"
1343
+ if [ -n "$secret_content" ]; then
1344
+ openrouter_api_key="$secret_content"
1345
+ openrouter_api_key_source="secret file"
1346
+ fi
1347
+ fi
1348
+ unset OPENROUTER_API_KEY secret_content
1349
+
1350
+ if [ -z "$openrouter_api_key" ]; then
1351
+ set_current_stage "agent setup"
1352
+ printf 'Missing OpenRouter API key. Set OPENROUTER_API_KEY or provide /run/secrets/openrouter_api_key.\n' | tee -a /results/pi-stderr.log >&2
1353
+ : > "$RAW_EVENTS"
1354
+ PI_EXIT=2
1355
+ STATUS=2
1356
+ FAILED_COMMAND="missing OPENROUTER_API_KEY"
1357
+ exit 0
1358
+ fi
1359
+
1360
+ if ! run_clone_repository; then
1361
+ exit 0
1362
+ fi
1363
+ cd /workspace/repo || { STATUS=1; FAILED_COMMAND="enter repository"; exit "$STATUS"; }
1364
+
1365
+ prepare_dependencies() {
1366
+ if [ ! -f package.json ]; then
1367
+ printf 'No package.json found; skipping dependency installation.\n'
1368
+ return 0
1369
+ fi
1370
+
1371
+ local lock_source=""
1372
+ if [ -f package-lock.json ]; then
1373
+ lock_source="package-lock.json"
1374
+ elif [ -f npm-shrinkwrap.json ]; then
1375
+ lock_source="npm-shrinkwrap.json"
1376
+ else
1377
+ printf 'Dependency install requires package-lock.json or npm-shrinkwrap.json; lockfile missing.\n' >&2
1378
+ set_dependency_cache_status "lockfile-missing" "cache_key=none repo_url=$REPO_URL git_ref=$GIT_REF"
1379
+ emit_progress "dependency install" "failed lockfile missing; refusing non-deterministic install" "error"
1380
+ return 1
1381
+ fi
1382
+
1383
+ local repo_ref_key lock_hash flags_hash cache_key workspace_cache_root workspace_cache_dir image_cache_dir stamp_file metadata_file
1384
+ local cache_lock_file cache_lock_fd tmp_cache_dir old_cache_dir install_start install_elapsed install_flags_display cache_detail
1385
+ local node_major cache_reused cache_source install_mode restore_mode restore_method
1386
+ local -a install_flags
1387
+ repo_ref_key="$(printf '%s@%s' "$REPO_URL" "$GIT_REF" | sha256sum | awk '{print $1}')"
1388
+ lock_hash="$(sha256sum "$lock_source" | awk '{print $1}')"
1389
+ node_major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo "unknown")"
1390
+ flags_hash="$(dependency_cache_flags_hash)"
1391
+ cache_key="$(dependency_cache_key "$lock_hash" "$node_major" "$flags_hash")"
1392
+ workspace_cache_root="${KASEKI_DEPENDENCY_CACHE_DIR}/${cache_key}"
1393
+ workspace_cache_dir="${workspace_cache_root}/node_modules"
1394
+ image_cache_dir="${KASEKI_IMAGE_DEPENDENCY_CACHE_DIR}/${cache_key}/node_modules"
1395
+ stamp_file="${workspace_cache_root}/stamp.txt"
1396
+ metadata_file="${workspace_cache_root}/repo-ref-metadata.tsv"
1397
+ cache_lock_file="${workspace_cache_root}.lock"
1398
+ cache_reused="false"
1399
+ cache_source="none"
1400
+ install_mode="skipped"
1401
+ restore_mode="$KASEKI_DEPENDENCY_RESTORE_MODE"
1402
+ restore_method="$restore_mode"
1403
+ case "$restore_mode" in
1404
+ copy|hardlink|symlink) ;;
1405
+ *)
1406
+ printf 'Unsupported KASEKI_DEPENDENCY_RESTORE_MODE: %s (expected copy, hardlink, or symlink)\n' "$restore_mode" >&2
1407
+ set_dependency_cache_status "restore-mode-invalid" "restore_mode=$restore_mode repo_url=$REPO_URL git_ref=$GIT_REF"
1408
+ emit_progress "dependency install" "failed invalid restore_mode=$restore_mode" "error"
1409
+ return 1
1410
+ ;;
1411
+ esac
1412
+ append_npm_install_flags install_flags
1413
+ install_flags_display="$(render_npm_install_flags "${install_flags[@]}")"
1414
+ cache_detail="lock_hash=$lock_hash cache_key=$cache_key repo_ref_key=$repo_ref_key repo_url=$REPO_URL git_ref=$GIT_REF lockfile=$lock_source node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display restore_mode=$restore_mode"
1415
+
1416
+ if ! mkdir -p "$(dirname "$workspace_cache_root")"; then
1417
+ return 1
1418
+ fi
1419
+ if ! exec {cache_lock_fd}>"$cache_lock_file"; then
1420
+ return 1
1421
+ fi
1422
+ if ! flock "$cache_lock_fd"; then
1423
+ exec {cache_lock_fd}>&-
1424
+ return 1
1425
+ fi
1426
+
1427
+ if ! mkdir -p "$workspace_cache_root"; then
1428
+ exec {cache_lock_fd}>&-
1429
+ return 1
1430
+ fi
1431
+
1432
+ if [ -d node_modules ] && [ -f "$stamp_file" ]; then
1433
+ if grep -qx "$lock_hash" "$stamp_file"; then
1434
+ printf 'Dependency cache status: using existing repo node_modules for lock hash %s (repo_ref_key=%s).\n' "$lock_hash" "$repo_ref_key"
1435
+ set_dependency_cache_status "existing-node-modules" "$cache_detail restore_method=none"
1436
+ emit_event "dependency_cache_decision" "strategy=existing_node_modules" "restore_mode=$restore_mode" "restore_method=none" "reason=lock_hash_match" "location=repo" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1437
+ emit_progress "dependency install" "cache hit source=repo restore_mode=$restore_mode restore_method=none lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1438
+ record_stage_timing "dependency install" "0" "0" "cache_hit=true cache_source=repo install_mode=skipped restore_mode=$restore_mode restore_method=none lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1439
+ exec {cache_lock_fd}>&-
1440
+ return 0
1441
+ fi
1442
+ fi
1443
+
1444
+ if [ ! -d node_modules ] && [ -d "$workspace_cache_dir" ]; then
1445
+ printf 'Dependency cache status: restoring node_modules from workspace cache (%s; lock_hash=%s; repo_ref_key=%s).\n' "$workspace_cache_dir" "$lock_hash" "$repo_ref_key"
1446
+ set_dependency_cache_status "workspace-cache-hit" "$cache_detail"
1447
+ emit_event "dependency_cache_decision" "strategy=workspace_cache_hit" "restore_mode=$restore_mode" "location=$workspace_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1448
+ if ! restore_node_modules_from_cache "$workspace_cache_dir" ./node_modules "$restore_mode"; then
1449
+ exec {cache_lock_fd}>&-
1450
+ return 1
1451
+ fi
1452
+ restore_method="$DEPENDENCY_RESTORE_METHOD"
1453
+ set_dependency_cache_status "workspace-cache-restored" "$cache_detail restore_method=$restore_method"
1454
+ emit_event "dependency_cache_decision" "strategy=workspace_cache_restored" "restore_mode=$restore_mode" "restore_method=$restore_method" "reason=restore_completed" "location=$workspace_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1455
+ cache_reused="true"
1456
+ cache_source="workspace"
1457
+ if ! npm ls --depth=0 >/dev/null 2>&1; then
1458
+ printf 'Dependency cache status: workspace cache failed npm ls validation; reinstalling.\n'
1459
+ set_dependency_cache_status "workspace-cache-invalid" "$cache_detail restore_method=$restore_method reason=npm_ls_failed"
1460
+ emit_event "dependency_cache_decision" "strategy=invalidate_workspace_cache" "restore_mode=$restore_mode" "restore_method=$restore_method" "reason=npm_ls_failed" "location=$workspace_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1461
+ rm -rf node_modules
1462
+ cache_reused="false"
1463
+ cache_source="none"
1464
+ fi
1465
+ elif [ ! -d node_modules ] && [ -d "$image_cache_dir" ]; then
1466
+ printf 'Dependency cache status: restoring node_modules from image cache (%s; lock_hash=%s; repo_ref_key=%s).\n' "$image_cache_dir" "$lock_hash" "$repo_ref_key"
1467
+ set_dependency_cache_status "image-cache-hit" "$cache_detail"
1468
+ emit_event "dependency_cache_decision" "strategy=image_cache_hit" "restore_mode=$restore_mode" "location=$image_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1469
+ if ! restore_node_modules_from_cache "$image_cache_dir" ./node_modules "$restore_mode"; then
1470
+ exec {cache_lock_fd}>&-
1471
+ return 1
1472
+ fi
1473
+ restore_method="$DEPENDENCY_RESTORE_METHOD"
1474
+ set_dependency_cache_status "image-cache-restored" "$cache_detail restore_method=$restore_method"
1475
+ emit_event "dependency_cache_decision" "strategy=image_cache_restored" "restore_mode=$restore_mode" "restore_method=$restore_method" "reason=restore_completed" "location=$image_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1476
+ cache_reused="true"
1477
+ cache_source="image"
1478
+ if ! npm ls --depth=0 >/dev/null 2>&1; then
1479
+ printf 'Dependency cache status: image cache failed npm ls validation; reinstalling.\n'
1480
+ set_dependency_cache_status "image-cache-invalid" "$cache_detail restore_method=$restore_method reason=npm_ls_failed"
1481
+ emit_event "dependency_cache_decision" "strategy=invalidate_image_cache" "restore_mode=$restore_mode" "restore_method=$restore_method" "reason=npm_ls_failed" "location=$image_cache_dir" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1482
+ rm -rf node_modules
1483
+ cache_reused="false"
1484
+ cache_source="none"
1485
+ fi
1486
+ fi
1487
+
1488
+ if [ ! -d node_modules ]; then
1489
+ printf 'Dependency cache status: cache miss for lock hash %s (repo_ref_key=%s), running install.\n' "$lock_hash" "$repo_ref_key"
1490
+ set_dependency_cache_status "cache-miss" "$cache_detail"
1491
+ emit_event "dependency_cache_decision" "strategy=fresh_install" "restore_mode=$restore_mode" "restore_method=none" "reason=no_cache_available" "location=none" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1492
+ emit_progress "dependency install" "started cache_hit=false restore_mode=$restore_mode restore_method=none lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1493
+ install_start="$(date +%s)"
1494
+ if ! npm ci --prefer-offline "${install_flags[@]}"; then
1495
+ exec {cache_lock_fd}>&-
1496
+ return 1
1497
+ fi
1498
+ install_elapsed="$(($(date +%s) - install_start))"
1499
+ install_mode="npm_ci_lockfile"
1500
+ emit_progress "dependency install" "finished elapsed=${install_elapsed}s cache_hit=false restore_mode=$restore_mode restore_method=none lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1501
+ record_stage_timing "dependency install" "0" "$install_elapsed" "cache_hit=false cache_source=none install_mode=$install_mode restore_mode=$restore_mode restore_method=none lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1502
+ else
1503
+ printf 'Dependency cache status: install skipped due to cache hit.\n'
1504
+ set_dependency_cache_status "install-skipped" "$cache_detail restore_method=$restore_method"
1505
+ emit_event "dependency_cache_decision" "strategy=skip_install" "restore_mode=$restore_mode" "restore_method=$restore_method" "reason=cache_hit" "location=local" "lock_hash=$lock_hash" "cache_key=$cache_key" "repo_ref_key=$repo_ref_key" "repo_url=$REPO_URL" "git_ref=$GIT_REF" "node_major=$node_major" "flags_hash=$flags_hash"
1506
+ if [ "$cache_reused" = "true" ]; then
1507
+ emit_progress "dependency install" "cache hit source=$cache_source restore_mode=$restore_mode restore_method=$restore_method lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1508
+ record_stage_timing "dependency install" "0" "0" "cache_hit=true cache_source=$cache_source install_mode=skipped restore_mode=$restore_mode restore_method=$restore_method lockfile=$lock_source lock_hash=$lock_hash repo_ref_key=$repo_ref_key node_major=$node_major flags_hash=$flags_hash flags=$install_flags_display"
1509
+ fi
1510
+ fi
1511
+
1512
+ if ! mkdir -p "$workspace_cache_root"; then
1513
+ exec {cache_lock_fd}>&-
1514
+ return 1
1515
+ fi
1516
+ tmp_cache_dir="${workspace_cache_dir}.tmp.$$"
1517
+ old_cache_dir="${workspace_cache_dir}.old.$$"
1518
+ rm -rf "$tmp_cache_dir" "$old_cache_dir"
1519
+ if ! publish_node_modules_cache node_modules "$tmp_cache_dir"; then
1520
+ exec {cache_lock_fd}>&-
1521
+ return 1
1522
+ fi
1523
+ # Keep this publish path single-pass and atomic to avoid cache corruption.
1524
+ if [ -d "$workspace_cache_dir" ] && ! mv "$workspace_cache_dir" "$old_cache_dir"; then
1525
+ exec {cache_lock_fd}>&-
1526
+ return 1
1527
+ fi
1528
+ if ! mv "$tmp_cache_dir" "$workspace_cache_dir"; then
1529
+ exec {cache_lock_fd}>&-
1530
+ return 1
1531
+ fi
1532
+ if ! rm -rf "$old_cache_dir"; then
1533
+ exec {cache_lock_fd}>&-
1534
+ return 1
1535
+ fi
1536
+ if ! printf '%s\n' "$lock_hash" > "$stamp_file"; then
1537
+ exec {cache_lock_fd}>&-
1538
+ return 1
1539
+ fi
1540
+ if ! printf 'repo_ref_key=%s repo_url=%s git_ref=%s lock_hash=%s cache_key=%s flags_hash=%s restore_mode=%s restore_method=%s\n' \
1541
+ "$repo_ref_key" "$REPO_URL" "$GIT_REF" "$lock_hash" "$cache_key" "$flags_hash" "$restore_mode" "$restore_method" > "$metadata_file"; then
1542
+ exec {cache_lock_fd}>&-
1543
+ return 1
1544
+ fi
1545
+
1546
+ exec {cache_lock_fd}>&-
1547
+ return 0
1548
+ }
1549
+
1550
+ if ! run_step "prepare node dependencies" prepare_dependencies; then
1551
+ exit 0
1552
+ fi
1553
+
1554
+ printf '\n==> pi coding agent\n'
1555
+ set_current_stage "pi coding agent"
1556
+ if [ "$KASEKI_DRY_RUN" = "1" ]; then
1557
+ printf '🔄 DRY-RUN MODE: Skipping Pi coding agent execution\n'
1558
+ PI_START_EPOCH="$(date +%s)"
1559
+ PI_EXIT=0
1560
+ PI_DURATION_SECONDS=$(($(date +%s) - PI_START_EPOCH))
1561
+ {
1562
+ printf 'DRY-RUN: Pi agent would have been invoked with the following configuration:\n'
1563
+ printf ' Provider: %s\n' "$KASEKI_PROVIDER"
1564
+ printf ' Model: %s\n' "$KASEKI_MODEL"
1565
+ printf ' Timeout: %s seconds\n' "$KASEKI_AGENT_TIMEOUT_SECONDS"
1566
+ printf ' Task: %s\n' "$TASK_PROMPT"
1567
+ } | tee -a /results/pi-stderr.log
1568
+ emit_progress "pi coding agent" "skipped (dry-run)"
1569
+ record_stage_timing "pi coding agent" "0" "$PI_DURATION_SECONDS" "dry_run=true"
1570
+ else
1571
+ set +e
1572
+ printf 'OpenRouter API key source: %s\n' "$openrouter_api_key_source"
1573
+ export KASEKI_STREAM_PROGRESS
1574
+ agent_prompt="$(build_agent_prompt)"
1575
+ PI_START_EPOCH="$(date +%s)"
1576
+ OPENROUTER_API_KEY="$openrouter_api_key" \
1577
+ timeout --signal=SIGTERM "$KASEKI_AGENT_TIMEOUT_SECONDS" \
1578
+ pi --mode json --no-session --provider "$KASEKI_PROVIDER" --model "$KASEKI_MODEL" "$agent_prompt" \
1579
+ 2> >(tee -a /results/pi-stderr.log >&2) \
1580
+ | tee "$RAW_EVENTS" \
1581
+ | kaseki-pi-progress-stream /results/progress.jsonl /results/progress.log
1582
+ PI_EXIT="${PIPESTATUS[0]}"
1583
+ unset agent_prompt
1584
+ PI_DURATION_SECONDS=$(($(date +%s) - PI_START_EPOCH))
1585
+ unset OPENROUTER_API_KEY openrouter_api_key openrouter_api_key_source
1586
+ set -e
1587
+ record_stage_timing "pi coding agent" "$PI_EXIT" "$PI_DURATION_SECONDS" "timeout_seconds=$KASEKI_AGENT_TIMEOUT_SECONDS"
1588
+
1589
+ if [ "$KASEKI_DEBUG_RAW_EVENTS" = "1" ]; then
1590
+ cp "$RAW_EVENTS" /results/pi-events.raw.jsonl
1591
+ fi
1592
+
1593
+ PI_EXTRACTION_DEPS_OK=1
1594
+ missing_executables=()
1595
+ missing_helpers=()
1596
+ for required_exec in kaseki-pi-event-filter kaseki-pi-progress-stream; do
1597
+ if ! command -v "$required_exec" >/dev/null 2>&1; then
1598
+ missing_executables+=("$required_exec")
1599
+ fi
1600
+ done
1601
+ for helper_file in /app/lib/event-aggregator.js /app/lib/timestamp-tracker.js /app/lib/progress-stream-utils.js; do
1602
+ if [ ! -f "$helper_file" ]; then
1603
+ missing_helpers+=("$helper_file")
1604
+ fi
1605
+ done
1606
+ if [ ${#missing_executables[@]} -gt 0 ] || [ ${#missing_helpers[@]} -gt 0 ]; then
1607
+ PI_EXTRACTION_DEPS_OK=0
1608
+ missing_execs_joined="${missing_executables[*]}"
1609
+ missing_helpers_joined="${missing_helpers[*]}"
1610
+ [ -z "$missing_execs_joined" ] && missing_execs_joined="none"
1611
+ [ -z "$missing_helpers_joined" ] && missing_helpers_joined="none"
1612
+ extraction_error=$(node -e "console.log(JSON.stringify({error:'pi_extraction_dependency_missing',missing_executables:process.argv[1],missing_helpers:process.argv[2],action:'Ensure required Pi binaries are on PATH and helper files exist in the image before running extraction'}))" "$missing_execs_joined" "$missing_helpers_joined")
1613
+ printf '%s
1614
+ ' "$extraction_error" | tee -a /results/pi-stderr.log /results/quality.log >&2
1615
+ emit_error_event "pi_extraction_dependency_missing" "missing executables: $missing_execs_joined; missing helpers: $missing_helpers_joined; ensure Pi binaries are in PATH and /app/lib helpers are present" "abort_extraction"
1616
+ if [ "$STATUS" -eq 0 ]; then
1617
+ STATUS=87
1618
+ FAILED_COMMAND="pi artifact extraction dependency validation"
1619
+ fi
1620
+ cp "$RAW_EVENTS" /results/pi-events.raw.jsonl 2>/dev/null || true
1621
+ fi
1622
+
1623
+ FILTER_EXIT=0
1624
+ if [ "$PI_EXTRACTION_DEPS_OK" -eq 1 ]; then
1625
+ set +e
1626
+ kaseki-pi-event-filter "$RAW_EVENTS" /results/pi-events.jsonl /results/pi-summary.json
1627
+ FILTER_EXIT=$?
1628
+ set -e
1629
+ fi
1630
+ if [ "$FILTER_EXIT" -ne 0 ]; then
1631
+ printf 'pi-event-filter failed with exit %s; raw events preserved as fallback artifact\n' "$FILTER_EXIT" | tee -a /results/quality.log
1632
+ printf 'ERROR: kaseki-pi-event-filter failed with exit %s while exporting Pi events\n' "$FILTER_EXIT" | tee -a /results/pi-stderr.log >&2
1633
+ emit_error_event "pi_event_filter_failed" "kaseki-pi-event-filter exited with code $FILTER_EXIT" "continue"
1634
+ if [ "$STATUS" -eq 0 ]; then
1635
+ STATUS="$FILTER_EXIT"
1636
+ FAILED_COMMAND="kaseki-pi-event-filter"
1637
+ fi
1638
+ cp "$RAW_EVENTS" /results/pi-events.raw.jsonl 2>/dev/null || true
1639
+ fi
1640
+ if [ -s "$RAW_EVENTS" ] && { [ ! -s /results/pi-events.jsonl ] || [ ! -s /results/pi-summary.json ]; }; then
1641
+ printf 'ERROR: pi event export incomplete; raw events are non-empty but event artifacts are missing/empty\n' | tee -a /results/pi-stderr.log >&2
1642
+ emit_error_event "pi_event_export_incomplete" "RAW_EVENTS has data but exported artifacts are empty or missing" "continue"
1643
+ if [ "$STATUS" -eq 0 ]; then
1644
+ STATUS=86
1645
+ FAILED_COMMAND="pi event export incomplete"
1646
+ fi
1647
+ fi
1648
+ ACTUAL_MODEL="$(node -e "
1649
+ var fs=require('fs');
1650
+ function clean(v){
1651
+ if(v===undefined||v===null) return '';
1652
+ v=String(v).trim();
1653
+ if(!v) return '';
1654
+ var low=v.toLowerCase();
1655
+ if(low==='unknown'||low==='null') return '';
1656
+ return v;
1657
+ }
1658
+ function fromSummaryModels(summary){
1659
+ var counters=summary&&summary.counters&&summary.counters.models;
1660
+ if(!counters||typeof counters!=='object'||Array.isArray(counters)) return '';
1661
+ var entries=Object.entries(counters).filter(function(ent){
1662
+ return clean(ent[0]) && Number(ent[1]) > 0;
1663
+ });
1664
+ if(entries.length!==1) return '';
1665
+ return clean(entries[0][0]);
1666
+ }
1667
+ var m='';
1668
+ try{
1669
+ var summary=require('/results/pi-summary.json');
1670
+ m=clean(summary.selected_model)||clean(summary.model)||fromSummaryModels(summary);
1671
+ }catch{}
1672
+ if(!m){
1673
+ try{
1674
+ var lines=fs.readFileSync('$RAW_EVENTS','utf8').split('\n');
1675
+ for(var i=0;i<lines.length;i++){
1676
+ try{
1677
+ var e=JSON.parse(lines[i]);
1678
+ m=clean(e&&e.model);
1679
+ if(m) break;
1680
+ }catch{}
1681
+ }
1682
+ }catch{}
1683
+ }
1684
+ console.log(m||'unknown');
1685
+ " 2>/dev/null)"
1686
+ if [ "$ACTUAL_MODEL" = "unknown" ]; then
1687
+ emit_event "warning" "warning_type=model_attribution_missing" "detail=Unable to resolve model from pi-summary.json or raw events"
1688
+ fi
1689
+ fi
1690
+
1691
+
1692
+
1693
+ if [ "$KASEKI_DRY_RUN" != "1" ]; then
1694
+ if [ "$PI_EXIT" -eq 124 ]; then
1695
+ printf 'pi timeout after %ss (exit 124)\n' "$KASEKI_AGENT_TIMEOUT_SECONDS" | tee -a /results/pi-stderr.log >&2
1696
+ if [ "$STATUS" -eq 0 ]; then
1697
+ STATUS=124
1698
+ FAILED_COMMAND="pi coding agent timeout"
1699
+ emit_error_event "pi_timeout" "Coding agent exceeded timeout of $KASEKI_AGENT_TIMEOUT_SECONDS seconds" "exit"
1700
+ fi
1701
+ elif [ "$PI_EXIT" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
1702
+ STATUS="$PI_EXIT"
1703
+ FAILED_COMMAND="pi coding agent"
1704
+ emit_error_event "pi_agent_failed" "Coding agent exited with non-zero code: $PI_EXIT" "exit"
1705
+ fi
1706
+ fi
1707
+
1708
+ printf '\n==> collect agent diff\n'
1709
+ set_current_stage "collect agent diff"
1710
+ emit_progress "collect agent diff" "started"
1711
+ stage_start="$(date +%s)"
1712
+ collect_git_artifacts
1713
+ restore_disallowed_changes
1714
+ record_stage_timing "collect agent diff" 0 "$(($(date +%s) - stage_start))" "diff_nonempty=$DIFF_NONEMPTY"
1715
+ emit_progress "collect agent diff" "finished"
1716
+
1717
+ printf '\n==> quality checks\n'
1718
+ set_current_stage "quality checks"
1719
+ emit_progress "quality checks" "started"
1720
+ stage_start="$(date +%s)"
1721
+ diff_size="$(wc -c < /results/git.diff | tr -d ' ')"
1722
+ if [ "$diff_size" -gt "$KASEKI_MAX_DIFF_BYTES" ]; then
1723
+ QUALITY_EXIT=4
1724
+ QUALITY_FAILURE_REASON="max_diff_bytes: $diff_size bytes exceeds limit of $KASEKI_MAX_DIFF_BYTES bytes"
1725
+ printf 'git.diff is too large: %s bytes > %s bytes\n' "$diff_size" "$KASEKI_MAX_DIFF_BYTES" | tee -a /results/quality.log
1726
+ emit_event "quality_gate_rule_evaluated" "rule=max_diff_bytes" "passed=false" "actual=$diff_size" "limit=$KASEKI_MAX_DIFF_BYTES"
1727
+ else
1728
+ emit_event "quality_gate_rule_evaluated" "rule=max_diff_bytes" "passed=true" "actual=$diff_size" "limit=$KASEKI_MAX_DIFF_BYTES"
1729
+ fi
1730
+ emit_progress "quality checks" "finished with exit $QUALITY_EXIT"
1731
+
1732
+ # Build a safe regex from glob-style repo-relative allowlist patterns.
1733
+ allowlist_regex="$(build_allowlist_regex)"
1734
+ if [ -n "$allowlist_regex" ]; then
1735
+ while IFS= read -r changed_file || [ -n "$changed_file" ]; do
1736
+ [ -z "$changed_file" ] && continue
1737
+ if ! printf '%s\n' "$changed_file" | grep -Eq "^(${allowlist_regex})$"; then
1738
+ QUALITY_EXIT=5
1739
+ QUALITY_FAILURE_REASON="allowlist_check: file '$changed_file' not in allowlist"
1740
+ printf 'changed file outside allowlist: %s\n' "$changed_file" | tee -a /results/quality.log
1741
+ emit_event "quality_gate_rule_evaluated" "rule=allowlist_check" "passed=false" "file=$changed_file"
1742
+ else
1743
+ emit_event "quality_gate_rule_evaluated" "rule=allowlist_check" "passed=true" "file=$changed_file"
1744
+ fi
1745
+ done < /results/changed-files.txt
1746
+ fi
1747
+
1748
+ if [ -f package.json ] && node -e "const p=require('./package.json'); process.exit(p.scripts && (p.scripts.format || p.scripts['format:check']) ? 0 : 1)" 2>/dev/null; then
1749
+ format_command="$(node -e "const p=require('./package.json'); console.log(p.scripts['format:check'] ? 'npm run format:check' : 'npm run format -- --check')" 2>/dev/null)"
1750
+ printf '%s\n' "$format_command" >> /results/format-check-command.txt
1751
+ fi
1752
+ record_stage_timing "quality checks" "$QUALITY_EXIT" "$(($(date +%s) - stage_start))" "diff_size_bytes=$diff_size"
1753
+
1754
+ printf '\n==> validation\n'
1755
+ set_current_stage "validation"
1756
+ emit_progress "validation" "started"
1757
+ stage_start="$(date +%s)"
1758
+ if [ "$KASEKI_DRY_RUN" = "1" ]; then
1759
+ printf '🔄 DRY-RUN MODE: Validation commands would be executed (not running in dry-run mode):\n' | tee -a /results/validation.log
1760
+ IFS=';' read -r -a VALIDATION_COMMANDS <<< "$KASEKI_VALIDATION_COMMANDS"
1761
+ for command in "${VALIDATION_COMMANDS[@]}"; do
1762
+ trimmed="$(printf '%s' "$command" | sed 's/^ *//; s/ *$//')"
1763
+ [ -z "$trimmed" ] && continue
1764
+ printf ' - %s\n' "$trimmed" | tee -a /results/validation.log
1765
+ done
1766
+ VALIDATION_EXIT=0
1767
+ record_stage_timing "validation" "0" "$(($(date +%s) - stage_start))" "dry_run=true"
1768
+ elif [ -z "$KASEKI_VALIDATION_COMMANDS" ] || [ "$KASEKI_VALIDATION_COMMANDS" = "none" ]; then
1769
+ printf 'Validation skipped because KASEKI_VALIDATION_COMMANDS=%s.\n' "${KASEKI_VALIDATION_COMMANDS:-<empty>}" | tee -a /results/validation.log
1770
+ record_stage_timing "validation" 0 0 "skipped_by_config"
1771
+ elif [ "$QUALITY_EXIT" -ne 0 ]; then
1772
+ printf 'Validation skipped because quality gates failed with exit %s.\n' "$QUALITY_EXIT" | tee -a /results/validation.log
1773
+ VALIDATION_EXIT="$QUALITY_EXIT"
1774
+ if [ -z "$VALIDATION_FAILURE_REASON" ]; then
1775
+ VALIDATION_FAILURE_REASON="quality_gate_failed: $QUALITY_FAILURE_REASON"
1776
+ fi
1777
+ record_stage_timing "validation" "$QUALITY_EXIT" 0 "skipped_after_quality_failure"
1778
+ elif [ "$PI_EXIT" -ne 0 ] && [ "$KASEKI_VALIDATE_AFTER_AGENT_FAILURE" != "1" ]; then
1779
+ printf 'Validation skipped because pi coding agent failed with exit %s. Set KASEKI_VALIDATE_AFTER_AGENT_FAILURE=1 to run validation anyway.\n' "$PI_EXIT" | tee -a /results/validation.log
1780
+ record_stage_timing "validation" "$PI_EXIT" 0 "skipped_after_agent_failure"
1781
+ else
1782
+ # Checkpoint: Verify working directory exists before validation
1783
+ if ! [ -d /workspace/repo ]; then
1784
+ printf 'ERROR: Working directory /workspace/repo does not exist before validation\n' | tee -a /results/validation.log
1785
+ printf 'Current pwd: %s\n' "$(pwd 2>&1 || echo '<pwd failed>')" | tee -a /results/validation.log
1786
+ printf 'Filesystem state:\n' | tee -a /results/validation.log
1787
+ find /workspace -maxdepth 3 -type f 2>&1 | head -100 | tee -a /results/validation.log
1788
+ VALIDATION_EXIT=1
1789
+ VALIDATION_FAILED_COMMAND_DETAIL="Working directory /workspace/repo missing before validation"
1790
+ record_stage_timing "validation" "$VALIDATION_EXIT" "$(($(date +%s) - stage_start))" "directory_missing"
1791
+ else
1792
+ set +e
1793
+ IFS=';' read -r -a VALIDATION_COMMANDS <<< "$KASEKI_VALIDATION_COMMANDS"
1794
+ for command in "${VALIDATION_COMMANDS[@]}"; do
1795
+ trimmed="$(printf '%s' "$command" | sed 's/^ *//; s/ *$//')"
1796
+ [ -z "$trimmed" ] && continue
1797
+ validation_start="$(date +%s)"
1798
+ if missing_npm_script="$(missing_npm_script_for_validation_command "$trimmed")"; then
1799
+ validation_end="$(date +%s)"
1800
+ duration=$((validation_end - validation_start))
1801
+ record_skipped_validation_command "$trimmed" "$missing_npm_script" "$duration"
1802
+ emit_event "validation_command_skipped" "command=$trimmed" "reason=missing_npm_script" "script=$missing_npm_script" "duration_seconds=$duration"
1803
+ continue
1804
+ fi
1805
+ ((VALIDATION_COMMANDS_ATTEMPTED++))
1806
+ emit_event "validation_command_started" "command=$trimmed"
1807
+ {
1808
+ printf '\n==> %s\n' "$trimmed"
1809
+ unset OPENROUTER_API_KEY
1810
+ # Use non-login shell (bash -c) to avoid initialization issues in --read-only containers
1811
+ # Login shell (bash -l) sources /etc/profile and ~/.bashrc, which can fail with getcwd()
1812
+ # errors when running in constrained filesystem environments (read-only root, etc.)
1813
+ bash -c "$trimmed"
1814
+ command_exit=$?
1815
+ printf 'exit_code=%s\n' "$command_exit"
1816
+ exit "$command_exit"
1817
+ } 2>&1 | tee -a /results/validation.log
1818
+ command_exit="${PIPESTATUS[0]}"
1819
+ validation_end="$(date +%s)"
1820
+ duration=$((validation_end - validation_start))
1821
+ printf '%s\t%s\t%s\n' "$trimmed" "$command_exit" "$duration" >> "$VALIDATION_TIMINGS_FILE"
1822
+ emit_event "validation_command_finished" "command=$trimmed" "exit_code=$command_exit" "duration_seconds=$duration"
1823
+ if [ "$command_exit" -ne 0 ] && [ "$VALIDATION_EXIT" -eq 0 ]; then
1824
+ VALIDATION_EXIT="$command_exit"
1825
+ VALIDATION_FAILED_COMMAND_DETAIL="first failing command was \"$trimmed\" with exit $command_exit"
1826
+ VALIDATION_FAILURE_REASON="validation_command_failed: $trimmed (exit $command_exit)"
1827
+ # Enhanced diagnostics for getcwd-type errors
1828
+ if grep -q 'getcwd\|No such file or directory\|cannot access parent directories' /results/validation.log; then
1829
+ {
1830
+ printf '\n[DIAGNOSTICS] Validation command failed with directory access error:\n'
1831
+ printf 'Working directory status:\n'
1832
+ printf ' Current pwd: %s\n' "$(pwd 2>&1 || echo '<pwd failed>')"
1833
+ printf ' /workspace/repo exists: %s\n' "$([ -d /workspace/repo ] && echo 'yes' || echo 'no')"
1834
+ if [ -L /workspace/repo/node_modules ]; then
1835
+ printf ' node_modules is symlink → %s\n' "$(readlink /workspace/repo/node_modules 2>&1 || echo '<readlink failed>')"
1836
+ fi
1837
+ printf 'Last 20 lines of validation log:\n'
1838
+ tail -20 /results/validation.log
1839
+ } | tee -a /results/quality.log
1840
+ fi
1841
+ # Fail-fast: if enabled, stop validation loop at first failure
1842
+ if [ "$KASEKI_VALIDATION_FAIL_FAST" -eq 1 ]; then
1843
+ VALIDATION_STOPPED_EARLY=true
1844
+ printf 'Validation stopped at first failure (fail-fast mode enabled).\n' | tee -a /results/validation.log
1845
+ break
1846
+ fi
1847
+ fi
1848
+ done
1849
+ if [ -n "$VALIDATION_FAILED_COMMAND_DETAIL" ]; then
1850
+ printf 'Validation failed: %s\n' "$VALIDATION_FAILED_COMMAND_DETAIL" | tee -a /results/validation.log
1851
+ fi
1852
+ set -e
1853
+ fi
1854
+ record_stage_timing "validation" "$VALIDATION_EXIT" "$(($(date +%s) - stage_start))" ""
1855
+ fi
1856
+ emit_progress "validation" "finished with exit $VALIDATION_EXIT"
1857
+
1858
+ # Check validation-phase allowlist (if configured)
1859
+ if [ "$VALIDATION_EXIT" -eq 0 ]; then
1860
+ collect_git_artifacts
1861
+ if ! check_validation_allowlist; then
1862
+ : # Exit code already set in check_validation_allowlist
1863
+ fi
1864
+ fi
1865
+
1866
+ printf '\n==> secret scan\n'
1867
+ set_current_stage "secret scan"
1868
+ emit_progress "secret scan" "started"
1869
+ stage_start="$(date +%s)"
1870
+ : > /results/secret-scan.log
1871
+ if [ "$KASEKI_DRY_RUN" = "1" ]; then
1872
+ printf '🔄 DRY-RUN MODE: Skipping secret scan (no artifacts to scan)\n' | tee -a /results/secret-scan.log
1873
+ SECRET_SCAN_EXIT=0
1874
+ record_stage_timing "secret scan" "0" "$(($(date +%s) - stage_start))" "dry_run=true"
1875
+ else
1876
+ if grep -R -n -E 'sk-or-[A-Za-z0-9_-]{20,}' /results /workspace/repo/.git /workspace/repo/src /workspace/repo/tests 2>/dev/null | grep -v '/secret-scan.log:' > /results/secret-scan.log; then
1877
+ SECRET_SCAN_EXIT=6
1878
+ fi
1879
+ record_stage_timing "secret scan" "$SECRET_SCAN_EXIT" "$(($(date +%s) - stage_start))" ""
1880
+ fi
1881
+ emit_progress "secret scan" "finished with exit $SECRET_SCAN_EXIT"
1882
+
1883
+ build_github_skip_reasons() {
1884
+ GITHUB_SKIP_REASONS=()
1885
+ [ "$GITHUB_APP_ENABLED" != "1" ] && GITHUB_SKIP_REASONS+=("github_app_disabled")
1886
+ [ "$PI_EXIT" -ne 0 ] && GITHUB_SKIP_REASONS+=("agent_failed")
1887
+ [ "$VALIDATION_EXIT" -ne 0 ] && GITHUB_SKIP_REASONS+=("validation_failed")
1888
+ [ "$QUALITY_EXIT" -ne 0 ] && GITHUB_SKIP_REASONS+=("quality_failed")
1889
+ [ "$SECRET_SCAN_EXIT" -ne 0 ] && GITHUB_SKIP_REASONS+=("secret_scan_failed")
1890
+ [ "$DIFF_NONEMPTY" != "true" ] && GITHUB_SKIP_REASONS+=("empty_diff")
1891
+ }
1892
+
1893
+ printf '\n==> github operations\n'
1894
+ set_current_stage "github operations"
1895
+ emit_progress "github operations" "started"
1896
+ stage_start="$(date +%s)"
1897
+ : > /results/git-push.log
1898
+ build_github_skip_reasons
1899
+ if [ "${#GITHUB_SKIP_REASONS[@]}" -eq 0 ]; then
1900
+ if [ -r /run/secrets/github_app_id ] && [ -r /run/secrets/github_app_client_id ] && [ -r /run/secrets/github_app_private_key ]; then
1901
+ run_github_operations
1902
+ else
1903
+ GITHUB_SKIP_REASONS+=("github_app_secrets_missing")
1904
+ printf -- 'GitHub operations: skipped (reasons: %s)\n' "$(IFS=,; printf '%s' "${GITHUB_SKIP_REASONS[*]}")" | tee -a /results/git-push.log >&2
1905
+ emit_progress "github operations" "skipped: $(IFS=,; printf '%s' "${GITHUB_SKIP_REASONS[*]}")"
1906
+ GITHUB_PUSH_EXIT=7
1907
+ fi
1908
+ else
1909
+ printf -- 'GitHub operations: skipped (reasons: %s; agent %s, validation %s, quality %s, secret_scan %s, diff %s, github_enabled %s)\n' \
1910
+ "$(IFS=,; printf '%s' "${GITHUB_SKIP_REASONS[*]}")" \
1911
+ "$([ "$PI_EXIT" -eq 0 ] && printf 'passed' || printf 'failed')" \
1912
+ "$([ "$VALIDATION_EXIT" -eq 0 ] && printf 'passed' || printf 'failed')" \
1913
+ "$([ "$QUALITY_EXIT" -eq 0 ] && printf 'passed' || printf 'failed')" \
1914
+ "$([ "$SECRET_SCAN_EXIT" -eq 0 ] && printf 'passed' || printf 'failed')" \
1915
+ "$DIFF_NONEMPTY" \
1916
+ "$GITHUB_APP_ENABLED" | tee -a /results/git-push.log
1917
+ emit_progress "github operations" "skipped: $(IFS=,; printf '%s' "${GITHUB_SKIP_REASONS[*]}")"
1918
+ fi
1919
+ if [ "$GITHUB_APP_ENABLED" = "1" ]; then
1920
+ emit_progress "github operations" "finished with push exit $GITHUB_PUSH_EXIT and pr exit $GITHUB_PR_EXIT"
1921
+ fi
1922
+ record_stage_timing "github operations" "$GITHUB_PUSH_EXIT" "$(($(date +%s) - stage_start))" "pr_exit=$GITHUB_PR_EXIT enabled=$GITHUB_APP_ENABLED"
1923
+
1924
+ if [ "$VALIDATION_EXIT" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
1925
+ STATUS="$VALIDATION_EXIT"
1926
+ FAILED_COMMAND="validation"
1927
+ if [ -n "$VALIDATION_FAILED_COMMAND_DETAIL" ]; then
1928
+ emit_error_event "validation_failed" "Validation failed: $VALIDATION_FAILED_COMMAND_DETAIL" "exit"
1929
+ else
1930
+ emit_error_event "validation_failed" "Validation command exited with code $VALIDATION_EXIT" "exit"
1931
+ fi
1932
+ fi
1933
+
1934
+ if [ "$QUALITY_EXIT" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
1935
+ STATUS="$QUALITY_EXIT"
1936
+ FAILED_COMMAND="quality checks"
1937
+ emit_error_event "quality_gate_failed" "Quality gate rule failed (exit code $QUALITY_EXIT)" "exit"
1938
+ fi
1939
+
1940
+ if [ "$SECRET_SCAN_EXIT" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
1941
+ STATUS="$SECRET_SCAN_EXIT"
1942
+ FAILED_COMMAND="secret scan"
1943
+ emit_error_event "secret_scan_failed" "Secret scan detected potential credential leak" "exit"
1944
+ fi
1945
+
1946
+ if [ "$GITHUB_PUSH_EXIT" -ne 0 ] && [ "$STATUS" -eq 0 ]; then
1947
+ STATUS="$GITHUB_PUSH_EXIT"
1948
+ FAILED_COMMAND="github push"
1949
+ emit_error_event "github_operation_failed" "GitHub push or PR creation failed (exit code $GITHUB_PUSH_EXIT)" "exit"
1950
+ fi
1951
+
1952
+ if [ "$DIFF_NONEMPTY" != "true" ] &&
1953
+ [ "$STATUS" -eq 0 ] &&
1954
+ [ "$KASEKI_ALLOW_EMPTY_DIFF" != "1" ] &&
1955
+ [ "$KASEKI_TASK_MODE" != "inspect" ]; then
1956
+ STATUS=3
1957
+ FAILED_COMMAND="empty git diff"
1958
+ emit_error_event "empty_diff" "Agent produced no changes to the repository" "exit"
1959
+ fi
1960
+
1961
+ set_current_stage "complete"