@askexenow/exe-os 0.9.348 → 0.9.350

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 (306) hide show
  1. package/deploy/compose/docker-compose.yml +15 -6
  2. package/deploy/compose/erp-nginx/nginx.conf +17 -10
  3. package/deploy/stack-manifests/v0.9.30-apps.json +81 -0
  4. package/deploy/stack-manifests/v0.9.json +111 -2
  5. package/dist/{active-agent-PX6KAEEQ.js → active-agent-PMCD3LFW.js} +3 -3
  6. package/dist/{active-agent-QVLZKWR2.js → active-agent-WQYB3JQY.js} +3 -3
  7. package/dist/{agentic-ontology-6MEXY5L7.js → agentic-ontology-HTU5EMEQ.js} +1 -1
  8. package/dist/{backfill-metadata-MCFBGWKX.js → backfill-metadata-QUUXE2TL.js} +5 -5
  9. package/dist/{backup-verify-RGB3KKD4.js → backup-verify-YJGUNZWJ.js} +1 -1
  10. package/dist/{behaviors-6GI6VND2.js → behaviors-P5FORAYU.js} +6 -6
  11. package/dist/bin/agentic-ontology-backfill.js +6 -6
  12. package/dist/bin/agentic-reflection-backfill.js +7 -7
  13. package/dist/bin/agentic-semantic-label.js +6 -6
  14. package/dist/bin/backfill-conversations.js +5 -5
  15. package/dist/bin/backfill-responses.js +5 -5
  16. package/dist/bin/backfill-vectors.js +6 -6
  17. package/dist/bin/bulk-sync-postgres.js +11 -11
  18. package/dist/bin/cc-doctor.js +4 -4
  19. package/dist/bin/cleanup-stale-review-tasks.js +12 -12
  20. package/dist/bin/cli.js +18 -18
  21. package/dist/bin/deferred-daemon-restart.js +68 -68
  22. package/dist/bin/exe-agent-config.js +2 -2
  23. package/dist/bin/exe-agent.js +5 -5
  24. package/dist/bin/exe-assign.js +6 -6
  25. package/dist/bin/exe-boot.js +26 -26
  26. package/dist/bin/exe-call.js +5 -5
  27. package/dist/bin/exe-cloud.js +11 -11
  28. package/dist/bin/exe-config-dump.js +2 -2
  29. package/dist/bin/exe-dispatch.js +12 -12
  30. package/dist/bin/exe-doctor.js +4 -4
  31. package/dist/bin/exe-export-behaviors.js +8 -8
  32. package/dist/bin/exe-forget.js +7 -7
  33. package/dist/bin/exe-gateway.js +7 -7
  34. package/dist/bin/exe-healthcheck.js +4 -4
  35. package/dist/bin/exe-heartbeat.js +14 -14
  36. package/dist/bin/exe-kill.js +16 -16
  37. package/dist/bin/exe-launch-agent.js +23 -23
  38. package/dist/bin/exe-memory-reads.js +5 -5
  39. package/dist/bin/exe-new-employee.js +9 -9
  40. package/dist/bin/exe-os-mcp-proxy.js +3 -3
  41. package/dist/bin/exe-pending-messages.js +13 -13
  42. package/dist/bin/exe-pending-notifications.js +12 -12
  43. package/dist/bin/exe-pending-reviews.js +12 -12
  44. package/dist/bin/exe-rename.js +5 -5
  45. package/dist/bin/exe-review.js +15 -15
  46. package/dist/bin/exe-search.js +6 -6
  47. package/dist/bin/exe-session-cleanup.js +16 -16
  48. package/dist/bin/exe-settings.js +11 -11
  49. package/dist/bin/exe-start-codex.js +13 -13
  50. package/dist/bin/exe-start-opencode.js +9 -9
  51. package/dist/bin/exe-status.js +13 -13
  52. package/dist/bin/exe-support.js +3 -3
  53. package/dist/bin/exe-team.js +3 -3
  54. package/dist/bin/git-sweep.js +13 -13
  55. package/dist/bin/graph-backfill.js +6 -6
  56. package/dist/bin/graph-export.js +6 -6
  57. package/dist/bin/import-history.js +9 -9
  58. package/dist/bin/install.js +70 -70
  59. package/dist/bin/intercom-check.js +4 -4
  60. package/dist/bin/mcp-sessions.js +2 -2
  61. package/dist/bin/orchestration-metrics.js +4 -4
  62. package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
  63. package/dist/bin/postgres-agentic-semantic-backfill.js +1 -1
  64. package/dist/bin/pre-publish.js +1 -1
  65. package/dist/bin/reconcile-backlog.js +2 -2
  66. package/dist/bin/registry-proxy.js +1 -1
  67. package/dist/bin/scan-tasks.js +12 -12
  68. package/dist/bin/setup.js +1 -1
  69. package/dist/bin/shard-migrate.js +5 -5
  70. package/dist/bin/stack-update.js +4 -4
  71. package/dist/bin/vps-health-gate.js +1 -1
  72. package/dist/{boot-generation-FB2J53IG.js → boot-generation-XNR2CBMX.js} +3 -3
  73. package/dist/{boot-review-sections-QF5MZGJI.js → boot-review-sections-ZWXRZ6YX.js} +11 -11
  74. package/dist/{capability-cards-YADEM2KN.js → capability-cards-IOGHQJ6Y.js} +2 -2
  75. package/dist/{capacity-monitor-CG2IGONS.js → capacity-monitor-3YN75QXX.js} +13 -13
  76. package/dist/{catchup-brief-QFVNMTU5.js → catchup-brief-HPB5CL7R.js} +16 -16
  77. package/dist/{chunk-YAIFZXOM.js → chunk-4EEML23J.js} +5 -5
  78. package/dist/{chunk-UTQWVRU6.js → chunk-4Z74A4A6.js} +5 -5
  79. package/dist/{chunk-27REQ7KR.js → chunk-53HZ23T5.js} +1 -1
  80. package/dist/{chunk-KZUU6MNF.js → chunk-55OFK4ZY.js} +1 -1
  81. package/dist/{chunk-DJYJDK53.js → chunk-5J4GLPTU.js} +7 -7
  82. package/dist/{chunk-WUMWXU6B.js → chunk-5SJXJSAI.js} +1 -1
  83. package/dist/{chunk-MH5HWMVH.js → chunk-6ON3JHGE.js} +1 -1
  84. package/dist/{chunk-A42356G7.js → chunk-6QHQCY3N.js} +1 -1
  85. package/dist/{chunk-YTPCJLEL.js → chunk-AAVVHD4C.js} +1 -1
  86. package/dist/{chunk-BUUGFQHL.js → chunk-ABIZWPSK.js} +2 -2
  87. package/dist/{chunk-AUILJKSM.js → chunk-AGCGXXIB.js} +2 -2
  88. package/dist/{chunk-6WFXNQDJ.js → chunk-AUEMXOJT.js} +1 -1
  89. package/dist/{chunk-FENYNW52.js → chunk-BAWZEI77.js} +1 -1
  90. package/dist/{chunk-MMXIE6HV.js → chunk-BHDOPDPY.js} +7 -1
  91. package/dist/{chunk-M7PBRIRH.js → chunk-CYBDUQDO.js} +1 -1
  92. package/dist/{chunk-PBLDPW4U.js → chunk-D2UZ35PQ.js} +2 -2
  93. package/dist/{chunk-KIMAQNSM.js → chunk-DKOWIBBM.js} +3 -3
  94. package/dist/{chunk-JORPYG4O.js → chunk-DXYHU5D6.js} +1 -1
  95. package/dist/{chunk-FPK753IZ.js → chunk-EDEVSNAF.js} +9 -9
  96. package/dist/{chunk-HL24OHHS.js → chunk-EGYDW57S.js} +9 -9
  97. package/dist/{chunk-WWT3E7DO.js → chunk-ENIFHOMF.js} +3 -3
  98. package/dist/{chunk-EFHGK6JP.js → chunk-FGII4IO7.js} +1 -1
  99. package/dist/{chunk-RBRGSOK5.js → chunk-FHOLN7BD.js} +1 -1
  100. package/dist/{chunk-7WQ3WHPW.js → chunk-FUOXMZUZ.js} +138 -128
  101. package/dist/{chunk-X6VEQ7MC.js → chunk-GIEIWDC2.js} +1 -1
  102. package/dist/{chunk-SLH2743O.js → chunk-GINAJJQR.js} +3 -3
  103. package/dist/{chunk-5KCC26FB.js → chunk-GIZLVGOM.js} +2 -2
  104. package/dist/{chunk-GJ6KIHOM.js → chunk-GWO54VKD.js} +4 -4
  105. package/dist/{chunk-FE6WBI2M.js → chunk-GXMY5ERQ.js} +1 -1
  106. package/dist/chunk-HCRUMO4B.js +58 -0
  107. package/dist/{chunk-HYZE3MNS.js → chunk-HUCMSRIF.js} +1 -1
  108. package/dist/{chunk-23O6LHII.js → chunk-I6UOIBCG.js} +1 -1
  109. package/dist/{chunk-RFRUDXOI.js → chunk-IC4O376V.js} +2 -2
  110. package/dist/{chunk-AXNUMBLO.js → chunk-IXDSQ2VO.js} +3 -3
  111. package/dist/{chunk-H3TA23PH.js → chunk-IXJGEZBS.js} +4 -4
  112. package/dist/{chunk-SQ4WACH4.js → chunk-JFGX7LHH.js} +1 -1
  113. package/dist/{chunk-MMOONM6D.js → chunk-JO6RMHMW.js} +1 -1
  114. package/dist/{chunk-DAODOSH4.js → chunk-KBFHODXV.js} +1 -1
  115. package/dist/{chunk-5RPCWLXE.js → chunk-KES3SNOH.js} +1 -1
  116. package/dist/{chunk-Z52QQB4Z.js → chunk-KMWRDB5U.js} +2 -2
  117. package/dist/{chunk-6JQTSKDR.js → chunk-KZ7BANUM.js} +1 -1
  118. package/dist/{chunk-WL3IQJKN.js → chunk-L3DKZC4R.js} +3 -3
  119. package/dist/{chunk-NY6DU5RB.js → chunk-L4SGSV4M.js} +1 -1
  120. package/dist/{chunk-GA655CUD.js → chunk-LFDW43MP.js} +2 -2
  121. package/dist/{chunk-ZXSFUR77.js → chunk-LOYK2BRY.js} +15 -15
  122. package/dist/{chunk-H5QNVWU4.js → chunk-LVG5ATTU.js} +10 -10
  123. package/dist/{chunk-AQ24P2O7.js → chunk-LVYIM3V4.js} +4 -4
  124. package/dist/{chunk-PAXAF2UY.js → chunk-LYMEWCFD.js} +24 -8
  125. package/dist/{chunk-OBWIVRD6.js → chunk-MFRJRJ6U.js} +1 -1
  126. package/dist/{chunk-Y3JXLIZV.js → chunk-MFX2RWJZ.js} +3 -3
  127. package/dist/{chunk-AMC2OPYP.js → chunk-MGFG7V3M.js} +1 -1
  128. package/dist/{chunk-A43SIGAO.js → chunk-NXC3Z7VI.js} +3 -3
  129. package/dist/{chunk-CCOKYSJA.js → chunk-OPDZ62TW.js} +3 -3
  130. package/dist/{chunk-5C46Y4AK.js → chunk-PBLFSNIA.js} +1 -1
  131. package/dist/{chunk-6QRS3IZF.js → chunk-PELEK2IT.js} +1 -1
  132. package/dist/{chunk-TF5E6O5Q.js → chunk-QABQ7C4C.js} +1 -1
  133. package/dist/{chunk-MGMXYI7Y.js → chunk-QIALP626.js} +2 -2
  134. package/dist/{chunk-FFCPSPNK.js → chunk-RYKHPC3Y.js} +1 -1
  135. package/dist/{chunk-MVZLF27P.js → chunk-SDNIBE22.js} +2 -2
  136. package/dist/{chunk-AFY4V4N3.js → chunk-SLROFI6M.js} +1 -1
  137. package/dist/{chunk-2AD2GAPC.js → chunk-SUZOENOA.js} +1 -1
  138. package/dist/{chunk-O522M4YZ.js → chunk-T346KHGF.js} +1 -1
  139. package/dist/{chunk-TLWYOMSS.js → chunk-T4VUDUZF.js} +3 -3
  140. package/dist/{chunk-43RTUMUT.js → chunk-TGGOWOLG.js} +1 -1
  141. package/dist/{chunk-E3BNUQHW.js → chunk-TGU2IB6P.js} +2 -2
  142. package/dist/{chunk-WZJ3VYRJ.js → chunk-THDM6HMQ.js} +3 -3
  143. package/dist/{chunk-YM6GJM2F.js → chunk-TXGOFNXD.js} +4 -4
  144. package/dist/{chunk-UAKGNQWE.js → chunk-U7HUQPVS.js} +1 -1
  145. package/dist/{chunk-GLOWJP5H.js → chunk-UCO3XIL5.js} +2 -2
  146. package/dist/{chunk-JW5XS35A.js → chunk-UXOCABN4.js} +4 -4
  147. package/dist/{chunk-L36RX54F.js → chunk-UZ6AA52H.js} +6 -6
  148. package/dist/{chunk-2WJYLXPF.js → chunk-VDEBUOPB.js} +1 -1
  149. package/dist/{chunk-GKNWKQFY.js → chunk-VDKI6PIZ.js} +1 -1
  150. package/dist/{chunk-6GSYN262.js → chunk-VF6HK2ID.js} +1 -1
  151. package/dist/{chunk-Z4UKLRML.js → chunk-VU2MZ7BP.js} +18 -6
  152. package/dist/{chunk-SMQ642D3.js → chunk-WAVZDYXS.js} +4 -4
  153. package/dist/{chunk-QNKR7WEL.js → chunk-WOGK3FIE.js} +3 -3
  154. package/dist/{chunk-DXMTJJP3.js → chunk-YJBBW6OT.js} +4 -4
  155. package/dist/{chunk-MOZY4IQO.js → chunk-Z4F6ZHDV.js} +1 -1
  156. package/dist/{chunk-BOQUXG7N.js → chunk-ZMDTRDTD.js} +3 -3
  157. package/dist/{cleanup-graph-garbage-XTSBQXD6.js → cleanup-graph-garbage-DQDJK6L7.js} +7 -7
  158. package/dist/{cloud-sync-shutdown-A2QB74YM.js → cloud-sync-shutdown-UHICWZ2B.js} +2 -2
  159. package/dist/{co-activation-S2FVGMQP.js → co-activation-PIRKOOPD.js} +2 -2
  160. package/dist/{co-occurrence-VQB6U5ME.js → co-occurrence-PO4EPN5C.js} +5 -5
  161. package/dist/{conversation-entity-extractor-YF3ZLTEJ.js → conversation-entity-extractor-3UUAYXS6.js} +3 -3
  162. package/dist/{crdt-sync-2SGCDWC3.js → crdt-sync-ZL3GKAQW.js} +1 -1
  163. package/dist/{crm-webhook-P64SXEHD.js → crm-webhook-BAF3VM34.js} +2 -2
  164. package/dist/{cto-delegation-gate-6KLGT2LB.js → cto-delegation-gate-AQE2M4HU.js} +11 -11
  165. package/dist/{daemon-orchestration-JQGKEWPD.js → daemon-orchestration-WHGMXCZW.js} +16 -16
  166. package/dist/{doc-graph-extractor-DCJMATVZ.js → doc-graph-extractor-IFLEVHUJ.js} +5 -5
  167. package/dist/{documents-7EPH3VPE.js → documents-2BXH6YE4.js} +6 -6
  168. package/dist/{dreaming-O4P2W5E5.js → dreaming-AQI4F7J4.js} +15 -15
  169. package/dist/{exe-drift-ED7FBS2C.js → exe-drift-OBXBE3BJ.js} +3 -3
  170. package/dist/{exe-export-YJI4USGZ.js → exe-export-VKRI6DK4.js} +8 -8
  171. package/dist/{exe-import-SUTAYKUW.js → exe-import-EH74J6HM.js} +8 -8
  172. package/dist/{exe-key-CRSDAOZD.js → exe-key-GYNPBQDF.js} +68 -68
  173. package/dist/{exe-snapshot-KB4YYDCA.js → exe-snapshot-CWQBLHZN.js} +16 -16
  174. package/dist/{fast-db-init-3K2PV6GJ.js → fast-db-init-5HF3TLAF.js} +1 -1
  175. package/dist/{founder-context-JHHFM4GF.js → founder-context-PWEQQF43.js} +2 -2
  176. package/dist/gateway/index.js +8 -8
  177. package/dist/{git-staleness-YPHGWE3B.js → git-staleness-ZQEAOK6X.js} +2 -2
  178. package/dist/{global-procedures-PEWDBSPJ.js → global-procedures-LBAA7VLL.js} +4 -4
  179. package/dist/{graph-auto-extract-7OJYYV4G.js → graph-auto-extract-XOJSREN4.js} +5 -5
  180. package/dist/{graph-rag-YFITUAVO.js → graph-rag-P3PSYW7P.js} +3 -3
  181. package/dist/{hook-prefetch-O6ZCZCTX.js → hook-prefetch-HGHMON76.js} +12 -12
  182. package/dist/hooks/bug-report-worker.js +14 -14
  183. package/dist/hooks/codex-stop-task-finalizer.js +15 -15
  184. package/dist/hooks/commit-complete.js +14 -14
  185. package/dist/hooks/error-recall.js +7 -7
  186. package/dist/hooks/exe-heartbeat-hook.js +3 -3
  187. package/dist/hooks/ingest.js +6 -6
  188. package/dist/hooks/instructions-loaded.js +4 -4
  189. package/dist/hooks/manifest.json +19 -19
  190. package/dist/hooks/notification.js +4 -4
  191. package/dist/hooks/post-compact.js +13 -13
  192. package/dist/hooks/post-tool-combined.js +9 -9
  193. package/dist/hooks/pre-compact.js +14 -14
  194. package/dist/hooks/pre-tool-use.js +19 -19
  195. package/dist/hooks/prompt-submit.js +26 -26
  196. package/dist/hooks/session-end.js +18 -18
  197. package/dist/hooks/session-start.js +13 -13
  198. package/dist/hooks/stop.js +17 -17
  199. package/dist/hooks/subagent-stop.js +15 -15
  200. package/dist/hooks/summary-worker.js +18 -18
  201. package/dist/index.js +21 -21
  202. package/dist/{installer-CY5PN7TN.js → installer-DWOXEACJ.js} +5 -5
  203. package/dist/{installer-N4QXGINR.js → installer-LDICNL4N.js} +5 -5
  204. package/dist/{installer-3ONNP5JV.js → installer-WZLGX2W6.js} +5 -5
  205. package/dist/{intercom-queue-A52WQOCD.js → intercom-queue-S3XGJ632.js} +11 -11
  206. package/dist/lib/cloud-sync.js +11 -11
  207. package/dist/lib/consolidation.js +7 -7
  208. package/dist/lib/database.js +2 -2
  209. package/dist/lib/db-backup.js +1 -1
  210. package/dist/lib/db-daemon-client.js +5 -1
  211. package/dist/lib/db-write-worker.js +2 -2
  212. package/dist/lib/db.js +2 -2
  213. package/dist/lib/device-registry.js +1 -1
  214. package/dist/lib/employee-templates.js +5 -5
  215. package/dist/lib/employees.js +2 -2
  216. package/dist/lib/exe-daemon.js +55 -55
  217. package/dist/lib/hybrid-search.js +6 -6
  218. package/dist/lib/identity.js +2 -2
  219. package/dist/lib/license.js +2 -2
  220. package/dist/lib/messaging.js +12 -12
  221. package/dist/lib/registry-proxy.js +1 -1
  222. package/dist/lib/reminders.js +3 -3
  223. package/dist/lib/schedules.js +6 -6
  224. package/dist/lib/session-registry.js +4 -4
  225. package/dist/lib/skill-learning.js +7 -7
  226. package/dist/lib/store.js +5 -5
  227. package/dist/lib/task-router.js +3 -3
  228. package/dist/lib/tasks.js +13 -13
  229. package/dist/lib/tmux-routing.js +11 -11
  230. package/dist/lib/token-spend.js +3 -3
  231. package/dist/{license-gate-ZRORTAQ2.js → license-gate-H3AEDCEX.js} +3 -3
  232. package/dist/{license-sync-CQHBDUQE.js → license-sync-FBYT75E3.js} +2 -2
  233. package/dist/mcp/register-tools.js +67 -67
  234. package/dist/mcp/server.js +68 -68
  235. package/dist/mcp/tools/complete-reminder.js +4 -4
  236. package/dist/mcp/tools/create-reminder.js +4 -4
  237. package/dist/mcp/tools/create-task.js +15 -15
  238. package/dist/mcp/tools/deactivate-behavior.js +8 -8
  239. package/dist/mcp/tools/list-reminders.js +4 -4
  240. package/dist/mcp/tools/list-tasks.js +15 -15
  241. package/dist/mcp/tools/send-message.js +14 -14
  242. package/dist/mcp/tools/update-task.js +14 -14
  243. package/dist/{mcp-http-config-2A6HKQP5.js → mcp-http-config-XE5KEKOR.js} +3 -3
  244. package/dist/{mcp-session-identity-OPRX32H5.js → mcp-session-identity-QA4ZXYCY.js} +11 -11
  245. package/dist/{memory-cards-GFO2CSPV.js → memory-cards-GHAU5A4O.js} +5 -5
  246. package/dist/{memory-graph-extractor-2Z72HN5P.js → memory-graph-extractor-P7B2V6CN.js} +6 -6
  247. package/dist/{memory-poisoning-defense-WZJ52KXT.js → memory-poisoning-defense-OY4EJES5.js} +2 -2
  248. package/dist/{memory-pressure-watchdog-IG7VRFIX.js → memory-pressure-watchdog-ZGA5GAUE.js} +12 -12
  249. package/dist/{memory-reflection-LTRHDGUL.js → memory-reflection-5NAOS2OJ.js} +2 -2
  250. package/dist/{notification-ledger-KSCPGLQG.js → notification-ledger-LCG3TRSE.js} +12 -12
  251. package/dist/{notifications-YKHZPCXU.js → notifications-YX43A36Z.js} +11 -11
  252. package/dist/{orchestration-events-HAFDPTLV.js → orchestration-events-EXQRGVEJ.js} +3 -3
  253. package/dist/{orchestrator-4MZEQULR.js → orchestrator-3OO5FV6O.js} +13 -13
  254. package/dist/{pipeline-router-RSRJPZNI.js → pipeline-router-U6U3M5DB.js} +3 -3
  255. package/dist/{plan-limits-5QYYPIPT.js → plan-limits-FNYZOE7J.js} +5 -5
  256. package/dist/{platform-procedures-DZ6YABEK.js → platform-procedures-NBKHO33A.js} +1 -1
  257. package/dist/{projection-worker-SKAMSZJF.js → projection-worker-HIXCZRPD.js} +2 -2
  258. package/dist/{prospective-memory-V7LFCPU4.js → prospective-memory-WQJJB2PS.js} +2 -2
  259. package/dist/{reranker-HXMJ7SKW.js → reranker-GJEC7PVT.js} +1 -1
  260. package/dist/{retrieval-health-4NCBHUUS.js → retrieval-health-IP74ZJ3D.js} +1 -1
  261. package/dist/{review-polling-KQUALBEU.js → review-polling-AXQWGH4M.js} +12 -12
  262. package/dist/runtime/index.js +15 -15
  263. package/dist/{session-events-3KL735DZ.js → session-events-POBJERCM.js} +12 -12
  264. package/dist/{session-kill-telemetry-KVRGLYVI.js → session-kill-telemetry-WRT5UFGM.js} +12 -12
  265. package/dist/{session-scope-LBG6DT5O.js → session-scope-J4JJHTOC.js} +11 -11
  266. package/dist/{setup-wizard-UL3AKI6E.js → setup-wizard-TEBZRHIB.js} +1 -1
  267. package/dist/{skill-refinement-6HPISXQC.js → skill-refinement-E2X3FHGX.js} +3 -3
  268. package/dist/{stack-promote-RSOWO6EY.js → stack-promote-OQWEY3RN.js} +13 -3
  269. package/dist/{stack-release-HGFFFCGC.js → stack-release-NUITUMB4.js} +464 -169
  270. package/dist/{stack-update-WBDTJJEF.js → stack-update-5XFOBX3A.js} +3 -3
  271. package/dist/{steward-gate-CWPD4HEK.js → steward-gate-ODMUO6TD.js} +3 -3
  272. package/dist/{support-inbox-HRIHHKPU.js → support-inbox-HRU4TVLN.js} +3 -3
  273. package/dist/{task-enforcement-OAOL7XFL.js → task-enforcement-DTQ5QMDD.js} +12 -12
  274. package/dist/{task-scope-PSVAE3EM.js → task-scope-3F3MSGSX.js} +11 -11
  275. package/dist/{tasks-crud-TLELWWUL.js → tasks-crud-DBQFOYOX.js} +11 -11
  276. package/dist/{tasks-notify-7Q2XTDZ7.js → tasks-notify-X7AJMSX6.js} +12 -12
  277. package/dist/{tasks-review-S6QB5KGS.js → tasks-review-QQXKYQ52.js} +11 -11
  278. package/dist/{telemetry-upload-LTL5SW24.js → telemetry-upload-3KLDXO4Z.js} +7 -7
  279. package/dist/{token-budget-SK32GKFZ.js → token-budget-T3L6OHAN.js} +3 -3
  280. package/dist/{tool-telemetry-QJGJAHOC.js → tool-telemetry-TKENX37V.js} +1 -1
  281. package/dist/tui/App.js +20 -20
  282. package/dist/{tui-data-3JSV56ME.js → tui-data-4ITCYY3G.js} +11 -11
  283. package/dist/{update-support-LMPGWJVG.js → update-support-TT2ESH4G.js} +9 -9
  284. package/dist/{work-state-heartbeat-MQQOCU54.js → work-state-heartbeat-UAVYBQOK.js} +1 -1
  285. package/dist/{worker-gate-P2O64XMH.js → worker-gate-GK46QYHU.js} +1 -1
  286. package/dist/{workflow-engine-BLLQKA3O.js → workflow-engine-YYRUTNCM.js} +2 -2
  287. package/dist/{worktree-FJZXHDCJ.js → worktree-5LHP677X.js} +4 -4
  288. package/dist/{worktree-sweep-SW4AG6JR.js → worktree-sweep-TVQVQTVD.js} +5 -5
  289. package/package.json +3 -2
  290. package/release-notes.json +43 -51
  291. /package/dist/{chunk-GFIWADEH.js → chunk-2BJYY4SK.js} +0 -0
  292. /package/dist/{chunk-GPZQZTJB.js → chunk-2XQZZAFI.js} +0 -0
  293. /package/dist/{chunk-HTJKAAHD.js → chunk-4NPP7ASA.js} +0 -0
  294. /package/dist/{chunk-BTP7BORU.js → chunk-AGV4GKNY.js} +0 -0
  295. /package/dist/{chunk-XF2D4CDA.js → chunk-IMGD6PW3.js} +0 -0
  296. /package/dist/{chunk-PIUDQ7P2.js → chunk-JAF64HJT.js} +0 -0
  297. /package/dist/{chunk-QUOMJKVF.js → chunk-JGTJAA6B.js} +0 -0
  298. /package/dist/{chunk-H4JKOLXU.js → chunk-PENPH5AG.js} +0 -0
  299. /package/dist/{chunk-DRHXISWL.js → chunk-PIBEPI5W.js} +0 -0
  300. /package/dist/{chunk-FXZRJT3H.js → chunk-S6APJBIZ.js} +0 -0
  301. /package/dist/{chunk-E42D2E5F.js → chunk-TEJOHJZQ.js} +0 -0
  302. /package/dist/{chunk-AQ6ECPUP.js → chunk-TZMXGMSV.js} +0 -0
  303. /package/dist/{core-memory-Z7LPR3RX.js → core-memory-F2TJ2VVZ.js} +0 -0
  304. /package/dist/{oauth-server-FSKABATB.js → oauth-server-DCZN5VOK.js} +0 -0
  305. /package/dist/{project-boot-FVSC4VQC.js → project-boot-GVT2NKGJ.js} +0 -0
  306. /package/dist/{wiki-acl-CW2UALTJ.js → wiki-acl-NCAPBNOZ.js} +0 -0
@@ -1,49 +1,181 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ writeSignedStackManifest
4
+ } from "./chunk-HCRUMO4B.js";
5
+ import "./chunk-KZ7BANUM.js";
2
6
  import {
3
7
  assertPreflightGate,
4
8
  runPreflightOnManifest
5
9
  } from "./chunk-3QWOO74G.js";
10
+ import "./chunk-JO6RMHMW.js";
11
+ import "./chunk-IMGD6PW3.js";
12
+ import "./chunk-BCZF3AGJ.js";
13
+ import "./chunk-HSGBZSCA.js";
6
14
  import "./chunk-MLKGABMK.js";
7
15
 
8
16
  // src/bin/stack-release.ts
9
- import { readFileSync, writeFileSync, existsSync } from "fs";
17
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
10
18
  import { execFile as execFileCb } from "child_process";
11
19
  import { promisify } from "util";
12
- import path from "path";
20
+ import path2 from "path";
13
21
  import { createInterface } from "readline";
22
+
23
+ // src/lib/release-lock.ts
24
+ import {
25
+ closeSync,
26
+ existsSync,
27
+ mkdirSync,
28
+ openSync,
29
+ readFileSync,
30
+ unlinkSync,
31
+ writeFileSync
32
+ } from "fs";
33
+ import { homedir, hostname as osHostname } from "os";
34
+ import path from "path";
35
+ import process2 from "process";
36
+ var DEFAULT_RELEASE_LOCK_TTL_MS = 2 * 60 * 60 * 1e3;
37
+ function defaultReleaseLockPath() {
38
+ return path.join(homedir(), ".exe-os", "release.lock");
39
+ }
40
+ function defaultIsPidAlive(pid) {
41
+ if (!Number.isInteger(pid) || pid <= 0) return false;
42
+ try {
43
+ process2.kill(pid, 0);
44
+ return true;
45
+ } catch (err) {
46
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
47
+ return code === "EPERM";
48
+ }
49
+ }
50
+ function readLock(lockPath) {
51
+ const raw = readFileSync(lockPath, "utf8");
52
+ const parsed = JSON.parse(raw);
53
+ if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string" || typeof parsed.targetVersion !== "string" || typeof parsed.branch !== "string" || typeof parsed.hostname !== "string") {
54
+ throw new Error("lock file is missing required fields");
55
+ }
56
+ return parsed;
57
+ }
58
+ function writeLockAtomic(lockPath, lock) {
59
+ mkdirSync(path.dirname(lockPath), { recursive: true });
60
+ const fd = openSync(lockPath, "wx", 384);
61
+ try {
62
+ writeFileSync(fd, JSON.stringify(lock, null, 2) + "\n", "utf8");
63
+ } finally {
64
+ closeSync(fd);
65
+ }
66
+ }
67
+ function sameLock(a, b) {
68
+ return a.pid === b.pid && a.startedAt === b.startedAt && a.targetVersion === b.targetVersion && a.branch === b.branch && a.hostname === b.hostname;
69
+ }
70
+ function acquireReleaseLock(opts) {
71
+ const lockPath = opts.lockPath ?? defaultReleaseLockPath();
72
+ const ttlMs = opts.ttlMs ?? DEFAULT_RELEASE_LOCK_TTL_MS;
73
+ const now = opts.now ?? /* @__PURE__ */ new Date();
74
+ const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
75
+ const logNote = opts.log ?? (() => {
76
+ });
77
+ if (existsSync(lockPath)) {
78
+ let existing = null;
79
+ try {
80
+ existing = readLock(lockPath);
81
+ } catch (err) {
82
+ const detail = err instanceof Error ? err.message : String(err);
83
+ if (!opts.force) {
84
+ throw new Error(
85
+ `release lock exists but cannot be read safely (${detail}); re-run with --force to override: ${lockPath}`
86
+ );
87
+ }
88
+ unlinkSync(lockPath);
89
+ logNote(`release lock force-cleared unreadable lock at ${lockPath}`);
90
+ }
91
+ if (existing) {
92
+ const startedMs = Date.parse(existing.startedAt);
93
+ if (!Number.isFinite(startedMs) && !opts.force) {
94
+ throw new Error(
95
+ `release lock exists with an invalid timestamp (${existing.startedAt}); re-run with --force to override: ${lockPath}`
96
+ );
97
+ }
98
+ const ageMs = Number.isFinite(startedMs) ? now.getTime() - startedMs : Number.POSITIVE_INFINITY;
99
+ const pidAlive = isPidAlive(existing.pid);
100
+ const fresh = ageMs < ttlMs && pidAlive;
101
+ if (fresh && !opts.force) {
102
+ throw new Error(
103
+ `release already in progress: pid ${existing.pid} started ${existing.startedAt}; re-run with --force to override`
104
+ );
105
+ }
106
+ unlinkSync(lockPath);
107
+ if (fresh) {
108
+ logNote(`release lock force-cleared: pid ${existing.pid} started ${existing.startedAt}`);
109
+ } else {
110
+ const reason = !pidAlive ? "pid is not alive" : `older than ${Math.round(ttlMs / 6e4)}m TTL`;
111
+ logNote(`stale release lock cleared: pid ${existing.pid} started ${existing.startedAt} (${reason})`);
112
+ }
113
+ }
114
+ }
115
+ const lock = {
116
+ pid: opts.pid ?? process2.pid,
117
+ startedAt: now.toISOString(),
118
+ targetVersion: opts.targetVersion,
119
+ branch: opts.branch,
120
+ hostname: opts.hostname ?? osHostname()
121
+ };
122
+ try {
123
+ writeLockAtomic(lockPath, lock);
124
+ } catch (err) {
125
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
126
+ if (code === "EEXIST") {
127
+ throw new Error(`release lock was acquired by another process; re-run when it completes: ${lockPath}`);
128
+ }
129
+ throw err;
130
+ }
131
+ return {
132
+ lockPath,
133
+ lock,
134
+ release: () => {
135
+ if (!existsSync(lockPath)) return;
136
+ try {
137
+ const current = readLock(lockPath);
138
+ if (sameLock(current, lock)) unlinkSync(lockPath);
139
+ } catch {
140
+ }
141
+ }
142
+ };
143
+ }
144
+
145
+ // src/bin/stack-release.ts
14
146
  var execFile = promisify(execFileCb);
15
147
  function findExeOsRoot() {
16
148
  const envRoot = process.env.EXE_OS_ROOT;
17
- if (envRoot && existsSync(envRoot)) return path.resolve(envRoot);
18
- let dir = path.dirname(new URL(import.meta.url).pathname);
149
+ if (envRoot && existsSync2(envRoot)) return path2.resolve(envRoot);
150
+ let dir = path2.dirname(new URL(import.meta.url).pathname);
19
151
  for (let i = 0; i < 5; i++) {
20
- const pkgPath = path.join(dir, "package.json");
21
- if (existsSync(pkgPath)) {
152
+ const pkgPath = path2.join(dir, "package.json");
153
+ if (existsSync2(pkgPath)) {
22
154
  try {
23
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
155
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
24
156
  if (pkg.name === "@askexenow/exe-os") return dir;
25
157
  } catch {
26
158
  }
27
159
  }
28
- dir = path.dirname(dir);
160
+ dir = path2.dirname(dir);
29
161
  }
30
- return path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..");
162
+ return path2.resolve(path2.dirname(new URL(import.meta.url).pathname), "..", "..");
31
163
  }
32
164
  var EXE_OS_ROOT = findExeOsRoot();
33
- var PARENT_DIR = path.dirname(EXE_OS_ROOT);
34
- var MANIFEST_PATH = path.join(EXE_OS_ROOT, "deploy", "stack-manifests", "v0.9.json");
165
+ var PARENT_DIR = path2.dirname(EXE_OS_ROOT);
166
+ var MANIFEST_PATH = path2.join(EXE_OS_ROOT, "deploy", "stack-manifests", "v0.9.json");
35
167
  function applyRootOverride(rootPath) {
36
- const resolved = path.resolve(rootPath);
37
- if (!existsSync(resolved)) {
168
+ const resolved = path2.resolve(rootPath);
169
+ if (!existsSync2(resolved)) {
38
170
  throw new Error(`--root path does not exist: ${resolved}`);
39
171
  }
40
172
  EXE_OS_ROOT = resolved;
41
- PARENT_DIR = path.dirname(EXE_OS_ROOT);
42
- MANIFEST_PATH = path.join(EXE_OS_ROOT, "deploy", "stack-manifests", "v0.9.json");
173
+ PARENT_DIR = path2.dirname(EXE_OS_ROOT);
174
+ MANIFEST_PATH = path2.join(EXE_OS_ROOT, "deploy", "stack-manifests", "v0.9.json");
43
175
  }
44
176
  function resolveRepoPath(dirName) {
45
- const direct = path.join(PARENT_DIR, dirName);
46
- if (existsSync(direct)) return direct;
177
+ const direct = path2.join(PARENT_DIR, dirName);
178
+ if (existsSync2(direct)) return direct;
47
179
  const aliases = {
48
180
  "exe-crm": ["openclaw", "exe-crm"],
49
181
  "exe-wiki": ["ink", "exe-wiki"]
@@ -51,8 +183,8 @@ function resolveRepoPath(dirName) {
51
183
  const candidates = aliases[dirName];
52
184
  if (candidates) {
53
185
  for (const alias of candidates) {
54
- const p = path.join(PARENT_DIR, alias);
55
- if (existsSync(p)) return p;
186
+ const p = path2.join(PARENT_DIR, alias);
187
+ if (existsSync2(p)) return p;
56
188
  }
57
189
  }
58
190
  return direct;
@@ -146,7 +278,8 @@ function parseReleaseArgs(argv) {
146
278
  root: null,
147
279
  forceBuild: false,
148
280
  canary: false,
149
- allowDirty: false
281
+ allowDirty: false,
282
+ force: false
150
283
  };
151
284
  for (let i = 0; i < argv.length; i++) {
152
285
  const arg = argv[i];
@@ -159,6 +292,7 @@ function parseReleaseArgs(argv) {
159
292
  else if (arg === "--allow-red") flags.allowRed = true;
160
293
  else if (arg === "--allow-dirty") flags.allowDirty = true;
161
294
  else if (arg === "--canary") flags.canary = true;
295
+ else if (arg === "--force") flags.force = true;
162
296
  else if (arg === "--repos" && argv[i + 1]) {
163
297
  flags.repos = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
164
298
  } else if (arg.startsWith("--repos=")) {
@@ -167,7 +301,7 @@ function parseReleaseArgs(argv) {
167
301
  flags.root = argv[++i];
168
302
  } else if (arg.startsWith("--root=")) {
169
303
  flags.root = arg.slice("--root=".length);
170
- } else if (arg === "--force" || arg === "--force-build") {
304
+ } else if (arg === "--force-build") {
171
305
  flags.forceBuild = true;
172
306
  }
173
307
  }
@@ -214,6 +348,61 @@ async function ghRun(args) {
214
348
  const { stdout } = await execFile("gh", args);
215
349
  return stdout.trim();
216
350
  }
351
+ async function assertReleaseFreshness(opts) {
352
+ const runGit = opts.gitRunner ?? git;
353
+ const logFn = opts.log ?? log;
354
+ const originRef = `origin/${opts.defaultBranch}`;
355
+ logFn("info", `Freshness gate: fetching origin ${opts.defaultBranch} before publish...`);
356
+ await runGit(opts.repoPath, "fetch", "origin", opts.defaultBranch);
357
+ const missing = (await runGit(opts.repoPath, "log", `${opts.releaseCommit}..${originRef}`, "--oneline")).trim();
358
+ if (missing) {
359
+ throw new Error(
360
+ `Freshness gate failed: ${originRef} contains commits not included in release commit ${opts.releaseCommit}.
361
+ Missing-PR list:
362
+ ${missing}
363
+ Abort: rebase/re-cut the release from current ${originRef} before publishing.`
364
+ );
365
+ }
366
+ try {
367
+ await runGit(opts.repoPath, "merge-base", "--is-ancestor", originRef, opts.releaseCommit);
368
+ } catch (err) {
369
+ const detail = err instanceof Error ? err.message : String(err);
370
+ throw new Error(
371
+ `Freshness gate failed: release commit ${opts.releaseCommit} is not a descendant of ${originRef}. Abort: rebase/re-cut the release from current ${originRef} before publishing. (${detail})`
372
+ );
373
+ }
374
+ logFn("ok", `Freshness gate passed: release commit contains current ${originRef}`);
375
+ }
376
+ async function printOpenPrGate(opts) {
377
+ const baseBranch = opts.baseBranch ?? "main";
378
+ const runGh = opts.ghRunner ?? ghRun;
379
+ const logFn = opts.log ?? log;
380
+ try {
381
+ const output = await runGh([
382
+ "pr",
383
+ "list",
384
+ "--repo",
385
+ opts.ghRepo,
386
+ "--base",
387
+ baseBranch,
388
+ "--json",
389
+ "number,title"
390
+ ]);
391
+ const prs = JSON.parse(output);
392
+ if (prs.length === 0) {
393
+ logFn("info", `Open PR gate: no open PRs targeting ${baseBranch} in ${opts.ghRepo}`);
394
+ return;
395
+ }
396
+ logFn("warn", `Open PR gate: ${prs.length} open PR(s) targeting ${baseBranch} in ${opts.ghRepo}:`);
397
+ for (const pr of prs) {
398
+ const number = typeof pr.number === "number" ? `#${pr.number}` : "#?";
399
+ logFn("warn", ` ${number} ${pr.title ?? "(untitled)"}`);
400
+ }
401
+ } catch (err) {
402
+ const detail = err instanceof Error ? err.message : String(err);
403
+ logFn("warn", `Open PR gate: unable to list open PRs for ${opts.ghRepo} targeting ${baseBranch}; continuing (${detail})`);
404
+ }
405
+ }
217
406
  function bumpVersion(version, mode) {
218
407
  const parts = version.split(".").map(Number);
219
408
  if (parts.length < 3) throw new Error(`Invalid version: ${version}`);
@@ -229,16 +418,16 @@ function bumpVersion(version, mode) {
229
418
  }
230
419
  }
231
420
  function readPackageVersion(repoPath) {
232
- const pkgPath = path.join(repoPath, "package.json");
233
- if (!existsSync(pkgPath)) {
421
+ const pkgPath = path2.join(repoPath, "package.json");
422
+ if (!existsSync2(pkgPath)) {
234
423
  throw new Error(`Missing package.json at ${pkgPath}`);
235
424
  }
236
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
425
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
237
426
  return pkg.version;
238
427
  }
239
428
  function readPackageVersionOrNull(repoPath) {
240
- const pkgPath = path.join(repoPath, "package.json");
241
- if (!existsSync(pkgPath)) return null;
429
+ const pkgPath = path2.join(repoPath, "package.json");
430
+ if (!existsSync2(pkgPath)) return null;
242
431
  return readPackageVersion(repoPath);
243
432
  }
244
433
  function repoName(repo) {
@@ -251,15 +440,15 @@ function warnMissingRepoPackage(repo) {
251
440
  );
252
441
  }
253
442
  function hasPackageJson(repo) {
254
- return existsSync(path.join(repo.localPath, "package.json"));
443
+ return existsSync2(path2.join(repo.localPath, "package.json"));
255
444
  }
256
445
  function deriveCloneUrl(repo) {
257
446
  return `https://github.com/${repo.ghRepo}.git`;
258
447
  }
259
448
  function isCloneTargetSafe(targetPath, parentDir) {
260
- const resolvedParent = path.resolve(parentDir);
261
- const resolvedTarget = path.resolve(targetPath);
262
- return resolvedTarget === resolvedParent || resolvedTarget.startsWith(resolvedParent + path.sep);
449
+ const resolvedParent = path2.resolve(parentDir);
450
+ const resolvedTarget = path2.resolve(targetPath);
451
+ return resolvedTarget === resolvedParent || resolvedTarget.startsWith(resolvedParent + path2.sep);
263
452
  }
264
453
  var defaultCloneRunner = async (url, targetPath) => {
265
454
  await execFile("git", ["clone", url, targetPath]);
@@ -284,9 +473,9 @@ async function ensureRepoCloned(repo, parentDir = PARENT_DIR, runClone = default
284
473
  return true;
285
474
  }
286
475
  function readStackReleaseJson(repoPath) {
287
- const p = path.join(repoPath, "stack.release.json");
288
- if (!existsSync(p)) return null;
289
- return JSON.parse(readFileSync(p, "utf8"));
476
+ const p = path2.join(repoPath, "stack.release.json");
477
+ if (!existsSync2(p)) return null;
478
+ return JSON.parse(readFileSync2(p, "utf8"));
290
479
  }
291
480
  async function detectChanges(repos) {
292
481
  const changes = [];
@@ -338,10 +527,10 @@ async function bumpVersions(changes, mode, dryRun) {
338
527
  log("dry", `${change.repo.serviceKey}: ${change.currentVersion} -> ${newVersion}`);
339
528
  continue;
340
529
  }
341
- const pkgPath = path.join(change.repo.localPath, "package.json");
342
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
530
+ const pkgPath = path2.join(change.repo.localPath, "package.json");
531
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
343
532
  pkg.version = newVersion;
344
- writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
533
+ writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
345
534
  const srf = readStackReleaseJson(change.repo.localPath);
346
535
  if (srf) {
347
536
  srf.version = newVersion;
@@ -363,8 +552,8 @@ async function bumpVersions(changes, mode, dryRun) {
363
552
  }
364
553
  }
365
554
  }
366
- writeFileSync(
367
- path.join(change.repo.localPath, "stack.release.json"),
555
+ writeFileSync2(
556
+ path2.join(change.repo.localPath, "stack.release.json"),
368
557
  JSON.stringify(srf, null, 2) + "\n"
369
558
  );
370
559
  }
@@ -374,6 +563,7 @@ async function bumpVersions(changes, mode, dryRun) {
374
563
  }
375
564
  }
376
565
  async function landViaPR(repoPath, ghRepo, defaultBranch, branch, title) {
566
+ const pushedSha = await git(repoPath, "rev-parse", "HEAD");
377
567
  await git(repoPath, "push", "--force-with-lease", "origin", `HEAD:refs/heads/${branch}`);
378
568
  try {
379
569
  await ghRun([
@@ -392,13 +582,41 @@ async function landViaPR(repoPath, ghRepo, defaultBranch, branch, title) {
392
582
  ]);
393
583
  } catch {
394
584
  }
395
- await ghRun(["pr", "merge", branch, "--repo", ghRepo, "--auto", "--squash"]);
396
585
  const deadline = Date.now() + 30 * 60 * 1e3;
586
+ let releasePr;
587
+ for (; ; ) {
588
+ try {
589
+ const output = await ghRun([
590
+ "pr",
591
+ "list",
592
+ "--repo",
593
+ ghRepo,
594
+ "--head",
595
+ branch,
596
+ "--state",
597
+ "all",
598
+ "--json",
599
+ "number,state,headRefOid"
600
+ ]);
601
+ releasePr = findPullRequestForHeadSha(JSON.parse(output), pushedSha);
602
+ } catch {
603
+ }
604
+ if (releasePr) break;
605
+ if (Date.now() > deadline) {
606
+ throw new Error(
607
+ `No PR on ${ghRepo} (${branch}) matches released commit ${pushedSha.slice(0, 12)} within 30m \u2014 failing closed rather than attributing another PR's verdict. Aborting release; ${defaultBranch} left untouched.`
608
+ );
609
+ }
610
+ await sleep(15e3);
611
+ }
612
+ if (releasePr.state !== "MERGED") {
613
+ await ghRun(["pr", "merge", String(releasePr.number), "--repo", ghRepo, "--auto", "--squash"]);
614
+ }
397
615
  for (; ; ) {
398
616
  const state = (await ghRun([
399
617
  "pr",
400
618
  "view",
401
- branch,
619
+ String(releasePr.number),
402
620
  "--repo",
403
621
  ghRepo,
404
622
  "--json",
@@ -409,10 +627,10 @@ async function landViaPR(repoPath, ghRepo, defaultBranch, branch, title) {
409
627
  if (state === "MERGED") break;
410
628
  if (Date.now() > deadline) {
411
629
  throw new Error(
412
- `Release PR ${ghRepo} (${branch}) did not merge within 30m \u2014 required CI likely red. Aborting release; ${defaultBranch} left untouched.`
630
+ `Release PR ${ghRepo}#${releasePr.number} (${branch} @ ${pushedSha.slice(0, 12)}) did not merge within 30m \u2014 required CI likely red. Aborting release; ${defaultBranch} left untouched.`
413
631
  );
414
632
  }
415
- await new Promise((r) => setTimeout(r, 3e4));
633
+ await sleep(3e4);
416
634
  }
417
635
  await git(repoPath, "fetch", "origin", defaultBranch);
418
636
  await git(repoPath, "checkout", defaultBranch);
@@ -432,9 +650,16 @@ async function tagAndTriggerCI(changes, dryRun) {
432
650
  branch,
433
651
  `chore: bump ${change.tag} for stack release`
434
652
  );
653
+ change.releaseSha = await git(change.repo.localPath, "rev-parse", "HEAD");
654
+ await assertReleaseFreshness({
655
+ repoPath: change.repo.localPath,
656
+ defaultBranch: change.repo.defaultBranch,
657
+ releaseCommit: change.releaseSha
658
+ });
659
+ await printOpenPrGate({ ghRepo: change.repo.ghRepo, baseBranch: "main" });
435
660
  await git(change.repo.localPath, "tag", change.tag);
436
661
  await git(change.repo.localPath, "push", "origin", change.tag);
437
- log("ok", `Landed ${change.repo.serviceKey} ${change.tag} via PR + tagged (green-CI gated)`);
662
+ log("ok", `Landed ${change.repo.serviceKey} ${change.tag} via PR + tagged ${change.releaseSha.slice(0, 12)} (green-CI gated)`);
438
663
  }
439
664
  }
440
665
  async function rollbackFailedReleases(changes, ciStatuses, dryRun) {
@@ -472,6 +697,26 @@ async function rollbackFailedReleases(changes, ciStatuses, dryRun) {
472
697
  }
473
698
  }
474
699
  }
700
+ function workflowRunHeadSha(run) {
701
+ return run.headSha ?? run.head_sha;
702
+ }
703
+ function findPullRequestForHeadSha(prs, pushedSha) {
704
+ const normalizedSha = pushedSha.toLowerCase();
705
+ return prs.find((pr) => pr.headRefOid?.toLowerCase() === normalizedSha);
706
+ }
707
+ function findWorkflowRunForHeadSha(runs, releaseSha) {
708
+ const normalizedReleaseSha = releaseSha.toLowerCase();
709
+ return runs.find((run) => workflowRunHeadSha(run)?.toLowerCase() === normalizedReleaseSha);
710
+ }
711
+ async function resolveReleaseSha(change) {
712
+ if (change.releaseSha) return change.releaseSha;
713
+ try {
714
+ change.releaseSha = await git(change.repo.localPath, "rev-parse", `${change.tag}^{}`);
715
+ return change.releaseSha;
716
+ } catch {
717
+ return void 0;
718
+ }
719
+ }
475
720
  async function waitForCI(changes, dryRun) {
476
721
  if (dryRun) {
477
722
  return changes.map((c) => ({
@@ -495,6 +740,10 @@ async function waitForCI(changes, dryRun) {
495
740
  if (pending.length === 0) break;
496
741
  for (const st of pending) {
497
742
  try {
743
+ const change = changes.find((c) => c.repo.serviceKey === st.repo.serviceKey && c.tag === st.tag);
744
+ if (!change) continue;
745
+ const releaseSha = await resolveReleaseSha(change);
746
+ if (!releaseSha) continue;
498
747
  const output = await ghRun([
499
748
  "run",
500
749
  "list",
@@ -503,17 +752,17 @@ async function waitForCI(changes, dryRun) {
503
752
  "--workflow",
504
753
  st.repo.releaseWorkflow,
505
754
  "--limit",
506
- "1",
755
+ "50",
507
756
  "--json",
508
- "status,conclusion,url"
757
+ "status,conclusion,url,headSha"
509
758
  ]);
510
759
  const runs = JSON.parse(output);
511
- const latest = runs[0];
512
- if (!latest) continue;
513
- if (latest.status === "completed") {
760
+ const releaseRun = findWorkflowRunForHeadSha(runs, releaseSha);
761
+ if (!releaseRun) continue;
762
+ if (releaseRun.status === "completed") {
514
763
  st.status = "completed";
515
- st.conclusion = latest.conclusion ?? "unknown";
516
- st.url = latest.url;
764
+ st.conclusion = releaseRun.conclusion ?? "unknown";
765
+ st.url = releaseRun.url;
517
766
  if (st.conclusion === "success") {
518
767
  log("ok", `CI passed: ${st.repo.serviceKey} ${st.tag}`);
519
768
  } else {
@@ -748,7 +997,7 @@ function printImageVerification(results) {
748
997
  console.log("");
749
998
  }
750
999
  async function updateStackManifest(changes, ciStatuses, dryRun, imageResults, canary = false) {
751
- const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
1000
+ const manifest = JSON.parse(readFileSync2(MANIFEST_PATH, "utf8"));
752
1001
  const currentLatest = manifest.latest;
753
1002
  const currentParts = currentLatest.split(".").map(Number);
754
1003
  const newStackVersion = `${currentParts[0]}.${currentParts[1]}.${(currentParts[2] ?? 0) + 1}`;
@@ -812,7 +1061,7 @@ async function updateStackManifest(changes, ciStatuses, dryRun, imageResults, ca
812
1061
  }
813
1062
  manifest.stacks[newStackVersion] = newEntry;
814
1063
  if (canary) {
815
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
1064
+ writeSignedStackManifest(MANIFEST_PATH, manifest, { warn: (msg) => log("warn", msg) });
816
1065
  log("ok", `Stack manifest updated: added CANARY ${newStackVersion} (latest unchanged: ${currentLatest})`);
817
1066
  log("warn", `${newStackVersion} is a CANARY release. Deploy to a designated canary host with:`);
818
1067
  log("warn", ` exe-os stack-update --target ${newStackVersion} --yes`);
@@ -820,7 +1069,7 @@ async function updateStackManifest(changes, ciStatuses, dryRun, imageResults, ca
820
1069
  log("warn", ` exe-os stack-promote --version ${newStackVersion} --yes`);
821
1070
  } else {
822
1071
  manifest.latest = newStackVersion;
823
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
1072
+ writeSignedStackManifest(MANIFEST_PATH, manifest, { warn: (msg) => log("warn", msg) });
824
1073
  log("ok", `Stack manifest updated: ${currentLatest} -> ${newStackVersion}`);
825
1074
  }
826
1075
  return newStackVersion;
@@ -828,18 +1077,18 @@ async function updateStackManifest(changes, ciStatuses, dryRun, imageResults, ca
828
1077
  function gatePublishWithPreflight(newStackVersion, dryRun, allowRed) {
829
1078
  let manifest;
830
1079
  try {
831
- manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
1080
+ manifest = JSON.parse(readFileSync2(MANIFEST_PATH, "utf8"));
832
1081
  } catch (err) {
833
1082
  throw new Error(`Preflight gate: cannot read manifest ${MANIFEST_PATH}: ${err instanceof Error ? err.message : String(err)}`);
834
1083
  }
835
- const composeDir = path.join(EXE_OS_ROOT, "deploy", "compose");
1084
+ const composeDir = path2.join(EXE_OS_ROOT, "deploy", "compose");
836
1085
  const report = runPreflightOnManifest({
837
1086
  manifest,
838
1087
  stackVersion: newStackVersion,
839
1088
  label: `${MANIFEST_PATH} (stack ${newStackVersion})`,
840
- envExamplePath: path.join(composeDir, ".env.example"),
841
- composePath: path.join(composeDir, "docker-compose.yml"),
842
- monitorComposePath: path.join(EXE_OS_ROOT, "deploy", "monitor", "docker-compose.hub.yml")
1089
+ envExamplePath: path2.join(composeDir, ".env.example"),
1090
+ composePath: path2.join(composeDir, "docker-compose.yml"),
1091
+ monitorComposePath: path2.join(EXE_OS_ROOT, "deploy", "monitor", "docker-compose.hub.yml")
843
1092
  });
844
1093
  log("info", "Running static preflight gate against the freshly-written manifest...");
845
1094
  if (dryRun) {
@@ -861,7 +1110,7 @@ function gatePublishWithPreflight(newStackVersion, dryRun, allowRed) {
861
1110
  function gatePreMutationStructure(changes, dryRun, allowRed) {
862
1111
  let baseManifest;
863
1112
  try {
864
- baseManifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
1113
+ baseManifest = JSON.parse(readFileSync2(MANIFEST_PATH, "utf8"));
865
1114
  } catch (err) {
866
1115
  throw new Error(`Pre-mutation gate: cannot read manifest ${MANIFEST_PATH}: ${err instanceof Error ? err.message : String(err)}`);
867
1116
  }
@@ -880,14 +1129,14 @@ function gatePreMutationStructure(changes, dryRun, allowRed) {
880
1129
  latest: simVersion,
881
1130
  stacks: { [simVersion]: { version: simVersion, services } }
882
1131
  };
883
- const composeDir = path.join(EXE_OS_ROOT, "deploy", "compose");
1132
+ const composeDir = path2.join(EXE_OS_ROOT, "deploy", "compose");
884
1133
  const report = runPreflightOnManifest({
885
1134
  manifest: simManifest,
886
1135
  stackVersion: simVersion,
887
1136
  label: `pre-mutation simulated manifest (stack ${simVersion})`,
888
- envExamplePath: path.join(composeDir, ".env.example"),
889
- composePath: path.join(composeDir, "docker-compose.yml"),
890
- monitorComposePath: path.join(EXE_OS_ROOT, "deploy", "monitor", "docker-compose.hub.yml")
1137
+ envExamplePath: path2.join(composeDir, ".env.example"),
1138
+ composePath: path2.join(composeDir, "docker-compose.yml"),
1139
+ monitorComposePath: path2.join(EXE_OS_ROOT, "deploy", "monitor", "docker-compose.hub.yml")
891
1140
  });
892
1141
  const adjusted = {
893
1142
  ...report,
@@ -929,6 +1178,13 @@ async function commitAndTagExeOs(newStackVersion, _exeOsChange, dryRun) {
929
1178
  log("ok", `Committed stack manifest v${newStackVersion} (via PR)`);
930
1179
  }
931
1180
  const stackTag = `stack-v${newStackVersion}`;
1181
+ const releaseCommit = await git(EXE_OS_ROOT, "rev-parse", "HEAD");
1182
+ await assertReleaseFreshness({
1183
+ repoPath: EXE_OS_ROOT,
1184
+ defaultBranch: "main",
1185
+ releaseCommit
1186
+ });
1187
+ await printOpenPrGate({ ghRepo: "AskExe/exe-os", baseBranch: "main" });
932
1188
  try {
933
1189
  await git(EXE_OS_ROOT, "tag", stackTag);
934
1190
  await git(EXE_OS_ROOT, "push", "origin", stackTag);
@@ -997,9 +1253,25 @@ async function confirm(message) {
997
1253
  });
998
1254
  });
999
1255
  }
1256
+ function nextStackVersionForLock() {
1257
+ try {
1258
+ const manifest = JSON.parse(readFileSync2(MANIFEST_PATH, "utf8"));
1259
+ const parts = String(manifest.latest ?? "0.9.0").split(".").map(Number);
1260
+ return `stack-v${parts[0] ?? 0}.${parts[1] ?? 9}.${(parts[2] ?? 0) + 1}`;
1261
+ } catch {
1262
+ return "stack-vunknown";
1263
+ }
1264
+ }
1265
+ async function currentReleaseBranch() {
1266
+ try {
1267
+ return await git(EXE_OS_ROOT, "branch", "--show-current") || "unknown";
1268
+ } catch {
1269
+ return "unknown";
1270
+ }
1271
+ }
1000
1272
  async function runStackRelease(flags) {
1001
1273
  if (!flags.stack) {
1002
- console.error("Usage: exe-os release --stack [--dry-run] [--repos <list>] [--yes] [--skip-image-verify] [--allow-red] [--allow-dirty] [--canary] [--root <path>] [--force]");
1274
+ console.error("Usage: exe-os release --stack [--dry-run] [--repos <list>] [--yes] [--skip-image-verify] [--allow-red] [--allow-dirty] [--canary] [--root <path>] [--force] [--force-build]");
1003
1275
  process.exit(1);
1004
1276
  }
1005
1277
  if (flags.root) {
@@ -1010,133 +1282,156 @@ async function runStackRelease(flags) {
1010
1282
  console.warn(" \x1B[33m Docker images will be built WITHOUT requiring green CI.\x1B[0m");
1011
1283
  console.warn(" \x1B[33m Use only for break-glass emergencies.\x1B[0m\n");
1012
1284
  }
1013
- await assertCleanWorkingTree({ allowDirty: flags.allowDirty || flags.dryRun });
1014
- console.log(`
1285
+ if (flags.force) {
1286
+ console.warn("\n \x1B[33;1m\u26A0 --force: release mutex override is ACTIVE\x1B[0m");
1287
+ console.warn(" \x1B[33m This only overrides ~/.exe-os/release.lock; freshness and publish gates still apply.\x1B[0m");
1288
+ console.warn(" \x1B[33m Use only after verifying no other release is actually running.\x1B[0m\n");
1289
+ }
1290
+ let releaseLock;
1291
+ if (!flags.dryRun) {
1292
+ releaseLock = acquireReleaseLock({
1293
+ targetVersion: nextStackVersionForLock(),
1294
+ branch: await currentReleaseBranch(),
1295
+ force: flags.force,
1296
+ log: (message) => log("warn", message)
1297
+ });
1298
+ log("ok", `Acquired release lock: ${releaseLock.lockPath}`);
1299
+ }
1300
+ try {
1301
+ await assertCleanWorkingTree({ allowDirty: flags.allowDirty || flags.dryRun });
1302
+ console.log(`
1015
1303
  \x1B[1mexe-os stack release\x1B[0m${flags.dryRun ? " (dry run)" : ""}
1016
1304
  `);
1017
- const allRepos = buildRepoConfigs();
1018
- let repos;
1019
- if (flags.repos) {
1020
- repos = [];
1021
- for (const name of flags.repos) {
1022
- const match = allRepos.find(
1023
- (r) => r.serviceKey === name || r.ghRepo.endsWith(`/${name}`) || r.ghRepo === name
1024
- );
1025
- if (match) {
1026
- repos.push(match);
1027
- } else {
1028
- log("warn", `Unknown repo: ${name} \u2014 skipping`);
1305
+ const allRepos = buildRepoConfigs();
1306
+ let repos;
1307
+ if (flags.repos) {
1308
+ repos = [];
1309
+ for (const name of flags.repos) {
1310
+ const match = allRepos.find(
1311
+ (r) => r.serviceKey === name || r.ghRepo.endsWith(`/${name}`) || r.ghRepo === name
1312
+ );
1313
+ if (match) {
1314
+ repos.push(match);
1315
+ } else {
1316
+ log("warn", `Unknown repo: ${name} \u2014 skipping`);
1317
+ }
1029
1318
  }
1319
+ if (repos.length === 0) {
1320
+ throw new Error("No valid repos specified.");
1321
+ }
1322
+ } else {
1323
+ repos = allRepos;
1030
1324
  }
1031
- if (repos.length === 0) {
1032
- console.error("No valid repos specified.");
1033
- process.exit(1);
1034
- }
1035
- } else {
1036
- repos = allRepos;
1037
- }
1038
- repos = repos.filter((repo) => {
1039
- if (hasPackageJson(repo)) return true;
1040
- warnMissingRepoPackage(repo);
1041
- return false;
1042
- });
1043
- log("info", "Detecting changes across repos...");
1044
- const changes = await detectChanges(repos);
1045
- if (changes.length === 0) {
1046
- console.log("\n No changes detected since last tags. Nothing to release.\n");
1047
- return;
1048
- }
1049
- const bumpMode = flags.major ? "major" : flags.minor ? "minor" : "patch";
1050
- for (const change of changes) {
1051
- const newVersion = bumpVersion(change.currentVersion, bumpMode);
1052
- change.newVersion = newVersion;
1053
- change.tag = `v${newVersion}`;
1054
- }
1055
- console.log("\n Planned releases:");
1056
- for (const change of changes) {
1057
- console.log(
1058
- ` ${change.repo.serviceKey.padEnd(14)} ${change.currentVersion} -> ${change.newVersion} (${change.commits.length} commit${change.commits.length === 1 ? "" : "s"})`
1059
- );
1060
- }
1061
- gatePreMutationStructure(changes, flags.dryRun, flags.allowRed);
1062
- if (!flags.yes && !flags.dryRun) {
1063
- const ok = await confirm("Proceed with release?");
1064
- if (!ok) {
1065
- console.log(" Aborted.\n");
1325
+ repos = repos.filter((repo) => {
1326
+ if (hasPackageJson(repo)) return true;
1327
+ warnMissingRepoPackage(repo);
1328
+ return false;
1329
+ });
1330
+ log("info", "Detecting changes across repos...");
1331
+ const changes = await detectChanges(repos);
1332
+ if (changes.length === 0) {
1333
+ console.log("\n No changes detected since last tags. Nothing to release.\n");
1066
1334
  return;
1067
1335
  }
1068
- }
1069
- await bumpVersions(changes, bumpMode, flags.dryRun);
1070
- log("info", "Tagging repos and triggering CI...");
1071
- await tagAndTriggerCI(changes, flags.dryRun);
1072
- const ciStatuses = await waitForCI(changes, flags.dryRun);
1073
- const ciFailures = ciStatuses.filter(
1074
- (s) => s.status !== "completed" || s.conclusion !== "success"
1075
- );
1076
- if (ciFailures.length > 0 && !flags.dryRun) {
1077
- await rollbackFailedReleases(changes, ciStatuses, flags.dryRun);
1078
- const ciSuccesses = ciStatuses.filter(
1079
- (s) => s.status === "completed" && s.conclusion === "success"
1080
- );
1081
- if (ciSuccesses.length === 0) {
1082
- log("fail", "All CI builds failed. Release aborted, all bumps rolled back.");
1083
- process.exit(1);
1336
+ const bumpMode = flags.major ? "major" : flags.minor ? "minor" : "patch";
1337
+ for (const change of changes) {
1338
+ const newVersion = bumpVersion(change.currentVersion, bumpMode);
1339
+ change.newVersion = newVersion;
1340
+ change.tag = `v${newVersion}`;
1084
1341
  }
1085
- log("warn", `${ciFailures.length} CI build(s) failed and were rolled back. Continuing with ${ciSuccesses.length} successful build(s).`);
1086
- }
1087
- let imageResults = [];
1088
- if (flags.skipImageVerify) {
1089
- log("warn", "Image verification skipped (--skip-image-verify)");
1090
- } else {
1091
- imageResults = await verifyImages(changes, ciStatuses, flags.dryRun);
1092
- const failed = imageResults.filter((r) => (r.ghcr === "missing" || r.ghcr === "error") && r.proxy !== "ok");
1093
- if (failed.length > 0 && !flags.dryRun) {
1094
- for (const m of failed) {
1095
- log("fail", `${shortImageName(m.repo.imageName)}:${m.tag} \u2014 GHCR status: ${m.ghcr}, proxy: ${m.proxy} \u2014 cannot release`);
1096
- }
1097
- log("fail", "Aborting: images not verified. Fix GHCR auth/CI or use --skip-image-verify to override.");
1098
- process.exit(1);
1342
+ console.log("\n Planned releases:");
1343
+ for (const change of changes) {
1344
+ console.log(
1345
+ ` ${change.repo.serviceKey.padEnd(14)} ${change.currentVersion} -> ${change.newVersion} (${change.commits.length} commit${change.commits.length === 1 ? "" : "s"})`
1346
+ );
1099
1347
  }
1100
- const pending = imageResults.filter((r) => r.proxy === "pending");
1101
- if (pending.length > 0) {
1102
- for (const p of pending) {
1103
- log("warn", `${shortImageName(p.repo.imageName)}:${p.tag} \u2014 proxy cache pending`);
1348
+ gatePreMutationStructure(changes, flags.dryRun, flags.allowRed);
1349
+ if (!flags.yes && !flags.dryRun) {
1350
+ const ok = await confirm("Proceed with release?");
1351
+ if (!ok) {
1352
+ console.log(" Aborted.\n");
1353
+ return;
1104
1354
  }
1105
- if (!flags.dryRun) {
1106
- log("fail", "Aborting: proxy cache not ready for all images. Wait for proxy propagation or use --skip-image-verify.");
1107
- process.exit(1);
1355
+ }
1356
+ await bumpVersions(changes, bumpMode, flags.dryRun);
1357
+ log("info", "Tagging repos and triggering CI...");
1358
+ await tagAndTriggerCI(changes, flags.dryRun);
1359
+ const ciStatuses = await waitForCI(changes, flags.dryRun);
1360
+ const ciFailures = ciStatuses.filter(
1361
+ (s) => s.status !== "completed" || s.conclusion !== "success"
1362
+ );
1363
+ if (ciFailures.length > 0 && !flags.dryRun) {
1364
+ await rollbackFailedReleases(changes, ciStatuses, flags.dryRun);
1365
+ const ciSuccesses = ciStatuses.filter(
1366
+ (s) => s.status === "completed" && s.conclusion === "success"
1367
+ );
1368
+ if (ciSuccesses.length === 0) {
1369
+ log("fail", "All CI builds failed. Release aborted, all bumps rolled back.");
1370
+ throw new Error("All CI builds failed. Release aborted, all bumps rolled back.");
1108
1371
  }
1372
+ log("warn", `${ciFailures.length} CI build(s) failed and were rolled back. Continuing with ${ciSuccesses.length} successful build(s).`);
1109
1373
  }
1110
- printImageVerification(imageResults);
1111
- }
1112
- log("info", "Updating stack manifest...");
1113
- const newStackVersion = await updateStackManifest(changes, ciStatuses, flags.dryRun, imageResults, flags.canary);
1114
- gatePublishWithPreflight(newStackVersion, flags.dryRun, flags.allowRed);
1115
- const exeOsChange = changes.find((c) => c.repo.serviceKey === "exe-os");
1116
- await commitAndTagExeOs(newStackVersion, exeOsChange, flags.dryRun);
1117
- printSummary(changes, ciStatuses, newStackVersion, flags.dryRun);
1118
- if (!flags.skipImageVerify && !flags.dryRun && imageResults.length > 0) {
1119
- const pendingImages = imageResults.filter((r) => r.proxy === "pending");
1120
- if (pendingImages.length > 0) {
1121
- log("info", "Re-checking proxy availability for pending images...");
1122
- for (const r of pendingImages) {
1123
- const shortName = shortImageName(r.repo.imageName);
1124
- const proxyRepo = `askexe/${shortName}`;
1125
- const proxyToken = process.env.EXE_REGISTRY_PROXY_PULL_TOKEN ?? void 0;
1126
- const recheck = await checkRegistryManifest(PROXY_REGISTRY, proxyRepo, r.tag, proxyToken);
1127
- if (recheck.ok) {
1128
- r.proxy = "ok";
1374
+ let imageResults = [];
1375
+ if (flags.skipImageVerify) {
1376
+ log("warn", "Image verification skipped (--skip-image-verify)");
1377
+ } else {
1378
+ imageResults = await verifyImages(changes, ciStatuses, flags.dryRun);
1379
+ const failed = imageResults.filter((r) => (r.ghcr === "missing" || r.ghcr === "error") && r.proxy !== "ok");
1380
+ if (failed.length > 0 && !flags.dryRun) {
1381
+ for (const m of failed) {
1382
+ log("fail", `${shortImageName(m.repo.imageName)}:${m.tag} \u2014 GHCR status: ${m.ghcr}, proxy: ${m.proxy} \u2014 cannot release`);
1383
+ }
1384
+ log("fail", "Aborting: images not verified. Fix GHCR auth/CI or use --skip-image-verify to override.");
1385
+ throw new Error("Aborting: images not verified. Fix GHCR auth/CI or use --skip-image-verify to override.");
1386
+ }
1387
+ const pending = imageResults.filter((r) => r.proxy === "pending");
1388
+ if (pending.length > 0) {
1389
+ for (const p of pending) {
1390
+ log("warn", `${shortImageName(p.repo.imageName)}:${p.tag} \u2014 proxy cache pending`);
1391
+ }
1392
+ if (!flags.dryRun) {
1393
+ log("fail", "Aborting: proxy cache not ready for all images. Wait for proxy propagation or use --skip-image-verify.");
1394
+ throw new Error("Aborting: proxy cache not ready for all images. Wait for proxy propagation or use --skip-image-verify.");
1129
1395
  }
1130
1396
  }
1131
1397
  printImageVerification(imageResults);
1132
1398
  }
1399
+ log("info", "Updating stack manifest...");
1400
+ const newStackVersion = await updateStackManifest(changes, ciStatuses, flags.dryRun, imageResults, flags.canary);
1401
+ gatePublishWithPreflight(newStackVersion, flags.dryRun, flags.allowRed);
1402
+ const exeOsChange = changes.find((c) => c.repo.serviceKey === "exe-os");
1403
+ await commitAndTagExeOs(newStackVersion, exeOsChange, flags.dryRun);
1404
+ printSummary(changes, ciStatuses, newStackVersion, flags.dryRun);
1405
+ if (!flags.skipImageVerify && !flags.dryRun && imageResults.length > 0) {
1406
+ const pendingImages = imageResults.filter((r) => r.proxy === "pending");
1407
+ if (pendingImages.length > 0) {
1408
+ log("info", "Re-checking proxy availability for pending images...");
1409
+ for (const r of pendingImages) {
1410
+ const shortName = shortImageName(r.repo.imageName);
1411
+ const proxyRepo = `askexe/${shortName}`;
1412
+ const proxyToken = process.env.EXE_REGISTRY_PROXY_PULL_TOKEN ?? void 0;
1413
+ const recheck = await checkRegistryManifest(PROXY_REGISTRY, proxyRepo, r.tag, proxyToken);
1414
+ if (recheck.ok) {
1415
+ r.proxy = "ok";
1416
+ }
1417
+ }
1418
+ printImageVerification(imageResults);
1419
+ }
1420
+ }
1421
+ } finally {
1422
+ releaseLock?.release();
1423
+ if (releaseLock) log("ok", `Released release lock: ${releaseLock.lockPath}`);
1133
1424
  }
1134
1425
  }
1135
1426
  export {
1136
1427
  assertCleanWorkingTree,
1428
+ assertReleaseFreshness,
1137
1429
  deriveCloneUrl,
1138
1430
  ensureRepoCloned,
1431
+ findPullRequestForHeadSha,
1432
+ findWorkflowRunForHeadSha,
1139
1433
  isCloneTargetSafe,
1140
1434
  parseReleaseArgs,
1435
+ printOpenPrGate,
1141
1436
  runStackRelease
1142
1437
  };