chainlesschain 0.162.188 → 0.162.193

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 (337) hide show
  1. package/README.md +61 -11
  2. package/bin/chainlesschain.js +7 -52
  3. package/completions/_cc +209 -0
  4. package/completions/cc.bash +18 -0
  5. package/completions/cc.fish +13 -0
  6. package/completions/cc.ps1 +226 -0
  7. package/package.json +10 -2
  8. package/src/assets/web-panel/assets/{AIOps-Brr1LVcE.js → AIOps-DBLkaTje.js} +1 -1
  9. package/src/assets/web-panel/assets/{ActionButton-DQU74DKG.js → ActionButton-BAy-Qebo.js} +1 -1
  10. package/src/assets/web-panel/assets/{Analytics-CYp9178y.js → Analytics-Dfb1Mlau.js} +3 -3
  11. package/src/assets/web-panel/assets/{AppLayout-DW4SCYsW.js → AppLayout-BbFkN8m0.js} +5 -5
  12. package/src/assets/web-panel/assets/{Artifacts-CYlD_ed1.js → Artifacts-kM4-cEEH.js} +1 -1
  13. package/src/assets/web-panel/assets/{Audit-CWl1rZOi.js → Audit-BSQfYWf3.js} +1 -1
  14. package/src/assets/web-panel/assets/{BackgroundAgents-BOR-LPsa.js → BackgroundAgents-CWDbI_fG.js} +1 -1
  15. package/src/assets/web-panel/assets/{Backup-DLpG7j1X.js → Backup-BxLPbq_Q.js} +1 -1
  16. package/src/assets/web-panel/assets/{BaseInput-B35K9wdr.js → BaseInput-DuIbdrp6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Chat-CwUJb9gB.js → Chat-NEmjGahU.js} +5 -5
  18. package/src/assets/web-panel/assets/{ChatBubbleRenderer-1v6aMS1m.js → ChatBubbleRenderer-DfJEo-5_.js} +1 -1
  19. package/src/assets/web-panel/assets/{Checkbox-pi3-tPNW.js → Checkbox-CaoeG3Ns.js} +1 -1
  20. package/src/assets/web-panel/assets/{Codegen-FyNF35eL.js → Codegen-BFf_g3xY.js} +1 -1
  21. package/src/assets/web-panel/assets/{Col-tGt_JDEr.js → Col-CNOd8mdQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Community-CC7LonQX.js → Community-BhpDqz-i.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compact-BGXrHHjE.js → Compact-CQ42T7Z6.js} +1 -1
  24. package/src/assets/web-panel/assets/{Compliance-DSW0TBZ8.js → Compliance-DI5PcLMN.js} +1 -1
  25. package/src/assets/web-panel/assets/{Cowork-Bhk7rbvE.js → Cowork-wYYZe0wR.js} +3 -3
  26. package/src/assets/web-panel/assets/{Cron-DxyiQ2NZ.js → Cron-Dlq03DD_.js} +2 -2
  27. package/src/assets/web-panel/assets/{Crosschain-B2ZkxHok.js → Crosschain-BzqS8PP3.js} +1 -1
  28. package/src/assets/web-panel/assets/{DID-yOJV99eJ.js → DID-It55Yem7.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dashboard-CKsttz7D.js → Dashboard-9HkH6oxn.js} +2 -2
  30. package/src/assets/web-panel/assets/{Dropdown-CVFldXSL.js → Dropdown-00e2YWRB.js} +1 -1
  31. package/src/assets/web-panel/assets/{EmailListRenderer-Bufv638J.js → EmailListRenderer-By-ugRDL.js} +1 -1
  32. package/src/assets/web-panel/assets/{FamilyGuardDashboard-4ncjoIjL.js → FamilyGuardDashboard-C396yu5m.js} +1 -1
  33. package/src/assets/web-panel/assets/{Federation-DsknEDoX.js → Federation-BME5sbZY.js} +1 -1
  34. package/src/assets/web-panel/assets/{FormItemContext-Cqe5jWXA.js → FormItemContext-B4TEZCr-.js} +1 -1
  35. package/src/assets/web-panel/assets/{GenericCardRenderer-NAJaFKxc.js → GenericCardRenderer-_9CX9ieY.js} +1 -1
  36. package/src/assets/web-panel/assets/{Git-CszKQpnl.js → Git-BrS6WVSr.js} +2 -2
  37. package/src/assets/web-panel/assets/{Governance-BXGodXi6.js → Governance-BMdCWnKF.js} +1 -1
  38. package/src/assets/web-panel/assets/{Inference-CkoYuG2c.js → Inference-Bcp3vzcA.js} +1 -1
  39. package/src/assets/web-panel/assets/{KnowledgeGraph-DFlEr_T0.js → KnowledgeGraph-DlLuqGY5.js} +1 -1
  40. package/src/assets/web-panel/assets/{Logs-CVaENddA.js → Logs-D-W_6x09.js} +2 -2
  41. package/src/assets/web-panel/assets/MarkdownRenderer-DElGPxCZ.js +1 -0
  42. package/src/assets/web-panel/assets/{Marketplace-CDlLT0fw.js → Marketplace-Dzn2guvI.js} +1 -1
  43. package/src/assets/web-panel/assets/{McpTools-DdwOyszN.js → McpTools-uPict-KL.js} +6 -6
  44. package/src/assets/web-panel/assets/{Memory-DKQnkKuG.js → Memory-BCw43kOb.js} +2 -2
  45. package/src/assets/web-panel/assets/{MobileBridge-DGseqlzJ.js → MobileBridge-DmV8Wv7F.js} +1 -1
  46. package/src/assets/web-panel/assets/{MobileProjects-COBbEOlj.js → MobileProjects-C-HaCmSM.js} +1 -1
  47. package/src/assets/web-panel/assets/{Mtc-O61Vwyli.js → Mtc-C6jxJ34Y.js} +5 -5
  48. package/src/assets/web-panel/assets/{MtcAudit-Dw2SyasS.js → MtcAudit-Za8ExwhQ.js} +5 -5
  49. package/src/assets/web-panel/assets/{Multisig-hSu_l8kf.js → Multisig-KKDbuEgb.js} +3 -3
  50. package/src/assets/web-panel/assets/{NLProgramming-CX1Epurr.js → NLProgramming-Dj97zbCk.js} +1 -1
  51. package/src/assets/web-panel/assets/{Notes-R9xX8o8_.js → Notes-DBXvMraU.js} +4 -4
  52. package/src/assets/web-panel/assets/{NotificationSettings-jtSNLCuK.js → NotificationSettings-DOxITLYl.js} +1 -1
  53. package/src/assets/web-panel/assets/OrderTableRenderer-7DVjZwMM.js +1 -0
  54. package/src/assets/web-panel/assets/{Organization-Pu2WO-U3.js → Organization-D1i73mjb.js} +4 -4
  55. package/src/assets/web-panel/assets/{Overflow-BjnqUyFp.js → Overflow-kW6Q3rvg.js} +1 -1
  56. package/src/assets/web-panel/assets/{P2P-jjkvz44c.js → P2P-C-v_8p2v.js} +2 -2
  57. package/src/assets/web-panel/assets/PdhVaultBrowser-tUbbY64W.js +7 -0
  58. package/src/assets/web-panel/assets/{Permissions-D4yDpivd.js → Permissions-DkHxEZ58.js} +4 -4
  59. package/src/assets/web-panel/assets/{PersonalDataHub-0qsL6iH1.js → PersonalDataHub-DWuMPGrK.js} +5 -5
  60. package/src/assets/web-panel/assets/{Pipeline-Cx7Ybrk2.js → Pipeline-30IYXWWm.js} +1 -1
  61. package/src/assets/web-panel/assets/{Privacy-PbfR_M_V.js → Privacy-DZNQ4RrH.js} +1 -1
  62. package/src/assets/web-panel/assets/{ProjectInit-CyV6Gsub.js → ProjectInit-De2MCElf.js} +2 -2
  63. package/src/assets/web-panel/assets/{ProjectSettings-C7KZ2Wdk.js → ProjectSettings-5uHk9wsC.js} +2 -2
  64. package/src/assets/web-panel/assets/{Projects-DQDjO-yK.js → Projects-XjDfrFLb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Providers-DyFVBBMX.js → Providers-Bgwj6gcE.js} +1 -1
  66. package/src/assets/web-panel/assets/{QrScannerModal-CzB1BC9_.js → QrScannerModal-gKql9umv.js} +1 -1
  67. package/src/assets/web-panel/assets/{QuickAsk-Bk636mC1.js → QuickAsk-BWvmwkh7.js} +1 -1
  68. package/src/assets/web-panel/assets/{Recommend-DoWxHIT3.js → Recommend-Uok4hZPd.js} +1 -1
  69. package/src/assets/web-panel/assets/{RemoteSession-oOVIv9wq.js → RemoteSession-DLJzTk9a.js} +3 -3
  70. package/src/assets/web-panel/assets/{Reputation-BucoReGG.js → Reputation-ZVwIPMSO.js} +1 -1
  71. package/src/assets/web-panel/assets/{Row-DzmOdJU7.js → Row-CN04HxWR.js} +1 -1
  72. package/src/assets/web-panel/assets/{RssFeed-C1tK7KGU.js → RssFeed-BdSrEG4g.js} +3 -3
  73. package/src/assets/web-panel/assets/{Search-CSnInOqk.js → Search-5PwVd0IR.js} +1 -1
  74. package/src/assets/web-panel/assets/{Security-dlejr15l.js → Security-DdBo4zjN.js} +3 -3
  75. package/src/assets/web-panel/assets/{Services-BioPZuNh.js → Services-D3letU7M.js} +2 -2
  76. package/src/assets/web-panel/assets/{Skeleton-DWMibwpC.js → Skeleton--Jb83f1v.js} +1 -1
  77. package/src/assets/web-panel/assets/{Skills-BAAnx1B2.js → Skills-Bg-CulHX.js} +1 -1
  78. package/src/assets/web-panel/assets/{Sla-jWNp4hNO.js → Sla-7QOa7S-X.js} +1 -1
  79. package/src/assets/web-panel/assets/{SpeechSettings-y5kqKha-.js → SpeechSettings-DQwppjRw.js} +1 -1
  80. package/src/assets/web-panel/assets/{SyncSettings-B9GNsc3e.js → SyncSettings-dNPQGL-s.js} +2 -2
  81. package/src/assets/web-panel/assets/{Tasks-BDBYKRen.js → Tasks-Ggs-Z48q.js} +1 -1
  82. package/src/assets/web-panel/assets/{Templates-DRiE5eaN.js → Templates-78TYHgRO.js} +1 -1
  83. package/src/assets/web-panel/assets/{Tenant-BGlAvExF.js → Tenant-xHwwv9vO.js} +1 -1
  84. package/src/assets/web-panel/assets/{Terminal-B8uzrbNY.js → Terminal-BJXSJfm4.js} +2 -2
  85. package/src/assets/web-panel/assets/{TimelineRenderer-Df48iveY.js → TimelineRenderer-fLbjRhJw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Tokens-CcmHndN6.js → Tokens-oMSBSBmB.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trigger-DPkWxmQ2.js → Trigger-CfnY_wX_.js} +1 -1
  88. package/src/assets/web-panel/assets/{Trust-DTb5a5AV.js → Trust-AdyTgYKO.js} +1 -1
  89. package/src/assets/web-panel/assets/{UkeySign-CBmg5vch.js → UkeySign-C5K_81dS.js} +1 -1
  90. package/src/assets/web-panel/assets/{VideoEditing-B0mZrz4O.js → VideoEditing-vcGVDqki.js} +1 -1
  91. package/src/assets/web-panel/assets/{Wallet-Bxs7_mpd.js → Wallet-hhOc3hlY.js} +4 -4
  92. package/src/assets/web-panel/assets/{WebAuthn-C8x3cMxU.js → WebAuthn-CVloC11s.js} +3 -3
  93. package/src/assets/web-panel/assets/{WorkflowEditor-DPrNznrP.js → WorkflowEditor-CYeE32R5.js} +1 -1
  94. package/src/assets/web-panel/assets/{chat-D3bYWu7S.js → chat-CNROR5bN.js} +1 -1
  95. package/src/assets/web-panel/assets/{colors-DY-PwMu4.js → colors-Cjx-cnwU.js} +1 -1
  96. package/src/assets/web-panel/assets/{compact-item-LDk10Kw6.js → compact-item-DOIfelvD.js} +1 -1
  97. package/src/assets/web-panel/assets/{createContext-C-2MZnFq.js → createContext-DfA7UZ8u.js} +1 -1
  98. package/src/assets/web-panel/assets/devWarning-ChcCBGs7.js +1 -0
  99. package/src/assets/web-panel/assets/{hasIn-BQIBjcnB.js → hasIn-BMSCzcfM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CQVb2dWp.js → index-5-HXgOX5.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BdXnYMCZ.js → index-AWg-0M1S.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cx6D_dx2.js → index-B4iYzpkL.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-_HRO_cIu.js → index-B5Udeb_6.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DlRYqt3S.js → index-B7gf77CJ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BrnRO9yE.js → index-BEMwBBHk.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-VCh5zNJo.js → index-BM1y_9hM.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BRI3ngNg.js → index-BUonmKSE.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C3GNvExG.js → index-BbQ2eQ8H.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-DSvo_bp0.js → index-BemJ2u_E.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-L6qixyLc.js → index-BolaOQxx.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BI4KgcYC.js → index-BxOk9vkG.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CY4KnaO9.js → index-By61ojUx.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-L3smcoQX.js → index-C2K9SNVX.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BX2b9Orc.js → index-C3L5gdiD.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cz4b6Z_7.js → index-C7xn0Nub.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COG61sRv.js → index-CAnvhwg6.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-C7FC8cP2.js → index-CEKMcOyU.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BN-xdSWZ.js → index-CNC5onm4.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DY2FIrmz.js → index-CVL27kXC.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CiTvBAr-.js → index-CVm1_i3t.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DhRt9cYJ.js → index-CdYBye8B.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B5pgtZnY.js → index-ClNMrqEY.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-7xKu7Fns.js → index-Cs6l7uRu.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D4K1weKc.js → index-CshQbilz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-k7C_oDCO.js → index-Cyweh0Hy.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D3Oq-DLL.js → index-DBpWXFed.js} +3 -3
  127. package/src/assets/web-panel/assets/{index-DYyLJPlF.js → index-DHtpHZAQ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dh-tkpKQ.js → index-DLmn0BoK.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-8JC9BJa1.js → index-DPAWBrMf.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Dd2Km97U.js → index-DjEsxO5h.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-KFZO_lcx.js → index-FBc_CJBd.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DTIZpPtG.js → index-Icdoqmzt.js} +1 -1
  133. package/src/assets/web-panel/assets/index-Q2IajL54.js +1 -0
  134. package/src/assets/web-panel/assets/index-bRkHgOMc.js +1 -0
  135. package/src/assets/web-panel/assets/{index-lmm9p-jm.js → index-dCd8cSUq.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-HasPt4-h.js → index-m2vG9gG_.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-CaslYch6.js → index-mTpHoEzr.js} +1 -1
  138. package/src/assets/web-panel/assets/{index-CbR6ROOa.js → index-ti_lnTWl.js} +1 -1
  139. package/src/assets/web-panel/assets/{initDefaultProps-zQPSHf2C.js → initDefaultProps-Bu8k7H1Y.js} +1 -1
  140. package/src/assets/web-panel/assets/{motion-CKCStGVk.js → motion-s3iVK41k.js} +1 -1
  141. package/src/assets/web-panel/assets/{move-BufVDGYF.js → move-B6B-E5Jz.js} +1 -1
  142. package/src/assets/web-panel/assets/{mtc-parser-DcaFxy5D.js → mtc-parser-BiYgTidA.js} +1 -1
  143. package/src/assets/web-panel/assets/{omit-EScpkGdo.js → omit-pU1RdUDA.js} +1 -1
  144. package/src/assets/web-panel/assets/{pickAttrs-72hG_qfT.js → pickAttrs-Cdoq_SeM.js} +1 -1
  145. package/src/assets/web-panel/assets/{placementArrow-CMFWiH20.js → placementArrow-DIuvAn2K.js} +1 -1
  146. package/src/assets/web-panel/assets/{responsiveObserve-DKQHS41p.js → responsiveObserve-8w16YR2D.js} +1 -1
  147. package/src/assets/web-panel/assets/{slide-QVAFHTIQ.js → slide-VZ01cZe3.js} +1 -1
  148. package/src/assets/web-panel/assets/{statusUtils-DIz38e2G.js → statusUtils-lh8YMN8Z.js} +1 -1
  149. package/src/assets/web-panel/assets/{styleChecker-zK5cyLby.js → styleChecker-CmDyreec.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFlexGapSupport-DhZngk62.js → useFlexGapSupport-hiJWMU5y.js} +1 -1
  151. package/src/assets/web-panel/assets/{useFs-BvV2cvkg.js → useFs-Thnu-Iaj.js} +1 -1
  152. package/src/assets/web-panel/assets/{usePersonalDataHub-CRmxb9l3.js → usePersonalDataHub-LBdOxjwY.js} +1 -1
  153. package/src/assets/web-panel/assets/{vnode-CgQIbobP.js → vnode-Cg6lFY_X.js} +1 -1
  154. package/src/assets/web-panel/assets/{zoom-D4XrrO0R.js → zoom-DVfsR1YO.js} +1 -1
  155. package/src/assets/web-panel/index.html +1 -1
  156. package/src/cli-help.js +181 -0
  157. package/src/command-help-index.json +181 -0
  158. package/src/command-manifest.json +1107 -175
  159. package/src/command-surface-policy.js +197 -0
  160. package/src/commands/agent.js +98 -33
  161. package/src/commands/artifacts.js +400 -2
  162. package/src/commands/background-session.js +41 -3
  163. package/src/commands/checkpoint-managed.js +1059 -0
  164. package/src/commands/checkpoint-restore-recovery.js +1476 -0
  165. package/src/commands/checkpoint.js +950 -22
  166. package/src/commands/compact.js +101 -25
  167. package/src/commands/config.js +506 -20
  168. package/src/commands/doctor.js +4 -1
  169. package/src/commands/hub.js +1 -1
  170. package/src/commands/mcp.js +688 -24
  171. package/src/commands/pack.js +16 -2
  172. package/src/commands/plugin.js +20 -12
  173. package/src/commands/session-mcp-recovery.js +218 -0
  174. package/src/commands/session.js +470 -110
  175. package/src/commands/setup.js +36 -10
  176. package/src/commands/skill.js +181 -110
  177. package/src/commands/status.js +21 -5
  178. package/src/commands/team-distributed.js +3324 -0
  179. package/src/commands/team.js +1130 -74
  180. package/src/commands/update.js +68 -6
  181. package/src/constants.js +11 -0
  182. package/src/data/changelog.json +26 -0
  183. package/src/gateways/ws/action-protocol.js +10 -0
  184. package/src/gateways/ws/session-protocol.js +555 -27
  185. package/src/gateways/ws/ws-agent-handler.js +541 -14
  186. package/src/gateways/ws/ws-session-gateway.js +129 -22
  187. package/src/gateways/ws/ws-session-state.js +431 -0
  188. package/src/harness/background-task-manager.js +233 -36
  189. package/src/harness/jsonl-session-store.js +2551 -167
  190. package/src/harness/mcp-client.js +802 -61
  191. package/src/harness/prompt-compressor.js +227 -27
  192. package/src/harness/session-index.js +89 -38
  193. package/src/harness/session-list-index.js +254 -0
  194. package/src/harness/structured-handoff.js +410 -0
  195. package/src/lazy-dispatch.js +747 -84
  196. package/src/lib/__tests__/logger.test.js +50 -2
  197. package/src/lib/advisor-runtime.js +1047 -0
  198. package/src/lib/agent-core.js +2 -0
  199. package/src/lib/agent-sandbox.js +120 -26
  200. package/src/lib/agent-session-export.js +20 -0
  201. package/src/lib/agent-team/task-lease.js +271 -3
  202. package/src/lib/agent-team/team-adjudication.js +1501 -0
  203. package/src/lib/agent-team/team-budget.js +4 -1
  204. package/src/lib/agent-team/team-control-store.js +582 -0
  205. package/src/lib/agent-team/team-distributed-queue.js +5246 -0
  206. package/src/lib/agent-team/team-process-checkpoint.js +211 -0
  207. package/src/lib/agent-team/team-runner.js +543 -24
  208. package/src/lib/agent-team/team-worktree.js +1403 -95
  209. package/src/lib/artifact-store.js +131 -8
  210. package/src/lib/background-agent-supervisor.js +185 -13
  211. package/src/lib/background-interaction-journal.js +37 -9
  212. package/src/lib/background-launch-profile.js +1058 -0
  213. package/src/lib/checkpoint-restore-already-completed-controller.js +902 -0
  214. package/src/lib/checkpoint-restore-orchestrator.js +987 -0
  215. package/src/lib/checkpoint-restore-partial-rollback-controller.js +1953 -0
  216. package/src/lib/checkpoint-restore-recovery-controller.js +689 -0
  217. package/src/lib/checkpoint-restore-recovery.js +884 -0
  218. package/src/lib/checkpoint-restore-retention.js +126 -0
  219. package/src/lib/checkpoint-restore-saga.js +5079 -0
  220. package/src/lib/checkpoint-restore-session-recovery.js +1178 -0
  221. package/src/lib/checkpoint-restore-workspace-target-verifier.js +270 -0
  222. package/src/lib/checkpoint-store.js +1688 -97
  223. package/src/lib/checkpoint-timeline-authority.js +522 -0
  224. package/src/lib/checkpoint-timeline.js +642 -0
  225. package/src/lib/config-keys.js +18 -119
  226. package/src/lib/config-manager.js +446 -27
  227. package/src/lib/config-redaction.js +57 -0
  228. package/src/lib/config-schema.js +853 -0
  229. package/src/lib/cowork-mcp-tools.js +5 -0
  230. package/src/lib/cowork-task-runner.js +458 -63
  231. package/src/lib/delivery-coordinator.js +1234 -0
  232. package/src/lib/delivery-evidence.js +450 -0
  233. package/src/lib/doctor-checkup.js +199 -14
  234. package/src/lib/executable-path.js +34 -0
  235. package/src/lib/fatal-handler.js +16 -0
  236. package/src/lib/file-checkpoint.js +4909 -186
  237. package/src/lib/file-lines.js +125 -0
  238. package/src/lib/headless-config-command.js +16 -5
  239. package/src/lib/hook-runner.cjs +139 -38
  240. package/src/lib/hooks-v2-workspace-context.js +10 -0
  241. package/src/lib/impacted-gate-selector.js +424 -0
  242. package/src/lib/interaction-adapter.js +62 -0
  243. package/src/lib/jsonl-session-store.js +1 -35
  244. package/src/lib/logger.js +66 -14
  245. package/src/lib/managed-checkpoint-render.js +60 -0
  246. package/src/lib/managed-tool-checkpoint.js +194 -0
  247. package/src/lib/mcp-call-ledger-store.js +1069 -0
  248. package/src/lib/mcp-call-ledger.js +747 -0
  249. package/src/lib/mcp-client.js +5 -0
  250. package/src/lib/mcp-conflict-scheduler.js +468 -0
  251. package/src/lib/mcp-effect-contract.js +102 -0
  252. package/src/lib/mcp-headers-helper-trust.js +145 -0
  253. package/src/lib/mcp-headers-helper.js +616 -0
  254. package/src/lib/mcp-host-recovery-runtime.js +131 -0
  255. package/src/lib/mcp-ledger-recovery-admission.js +1065 -0
  256. package/src/lib/mcp-recovery-adjudication.js +443 -0
  257. package/src/lib/output-context.js +86 -0
  258. package/src/lib/packer/file-identity.js +170 -0
  259. package/src/lib/packer/index.js +3 -0
  260. package/src/lib/packer/native-update-state.js +277 -0
  261. package/src/lib/packer/pack-update-applier.js +1983 -136
  262. package/src/lib/packer/pack-update-checker.js +34 -0
  263. package/src/lib/packer/pack-update-downloader.js +875 -86
  264. package/src/lib/packer/pack-update-signature.js +86 -0
  265. package/src/lib/packer/pkg-config-generator.js +6 -0
  266. package/src/lib/paths.js +65 -10
  267. package/src/lib/plan-mode.js +831 -14
  268. package/src/lib/plan-persistence.js +540 -0
  269. package/src/lib/plugin-runtime/hooks.js +227 -69
  270. package/src/lib/plugin-runtime/plugin-options.js +189 -44
  271. package/src/lib/pr-automation-policy.js +146 -8
  272. package/src/lib/process-execution-broker/credential-agent.js +7 -0
  273. package/src/lib/process-execution-broker/index.js +491 -13
  274. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +28 -1
  275. package/src/lib/process-execution-broker/linux-generic-bwrap.js +28 -4
  276. package/src/lib/process-execution-broker/platform-sandbox.js +92 -70
  277. package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
  278. package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
  279. package/src/lib/process-execution-broker/windows-sandbox.cs +284 -19
  280. package/src/lib/process-execution-broker/workspace-transaction.js +4459 -0
  281. package/src/lib/project-instructions.js +165 -29
  282. package/src/lib/project-mcp-trust.js +67 -0
  283. package/src/lib/public-ide-capability-manifest.js +272 -0
  284. package/src/lib/public-ide-capability-validation.js +351 -0
  285. package/src/lib/secret-store.js +267 -43
  286. package/src/lib/secure-file-identity.js +433 -0
  287. package/src/lib/secure-fs.js +1089 -0
  288. package/src/lib/security-audit.js +58 -0
  289. package/src/lib/service-manager.js +34 -13
  290. package/src/lib/session-budget-runtime.js +1452 -0
  291. package/src/lib/session-host-snapshot.js +388 -0
  292. package/src/lib/session-message-provenance.js +429 -0
  293. package/src/lib/session-projection.js +804 -0
  294. package/src/lib/session-resource-budget.js +1495 -0
  295. package/src/lib/settings-hooks.cjs +138 -22
  296. package/src/lib/settings-loader.cjs +25 -2
  297. package/src/lib/side-effect-ledger-store.js +45 -23
  298. package/src/lib/skill-execution-identity.js +217 -0
  299. package/src/lib/skill-loader.js +299 -18
  300. package/src/lib/sub-agent-context.js +236 -28
  301. package/src/lib/sub-agent-registry.js +5 -0
  302. package/src/lib/subagent-contract.js +94 -0
  303. package/src/lib/todo-manager.js +185 -18
  304. package/src/lib/todo-persistence.js +417 -0
  305. package/src/lib/turn-binding-store.js +41 -23
  306. package/src/lib/turn-binding.js +160 -2
  307. package/src/lib/with-file-lock.js +165 -24
  308. package/src/lib/ws-chat-handler.js +21 -0
  309. package/src/program-base.js +18 -0
  310. package/src/repl/advisor-command.js +103 -0
  311. package/src/repl/agent-repl.js +2175 -146
  312. package/src/repl/btw-command.js +93 -10
  313. package/src/repl/clipboard-image.js +119 -0
  314. package/src/repl/config-summary.js +64 -108
  315. package/src/repl/prompt-editor.js +233 -0
  316. package/src/repl/prompt-interactions.js +344 -0
  317. package/src/repl/prompt-stash.js +354 -0
  318. package/src/repl/prompt-suggestions.js +312 -0
  319. package/src/repl/repl-keybindings.js +227 -0
  320. package/src/repl/session-recap.js +217 -0
  321. package/src/repl/slash-command-registry.js +22 -0
  322. package/src/repl/terminal-layout.js +88 -0
  323. package/src/runtime/agent-core.js +1630 -447
  324. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  325. package/src/runtime/coding-agent-managed-tool-policy.cjs +7 -2
  326. package/src/runtime/diagnostics.js +32 -13
  327. package/src/runtime/headless-runner.js +310 -118
  328. package/src/runtime/headless-stream.js +356 -95
  329. package/src/runtime/mcp-config.js +148 -14
  330. package/src/runtime/policies/agent-policy.js +14 -0
  331. package/src/runtime/status-diagnostics-lite.js +137 -0
  332. package/src/assets/web-panel/assets/MarkdownRenderer-7iWGwZ-r.js +0 -1
  333. package/src/assets/web-panel/assets/OrderTableRenderer-BU2Yqq5H.js +0 -1
  334. package/src/assets/web-panel/assets/PdhVaultBrowser-69ca_WeR.js +0 -7
  335. package/src/assets/web-panel/assets/devWarning-B4dXIJqD.js +0 -1
  336. package/src/assets/web-panel/assets/index-C-C7DaG6.js +0 -1
  337. package/src/assets/web-panel/assets/index-DkfWzWtd.js +0 -1
@@ -1,39 +1,46 @@
1
1
  /**
2
- * Phase 5b: stream a packed exe to disk and verify its SHA-256 against the
3
- * manifest. Self-replacement is Phase 5c; this module just writes the new
4
- * artifact next to the old one (conventionally `<exePath>.new`).
2
+ * Stream a packed executable to a unique sibling file, verify its SHA-256,
3
+ * and atomically commit it to `outputPath`.
5
4
  *
6
- * Design notes:
7
- * - Streaming, not buffered: artifacts are ~80-140 MB, so we write chunks
8
- * straight to disk and hash incrementally. Node's fetch yields a web
9
- * ReadableStream, which we consume via its async iterator.
10
- * - Atomicity: we write to `<outputPath>.partial` and rename on success.
11
- * A SHA mismatch or mid-stream failure deletes the partial so a retry
12
- * starts from scratch.
13
- * - No resume: first cut skips Range / If-Range. Large-artifact resume
14
- * is a Phase 5d concern if it becomes painful.
5
+ * The active output is never unlinked before the rename. Concurrent writers
6
+ * are serialized by an exclusive sibling lock, and a crashed writer's lock
7
+ * is deliberately left fail-closed for an operator to inspect.
15
8
  */
16
9
 
17
10
  import fs from "node:fs";
18
11
  import path from "node:path";
19
12
  import crypto from "node:crypto";
13
+ import {
14
+ hasPreciseFileIdentity,
15
+ pathMatchesOpenedFileIdentitySync,
16
+ sameOpenedFileIdentity,
17
+ } from "./file-identity.js";
18
+ import {
19
+ assertSafeRegularFile as assertStateSafeRegularFile,
20
+ assertSafePathAncestors,
21
+ } from "./native-update-state.js";
20
22
 
21
23
  const SHA256_HEX = /^[0-9a-f]{64}$/;
22
24
 
23
25
  /**
24
26
  * @param {object} ctx
25
- * @param {string} ctx.url absolute http(s) URL to the artifact
26
- * @param {string} ctx.sha256 lowercase hex SHA-256 from the manifest
27
- * @param {string} ctx.outputPath where the verified artifact lands
28
- * @param {typeof fetch} [ctx.fetchImpl] injected for tests
27
+ * @param {string} ctx.url
28
+ * @param {string} ctx.sha256
29
+ * @param {string} ctx.outputPath
30
+ * @param {typeof fetch} [ctx.fetchImpl]
29
31
  * @param {(p:{bytes:number,total:number|null})=>void} [ctx.onProgress]
30
- * fires as chunks arrive; total
31
- * is null if Content-Length was
32
- * absent or unparseable
32
+ * @param {number} [ctx.timeoutMs]
33
33
  * @returns {Promise<{outputPath:string,bytes:number,sha256:string}>}
34
34
  */
35
35
  export async function downloadAndVerify(ctx) {
36
- const { url, sha256, outputPath, fetchImpl = fetch, onProgress } = ctx;
36
+ const {
37
+ url,
38
+ sha256,
39
+ outputPath,
40
+ fetchImpl = fetch,
41
+ onProgress,
42
+ timeoutMs = 300_000,
43
+ } = ctx;
37
44
 
38
45
  if (!url || typeof url !== "string") {
39
46
  throw new DownloadError("url is required", "NO_URL");
@@ -47,112 +54,894 @@ export async function downloadAndVerify(ctx) {
47
54
  if (!outputPath || typeof outputPath !== "string") {
48
55
  throw new DownloadError("outputPath is required", "NO_OUTPUT");
49
56
  }
57
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
58
+ throw new DownloadError(
59
+ "timeoutMs must be a positive safe integer",
60
+ "BAD_TIMEOUT",
61
+ );
62
+ }
63
+
64
+ const abortController = new AbortController();
65
+ const deadline = Date.now() + timeoutMs;
50
66
 
67
+ assertSafeDownloadPath(outputPath, "output path", true);
51
68
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
52
- const partialPath = outputPath + ".partial";
69
+ assertSafeDownloadPath(outputPath, "output path", true);
70
+ assertSafeFileOrMissing(outputPath, "output path");
71
+
72
+ const lockPath = `${outputPath}.lock`;
73
+ const lock = acquireLock(lockPath);
74
+ const partialPath = `${outputPath}.partial-${process.pid}-${crypto.randomUUID()}`;
75
+ let partialFd = null;
76
+ let partialIdentity = null;
77
+ let committedPath = false;
78
+ let previousOutput = null;
79
+ let retainLockForRecovery = false;
80
+ const assertOwnedDownloadLock = () => {
81
+ try {
82
+ assertDownloadLockOwned(lock);
83
+ } catch (error) {
84
+ if (error?.code === "DOWNLOAD_LOCK_LOST") {
85
+ // The pathname may already belong to another writer. From this point
86
+ // onward, cleanup may close our old descriptor but must never inspect,
87
+ // restore, delete, or otherwise mutate the current lock pathname.
88
+ retainLockForRecovery = true;
89
+ }
90
+ throw error;
91
+ }
92
+ };
53
93
 
54
- // Clean up any stale partial from a prior interrupted attempt. Ignoring
55
- // ENOENT is fine — we just want to start with a blank slate.
56
94
  try {
57
- fs.unlinkSync(partialPath);
58
- } catch {
59
- /* no prior partial */
95
+ // Re-check the complete ancestor chain after lock acquisition. This
96
+ // closes the practical junction/symlink replacement window between the
97
+ // initial validation and opening the same-directory partial file.
98
+ assertSafeDownloadPath(outputPath, "output path", true);
99
+ assertSafeFileOrMissing(outputPath, "output path");
100
+ assertSafeDownloadPath(partialPath, "partial download path", true);
101
+ let response;
102
+ try {
103
+ response = await awaitBeforeDeadline(
104
+ fetchImpl(url, {
105
+ headers: { Accept: "*/*" },
106
+ signal: abortController.signal,
107
+ }),
108
+ { deadline, abortController, label: "artifact response" },
109
+ );
110
+ } catch (err) {
111
+ if (err instanceof DownloadError) throw err;
112
+ throw new DownloadError(
113
+ `fetch failed: ${err.message}`,
114
+ err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR",
115
+ );
116
+ }
117
+ if (!response.ok) {
118
+ throw new DownloadError(
119
+ `artifact fetch failed: HTTP ${response.status}`,
120
+ "FETCH_FAILED",
121
+ );
122
+ }
123
+
124
+ const totalRaw = response.headers?.get?.("content-length");
125
+ const total = totalRaw ? Number(totalRaw) : null;
126
+ const body = response.body;
127
+ if (!body) {
128
+ throw new DownloadError(
129
+ "response has no body stream (fetch impl returned no body)",
130
+ "NO_BODY",
131
+ );
132
+ }
133
+
134
+ // Network I/O can take arbitrarily long. Revalidate after the response is
135
+ // available and immediately before opening the lexical partial path.
136
+ assertSafeDownloadPath(outputPath, "output path", true);
137
+ assertSafeFileOrMissing(outputPath, "output path");
138
+ assertSafeDownloadPath(partialPath, "partial download path", true);
139
+
140
+ const hasher = crypto.createHash("sha256");
141
+ let bytes = 0;
142
+
143
+ try {
144
+ // Keep a read/write descriptor open through the rename. The pathname can
145
+ // be exchanged by another same-user process even while the download lock
146
+ // is held; descriptor/path identity checks on both sides of the commit
147
+ // bind the verified bytes to the file that actually becomes outputPath.
148
+ partialFd = fs.openSync(partialPath, "wx+", 0o600);
149
+ partialIdentity = snapshotDescriptorIdentity(partialFd);
150
+ const iterator = body[Symbol.asyncIterator]();
151
+ while (true) {
152
+ const { value: chunk, done } = await awaitBeforeDeadline(
153
+ iterator.next(),
154
+ { deadline, abortController, label: "artifact body" },
155
+ );
156
+ if (done) break;
157
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
158
+ hasher.update(buf);
159
+ bytes += buf.length;
160
+ writeAllSync(partialFd, buf);
161
+ if (typeof onProgress === "function") {
162
+ try {
163
+ onProgress({ bytes, total: Number.isFinite(total) ? total : null });
164
+ } catch {
165
+ /* progress callback errors must not interrupt the download */
166
+ }
167
+ }
168
+ }
169
+ fs.fsyncSync(partialFd);
170
+ } catch (err) {
171
+ if (partialFd !== null) {
172
+ try {
173
+ fs.closeSync(partialFd);
174
+ } catch {
175
+ /* best effort */
176
+ }
177
+ partialFd = null;
178
+ }
179
+ if (err instanceof DownloadError) throw err;
180
+ throw new DownloadError(`stream aborted: ${err.message}`, "STREAM_ERROR");
181
+ }
182
+
183
+ const actualSha = hasher.digest("hex");
184
+ if (actualSha !== sha256.toLowerCase()) {
185
+ throw new DownloadError(
186
+ `SHA-256 mismatch: expected ${sha256}, got ${actualSha}`,
187
+ "SHA_MISMATCH",
188
+ );
189
+ }
190
+
191
+ assertPathMatchesDescriptor(
192
+ partialPath,
193
+ partialFd,
194
+ "verified partial download",
195
+ );
196
+ if (sha256Descriptor(partialFd) !== actualSha) {
197
+ throw new DownloadError(
198
+ "partial download bytes changed after streaming verification",
199
+ "PARTIAL_CHANGED",
200
+ );
201
+ }
202
+
203
+ // Re-check immediately before the commit point. renameSync atomically
204
+ // replaces an existing regular file; there is deliberately no unlink gap.
205
+ assertSafeFileOrMissing(outputPath, "output path");
206
+ assertOwnedDownloadLock();
207
+ previousOutput = snapshotExistingOutput(outputPath);
208
+ try {
209
+ assertPathMatchesDescriptor(
210
+ partialPath,
211
+ partialFd,
212
+ "verified partial download",
213
+ );
214
+ assertOutputMatchesSnapshotForCommit(outputPath, previousOutput);
215
+ // Snapshotting and hashing the previous output can take long enough for
216
+ // lock ownership to change. Keep the final ownership check adjacent to
217
+ // the commit point so a lock already known to be foreign is never
218
+ // followed by an output mutation.
219
+ assertOwnedDownloadLock();
220
+ fs.renameSync(partialPath, outputPath);
221
+ committedPath = true;
222
+ assertOwnedDownloadLock();
223
+ assertPathMatchesDescriptor(outputPath, partialFd, "committed download");
224
+ if (sha256Descriptor(partialFd) !== actualSha) {
225
+ throw new DownloadError(
226
+ "committed download bytes changed before final verification",
227
+ "COMMIT_CHANGED",
228
+ );
229
+ }
230
+ fsyncDirectory(path.dirname(outputPath));
231
+ assertOwnedDownloadLock();
232
+ if (previousOutput) {
233
+ discardPreviousOutputSnapshot(previousOutput);
234
+ previousOutput = null;
235
+ }
236
+ } catch (err) {
237
+ let recoveryError = null;
238
+ let manualRecoveryError = null;
239
+ if (err instanceof DownloadError && err.code === "DOWNLOAD_LOCK_LOST") {
240
+ if (previousOutput) previousOutput.retained = true;
241
+ retainLockForRecovery = true;
242
+ if (committedPath) {
243
+ throw createOutputRecoveryRequiredError({
244
+ cause: err,
245
+ outputPath,
246
+ snapshot: previousOutput,
247
+ });
248
+ }
249
+ throw err;
250
+ }
251
+ if (!committedPath && err?.retainDownloadLock === true) {
252
+ if (previousOutput) previousOutput.retained = true;
253
+ retainLockForRecovery = true;
254
+ throw err;
255
+ }
256
+ if (
257
+ committedPath &&
258
+ err?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
259
+ err?.recoveryArtifactRemoved === true
260
+ ) {
261
+ // The verified output is committed and the recovery name is gone in
262
+ // the live namespace, but the unlink could not be made durable. There
263
+ // is no safe rollback source anymore, so preserve the output and lock.
264
+ previousOutput = null;
265
+ retainLockForRecovery = true;
266
+ throw err;
267
+ }
268
+ if (committedPath) {
269
+ if (previousOutput) {
270
+ try {
271
+ assertOwnedDownloadLock();
272
+ const committedOutput = snapshotOpenFile(
273
+ outputPath,
274
+ partialFd,
275
+ "committed download before recovery",
276
+ );
277
+ fs.closeSync(partialFd);
278
+ partialFd = null;
279
+ restorePreviousOutput({
280
+ snapshot: previousOutput,
281
+ outputPath,
282
+ committedOutput,
283
+ assertLockOwned: assertOwnedDownloadLock,
284
+ });
285
+ previousOutput = null;
286
+ committedPath = false;
287
+ } catch (restoreError) {
288
+ retainLockForRecovery = true;
289
+ if (restoreError?.code === "DOWNLOAD_LOCK_LOST") {
290
+ previousOutput.retained = true;
291
+ manualRecoveryError = createOutputRecoveryRequiredError({
292
+ cause: restoreError,
293
+ outputPath,
294
+ snapshot: previousOutput,
295
+ });
296
+ } else if (
297
+ restoreError?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
298
+ restoreError?.recoveryArtifactRemoved === true &&
299
+ restoreError?.outputRestored === true
300
+ ) {
301
+ previousOutput = null;
302
+ committedPath = false;
303
+ recoveryError = restoreError;
304
+ } else {
305
+ previousOutput.retained = true;
306
+ recoveryError = restoreError;
307
+ }
308
+ }
309
+ } else {
310
+ let lockStillOwned = false;
311
+ let lockOwnershipError = null;
312
+ try {
313
+ assertOwnedDownloadLock();
314
+ lockStillOwned = true;
315
+ } catch (error) {
316
+ lockOwnershipError = error;
317
+ }
318
+ if (lockOwnershipError?.code === "DOWNLOAD_LOCK_LOST") {
319
+ manualRecoveryError = createOutputRecoveryRequiredError({
320
+ cause: lockOwnershipError,
321
+ outputPath,
322
+ snapshot: null,
323
+ });
324
+ } else if (lockStillOwned) {
325
+ removeRegularFileIfPresent(outputPath);
326
+ } else {
327
+ removePathIfMatchesDescriptor(outputPath, partialFd);
328
+ }
329
+ }
330
+ }
331
+ if (manualRecoveryError) throw manualRecoveryError;
332
+ if (!committedPath && previousOutput && !previousOutput.retained) {
333
+ try {
334
+ discardPreviousOutputSnapshot(previousOutput);
335
+ previousOutput = null;
336
+ } catch (cleanupError) {
337
+ retainLockForRecovery = true;
338
+ if (cleanupError?.recoveryArtifactRemoved === true) {
339
+ previousOutput = null;
340
+ } else {
341
+ previousOutput.retained = true;
342
+ }
343
+ recoveryError ||= cleanupError;
344
+ }
345
+ }
346
+ if (recoveryError) {
347
+ if (
348
+ recoveryError?.code === "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED" &&
349
+ recoveryError?.recoveryArtifactRemoved === true
350
+ ) {
351
+ throw recoveryError;
352
+ }
353
+ throw new DownloadError(
354
+ `could not restore the previous output after commit failure (${err.code || err.message}): ${recoveryError.message}; recovery artifact retained at ${previousOutput?.recoveryPath || "unknown"}`,
355
+ "OUTPUT_RECOVERY_FAILED",
356
+ );
357
+ }
358
+ if (err instanceof DownloadError && err.code === "DOWNLOAD_LOCK_LOST") {
359
+ throw err;
360
+ }
361
+ throw new DownloadError(
362
+ `could not commit verified artifact: ${err.message}`,
363
+ "FINALIZE_FAILED",
364
+ );
365
+ }
366
+
367
+ try {
368
+ fs.closeSync(partialFd);
369
+ } catch {
370
+ /* committed bytes and transaction state are already verified */
371
+ }
372
+ partialFd = null;
373
+ return { outputPath, bytes, sha256: actualSha };
374
+ } catch (error) {
375
+ if (
376
+ error?.code === "DOWNLOAD_LOCK_LOST" ||
377
+ error?.retainDownloadLock === true
378
+ ) {
379
+ retainLockForRecovery = true;
380
+ }
381
+ throw error;
382
+ } finally {
383
+ if (partialFd !== null) {
384
+ try {
385
+ fs.closeSync(partialFd);
386
+ } catch {
387
+ /* best effort */
388
+ }
389
+ }
390
+ if (!committedPath && partialIdentity) {
391
+ removePathIfMatchesIdentity(partialPath, partialIdentity);
392
+ }
393
+ if (retainLockForRecovery) retainLock(lock);
394
+ else releaseLock(lock);
60
395
  }
396
+ }
61
397
 
62
- let response;
398
+ async function awaitBeforeDeadline(
399
+ promise,
400
+ { deadline, abortController, label },
401
+ ) {
402
+ const remaining = deadline - Date.now();
403
+ if (remaining <= 0) {
404
+ abortController.abort();
405
+ throw new DownloadError(`${label} timed out`, "TIMEOUT");
406
+ }
407
+ let timer;
63
408
  try {
64
- response = await fetchImpl(url, { headers: { Accept: "*/*" } });
65
- } catch (err) {
409
+ return await Promise.race([
410
+ promise,
411
+ new Promise((_, reject) => {
412
+ timer = setTimeout(() => {
413
+ abortController.abort();
414
+ reject(new DownloadError(`${label} timed out`, "TIMEOUT"));
415
+ }, remaining);
416
+ }),
417
+ ]);
418
+ } finally {
419
+ clearTimeout(timer);
420
+ }
421
+ }
422
+
423
+ function assertPathMatchesDescriptor(filePath, fd, label) {
424
+ let descriptorStat;
425
+ try {
426
+ descriptorStat = fs.fstatSync(fd, { bigint: true });
427
+ } catch (error) {
66
428
  throw new DownloadError(
67
- `fetch failed: ${err.message}`,
68
- err?.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR",
429
+ `${label} identity could not be verified: ${error.message}`,
430
+ "PARTIAL_REPLACED",
69
431
  );
70
432
  }
71
- if (!response.ok) {
433
+ if (!pathMatchesOpenedFileIdentitySync(filePath, descriptorStat)) {
72
434
  throw new DownloadError(
73
- `artifact fetch failed: HTTP ${response.status}`,
74
- "FETCH_FAILED",
435
+ `${label} pathname no longer identifies the verified file`,
436
+ "PARTIAL_REPLACED",
75
437
  );
76
438
  }
439
+ }
440
+
441
+ function removePathIfMatchesDescriptor(filePath, fd) {
442
+ try {
443
+ const descriptorStat = fs.fstatSync(fd, { bigint: true });
444
+ if (pathMatchesOpenedFileIdentitySync(filePath, descriptorStat)) {
445
+ fs.unlinkSync(filePath);
446
+ return true;
447
+ }
448
+ } catch {
449
+ /* a missing or exchanged pathname must not be removed */
450
+ }
451
+ return false;
452
+ }
77
453
 
78
- const totalRaw = response.headers?.get?.("content-length");
79
- const total = totalRaw ? Number(totalRaw) : null;
80
- const body = response.body;
81
- if (!body) {
454
+ function snapshotDescriptorIdentity(fd) {
455
+ const stat = fs.fstatSync(fd, { bigint: true });
456
+ if (!stat.isFile() || !hasPreciseFileIdentity(stat)) {
82
457
  throw new DownloadError(
83
- "response has no body stream (fetch impl returned no body)",
84
- "NO_BODY",
458
+ "partial download descriptor has no stable file identity",
459
+ "PARTIAL_REPLACED",
85
460
  );
86
461
  }
462
+ return { dev: stat.dev, ino: stat.ino };
463
+ }
87
464
 
88
- const hasher = crypto.createHash("sha256");
89
- const out = fs.createWriteStream(partialPath);
90
- let bytes = 0;
465
+ function removePathIfMatchesIdentity(filePath, identity) {
466
+ try {
467
+ if (pathMatchesOpenedFileIdentitySync(filePath, identity)) {
468
+ fs.unlinkSync(filePath);
469
+ return true;
470
+ }
471
+ } catch {
472
+ /* a missing or exchanged pathname must not be removed */
473
+ }
474
+ return false;
475
+ }
476
+
477
+ function createOutputRecoveryRequiredError({ cause, outputPath, snapshot }) {
478
+ const recoveryPath = snapshot?.recoveryPath || null;
479
+ const retained = recoveryPath
480
+ ? `the verified previous output is retained at ${recoveryPath}`
481
+ : "no previous output snapshot exists";
482
+ const error = new DownloadError(
483
+ `download lock ownership was lost after the output commit (${cause?.message || "unknown"}); ${outputPath} was left untouched and ${retained}; inspect the output and recovery artifact manually`,
484
+ "OUTPUT_RECOVERY_REQUIRED",
485
+ );
486
+ error.recoveryPath = recoveryPath;
487
+ error.retainDownloadLock = true;
488
+ return error;
489
+ }
91
490
 
491
+ function createRecoveryCleanupSyncError(recoveryPath, cause) {
492
+ const error = new DownloadError(
493
+ `the recovery artifact was removed but its directory entry could not be synchronized (${cause?.message || "unknown"}); inspect ${path.dirname(recoveryPath)} before retrying`,
494
+ "OUTPUT_RECOVERY_CLEANUP_SYNC_FAILED",
495
+ );
496
+ error.recoveryPath = recoveryPath;
497
+ error.recoveryArtifactRemoved = true;
498
+ error.retainDownloadLock = true;
499
+ return error;
500
+ }
501
+
502
+ function createSnapshotRecoveryRequiredError(recoveryPath, cause) {
503
+ const retained = recoveryPath
504
+ ? `a recovery artifact was retained at ${recoveryPath}`
505
+ : "no verified recovery artifact exists";
506
+ const error = new DownloadError(
507
+ `the existing output changed while its recovery snapshot was being created (${cause?.code || cause?.message || "unknown"}); ${retained} and the download lock was retained for manual inspection`,
508
+ "OUTPUT_SNAPSHOT_RECOVERY_REQUIRED",
509
+ );
510
+ error.recoveryPath = recoveryPath;
511
+ error.retainDownloadLock = true;
512
+ return error;
513
+ }
514
+
515
+ function snapshotExistingOutput(outputPath) {
516
+ if (!lstatOrNull(outputPath)) return null;
517
+
518
+ const recoveryPath = `${outputPath}.recovery-${process.pid}-${crypto.randomUUID()}`;
519
+ assertSafeDownloadPath(recoveryPath, "output recovery path", true);
520
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
521
+ let fd = null;
522
+ let linked = false;
523
+ let snapshot = null;
92
524
  try {
93
- // Web ReadableStream exposes an async iterator in Node ≥18. We prefer
94
- // that over pumping via pipe() because we need to see each chunk for
95
- // hashing + progress, and we want the final rename deterministic.
96
- for await (const chunk of body) {
97
- // `chunk` is a Uint8Array
98
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
99
- hasher.update(buf);
100
- bytes += buf.length;
101
- const ok = out.write(buf);
102
- if (!ok) {
103
- await new Promise((r) => out.once("drain", r));
104
- }
105
- if (typeof onProgress === "function") {
525
+ fd = fs.openSync(outputPath, fs.constants.O_RDONLY | noFollow);
526
+ const recorded = snapshotOpenFile(outputPath, fd, "existing output");
527
+ snapshot = {
528
+ recoveryPath,
529
+ ...recorded,
530
+ retained: false,
531
+ };
532
+ fs.closeSync(fd);
533
+ fd = null;
534
+
535
+ // Windows cannot replace a pathname while the old inode has an open
536
+ // handle. Close the verifier first, then bind a sibling hard link to the
537
+ // recorded inode and re-open both names to close the exchange window.
538
+ fs.linkSync(outputPath, recoveryPath);
539
+ linked = true;
540
+ try {
541
+ assertPathMatchesSnapshot(outputPath, snapshot, "existing output");
542
+ assertPathMatchesSnapshot(
543
+ recoveryPath,
544
+ snapshot,
545
+ "existing output recovery snapshot",
546
+ );
547
+ } catch (validationError) {
548
+ snapshot.retained = true;
549
+ throw createSnapshotRecoveryRequiredError(recoveryPath, validationError);
550
+ }
551
+ fsyncDirectory(path.dirname(outputPath));
552
+ return snapshot;
553
+ } catch (error) {
554
+ let cleanupError = null;
555
+ if (error?.code === "OUTPUT_SNAPSHOT_RECOVERY_REQUIRED") {
556
+ cleanupError = error;
557
+ } else if (linked && snapshot) {
558
+ if (removePathIfMatchesSnapshot(recoveryPath, snapshot)) {
106
559
  try {
107
- onProgress({ bytes, total: Number.isFinite(total) ? total : null });
108
- } catch {
109
- /* progress callback errors must not interrupt the download */
560
+ fsyncDirectory(path.dirname(recoveryPath));
561
+ } catch (syncError) {
562
+ cleanupError = createRecoveryCleanupSyncError(
563
+ recoveryPath,
564
+ syncError,
565
+ );
110
566
  }
567
+ } else {
568
+ cleanupError = createSnapshotRecoveryRequiredError(recoveryPath, error);
569
+ }
570
+ }
571
+ if (fd !== null) {
572
+ try {
573
+ fs.closeSync(fd);
574
+ } catch {
575
+ /* best effort */
576
+ }
577
+ }
578
+ if (cleanupError) throw cleanupError;
579
+ if (error instanceof DownloadError) throw error;
580
+ throw new DownloadError(
581
+ `could not snapshot the existing output: ${error.message}`,
582
+ "OUTPUT_SNAPSHOT_FAILED",
583
+ );
584
+ }
585
+ }
586
+
587
+ function assertOutputMatchesSnapshotForCommit(outputPath, snapshot) {
588
+ if (!snapshot) {
589
+ if (lstatOrNull(outputPath)) {
590
+ throw createSnapshotRecoveryRequiredError(
591
+ null,
592
+ new DownloadError(
593
+ "output appeared after the no-output snapshot",
594
+ "OUTPUT_RECOVERY_CHANGED",
595
+ ),
596
+ );
597
+ }
598
+ return;
599
+ }
600
+
601
+ try {
602
+ assertPathMatchesSnapshot(
603
+ outputPath,
604
+ snapshot,
605
+ "existing output before commit",
606
+ );
607
+ } catch (error) {
608
+ snapshot.retained = true;
609
+ throw createSnapshotRecoveryRequiredError(snapshot.recoveryPath, error);
610
+ }
611
+ }
612
+
613
+ function snapshotOpenFile(filePath, fd, label) {
614
+ assertPathMatchesDescriptor(filePath, fd, label);
615
+ const before = fs.fstatSync(fd, { bigint: true });
616
+ const sha256 = sha256Descriptor(fd);
617
+ const after = fs.fstatSync(fd, { bigint: true });
618
+ if (!sameSnapshotState(before, after)) {
619
+ throw new DownloadError(
620
+ `${label} changed while it was being hashed`,
621
+ "OUTPUT_RECOVERY_CHANGED",
622
+ );
623
+ }
624
+ assertPathMatchesDescriptor(filePath, fd, label);
625
+ return {
626
+ sha256,
627
+ dev: after.dev,
628
+ ino: after.ino,
629
+ size: after.size,
630
+ mode: after.mode,
631
+ mtimeNs: after.mtimeNs,
632
+ };
633
+ }
634
+
635
+ function sameSnapshotState(first, second) {
636
+ return (
637
+ first.isFile() &&
638
+ second.isFile() &&
639
+ sameOpenedFileIdentity(first, second, ["size", "mode", "mtimeNs"])
640
+ );
641
+ }
642
+
643
+ function assertPreviousOutputSnapshot(snapshot) {
644
+ assertPathMatchesSnapshot(
645
+ snapshot.recoveryPath,
646
+ snapshot,
647
+ "existing output recovery snapshot",
648
+ );
649
+ }
650
+
651
+ function restorePreviousOutput({
652
+ snapshot,
653
+ outputPath,
654
+ committedOutput,
655
+ assertLockOwned,
656
+ }) {
657
+ const restorePath = `${outputPath}.restore-${process.pid}-${crypto.randomUUID()}`;
658
+ assertSafeDownloadPath(restorePath, "output restore path", true);
659
+ try {
660
+ assertLockOwned();
661
+ assertPathMatchesSnapshot(
662
+ outputPath,
663
+ committedOutput,
664
+ "committed download before recovery",
665
+ );
666
+ assertPreviousOutputSnapshot(snapshot);
667
+ fs.linkSync(snapshot.recoveryPath, restorePath);
668
+ assertPathMatchesSnapshot(
669
+ restorePath,
670
+ snapshot,
671
+ "output restore candidate",
672
+ );
673
+ assertPathMatchesSnapshot(
674
+ outputPath,
675
+ committedOutput,
676
+ "committed download before recovery",
677
+ );
678
+ assertLockOwned();
679
+ fs.renameSync(restorePath, outputPath);
680
+ assertPathMatchesSnapshot(outputPath, snapshot, "restored previous output");
681
+ fsyncDirectory(path.dirname(outputPath));
682
+ try {
683
+ discardPreviousOutputSnapshot(snapshot);
684
+ } catch (error) {
685
+ if (error?.recoveryArtifactRemoved === true) {
686
+ error.outputRestored = true;
111
687
  }
688
+ throw error;
112
689
  }
113
- await new Promise((resolve, reject) => {
114
- out.end((err) => (err ? reject(err) : resolve()));
690
+ } finally {
691
+ removePathIfMatchesSnapshot(restorePath, snapshot);
692
+ }
693
+ }
694
+
695
+ function discardPreviousOutputSnapshot(snapshot) {
696
+ assertPathMatchesSnapshot(
697
+ snapshot.recoveryPath,
698
+ snapshot,
699
+ "existing output recovery snapshot",
700
+ );
701
+ if (!removePathIfMatchesSnapshot(snapshot.recoveryPath, snapshot)) {
702
+ throw new DownloadError(
703
+ "existing output recovery snapshot could not be removed safely",
704
+ "OUTPUT_RECOVERY_CLEANUP_FAILED",
705
+ );
706
+ }
707
+ try {
708
+ fsyncDirectory(path.dirname(snapshot.recoveryPath));
709
+ } catch (error) {
710
+ throw createRecoveryCleanupSyncError(snapshot.recoveryPath, error);
711
+ }
712
+ }
713
+
714
+ function assertPathMatchesSnapshot(filePath, snapshot, label) {
715
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
716
+ let fd = null;
717
+ try {
718
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
719
+ assertPathMatchesDescriptor(filePath, fd, label);
720
+ const before = fs.fstatSync(fd, { bigint: true });
721
+ const sha256 = sha256Descriptor(fd);
722
+ const after = fs.fstatSync(fd, { bigint: true });
723
+ if (
724
+ !sameSnapshotState(before, after) ||
725
+ after.dev !== snapshot.dev ||
726
+ after.ino !== snapshot.ino ||
727
+ after.size !== snapshot.size ||
728
+ after.mode !== snapshot.mode ||
729
+ after.mtimeNs !== snapshot.mtimeNs ||
730
+ sha256 !== snapshot.sha256
731
+ ) {
732
+ throw new DownloadError(
733
+ `${label} changed before it could be used safely`,
734
+ "OUTPUT_RECOVERY_CHANGED",
735
+ );
736
+ }
737
+ } finally {
738
+ if (fd !== null) fs.closeSync(fd);
739
+ }
740
+ }
741
+
742
+ function removePathIfMatchesSnapshot(filePath, snapshot) {
743
+ try {
744
+ assertPathMatchesSnapshot(filePath, snapshot, "output recovery artifact");
745
+ if (pathMatchesOpenedFileIdentitySync(filePath, snapshot)) {
746
+ fs.unlinkSync(filePath);
747
+ return true;
748
+ }
749
+ } catch {
750
+ /* exchanged or missing recovery paths are retained */
751
+ }
752
+ return false;
753
+ }
754
+
755
+ function sha256Descriptor(fd) {
756
+ const hash = crypto.createHash("sha256");
757
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
758
+ let position = 0;
759
+ while (true) {
760
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position);
761
+ if (bytesRead === 0) break;
762
+ hash.update(buffer.subarray(0, bytesRead));
763
+ position += bytesRead;
764
+ }
765
+ return hash.digest("hex");
766
+ }
767
+
768
+ function writeAllSync(fd, buffer) {
769
+ let offset = 0;
770
+ while (offset < buffer.length) {
771
+ offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
772
+ }
773
+ }
774
+
775
+ function lstatOrNull(filePath) {
776
+ try {
777
+ return fs.lstatSync(filePath);
778
+ } catch (err) {
779
+ if (err?.code === "ENOENT") return null;
780
+ throw err;
781
+ }
782
+ }
783
+
784
+ function assertSafeFileOrMissing(filePath, label) {
785
+ try {
786
+ assertStateSafeRegularFile(filePath, {
787
+ label,
788
+ allowMissingLeaf: true,
115
789
  });
790
+ } catch (error) {
791
+ throw new DownloadError(error.message, "UNSAFE_PATH");
792
+ }
793
+ }
794
+
795
+ function assertSafeDownloadPath(filePath, label, allowMissingLeaf) {
796
+ try {
797
+ assertSafePathAncestors(filePath, { label, allowMissingLeaf });
798
+ } catch (error) {
799
+ throw new DownloadError(error.message, "UNSAFE_PATH");
800
+ }
801
+ }
802
+
803
+ function acquireLock(lockPath) {
804
+ assertSafeDownloadPath(lockPath, "download lock", true);
805
+ const existing = lstatOrNull(lockPath);
806
+ if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
807
+ throw new DownloadError(
808
+ `download lock is not a regular file: ${lockPath}`,
809
+ "UNSAFE_PATH",
810
+ );
811
+ }
812
+
813
+ const token = `${process.pid}:${crypto.randomUUID()}`;
814
+ let fd;
815
+ try {
816
+ fd = fs.openSync(lockPath, "wx+", 0o600);
116
817
  } catch (err) {
117
- try {
118
- out.destroy();
119
- fs.unlinkSync(partialPath);
120
- } catch {
121
- /* best effort */
818
+ if (err?.code === "EEXIST") {
819
+ throw new DownloadError(
820
+ `another download is already using ${outputFromLock(lockPath)}`,
821
+ "DOWNLOAD_LOCKED",
822
+ );
122
823
  }
123
- throw new DownloadError(`stream aborted: ${err.message}`, "STREAM_ERROR");
824
+ throw new DownloadError(
825
+ `could not acquire download lock: ${err.message}`,
826
+ "LOCK_FAILED",
827
+ );
124
828
  }
125
829
 
126
- const actualSha = hasher.digest("hex");
127
- if (actualSha !== sha256.toLowerCase()) {
128
- try {
129
- fs.unlinkSync(partialPath);
130
- } catch {
131
- /* best effort */
830
+ try {
831
+ fs.writeFileSync(fd, token, { encoding: "utf8" });
832
+ fs.fsyncSync(fd);
833
+ } catch (err) {
834
+ releaseLock({ fd, lockPath, token });
835
+ throw new DownloadError(
836
+ `could not initialize download lock: ${err.message}`,
837
+ "LOCK_FAILED",
838
+ );
839
+ }
840
+ return { fd, lockPath, token };
841
+ }
842
+
843
+ function assertDownloadLockOwned(lock) {
844
+ try {
845
+ const before = fs.fstatSync(lock.fd, { bigint: true });
846
+ const pathOwnedBefore = pathMatchesOpenedFileIdentitySync(
847
+ lock.lockPath,
848
+ before,
849
+ { stateFields: ["size", "mtimeNs", "ctimeNs"] },
850
+ );
851
+ const expected = Buffer.from(lock.token, "utf8");
852
+ const actual = Buffer.alloc(expected.length + 1);
853
+ const bytesRead = fs.readSync(lock.fd, actual, 0, actual.length, 0);
854
+ const after = fs.fstatSync(lock.fd, { bigint: true });
855
+ const pathOwnedAfter = pathMatchesOpenedFileIdentitySync(
856
+ lock.lockPath,
857
+ after,
858
+ { stateFields: ["size", "mtimeNs", "ctimeNs"] },
859
+ );
860
+ const descriptorStable =
861
+ before.isFile() &&
862
+ after.isFile() &&
863
+ before.nlink > 0n &&
864
+ after.nlink > 0n &&
865
+ sameOpenedFileIdentity(before, after, ["size", "mtimeNs", "ctimeNs"]);
866
+ const pathStillOwned = pathOwnedBefore && pathOwnedAfter;
867
+ const tokenMatches =
868
+ bytesRead === expected.length &&
869
+ actual.subarray(0, bytesRead).equals(expected);
870
+ if (!descriptorStable || !pathStillOwned || !tokenMatches) {
871
+ throw new Error("lock pathname, descriptor, or token changed");
132
872
  }
873
+ } catch (error) {
133
874
  throw new DownloadError(
134
- `SHA-256 mismatch: expected ${sha256}, got ${actualSha}`,
135
- "SHA_MISMATCH",
875
+ `download lock ownership was lost: ${error.message}`,
876
+ "DOWNLOAD_LOCK_LOST",
136
877
  );
137
878
  }
879
+ }
880
+
881
+ function outputFromLock(lockPath) {
882
+ return lockPath.endsWith(".lock") ? lockPath.slice(0, -5) : lockPath;
883
+ }
138
884
 
139
- // Atomic rename — on Windows this fails if the destination exists; delete
140
- // first. On POSIX rename atomically replaces, but the extra unlink is
141
- // harmless and keeps behavior symmetric across platforms.
885
+ function releaseLock(lock) {
886
+ if (!lock) return;
887
+ closeLockHandle(lock);
142
888
  try {
143
- fs.unlinkSync(outputPath);
889
+ const stat = fs.lstatSync(lock.lockPath);
890
+ if (stat.isSymbolicLink() || !stat.isFile()) return;
891
+ if (fs.readFileSync(lock.lockPath, "utf8") === lock.token) {
892
+ fs.unlinkSync(lock.lockPath);
893
+ }
144
894
  } catch {
145
- /* no prior artifact */
895
+ /* best effort; a stale lock fails closed on the next attempt */
146
896
  }
147
- fs.renameSync(partialPath, outputPath);
897
+ }
148
898
 
149
- return { outputPath, bytes, sha256: actualSha };
899
+ function retainLock(lock) {
900
+ closeLockHandle(lock);
150
901
  }
151
902
 
152
- /**
153
- * Typed error with a machine-readable `code`. The `cc pack check-update
154
- * --download` command turns these into friendly messages + exit codes.
155
- */
903
+ function closeLockHandle(lock) {
904
+ if (!lock || lock.fd === null) return;
905
+ try {
906
+ fs.closeSync(lock.fd);
907
+ } catch {
908
+ /* best effort */
909
+ }
910
+ lock.fd = null;
911
+ }
912
+
913
+ function removeRegularFileIfPresent(filePath) {
914
+ try {
915
+ const stat = fs.lstatSync(filePath);
916
+ if (stat.isFile() && !stat.isSymbolicLink()) fs.unlinkSync(filePath);
917
+ } catch {
918
+ /* missing or unsafe paths are intentionally left alone */
919
+ }
920
+ }
921
+
922
+ function fsyncDirectory(directoryPath) {
923
+ let fd;
924
+ try {
925
+ fd = fs.openSync(directoryPath, "r");
926
+ fs.fsyncSync(fd);
927
+ } catch (err) {
928
+ // Windows and a few filesystems do not expose directory fsync. The file
929
+ // itself was already fsynced, so only ignore known unsupported cases.
930
+ if (!["EACCES", "EBADF", "EINVAL", "EISDIR", "EPERM"].includes(err?.code)) {
931
+ throw err;
932
+ }
933
+ } finally {
934
+ if (fd !== undefined) {
935
+ try {
936
+ fs.closeSync(fd);
937
+ } catch {
938
+ /* best effort */
939
+ }
940
+ }
941
+ }
942
+ }
943
+
944
+ /** Typed error with a stable, machine-readable code. */
156
945
  export class DownloadError extends Error {
157
946
  constructor(message, code) {
158
947
  super(message);