chainlesschain 0.162.162 → 0.162.164

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 (237) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
  123. package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-tFHy5Wol.js} +1 -1
  132. package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
  133. package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +320 -32
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/doctor.js +85 -1
  154. package/src/commands/hook.js +181 -7
  155. package/src/commands/plugin.js +456 -1
  156. package/src/commands/review.js +363 -2
  157. package/src/commands/session.js +191 -0
  158. package/src/commands/team.js +19 -1
  159. package/src/data/changelog.json +10 -4
  160. package/src/gateways/ws/remote-session-protocol.js +39 -0
  161. package/src/gateways/ws/session-protocol.js +55 -2
  162. package/src/gateways/ws/ws-agent-handler.js +50 -0
  163. package/src/harness/jsonl-session-store.js +60 -0
  164. package/src/harness/worktree-isolator.js +140 -5
  165. package/src/lib/agent-schedule-store.js +318 -10
  166. package/src/lib/agent-team/team-worktree.js +37 -2
  167. package/src/lib/agent-tool-admission.js +125 -0
  168. package/src/lib/app-preview.js +360 -0
  169. package/src/lib/async-hook-supervisor.cjs +54 -0
  170. package/src/lib/capability-manifest.js +319 -0
  171. package/src/lib/capability-negotiation.js +23 -12
  172. package/src/lib/channels/channel-manager.js +54 -2
  173. package/src/lib/cli-context-engineering.js +9 -2
  174. package/src/lib/context-breakdown.js +116 -0
  175. package/src/lib/diagnostic-bundle.js +230 -0
  176. package/src/lib/doctor-checkup.js +275 -4
  177. package/src/lib/execution-location.js +286 -0
  178. package/src/lib/headless-manifest.js +13 -40
  179. package/src/lib/hook-event-log.cjs +193 -0
  180. package/src/lib/hook-stats-store.cjs +197 -0
  181. package/src/lib/idempotency.js +109 -0
  182. package/src/lib/interaction-adapter.js +34 -4
  183. package/src/lib/json-schema-output.js +46 -12
  184. package/src/lib/json-schema-validate.js +143 -3
  185. package/src/lib/jsonl-session-store.js +1 -0
  186. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  187. package/src/lib/lsp/lsp-manager.js +56 -0
  188. package/src/lib/monitor-event.js +290 -0
  189. package/src/lib/operation-fingerprint.js +217 -0
  190. package/src/lib/plugin-runtime/bin.js +37 -0
  191. package/src/lib/plugin-runtime/capabilities.js +19 -0
  192. package/src/lib/plugin-runtime/capability-consent.js +203 -0
  193. package/src/lib/plugin-runtime/hooks.js +37 -0
  194. package/src/lib/plugin-runtime/lsp.js +38 -0
  195. package/src/lib/plugin-runtime/mcp.js +40 -0
  196. package/src/lib/plugin-runtime/monitors.js +37 -0
  197. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  198. package/src/lib/plugin-runtime/policy.js +62 -0
  199. package/src/lib/plugin-runtime/scopes.js +20 -4
  200. package/src/lib/pr-automation-policy.js +224 -0
  201. package/src/lib/project-instructions.js +128 -0
  202. package/src/lib/remote-approval-bridge.js +66 -13
  203. package/src/lib/repl-goal.js +132 -0
  204. package/src/lib/repl-rewind.js +153 -0
  205. package/src/lib/review-comment-anchor.js +241 -0
  206. package/src/lib/review-pipeline.js +46 -7
  207. package/src/lib/runtime-checkup.js +221 -0
  208. package/src/lib/schedule-planner.js +17 -1
  209. package/src/lib/secret-scan.js +409 -0
  210. package/src/lib/session-branch.js +0 -0
  211. package/src/lib/settings-hook-events.cjs +13 -0
  212. package/src/lib/settings-loader.cjs +42 -0
  213. package/src/lib/side-effect-ledger-store.js +88 -0
  214. package/src/lib/side-effect-ledger.js +401 -0
  215. package/src/lib/skill-loader.js +32 -0
  216. package/src/lib/skill-path-scope.js +63 -0
  217. package/src/lib/sub-agent-context.js +110 -9
  218. package/src/lib/sub-agent-registry.js +19 -0
  219. package/src/lib/subagent-inheritance.js +112 -0
  220. package/src/lib/telemetry/span-recorder.js +17 -4
  221. package/src/lib/terminal-context.js +195 -0
  222. package/src/lib/turn-binding-store.js +119 -0
  223. package/src/lib/unattended-action-policy.js +182 -0
  224. package/src/lib/worktree-cleanup-safety.js +138 -0
  225. package/src/lib/worktree-isolator.js +8 -0
  226. package/src/lib/worktree-sparse.js +131 -0
  227. package/src/repl/agent-repl.js +280 -30
  228. package/src/repl/permission-tier.js +28 -0
  229. package/src/runtime/agent-core.js +565 -21
  230. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  231. package/src/runtime/diagnostics.js +60 -0
  232. package/src/runtime/headless-runner.js +393 -1
  233. package/src/runtime/headless-stream.js +60 -1
  234. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
  235. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
  236. package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
  237. package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
@@ -257,6 +257,64 @@ describe("crash-loop restart guard", () => {
257
257
  expect(lastClient).not.toBe(quarantined);
258
258
  await mgr.disposeAll();
259
259
  });
260
+
261
+ describe("exponential restart backoff (opt-in)", () => {
262
+ it("computes exponential backoff per crash, capped, and 0 when disabled", () => {
263
+ const on = new LSPManager({
264
+ restartBackoffBaseMs: 1000,
265
+ restartBackoffMaxMs: 8000,
266
+ });
267
+ expect(on._restartBackoffMs(0)).toBe(0); // first start — no wait
268
+ expect(on._restartBackoffMs(1)).toBe(1000);
269
+ expect(on._restartBackoffMs(2)).toBe(2000);
270
+ expect(on._restartBackoffMs(3)).toBe(4000);
271
+ expect(on._restartBackoffMs(4)).toBe(8000);
272
+ expect(on._restartBackoffMs(5)).toBe(8000); // capped
273
+ // Disabled by default → always 0 (byte-identical immediate respawn).
274
+ const off = new LSPManager();
275
+ expect(off.restartBackoffBaseMs).toBe(0);
276
+ expect(off._restartBackoffMs(2)).toBe(0);
277
+ });
278
+
279
+ it("degrades (backing off) during the cooldown, then re-spawns once it elapses", async () => {
280
+ let t = 1000;
281
+ const mgr = new LSPManager({
282
+ maxRestarts: 5,
283
+ restartWindowMs: 60000,
284
+ restartBackoffBaseMs: 1000,
285
+ restartBackoffMaxMs: 8000,
286
+ now: () => t,
287
+ });
288
+ await mgr.ensureFor("/proj/src/a.ts"); // spawn #1
289
+ const first = lastClient;
290
+ crash(first); // 1 crash at t=1000
291
+ // Same tick → within the 1st-crash backoff (1000ms) → degrade, no respawn.
292
+ const backingOff = await mgr.ensureFor("/proj/src/a.ts");
293
+ expect(backingOff.unavailable).toBe(true);
294
+ expect(backingOff.backoff).toBe(true);
295
+ expect(backingOff.quarantined).toBeUndefined();
296
+ expect(backingOff.retryInMs).toBe(1000);
297
+ expect(lastClient).toBe(first); // did NOT spawn during cooldown
298
+ // Advance past the cooldown → a fresh spawn is allowed.
299
+ t += 1000;
300
+ const ready = await mgr.ensureFor("/proj/src/a.ts");
301
+ expect(ready.unavailable).toBeUndefined();
302
+ expect(lastClient).not.toBe(first);
303
+ await mgr.disposeAll();
304
+ });
305
+
306
+ it("does NOT back off by default — respawns a crashed server on the next request", async () => {
307
+ const mgr = new LSPManager({ maxRestarts: 3, now: () => 5000 });
308
+ await mgr.ensureFor("/proj/src/a.ts");
309
+ const first = lastClient;
310
+ crash(first);
311
+ // Same tick, backoff disabled → immediate fresh spawn (prior behaviour).
312
+ const ready = await mgr.ensureFor("/proj/src/a.ts");
313
+ expect(ready.unavailable).toBeUndefined();
314
+ expect(lastClient).not.toBe(first);
315
+ await mgr.disposeAll();
316
+ });
317
+ });
260
318
  });
261
319
 
262
320
  describe("diagnostics", () => {
@@ -33,6 +33,14 @@ export class LSPManager {
33
33
  * @param {number} [opts.restartWindowMs] sliding window for the crash count.
34
34
  * A server that stays healthy for this long has its crash history pruned and
35
35
  * can restart fresh. Default 30000.
36
+ * @param {number} [opts.restartBackoffBaseMs] exponential backoff BETWEEN
37
+ * restart attempts: after the Nth recent crash the (N+1)th re-spawn waits
38
+ * `base·2^(N-1)` (capped by `restartBackoffMaxMs`) before it is allowed,
39
+ * spreading out a fast startup-crash loop instead of bursting `maxRestarts`
40
+ * spawns back-to-back. Default 0 = OFF (byte-identical to the prior
41
+ * immediate-respawn behaviour); opt-in pending evaluation.
42
+ * @param {number} [opts.restartBackoffMaxMs] cap for the exponential backoff.
43
+ * Default 8000.
36
44
  * @param {() => number} [opts.now] injected clock (deterministic crash-loop tests)
37
45
  */
38
46
  constructor(opts = {}) {
@@ -65,6 +73,19 @@ export class LSPManager {
65
73
  this.restartWindowMs = Number.isFinite(opts.restartWindowMs)
66
74
  ? opts.restartWindowMs
67
75
  : 30000;
76
+ // Exponential backoff BETWEEN restart attempts (distinct from the quarantine
77
+ // cap above). Without it, a server that crashes on startup is re-spawned on
78
+ // the very next request — bursting `maxRestarts` spawns back-to-back in
79
+ // milliseconds before quarantine. When enabled, the (N+1)th spawn after N
80
+ // recent crashes waits `base·2^(N-1)` (capped), so a fast crash loop can't
81
+ // storm the process table. Default 0 = OFF → byte-identical to the prior
82
+ // immediate-respawn behaviour (opt-in pending evaluation).
83
+ this.restartBackoffBaseMs = Number.isFinite(opts.restartBackoffBaseMs)
84
+ ? Math.max(0, opts.restartBackoffBaseMs)
85
+ : 0;
86
+ this.restartBackoffMaxMs = Number.isFinite(opts.restartBackoffMaxMs)
87
+ ? opts.restartBackoffMaxMs
88
+ : 8000;
68
89
  this._now = typeof opts.now === "function" ? opts.now : () => Date.now();
69
90
  /** key → array of recent crash timestamps (survives entry re-spawn) */
70
91
  this._crashLog = new Map();
@@ -80,6 +101,18 @@ export class LSPManager {
80
101
  return pruned;
81
102
  }
82
103
 
104
+ /**
105
+ * Minimum delay before the next restart attempt given `crashCount` recent
106
+ * crashes: exponential `base·2^(crashCount-1)`, capped at `restartBackoffMaxMs`.
107
+ * Returns 0 when backoff is disabled (base 0) or `crashCount <= 0` (a first
108
+ * start). Deterministic (no jitter) so crash-loop tests stay reproducible.
109
+ */
110
+ _restartBackoffMs(crashCount) {
111
+ if (this.restartBackoffBaseMs <= 0 || crashCount <= 0) return 0;
112
+ const exp = this.restartBackoffBaseMs * 2 ** (crashCount - 1);
113
+ return Math.min(exp, this.restartBackoffMaxMs);
114
+ }
115
+
83
116
  /**
84
117
  * Ensure a server is running for `filePath` and the file is open. Returns
85
118
  * `{ client, uri, languageId }`, or `{ unavailable: true, reason }` when no
@@ -125,6 +158,29 @@ export class LSPManager {
125
158
  quarantined: true,
126
159
  };
127
160
  }
161
+ // Under the quarantine cap but recently crashed: when backoff is enabled,
162
+ // space out the re-spawn exponentially so a fast startup-crash loop doesn't
163
+ // storm the process table. The caller degrades to text search until the
164
+ // cooldown elapses — a transient state, distinct from quarantine.
165
+ if (crashes.length > 0) {
166
+ const backoffMs = this._restartBackoffMs(crashes.length);
167
+ if (backoffMs > 0) {
168
+ const lastCrash = crashes[crashes.length - 1];
169
+ const sinceLast = this._now() - lastCrash;
170
+ if (sinceLast < backoffMs) {
171
+ const retryInMs = backoffMs - sinceLast;
172
+ return {
173
+ unavailable: true,
174
+ reason:
175
+ `${resolved.id} restarting after a crash — backing off ` +
176
+ `~${Math.max(0, Math.round(retryInMs / 1000))}s ` +
177
+ `(attempt ${crashes.length + 1}/${this.maxRestarts})`,
178
+ backoff: true,
179
+ retryInMs,
180
+ };
181
+ }
182
+ }
183
+ }
128
184
  entry = await this._getOrStartServer(key, resolved, projectRoot);
129
185
  }
130
186
 
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Monitor event envelope + delivery guards (P1 daemon Event Runtime —
3
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"守护进程事件运行时闭环").
4
+ *
5
+ * A Monitor watches a source (command stdout, file change, URL body, …) and
6
+ * "fires" when its condition matches. Before that firing reaches the agent as a
7
+ * turn it needs the same delivery guarantees Claude Code gives scheduled/event
8
+ * tasks:
9
+ *
10
+ * - **event_id** — a *deterministic* identity (no RNG) derived from the
11
+ * monitor id + the observation, so the same observation across a daemon
12
+ * restart hashes to the same id and can be de-duplicated.
13
+ * - **dedup window** — a time+count bounded set of recently-seen ids so a
14
+ * monitor that matches every poll does not re-fire the identical event.
15
+ * - **size cap** — oversized payloads are truncated (with a flag) so a
16
+ * runaway command output cannot blow up a turn / the audit log.
17
+ * - **backpressure** — a bounded queue that drops (and counts) events rather
18
+ * than growing without limit when the agent is slower than the source.
19
+ * - **authority** — a monitor event is an internal SYSTEM event: it tops out
20
+ * at `steer` (agent-authority.js) and can NEVER answer a permission gate.
21
+ *
22
+ * Pure + dependency-light (node:crypto + agent-authority.js) so any monitor
23
+ * firing seam — daemon runtime, `cc agenda run`, a future in-process runtime —
24
+ * can share exactly one implementation of these invariants.
25
+ */
26
+
27
+ import { createHash } from "node:crypto";
28
+ import {
29
+ ORIGIN,
30
+ authorityForOrigin,
31
+ canApprove,
32
+ describeAuthorityChain,
33
+ } from "./agent-authority.js";
34
+
35
+ /** Default per-event payload cap (64 KiB) — matches the schedule store's audit budget. */
36
+ export const DEFAULT_MAX_EVENT_BYTES = 64 * 1024;
37
+
38
+ /** Default dedup window: an id seen within 5 minutes (or the last 1000 ids) is a repeat. */
39
+ export const DEFAULT_DEDUP_WINDOW_MS = 5 * 60 * 1000;
40
+ export const DEFAULT_DEDUP_MAX_ENTRIES = 1000;
41
+
42
+ /** Default bounded queue depth before backpressure drops kick in. */
43
+ export const DEFAULT_QUEUE_MAX_SIZE = 256;
44
+
45
+ /**
46
+ * A stable string form of an observation so structurally-equal observations
47
+ * hash identically and any real change hashes differently. Mirrors
48
+ * agent-authority's `normalizeToolArgs` intent (key-order independent).
49
+ */
50
+ function stablePayload(payload) {
51
+ return stableStringify(payload === undefined ? null : payload);
52
+ }
53
+
54
+ function stableStringify(value) {
55
+ if (value === null || typeof value !== "object") {
56
+ return JSON.stringify(value === undefined ? null : value);
57
+ }
58
+ if (Array.isArray(value)) {
59
+ return "[" + value.map(stableStringify).join(",") + "]";
60
+ }
61
+ const keys = Object.keys(value).sort();
62
+ return (
63
+ "{" +
64
+ keys
65
+ .map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]))
66
+ .join(",") +
67
+ "}"
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Deterministic event id for a monitor observation. Same `(monitorId, payload)`
73
+ * → same id, forever, with no RNG — so a daemon restart re-observing the same
74
+ * thing is recognised as a repeat instead of re-firing.
75
+ */
76
+ export function monitorEventId(monitorId, payload) {
77
+ const h = createHash("sha256");
78
+ h.update("cc-monitor-event-v1\n");
79
+ h.update(String(monitorId ?? "") + "\n");
80
+ h.update(stablePayload(payload) + "\n");
81
+ return "ev_" + h.digest("hex").slice(0, 24);
82
+ }
83
+
84
+ /**
85
+ * Cap a payload's serialized size. Returns `{ value, truncated, bytes }`.
86
+ * Strings are truncated by UTF-8 byte length; non-strings are JSON-serialized,
87
+ * and if the JSON is over budget the *serialized string* is truncated and
88
+ * returned (so the event still carries a bounded, human-readable snippet rather
89
+ * than an unbounded object).
90
+ */
91
+ export function capEventPayload(payload, maxBytes = DEFAULT_MAX_EVENT_BYTES) {
92
+ const limit = Math.max(0, Number(maxBytes) || 0);
93
+ const asString =
94
+ typeof payload === "string" ? payload : safeJsonStringify(payload);
95
+ const bytes = Buffer.byteLength(asString, "utf8");
96
+ if (limit === 0 || bytes <= limit) {
97
+ return { value: payload, truncated: false, bytes };
98
+ }
99
+ const truncated = truncateUtf8(asString, limit);
100
+ return {
101
+ value: truncated,
102
+ truncated: true,
103
+ bytes: Buffer.byteLength(truncated, "utf8"),
104
+ };
105
+ }
106
+
107
+ function safeJsonStringify(value) {
108
+ try {
109
+ return JSON.stringify(value === undefined ? null : value);
110
+ } catch {
111
+ return String(value);
112
+ }
113
+ }
114
+
115
+ /** Truncate a string to at most `maxBytes` UTF-8 bytes without splitting a code point. */
116
+ function truncateUtf8(str, maxBytes) {
117
+ const buf = Buffer.from(str, "utf8");
118
+ if (buf.length <= maxBytes) return str;
119
+ let end = maxBytes;
120
+ // Back off if we would land in the middle of a multi-byte sequence.
121
+ while (end > 0 && (buf[end] & 0xc0) === 0x80) end--;
122
+ return buf.toString("utf8", 0, end);
123
+ }
124
+
125
+ /**
126
+ * The unforgeable authority envelope for a monitor firing. A monitor event is
127
+ * an internal SYSTEM observation: it may add a turn (`steer`) but can NEVER
128
+ * answer a permission gate — matching the channel/hook ceiling. `origin` is set
129
+ * here by WHAT produced the event (a monitor), never read from its content.
130
+ *
131
+ * Returns `{ event_id, monitorId, origin, authority, canApprove, provenance,
132
+ * at, truncated?, source? }` — a caller merges its own observation fields in.
133
+ */
134
+ export function monitorEventEnvelope(
135
+ monitorId,
136
+ payload,
137
+ { at = null, source = null, maxBytes = DEFAULT_MAX_EVENT_BYTES } = {},
138
+ ) {
139
+ const capped = capEventPayload(payload, maxBytes);
140
+ const envelope = {
141
+ origin: ORIGIN.SYSTEM,
142
+ principalId: monitorId != null ? String(monitorId) : null,
143
+ correlationId: source != null ? String(source) : null,
144
+ };
145
+ return {
146
+ event_id: monitorEventId(monitorId, payload),
147
+ monitorId: monitorId != null ? String(monitorId) : null,
148
+ source: source != null ? String(source) : null,
149
+ at: at != null ? Number(at) : null,
150
+ payload: capped.value,
151
+ truncated: capped.truncated,
152
+ origin: envelope.origin,
153
+ authority: authorityForOrigin(ORIGIN.SYSTEM, envelope), // always "steer"
154
+ canApprove: canApprove(envelope), // always false
155
+ provenance: describeAuthorityChain(envelope),
156
+ };
157
+ }
158
+
159
+ /**
160
+ * A time + count bounded set of recently-seen event ids. `shouldEmit(id, now)`
161
+ * records the id and returns `true` only when it is NOT a repeat within the
162
+ * window — so a monitor matching every poll fires the identical event once.
163
+ * Eviction is by age (older than `windowMs`) and by count (`maxEntries`, oldest
164
+ * first), both bounded so memory never grows without limit.
165
+ */
166
+ export class DedupWindow {
167
+ constructor({
168
+ windowMs = DEFAULT_DEDUP_WINDOW_MS,
169
+ maxEntries = DEFAULT_DEDUP_MAX_ENTRIES,
170
+ } = {}) {
171
+ this._windowMs = Math.max(0, Number(windowMs) || 0);
172
+ this._maxEntries = Math.max(1, Number(maxEntries) || 1);
173
+ // Insertion-ordered id → lastSeen epoch-ms (Map preserves insertion order).
174
+ this._seen = new Map();
175
+ }
176
+
177
+ /**
178
+ * Drop entries older than the window relative to `now`. A full sweep (no
179
+ * early break): refreshing an id updates its timestamp but keeps its Map
180
+ * position, so timestamps are NOT monotonic in iteration order and an early
181
+ * break could skip a genuinely-expired later entry — which would then wrongly
182
+ * suppress its legitimate re-emit. The map is bounded by `maxEntries`, so a
183
+ * full pass is cheap and strictly correct.
184
+ */
185
+ _evictExpired(now) {
186
+ if (this._windowMs === 0) return;
187
+ const cutoff = now - this._windowMs;
188
+ for (const [id, ts] of this._seen) {
189
+ if (ts <= cutoff) this._seen.delete(id);
190
+ }
191
+ }
192
+
193
+ /** Enforce the count cap by evicting the oldest entries. */
194
+ _evictOverflow() {
195
+ while (this._seen.size > this._maxEntries) {
196
+ const oldest = this._seen.keys().next().value;
197
+ this._seen.delete(oldest);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * True if `eventId` is new within the window (and records it); false if it is
203
+ * a repeat (refreshes its timestamp so a steady stream keeps it suppressed).
204
+ */
205
+ shouldEmit(eventId, now = 0) {
206
+ const nowMs = Number(now) || 0;
207
+ this._evictExpired(nowMs);
208
+ if (this._seen.has(eventId)) {
209
+ // Refresh recency without changing insertion order semantics for eviction.
210
+ this._seen.set(eventId, nowMs);
211
+ return false;
212
+ }
213
+ this._seen.set(eventId, nowMs);
214
+ this._evictOverflow();
215
+ return true;
216
+ }
217
+
218
+ /** Number of live (un-evicted) ids currently tracked. */
219
+ get size() {
220
+ return this._seen.size;
221
+ }
222
+
223
+ /** Forget everything (e.g. on monitor removal). */
224
+ clear() {
225
+ this._seen.clear();
226
+ }
227
+ }
228
+
229
+ /**
230
+ * A bounded event queue providing backpressure: once full, a `push` drops an
231
+ * event rather than growing the queue, and counts what it dropped so the loss
232
+ * is observable (and auditable) instead of silent.
233
+ *
234
+ * - `dropPolicy: "oldest"` (default) — evict the head to admit the newcomer
235
+ * (keep the freshest signal; good for state monitors).
236
+ * - `dropPolicy: "newest"` — reject the newcomer (preserve order; good when
237
+ * the first observations matter most).
238
+ */
239
+ export class BoundedEventQueue {
240
+ constructor({
241
+ maxSize = DEFAULT_QUEUE_MAX_SIZE,
242
+ dropPolicy = "oldest",
243
+ } = {}) {
244
+ this._maxSize = Math.max(1, Number(maxSize) || 1);
245
+ this._dropPolicy = dropPolicy === "newest" ? "newest" : "oldest";
246
+ this._items = [];
247
+ this._dropped = 0;
248
+ }
249
+
250
+ /**
251
+ * Push an event. Returns `{ accepted, dropped }` where `dropped` is the event
252
+ * evicted/rejected to stay within bounds (or null when nothing was dropped).
253
+ */
254
+ push(event) {
255
+ if (this._items.length < this._maxSize) {
256
+ this._items.push(event);
257
+ return { accepted: true, dropped: null };
258
+ }
259
+ if (this._dropPolicy === "newest") {
260
+ this._dropped++;
261
+ return { accepted: false, dropped: event };
262
+ }
263
+ // oldest: evict head, admit newcomer.
264
+ const dropped = this._items.shift();
265
+ this._items.push(event);
266
+ this._dropped++;
267
+ return { accepted: true, dropped };
268
+ }
269
+
270
+ /** Remove and return the next event, or undefined when empty. */
271
+ shift() {
272
+ return this._items.shift();
273
+ }
274
+
275
+ /** Remove and return every queued event, oldest first. */
276
+ drain() {
277
+ const out = this._items;
278
+ this._items = [];
279
+ return out;
280
+ }
281
+
282
+ get size() {
283
+ return this._items.length;
284
+ }
285
+
286
+ /** Total events dropped due to backpressure since construction. */
287
+ get dropped() {
288
+ return this._dropped;
289
+ }
290
+ }
@@ -93,3 +93,220 @@ export function approvalFingerprintOk(pending, resolved) {
93
93
  if (!pending) return true; // nothing to bind against (legacy pending ask)
94
94
  return fingerprintsMatch(pending, resolved);
95
95
  }
96
+
97
+ // ── §8.2 cross-device operation fingerprint ──────────────────────────────────
98
+ //
99
+ // The fingerprint above binds tool + params only. IDE gap §8.2 requires a
100
+ // CROSS-DEVICE identity that also covers target environment, workspace, session,
101
+ // policy VERSION and a validity window, plus a fail-closed resolver: offline
102
+ // replay, multi-card concurrency, timeout and duplicate submission must all be
103
+ // rejected. The functions below add that richer tuple WITHOUT changing the
104
+ // legacy fingerprint (existing devices/tests keep working).
105
+ //
106
+ // Two layers of identity:
107
+ // - the LOGICAL key = tool + params + env + workspace + session + policyVersion
108
+ // (what the human is approving). Any change here invalidates an old approval.
109
+ // - the FINGERPRINT = the logical key PLUS the validity window, so a card
110
+ // re-issued with a fresh window is a distinct fingerprint (an old card can
111
+ // never be replayed) while still mapping to the same logical operation.
112
+
113
+ /**
114
+ * The logical-operation key preimage: everything the human is authorizing,
115
+ * EXCLUDING the validity window. Two asks with this equal are the same
116
+ * operation (so re-issuing one supersedes the other). Deterministic.
117
+ * @param {object} desc
118
+ */
119
+ export function operationDescriptorKey(desc = {}) {
120
+ return canonicalize({
121
+ tool: desc.toolName ?? desc.tool ?? null,
122
+ params: desc.params !== undefined ? desc.params : (desc.detail ?? null),
123
+ targetEnv: desc.targetEnv ?? null,
124
+ workspace: desc.workspace ?? null,
125
+ session: desc.session ?? null,
126
+ policyVersion: desc.policyVersion ?? null,
127
+ });
128
+ }
129
+
130
+ function intOrNull(value) {
131
+ const n = Number(value);
132
+ return Number.isFinite(n) ? n : null;
133
+ }
134
+
135
+ /**
136
+ * The full §8.2 cross-device fingerprint: the logical key plus the validity
137
+ * window `[notBefore, notAfter]`. Prefixed `opf_` (40 hex) so it is never
138
+ * confused with the 32-hex legacy fingerprint in a log or on the wire.
139
+ *
140
+ * @param {object} desc
141
+ * @param {string} [desc.toolName]
142
+ * @param {*} [desc.params]
143
+ * @param {string} [desc.targetEnv] e.g. local / wsl / ssh:host / container
144
+ * @param {string} [desc.workspace]
145
+ * @param {string} [desc.session]
146
+ * @param {string} [desc.policyVersion] a REAL policy version, not a rule label
147
+ * @param {number} [desc.notBefore] ms epoch the approval becomes valid
148
+ * @param {number} [desc.notAfter] ms epoch the approval expires
149
+ * @returns {string} `opf_` + 40 lowercase hex
150
+ */
151
+ export function computeOperationFingerprint(desc = {}) {
152
+ const preimage =
153
+ "cc-operation-fingerprint-v2\n" +
154
+ operationDescriptorKey(desc) +
155
+ "\n" +
156
+ canonicalize({
157
+ notBefore: intOrNull(desc.notBefore),
158
+ notAfter: intOrNull(desc.notAfter),
159
+ });
160
+ return (
161
+ "opf_" +
162
+ createHash("sha256").update(preimage, "utf8").digest("hex").slice(0, 40)
163
+ );
164
+ }
165
+
166
+ /**
167
+ * A short, human-verifiable id for the approval card — the operator eyeballs it
168
+ * on the device against the terminal to confirm they answer the SAME card. Two
169
+ * groups of 4 uppercase hex from the fingerprint (never the secret params).
170
+ * @param {string} fingerprint
171
+ * @returns {string} e.g. "A1B2-C3D4"
172
+ */
173
+ export function shortOperationId(fingerprint) {
174
+ const hex = String(fingerprint || "").replace(/^opf_/, "");
175
+ const s = (hex.slice(0, 8) || "00000000").toUpperCase();
176
+ return `${s.slice(0, 4)}-${s.slice(4, 8)}`;
177
+ }
178
+
179
+ /**
180
+ * A readable, SECRET-FREE one-line summary for the approval card. It NEVER
181
+ * echoes param VALUES (a shell command / token could be secret) — only the
182
+ * tool, the param key names (or an opaque marker), and the target coordinates.
183
+ * @param {object} desc
184
+ * @returns {string}
185
+ */
186
+ export function summarizeOperation(desc = {}) {
187
+ const tool = String(desc.toolName ?? desc.tool ?? "operation");
188
+ let paramHint = "";
189
+ const p = desc.params;
190
+ if (p && typeof p === "object" && !Array.isArray(p)) {
191
+ const keys = Object.keys(p).sort();
192
+ paramHint = keys.length ? `(${keys.join(", ")})` : "()";
193
+ } else if (p !== undefined && p !== null && p !== "") {
194
+ paramHint = "(…)"; // opaque scalar/string/array — do not reveal the value
195
+ }
196
+ const coords = [];
197
+ if (desc.workspace) coords.push(`ws:${desc.workspace}`);
198
+ if (desc.targetEnv) coords.push(`env:${desc.targetEnv}`);
199
+ if (desc.session) coords.push(`sess:${desc.session}`);
200
+ if (desc.policyVersion) coords.push(`pol:${desc.policyVersion}`);
201
+ return `${tool}${paramHint}${coords.length ? " · " + coords.join(" · ") : ""}`;
202
+ }
203
+
204
+ /**
205
+ * Fail-closed verdict for resolving one pending approval. Every rejection is
206
+ * named so the audit log records WHY. Ordering matters: a missing input or a
207
+ * superseded/duplicate card is rejected before the fingerprint compare so a
208
+ * stale card can never settle a gate.
209
+ *
210
+ * @param {object} pending {fingerprint, notBefore?, notAfter?, resolved?, supersededBy?}
211
+ * @param {object} resolution {fingerprint}
212
+ * @param {object} [opts]
213
+ * @param {number} [opts.now] ms epoch (injected clock; required for window checks)
214
+ * @returns {{ok:boolean, reason:(string|null)}}
215
+ */
216
+ export function resolveOperationApproval(pending, resolution, opts = {}) {
217
+ if (!pending || typeof pending !== "object")
218
+ return { ok: false, reason: "no-pending" };
219
+ if (!resolution || typeof resolution !== "object")
220
+ return { ok: false, reason: "no-resolution" };
221
+ if (pending.resolved) return { ok: false, reason: "duplicate" };
222
+ if (pending.supersededBy != null) return { ok: false, reason: "superseded" };
223
+
224
+ if (!fingerprintsMatch(pending.fingerprint, resolution.fingerprint)) {
225
+ return { ok: false, reason: "fingerprint-mismatch" };
226
+ }
227
+
228
+ const now = intOrNull(opts.now);
229
+ const nb = intOrNull(pending.notBefore);
230
+ const na = intOrNull(pending.notAfter);
231
+ if (nb != null || na != null) {
232
+ if (now == null) return { ok: false, reason: "no-clock" }; // can't prove validity → closed
233
+ if (nb != null && now < nb) return { ok: false, reason: "not-yet-valid" };
234
+ if (na != null && now > na) return { ok: false, reason: "expired" };
235
+ }
236
+ return { ok: true, reason: null };
237
+ }
238
+
239
+ /**
240
+ * In-memory registry that enforces the §8.2 cross-device guarantees for a set
241
+ * of live approval cards: single-winner across concurrent cards for one logical
242
+ * operation (multi-card), at-most-once (duplicate submission), and validity
243
+ * window (timeout / not-yet-valid). Pure aside from an injected `clock` — no
244
+ * fs/RNG — so it is fully unit-testable and serializable alongside a session.
245
+ */
246
+ export class OperationApprovalRegistry {
247
+ constructor({ clock } = {}) {
248
+ this._byFingerprint = new Map(); // fingerprint -> pending record
249
+ this._activeByLogicalKey = new Map(); // logical key -> active fingerprint
250
+ this._clock = typeof clock === "function" ? clock : null;
251
+ }
252
+
253
+ _now() {
254
+ return this._clock ? intOrNull(this._clock()) : null;
255
+ }
256
+
257
+ /**
258
+ * Issue a card for an operation. Any still-unresolved card for the SAME
259
+ * logical operation is superseded (single-winner) so only this newest card
260
+ * can resolve.
261
+ * @returns {{fingerprint:string, shortId:string, summary:string}}
262
+ */
263
+ issue(desc = {}) {
264
+ const fingerprint = computeOperationFingerprint(desc);
265
+ const logicalKey = operationDescriptorKey(desc);
266
+
267
+ const priorFp = this._activeByLogicalKey.get(logicalKey);
268
+ if (priorFp && priorFp !== fingerprint) {
269
+ const prior = this._byFingerprint.get(priorFp);
270
+ if (prior && !prior.resolved) prior.supersededBy = fingerprint;
271
+ }
272
+
273
+ this._byFingerprint.set(fingerprint, {
274
+ fingerprint,
275
+ logicalKey,
276
+ notBefore: intOrNull(desc.notBefore),
277
+ notAfter: intOrNull(desc.notAfter),
278
+ resolved: false,
279
+ supersededBy: null,
280
+ });
281
+ this._activeByLogicalKey.set(logicalKey, fingerprint);
282
+
283
+ return {
284
+ fingerprint,
285
+ shortId: shortOperationId(fingerprint),
286
+ summary: summarizeOperation(desc),
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Resolve a card by fingerprint. Fails closed (with a named reason) on an
292
+ * unknown, superseded, duplicate, mismatched, not-yet-valid or expired card.
293
+ * On success the card is marked resolved so a replay is a `duplicate`.
294
+ * @param {string} fingerprint
295
+ * @param {object} [opts] {now?} overrides the injected clock
296
+ * @returns {{ok:boolean, reason:(string|null)}}
297
+ */
298
+ resolve(fingerprint, opts = {}) {
299
+ const pending = this._byFingerprint.get(fingerprint);
300
+ if (!pending) return { ok: false, reason: "unknown" };
301
+ const now = opts.now !== undefined ? intOrNull(opts.now) : this._now();
302
+ const verdict = resolveOperationApproval(pending, { fingerprint }, { now });
303
+ if (verdict.ok) pending.resolved = true;
304
+ return verdict;
305
+ }
306
+
307
+ /** A read-only view of a pending card (or null). */
308
+ get(fingerprint) {
309
+ const p = this._byFingerprint.get(fingerprint);
310
+ return p ? { ...p } : null;
311
+ }
312
+ }