chainlesschain 0.162.95 → 0.162.97

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 (219) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-C1YvIYh5.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-jqcXEakj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-C8Xwqd3_.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-Dx5ypTfA.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-1GZ4Tv2f.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CexR2hyk.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-DwAruKdy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-W2FkU5BY.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-BCLnhLfu.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Dkh5JY5B.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-LfYOgsdA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-DEgketAC.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-LeFQMaZm.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CrlKYvBc.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-ca_4PGYF.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-BcMWqh2a.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-pVVEA3dw.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-CiKSGTqp.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID-B2bGzdOj.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-sQ6jcgCn.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-rFx_8P04.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-smw2hh6f.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-CZhg2_S2.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-DQE-368G.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-2XPb9izr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-DCGMz5E6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-BFwrxK4Q.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-ChxBRwG1.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-C7kyknu_.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-BsMd4XAt.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Bs3yrpow.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-Cc29jdBv.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-zYpYsUsl.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-x9r4yKtS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-CjUo8prx.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-Cmz4wVrF.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-Y-SBnNxm.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-ogiKnrxj.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C1hygkFR.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-fWWYiTsY.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-CkCNF4gC.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-DThB7WQG.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-Dh64aMq6.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-BSY16zvP.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-C_TDh18K.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CvxZk6og.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-0JZ5JF4M.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-DV8ubpjM.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-g2Dgls8e.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-DGp8zQ0C.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-C0uHrjWR.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-2XGICaFt.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Cfy72e0Q.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-CXydUXp8.js → Projects-Xsvk_kKT.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-vuvDVg7U.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-BUy7kPyQ.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-DlzpC_JE.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-BiWFwPMh.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-dmfZBK-k.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-Cf6tlRKq.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DhzXDRAJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-C-bTfgeJ.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-D3KNwBqu.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CHH8i1xY.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-DHiONX0z.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DwWMb-Ui.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-DAq64iWB.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-DJU-kHko.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-RAIzA3u-.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-By4cbadT.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-kCAXsxWu.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-D8pup_CO.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-Bk2c9TUI.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-B0wTiZRa.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-Ca_9t0gk.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-BnfNDDDm.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-fnlF2-Yr.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-Cfshhzyw.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-BWMpDOLG.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn-B6xeXZvw.js} +3 -3
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-Cb3-FPCE.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CP_opuMK.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-RLzfjxRQ.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-BTRloNKV.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-gXgXTLlU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-CYlmApo_.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-CrIpxkRC.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-AsI1SqnE.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B3L5vTSs.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-B5sN_pwd.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-BEcRPNHv.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BVrcoJe2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BewxG36A.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-Bi87H9yk.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-Bl4OQ--4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-BlE2lCS-.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-BlS91Ac0.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-Bm0cNeeQ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-Bo2Fl2j7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-Bsz3X-aU.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-BurvDWMS.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-BzBLtthb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-C0wmEN2j.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-C4bBR-Vd.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-COhACL0I.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-CQ-cnhZM.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-CRr99L7q.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-CaTkUkJp.js} +3 -3
  112. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CfOjPxso.js} +1 -1
  113. package/src/assets/web-panel/assets/index-Cf_SLgtj.js +1 -0
  114. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-Ch_TYQNW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CisGGaF3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-DA-_2qTs.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DYBIDVGn.js +1 -0
  118. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-DZxK7MW9.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DiXnGU0A.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-DsiyfyBE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-G_OgtHiC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-IjV-dnE8.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-Mltfj22U.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-U-kmEJdu.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-cgf-OVry.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-ch8zcf-h.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-cqOT5RiJ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-cywkZJ-I.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-sE_I7gVL.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-DMwnVZzH.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-Dnpjpvo7.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-iuRh5U9Y.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-Bl4WRV59.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-B9KSXaep.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-TEGIow4r.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve--BECM92v.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-Ca8G7XbL.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-8QPG_T3D.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-p3FsA6mg.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-Ct0yIzrp.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-3nlByl_3.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-N3OsoSRM.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-Cfe3ywq3.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-DVBuDS_3.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +4 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/agents.js +18 -4
  149. package/src/commands/ask.js +13 -0
  150. package/src/commands/command.js +12 -2
  151. package/src/commands/config.js +17 -1
  152. package/src/commands/cost.js +6 -1
  153. package/src/commands/dao.js +4 -3
  154. package/src/commands/dlp.js +2 -1
  155. package/src/commands/hub.js +138 -0
  156. package/src/commands/mtc.js +37 -1
  157. package/src/gateways/discord/discord-formatter.js +18 -1
  158. package/src/gateways/telegram/telegram-formatter.js +6 -3
  159. package/src/gateways/ws/ws-server.js +19 -1
  160. package/src/harness/plugin-manager.js +20 -0
  161. package/src/harness/worktree-isolator.js +17 -1
  162. package/src/index.js +2 -0
  163. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  164. package/src/lib/a2a-protocol.js +24 -9
  165. package/src/lib/agent-core.js +4 -5
  166. package/src/lib/audit-mtc.js +40 -2
  167. package/src/lib/chat-core.js +146 -18
  168. package/src/lib/cli-anything-bridge.js +5 -0
  169. package/src/lib/cli-numeric.js +38 -0
  170. package/src/lib/config-manager.js +37 -6
  171. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  172. package/src/lib/cowork-cron.js +45 -34
  173. package/src/lib/cowork-learning.js +31 -1
  174. package/src/lib/cowork-share.js +39 -3
  175. package/src/lib/cowork-template-marketplace.js +54 -2
  176. package/src/lib/cowork-workflow.js +33 -6
  177. package/src/lib/cross-chain-mtc.js +41 -12
  178. package/src/lib/crypto-manager.js +12 -2
  179. package/src/lib/dbevo.js +19 -8
  180. package/src/lib/downloader.js +24 -9
  181. package/src/lib/evomap-governance.js +4 -4
  182. package/src/lib/evomap-manager.js +54 -7
  183. package/src/lib/fatal-handler.js +48 -0
  184. package/src/lib/file-checkpoint.js +26 -0
  185. package/src/lib/git-integration.js +28 -0
  186. package/src/lib/goal-store.js +36 -10
  187. package/src/lib/hierarchical-memory.js +6 -0
  188. package/src/lib/llm-pricing.js +52 -2
  189. package/src/lib/mcp-oauth.js +37 -7
  190. package/src/lib/memory-manager.js +23 -2
  191. package/src/lib/model-deprecation.js +249 -0
  192. package/src/lib/pdh-feedback-ledger.js +131 -0
  193. package/src/lib/permanent-memory.js +30 -2
  194. package/src/lib/permission-rules.cjs +11 -3
  195. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  196. package/src/lib/personal-data-hub-wiring.js +39 -16
  197. package/src/lib/project-instructions.js +10 -4
  198. package/src/lib/runnable-provider.js +62 -31
  199. package/src/lib/session-usage.js +34 -1
  200. package/src/lib/skill-loader.js +4 -1
  201. package/src/lib/skill-packs/generator.js +25 -2
  202. package/src/lib/stream-retry.js +56 -0
  203. package/src/lib/sync-credentials.js +53 -17
  204. package/src/lib/token-tracker.js +12 -2
  205. package/src/lib/with-file-lock.js +87 -0
  206. package/src/repl/agent-repl.js +47 -7
  207. package/src/repl/chat-repl.js +19 -0
  208. package/src/repl/session-cost.js +31 -3
  209. package/src/runtime/agent-core.js +478 -53
  210. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  211. package/src/runtime/coding-agent-policy.cjs +10 -0
  212. package/src/runtime/file-ref-expander.js +29 -1
  213. package/src/runtime/headless-runner.js +12 -1
  214. package/src/runtime/headless-stream.js +200 -9
  215. package/src/skills/video-editing/tools/commit.js +15 -1
  216. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  217. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  218. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  219. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -5,6 +5,21 @@
5
5
  * capability negotiation, and peer discovery.
6
6
  */
7
7
 
8
+
9
+ // Safe JSON-array parse for values read back from the DB (capabilities / skills /
10
+ // history / artifacts are stored as JSON strings). Written via JSON.stringify so
11
+ // normally valid, but a corrupted or externally-edited DB must not crash agent
12
+ // discovery / task lifecycle with an uncaught SyntaxError — fall back to [].
13
+ function parseJsonArray(value) {
14
+ if (value == null || value === "") return [];
15
+ try {
16
+ const parsed = JSON.parse(value);
17
+ return Array.isArray(parsed) ? parsed : [];
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
8
23
  // ─── Task statuses ───────────────────────────────────────────────
9
24
  export const TASK_STATUS = {
10
25
  SUBMITTED: "submitted",
@@ -165,8 +180,8 @@ export function discoverAgents(db, filter = {}) {
165
180
  // Parse JSON fields
166
181
  rows = rows.map((r) => ({
167
182
  ...r,
168
- capabilities: JSON.parse(r.capabilities || "[]"),
169
- skills: JSON.parse(r.skills || "[]"),
183
+ capabilities: parseJsonArray(r.capabilities),
184
+ skills: parseJsonArray(r.skills),
170
185
  }));
171
186
 
172
187
  if (filter.capability) {
@@ -220,7 +235,7 @@ export function completeTask(db, taskId, output, artifacts = []) {
220
235
 
221
236
  const now = nowISO();
222
237
  const task = _getTask(db, taskId);
223
- const history = JSON.parse(task.history || "[]");
238
+ const history = parseJsonArray(task.history);
224
239
  history.push({ status: TASK_STATUS.COMPLETED, timestamp: now });
225
240
 
226
241
  db.prepare(
@@ -249,7 +264,7 @@ export function failTask(db, taskId, error) {
249
264
 
250
265
  const now = nowISO();
251
266
  const task = _getTask(db, taskId);
252
- const history = JSON.parse(task.history || "[]");
267
+ const history = parseJsonArray(task.history);
253
268
  history.push({ status: TASK_STATUS.FAILED, timestamp: now });
254
269
 
255
270
  db.prepare(
@@ -276,8 +291,8 @@ export function getTaskStatus(db, taskId) {
276
291
  const task = _getTask(db, taskId);
277
292
  return {
278
293
  ...task,
279
- history: JSON.parse(task.history || "[]"),
280
- artifacts: JSON.parse(task.artifacts || "[]"),
294
+ history: parseJsonArray(task.history),
295
+ artifacts: parseJsonArray(task.artifacts),
281
296
  };
282
297
  }
283
298
 
@@ -305,7 +320,7 @@ export function negotiateCapability(db, agentId, requiredCapabilities) {
305
320
  .get(agentId);
306
321
  if (!card) throw new Error(`Agent not found: ${agentId}`);
307
322
 
308
- const agentCaps = JSON.parse(card.capabilities || "[]");
323
+ const agentCaps = parseJsonArray(card.capabilities);
309
324
  const supported = requiredCapabilities.filter((c) => agentCaps.includes(c));
310
325
  const missing = requiredCapabilities.filter((c) => !agentCaps.includes(c));
311
326
 
@@ -329,8 +344,8 @@ export function listPeers(db) {
329
344
  .all();
330
345
  return rows.map((r) => ({
331
346
  ...r,
332
- capabilities: JSON.parse(r.capabilities || "[]"),
333
- skills: JSON.parse(r.skills || "[]"),
347
+ capabilities: parseJsonArray(r.capabilities),
348
+ skills: parseJsonArray(r.skills),
334
349
  }));
335
350
  }
336
351
 
@@ -1,8 +1,4 @@
1
- /**
2
- * @deprecated Re-export shim; canonical impl is `../runtime/agent-core.js`
3
- * (CLI Runtime Convergence, Phase 6b). Import from there in new code.
4
- */
5
-
1
+ /** @deprecated Thin re-export shim; canonical impl is `../runtime/agent-core.js` (CLI Runtime Convergence, Phase 6b) — import from there in new code. */
6
2
  export {
7
3
  AGENT_TOOLS,
8
4
  AGENT_TOOL_REGISTRY,
@@ -16,6 +12,8 @@ export {
16
12
  buildSystemPrompt,
17
13
  executeTool,
18
14
  computeProposedEdit,
15
+ editNotebookCell,
16
+ renderNotebook,
19
17
  classifyError,
20
18
  isValidPackageName,
21
19
  chatWithTools,
@@ -33,6 +31,7 @@ export {
33
31
  _streamErrorDisposition,
34
32
  _isRetryableStreamError,
35
33
  _retryStreamingChat,
34
+ _iterateStreamWithStall,
36
35
  _toAnthropicMessages,
37
36
  _anthropicThinkingParams,
38
37
  _normalizeAnthropicResponse,
@@ -22,6 +22,7 @@ import path from "node:path";
22
22
  import crypto from "node:crypto";
23
23
  import { ed25519 as nobleEd25519 } from "@noble/curves/ed25519.js";
24
24
  import mtcLib from "@chainlesschain/core-mtc";
25
+ import { withFileLock } from "./with-file-lock.js";
25
26
 
26
27
  const { sha256, jcs, encodeHashStr, ed25519, assembleBatch } = mtcLib;
27
28
 
@@ -70,6 +71,30 @@ function ensureDirs(dir) {
70
71
  }
71
72
  }
72
73
 
74
+ /**
75
+ * Atomically write a UTF-8 file (temp sibling + rename). Used for the config
76
+ * and per-event staging records: a crash mid-write would truncate config.json
77
+ * or leave a partial event that closeBatch silently drops as malformed. Temp
78
+ * names end in `.tmp` (never `.json`), so a hard-crash leftover is ignored by
79
+ * listStagingEvents' `.json` filter. NOTE: the issuer key (flag "wx" exclusive
80
+ * create) and closeBatch (whole tmp-dir rename) have their own atomicity and
81
+ * are intentionally left untouched.
82
+ */
83
+ function atomicWriteFileSync(filePath, data) {
84
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
85
+ try {
86
+ fs.writeFileSync(tmp, data, "utf-8");
87
+ fs.renameSync(tmp, filePath);
88
+ } catch (err) {
89
+ try {
90
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
91
+ } catch {
92
+ /* best-effort temp cleanup */
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+
73
98
  // ─────────────────────────────────────────────────────────────────────
74
99
  // Config
75
100
  // ─────────────────────────────────────────────────────────────────────
@@ -107,7 +132,7 @@ export function saveAuditMtcConfig(dir, patch) {
107
132
  if (typeof merged.issuer !== "string" || !merged.issuer) {
108
133
  throw new TypeError("config.issuer must be non-empty string");
109
134
  }
110
- fs.writeFileSync(configPath(dir), JSON.stringify(merged, null, 2), "utf-8");
135
+ atomicWriteFileSync(configPath(dir), JSON.stringify(merged, null, 2));
111
136
  return merged;
112
137
  }
113
138
 
@@ -227,7 +252,7 @@ export function emitEvent(dir, body, opts = {}) {
227
252
  };
228
253
 
229
254
  const filePath = path.join(stagingDir(dir), `${eventId}.json`);
230
- fs.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
255
+ atomicWriteFileSync(filePath, JSON.stringify(record, null, 2));
231
256
  return { eventId, path: filePath, record };
232
257
  }
233
258
 
@@ -293,6 +318,19 @@ function listStagingEvents(dir) {
293
318
  * @param {string} [opts.issuer] - override config.issuer
294
319
  */
295
320
  export function closeBatch(dir, opts = {}) {
321
+ // Serialize the whole staging-scan → batch-write → staging-cleanup section
322
+ // across processes. Without it, two concurrent closes (e.g. overlapping
323
+ // `cc audit mtc reconcile` runs) both scan the same staging events, allocate
324
+ // the same batch id, and the defensive rmSync(finalDir) lets the second
325
+ // silently overwrite the first's audit batch. Best-effort lock (never hangs);
326
+ // generous timeout since the section includes Merkle assembly + signing.
327
+ ensureDirs(dir);
328
+ return withFileLock(batchesDir(dir), () => _closeBatchImpl(dir, opts), {
329
+ timeoutMs: 15000,
330
+ });
331
+ }
332
+
333
+ function _closeBatchImpl(dir, opts = {}) {
296
334
  ensureDirs(dir);
297
335
  const cfg = loadAuditMtcConfig(dir);
298
336
  const events = listStagingEvents(dir);
@@ -11,6 +11,11 @@
11
11
 
12
12
  import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
13
13
  import { appendTokenUsage } from "../harness/jsonl-session-store.js";
14
+ import {
15
+ isRetryableStreamError,
16
+ STREAM_RETRY_MAX,
17
+ STREAM_RETRY_BASE_MS,
18
+ } from "./stream-retry.js";
14
19
 
15
20
  // A streaming chat call must not hang forever if the API accepts the connection
16
21
  // but then goes silent (TCP up, no bytes). Abort the request if no data arrives
@@ -19,18 +24,48 @@ import { appendTokenUsage } from "../harness/jsonl-session-store.js";
19
24
  // generous (and env-overridable) so a slow local model's first token isn't cut
20
25
  // off; raise CC_CHAT_STALL_MS if you run very large local models.
21
26
  export const STREAM_STALL_MS = Number(process.env.CC_CHAT_STALL_MS) || 180000;
27
+ // Before the hard abort above, surface a "waiting for API response" hint once
28
+ // per silent gap (cc agent parity — agent-core STREAM_STALL_MS). Purely
29
+ // informational; never aborts. Env-overridable; disabled if it would exceed the
30
+ // abort threshold.
31
+ export const STREAM_STALL_HINT_MS =
32
+ Number(process.env.CC_CHAT_STALL_HINT_MS) || 20000;
22
33
 
23
- function makeStallGuard(stallMs = STREAM_STALL_MS) {
34
+ function makeStallGuard(
35
+ stallMs = STREAM_STALL_MS,
36
+ { onHint, hintMs = STREAM_STALL_HINT_MS } = {},
37
+ ) {
24
38
  const controller = new AbortController();
25
39
  let timer = null;
40
+ let hintTimer = null;
41
+ const hintArmed =
42
+ typeof onHint === "function" && hintMs > 0 && hintMs < stallMs;
26
43
  const bump = () => {
27
44
  if (timer) clearTimeout(timer);
28
45
  timer = setTimeout(() => controller.abort(), stallMs);
29
46
  if (timer && typeof timer.unref === "function") timer.unref();
47
+ if (hintArmed) {
48
+ if (hintTimer) clearTimeout(hintTimer);
49
+ hintTimer = setTimeout(() => {
50
+ try {
51
+ // 2nd arg = the hard abort deadline (stallMs) so the hint can tell
52
+ // the user when the silent request will time out. Note the chat path
53
+ // surfaces a stall error rather than auto-retrying (see the dispatch
54
+ // loop in chatStream), so callers should word it "will time out",
55
+ // not "will retry".
56
+ onHint(hintMs, stallMs);
57
+ } catch {
58
+ /* hint is best-effort — never affect the stream */
59
+ }
60
+ }, hintMs);
61
+ if (hintTimer && typeof hintTimer.unref === "function") hintTimer.unref();
62
+ }
30
63
  };
31
64
  const stop = () => {
32
65
  if (timer) clearTimeout(timer);
66
+ if (hintTimer) clearTimeout(hintTimer);
33
67
  timer = null;
68
+ hintTimer = null;
34
69
  };
35
70
  return {
36
71
  signal: controller.signal,
@@ -40,13 +75,35 @@ function makeStallGuard(stallMs = STREAM_STALL_MS) {
40
75
  };
41
76
  }
42
77
 
78
+ // OpenAI-compatible cached-prompt-token count (mirrors agent-core's
79
+ // _openaiCachedTokens). OpenAI/volcengine report it as
80
+ // usage.prompt_tokens_details.cached_tokens; DeepSeek as
81
+ // usage.prompt_cache_hit_tokens. prompt_tokens INCLUDES the cached count, so
82
+ // callers subtract it to recover uncached input (avoids over-pricing in
83
+ // `cc cost` for cc chat sessions).
84
+ function _cachedPromptTokens(usage) {
85
+ if (!usage || typeof usage !== "object") return 0;
86
+ const detailed = Number(usage.prompt_tokens_details?.cached_tokens);
87
+ if (Number.isFinite(detailed) && detailed > 0) return detailed;
88
+ const deepseek = Number(usage.prompt_cache_hit_tokens);
89
+ if (Number.isFinite(deepseek) && deepseek > 0) return deepseek;
90
+ return 0;
91
+ }
92
+
43
93
  /**
44
94
  * Stream a response from Ollama.
45
95
  * If `onUsage` is provided, it's called with `{inputTokens, outputTokens}`
46
96
  * derived from Ollama's terminal `prompt_eval_count` / `eval_count` fields.
47
97
  */
48
- export async function streamOllama(messages, model, baseUrl, onToken, onUsage) {
49
- const guard = makeStallGuard();
98
+ export async function streamOllama(
99
+ messages,
100
+ model,
101
+ baseUrl,
102
+ onToken,
103
+ onUsage,
104
+ onStall,
105
+ ) {
106
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
50
107
  guard.bump();
51
108
  let response;
52
109
  try {
@@ -129,8 +186,9 @@ export async function streamOpenAI(
129
186
  apiKey,
130
187
  onToken,
131
188
  onUsage,
189
+ onStall,
132
190
  ) {
133
- const guard = makeStallGuard();
191
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
134
192
  guard.bump();
135
193
  let response;
136
194
  try {
@@ -189,10 +247,14 @@ export async function streamOpenAI(
189
247
  onToken(content);
190
248
  }
191
249
  if (json.usage && onUsage) {
192
- const inputTokens = Number(json.usage.prompt_tokens) || 0;
250
+ // Split cached prompt tokens out of prompt_tokens so cc cost
251
+ // prices the cached prefix correctly (matches agent-core).
252
+ const cacheReadTokens = _cachedPromptTokens(json.usage);
253
+ const prompt = Number(json.usage.prompt_tokens) || 0;
254
+ const inputTokens = Math.max(0, prompt - cacheReadTokens);
193
255
  const outputTokens = Number(json.usage.completion_tokens) || 0;
194
- if (inputTokens || outputTokens) {
195
- onUsage({ inputTokens, outputTokens });
256
+ if (inputTokens || outputTokens || cacheReadTokens) {
257
+ onUsage({ inputTokens, outputTokens, cacheReadTokens });
196
258
  }
197
259
  }
198
260
  } catch {
@@ -224,6 +286,7 @@ export async function streamAnthropic(
224
286
  apiKey,
225
287
  onToken,
226
288
  onUsage,
289
+ onStall,
227
290
  ) {
228
291
  // Split out a leading system prompt (Anthropic requires it as top-level
229
292
  // `system`, not an OpenAI-style role=system message).
@@ -237,7 +300,7 @@ export async function streamAnthropic(
237
300
  }
238
301
  }
239
302
 
240
- const guard = makeStallGuard();
303
+ const guard = makeStallGuard(STREAM_STALL_MS, { onHint: onStall });
241
304
  guard.bump();
242
305
  let response;
243
306
  try {
@@ -279,6 +342,8 @@ export async function streamAnthropic(
279
342
  let buf = "";
280
343
  let inputTokens = 0;
281
344
  let outputTokens = 0;
345
+ let cacheReadTokens = 0;
346
+ let cacheCreationTokens = 0;
282
347
 
283
348
  try {
284
349
  while (true) {
@@ -302,10 +367,13 @@ export async function streamAnthropic(
302
367
  onToken(delta);
303
368
  }
304
369
  } else if (obj.type === "message_start") {
305
- inputTokens =
306
- Number(obj.message?.usage?.input_tokens) || inputTokens;
307
- outputTokens =
308
- Number(obj.message?.usage?.output_tokens) || outputTokens;
370
+ const u = obj.message?.usage || {};
371
+ inputTokens = Number(u.input_tokens) || inputTokens;
372
+ outputTokens = Number(u.output_tokens) || outputTokens;
373
+ cacheReadTokens =
374
+ Number(u.cache_read_input_tokens) || cacheReadTokens;
375
+ cacheCreationTokens =
376
+ Number(u.cache_creation_input_tokens) || cacheCreationTokens;
309
377
  } else if (obj.type === "message_delta") {
310
378
  outputTokens = Number(obj.usage?.output_tokens) || outputTokens;
311
379
  }
@@ -324,8 +392,16 @@ export async function streamAnthropic(
324
392
  guard.stop();
325
393
  }
326
394
 
327
- if (onUsage && (inputTokens || outputTokens)) {
328
- onUsage({ inputTokens, outputTokens });
395
+ if (
396
+ onUsage &&
397
+ (inputTokens || outputTokens || cacheReadTokens || cacheCreationTokens)
398
+ ) {
399
+ onUsage({
400
+ inputTokens,
401
+ outputTokens,
402
+ cacheReadTokens,
403
+ cacheCreationTokens,
404
+ });
329
405
  }
330
406
 
331
407
  return fullResponse;
@@ -362,7 +438,12 @@ export async function* chatStream(messages, options) {
362
438
  waiter = null;
363
439
  w();
364
440
  };
441
+ // Count of tokens the provider has actually emitted this turn. A connection
442
+ // drop is only safe to retry while this is 0 — once any token has been
443
+ // streamed, re-issuing would duplicate visible output.
444
+ let tokensEmitted = 0;
365
445
  const onToken = (token) => {
446
+ tokensEmitted++;
366
447
  queue.push(token);
367
448
  wake();
368
449
  };
@@ -376,7 +457,7 @@ export async function* chatStream(messages, options) {
376
457
  let providerCall;
377
458
  if (provider === "ollama") {
378
459
  providerCall = () =>
379
- streamOllama(messages, model, baseUrl, onToken, onUsage);
460
+ streamOllama(messages, model, baseUrl, onToken, onUsage, options.onStall);
380
461
  } else if (provider === "anthropic") {
381
462
  const providerDef = BUILT_IN_PROVIDERS.anthropic;
382
463
  const url =
@@ -392,7 +473,15 @@ export async function* chatStream(messages, options) {
392
473
  );
393
474
  }
394
475
  providerCall = () =>
395
- streamAnthropic(messages, model, url, key, onToken, onUsage);
476
+ streamAnthropic(
477
+ messages,
478
+ model,
479
+ url,
480
+ key,
481
+ onToken,
482
+ onUsage,
483
+ options.onStall,
484
+ );
396
485
  } else {
397
486
  const providerDef = BUILT_IN_PROVIDERS[provider];
398
487
  const url =
@@ -408,7 +497,15 @@ export async function* chatStream(messages, options) {
408
497
  );
409
498
  }
410
499
  providerCall = () =>
411
- streamOpenAI(messages, model, url, key, onToken, onUsage);
500
+ streamOpenAI(
501
+ messages,
502
+ model,
503
+ url,
504
+ key,
505
+ onToken,
506
+ onUsage,
507
+ options.onStall,
508
+ );
412
509
  }
413
510
 
414
511
  // Run the provider stream concurrently with the queue-drain loop. finally
@@ -416,7 +513,36 @@ export async function* chatStream(messages, options) {
416
513
  // any token still terminates the generator cleanly.
417
514
  const streamPromise = (async () => {
418
515
  try {
419
- return await providerCall();
516
+ let attempt = 0;
517
+ for (;;) {
518
+ try {
519
+ return await providerCall();
520
+ } catch (err) {
521
+ // Retry ONLY a transient connection drop that hit before any token
522
+ // reached the user (tokensEmitted === 0) — otherwise the re-issue
523
+ // would duplicate the visible answer. Mirrors agent-core's
524
+ // zero-output retry safety (cc agent parity). Stall aborts (180s) and
525
+ // HTTP/auth errors are not retryable and surface immediately.
526
+ if (
527
+ attempt >= STREAM_RETRY_MAX ||
528
+ tokensEmitted > 0 ||
529
+ !isRetryableStreamError(err)
530
+ ) {
531
+ throw err;
532
+ }
533
+ attempt++;
534
+ if (typeof options.onStreamRetry === "function") {
535
+ try {
536
+ options.onStreamRetry(attempt, err);
537
+ } catch {
538
+ /* retry notice is best-effort */
539
+ }
540
+ }
541
+ await new Promise((r) =>
542
+ setTimeout(r, STREAM_RETRY_BASE_MS * 2 ** (attempt - 1)),
543
+ );
544
+ }
545
+ }
420
546
  } finally {
421
547
  done = true;
422
548
  wake();
@@ -451,6 +577,8 @@ export async function* chatStream(messages, options) {
451
577
  usage: {
452
578
  input_tokens: capturedUsage.inputTokens,
453
579
  output_tokens: capturedUsage.outputTokens,
580
+ cache_read_input_tokens: capturedUsage.cacheReadTokens || 0,
581
+ cache_creation_input_tokens: capturedUsage.cacheCreationTokens || 0,
454
582
  },
455
583
  });
456
584
  } catch {
@@ -258,6 +258,11 @@ module.exports = {
258
258
  async execute(task, context) {
259
259
  const input = (task?.params?.input || task?.action || "").trim();
260
260
  if (!input) return { success: false, error: "No input provided" };
261
+ // Security: input is interpolated into a shell command, so reject shell
262
+ // metacharacters to prevent command injection from untrusted skill input.
263
+ if (/[;&|\`$<>()\\n\\r]/.test(input)) {
264
+ return { success: false, error: "Input contains unsafe shell characters; refused." };
265
+ }
261
266
  try {
262
267
  const output = execSync(\`${command} \${input}\`, {
263
268
  encoding: "utf-8",
@@ -0,0 +1,38 @@
1
+ /**
2
+ * cli-numeric — validated parsing for user-supplied numeric CLI options.
3
+ *
4
+ * `parseInt`/`Number` on a bad arg yields NaN that silently corrupts whatever
5
+ * consumes it (a `LIMIT NaN`, a `Date.now()+NaN` deadline, a stored NaN config).
6
+ * This centralizes parse → validate → fall-back-or-throw so a command either
7
+ * fails LOUDLY with a clear message or falls back to a sane default — never
8
+ * threads NaN downstream.
9
+ *
10
+ * @param {*} raw the option value (string from commander, or undefined)
11
+ * @param {object} [opts]
12
+ * @param {string} [opts.name="value"] label for error messages (e.g. "--limit")
13
+ * @param {boolean} [opts.integer=false] parse as integer (parseInt) vs float (Number)
14
+ * @param {number} [opts.min] inclusive lower bound
15
+ * @param {number} [opts.max] inclusive upper bound
16
+ * @param {number} [opts.fallback] returned for missing/invalid input; when
17
+ * omitted, invalid input THROWS instead
18
+ * @returns {number}
19
+ */
20
+ export function numericOption(raw, opts = {}) {
21
+ const { name = "value", integer = false, min, max } = opts;
22
+ const hasFallback = Object.prototype.hasOwnProperty.call(opts, "fallback");
23
+ const fail = (msg) => {
24
+ if (hasFallback) return opts.fallback;
25
+ throw new Error(msg);
26
+ };
27
+
28
+ if (raw == null || raw === "") return fail(`${name} is required`);
29
+ const n = integer ? parseInt(raw, 10) : Number(raw);
30
+ if (!Number.isFinite(n)) {
31
+ return fail(
32
+ `${name} must be a ${integer ? "whole " : ""}number (got ${JSON.stringify(String(raw))})`,
33
+ );
34
+ }
35
+ if (min != null && n < min) return fail(`${name} must be >= ${min}`);
36
+ if (max != null && n > max) return fail(`${name} must be <= ${max}`);
37
+ return n;
38
+ }
@@ -1,7 +1,15 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ renameSync,
7
+ unlinkSync,
8
+ } from "node:fs";
2
9
  import { dirname } from "node:path";
3
10
  import { getConfigPath } from "./paths.js";
4
11
  import { DEFAULT_CONFIG } from "../constants.js";
12
+ import { withFileLock } from "./with-file-lock.js";
5
13
 
6
14
  export function loadConfig() {
7
15
  const configPath = getConfigPath();
@@ -20,7 +28,24 @@ export function loadConfig() {
20
28
  export function saveConfig(config) {
21
29
  const configPath = getConfigPath();
22
30
  mkdirSync(dirname(configPath), { recursive: true });
23
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
31
+ // Atomic write: config.json holds API keys + LLM settings, so a crash (or two
32
+ // concurrent `cc config set`) mid-write must never leave a truncated/corrupt
33
+ // file — that would break every cc command and could lose credentials. Write a
34
+ // temp sibling, then rename over the target (rename is atomic within a
35
+ // filesystem), so a reader/crash sees either the old file or the complete new
36
+ // one, never a half-written one.
37
+ const tmp = `${configPath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
38
+ try {
39
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
40
+ renameSync(tmp, configPath);
41
+ } catch (err) {
42
+ try {
43
+ if (existsSync(tmp)) unlinkSync(tmp);
44
+ } catch {
45
+ /* best-effort temp cleanup */
46
+ }
47
+ throw err;
48
+ }
24
49
  }
25
50
 
26
51
  export function getConfigValue(key) {
@@ -29,10 +54,16 @@ export function getConfigValue(key) {
29
54
  }
30
55
 
31
56
  export function setConfigValue(key, value) {
32
- const config = loadConfig();
33
- setNestedValue(config, key, parseValue(value));
34
- saveConfig(config);
35
- return config;
57
+ // Serialize the read-modify-write across processes: two concurrent
58
+ // `cc config set` invocations would otherwise each load, mutate, and write
59
+ // back, silently losing one update. Best-effort lock (see with-file-lock):
60
+ // on contention timeout it proceeds anyway, so the CLI never hangs.
61
+ return withFileLock(getConfigPath(), () => {
62
+ const config = loadConfig();
63
+ setNestedValue(config, key, parseValue(value));
64
+ saveConfig(config);
65
+ return config;
66
+ });
36
67
  }
37
68
 
38
69
  export function resetConfig() {
@@ -160,13 +160,17 @@ REASON: (1-2 sentence justification)`;
160
160
  };
161
161
  }
162
162
 
163
- function parseScores(text, variants, criteria) {
163
+ export function parseScores(text, variants, criteria) {
164
164
  const scores = [];
165
165
  for (let i = 0; i < variants.length; i++) {
166
166
  const variantScores = {};
167
167
  for (const c of criteria) {
168
+ // The criterion is matched LITERALLY — escape regex metacharacters so a
169
+ // name like "latency(ms)" or "f.score" doesn't silently mis-parse (and an
170
+ // unbalanced one like "a)b(" doesn't throw and crash the whole compare).
171
+ const cEsc = String(c).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
168
172
  const pattern = new RegExp(
169
- `variant\\s*${i + 1}[^\\n]*${c}\\s*=\\s*(\\d+)`,
173
+ `variant\\s*${i + 1}[^\\n]*${cEsc}\\s*=\\s*(\\d+)`,
170
174
  "i",
171
175
  );
172
176
  const match = text.match(pattern);