chainlesschain 0.162.162 → 0.162.163

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 (192) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BQ0kgfA1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-Dr1MDANe.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-CZ5vsaXW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-BIBga881.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-vpXHRU3u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-B8exuoub.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-923evRRN.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-sr_sswlQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-CnW-4XKT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-FZCsTZIL.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DFrxB59H.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-_b6WEel8.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-CkbnnZ-K.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-BxM_82zW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-DYinP45R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DyXp3sSP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BEr2DhW7.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-B-Vwo1hN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-DbLxKddd.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BsQ_LFw9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-lVlXQHu5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-Beo_foO7.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DszHIXED.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-CuJLa2eb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-Cxd1lxhc.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-WqPKetx6.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CWYtY3ke.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-BnX1iiNV.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-DqLMNntm.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-BqYwEYou.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-_vGfmhNl.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-BeJDOkDj.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-Da0EEZDO.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-DSuzcCCy.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-BE9YgdA0.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-fC2BpO5S.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-D24Xxqw3.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-Bs3oGvS8.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-BFjmAA-3.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-BX_9Bhv0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-DBcdsVEP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-DDyaF9vC.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-BB78L4he.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-INbnWv4j.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-BST-VmTq.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CE7_tS2k.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-hK5L8dOB.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-BvkkUoDn.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-u7_EUtkm.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-5Fmt_8Nw.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-B9eanarR.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-C0dX5N2f.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-CKTpG-PZ.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-Px_w_XQM.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-Di0fnYkM.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-vesnWjXt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-exbY6LkT.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-B3O3x70t.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-uRA0DhNz.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-CViqdH6K.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CkXS97WN.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DxW5WYnE.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-CLxmGFse.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Cr5fziXq.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-H0JSb3cp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-DMYWMae7.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-CbYyDdF4.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-Cpc8JjiK.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-CRh77Jda.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal-4D8tNBO5.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D41jzzbG.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-C_jWEy6b.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-Dk1ZUhsy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-CLVy4l7m.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-C-FeqFDj.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-CqVZzyMm.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-DX9BW9pu.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-7hQDpWw_.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-6XqUAO0j.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-5R_vkaJG.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-huUPZdC-.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-CfJlgisE.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-jO8heofr.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-DY8Vxqx7.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-A48PtXw9.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-B300RPnT.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-BbepULSc.js → index-B74OXFxC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-BAON7TzU.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-BCqxyvqp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BHE8OIls.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-BQS-uPOK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-BRXOP09X.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-BYLBPpt8.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-BfCx4pFT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-BliaT02i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BmqGerpC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-BsBtRWbA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C229ABfy.js → index-BtEXSoPL.js} +1 -1
  108. package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
  109. package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-Bxax9vQ6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-C2iVSFpy.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-CA6sO5VD.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CA90TZxG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-CLhBh-qE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-CRElRhyS.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-Cbe9G0V4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-ChvHubX3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-Cis_Ip7H.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-Cjxbu68P.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-CzKmteNS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-D0pet5Uy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D1VLuww0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-DXBcvUnf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-Db1cLWir.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-DoI2sZm7.js} +1 -1
  125. package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
  126. package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-KRHroryI.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-d1xkbtNz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-dKx57io7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-j3O2EEJe.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-p85YaaKw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-v3pTaEQm.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-vXs3B8GJ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-MCB7-raZ.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-UWDF9swV.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CsTmgXZU.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-fQy6KGLg.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-woPnEjd1.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-Bs2dZ1gf.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-C7EVgPkg.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BH2kYCTN.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BFBP2V_e.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-xdDxDYse.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-aAWgPPQf.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-BrOih40N.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-B8lutNSE.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-CffNre7V.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +254 -23
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/hook.js +181 -7
  154. package/src/commands/plugin.js +239 -1
  155. package/src/commands/review.js +347 -2
  156. package/src/data/changelog.json +9 -4
  157. package/src/gateways/ws/remote-session-protocol.js +39 -0
  158. package/src/gateways/ws/session-protocol.js +5 -2
  159. package/src/lib/agent-schedule-store.js +289 -10
  160. package/src/lib/channels/channel-manager.js +54 -2
  161. package/src/lib/cli-context-engineering.js +9 -2
  162. package/src/lib/context-breakdown.js +116 -0
  163. package/src/lib/doctor-checkup.js +52 -0
  164. package/src/lib/hook-event-log.cjs +193 -0
  165. package/src/lib/interaction-adapter.js +34 -4
  166. package/src/lib/json-schema-output.js +46 -12
  167. package/src/lib/json-schema-validate.js +143 -3
  168. package/src/lib/plugin-runtime/capability-consent.js +175 -0
  169. package/src/lib/plugin-runtime/policy.js +62 -0
  170. package/src/lib/plugin-runtime/scopes.js +20 -4
  171. package/src/lib/project-instructions.js +128 -0
  172. package/src/lib/repl-goal.js +132 -0
  173. package/src/lib/repl-rewind.js +52 -0
  174. package/src/lib/schedule-planner.js +17 -1
  175. package/src/lib/settings-hook-events.cjs +13 -0
  176. package/src/lib/settings-loader.cjs +42 -0
  177. package/src/lib/side-effect-ledger-store.js +88 -0
  178. package/src/lib/side-effect-ledger.js +296 -0
  179. package/src/lib/sub-agent-context.js +74 -5
  180. package/src/lib/subagent-inheritance.js +112 -0
  181. package/src/lib/turn-binding-store.js +119 -0
  182. package/src/repl/agent-repl.js +136 -0
  183. package/src/repl/permission-tier.js +28 -0
  184. package/src/runtime/agent-core.js +376 -10
  185. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  186. package/src/runtime/headless-runner.js +267 -0
  187. package/src/runtime/headless-stream.js +60 -1
  188. package/src/assets/web-panel/assets/ChatBubbleRenderer-HwJR-C6B.js +0 -1
  189. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
  190. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
  191. package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
  192. package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Plugin capability CONSENT (Phase 3 gap: capability declaration + re-consent).
3
+ *
4
+ * Complements version trust ([[trust.js]]): trust pins the exact CODE a plugin
5
+ * ships (re-consent on version bump); this pins the CAPABILITY SET the plugin is
6
+ * allowed to request (process / network / filesystem / mcp / monitor /
7
+ * credential). The two are orthogonal — a plugin can be trusted-to-run yet still
8
+ * WIDEN what it wants (add `network:*`, a new credential), which must re-prompt.
9
+ *
10
+ * Consent is keyed by the SET, not the version: once the user consents, later
11
+ * versions that keep the same (or a narrower) capability set stay consented; any
12
+ * WIDENING (a new grant) revokes consent until re-granted. This mirrors
13
+ * `consentRequiredForUpgrade` in [[capabilities.js]] exactly.
14
+ *
15
+ * Recorded in the USER data dir (never in the repo — a checked-in project file
16
+ * must not be able to pre-grant itself capabilities), keyed `<scope>:<name>` →
17
+ * { version, capabilities, consentedAt }. Pure over injected IO so unit tests
18
+ * never touch the real user-data dir.
19
+ */
20
+
21
+ import fs from "fs";
22
+ import path from "path";
23
+ import { getElectronUserDataDir } from "../paths.js";
24
+ import {
25
+ normalizeCapabilities,
26
+ diffCapabilities,
27
+ describeCapabilities,
28
+ } from "./capabilities.js";
29
+
30
+ export const _deps = {
31
+ existsSync: fs.existsSync,
32
+ readFileSync: fs.readFileSync,
33
+ writeFileSync: fs.writeFileSync,
34
+ mkdirSync: fs.mkdirSync,
35
+ now: () => new Date().toISOString(),
36
+ // Injectable so unit tests never touch the real user-data dir.
37
+ storePath: () =>
38
+ path.join(getElectronUserDataDir(), "plugin-capability-consent.json"),
39
+ };
40
+
41
+ function consentKey(scope, name) {
42
+ return `${scope}:${name}`;
43
+ }
44
+
45
+ export function loadConsentStore() {
46
+ try {
47
+ const p = _deps.storePath();
48
+ if (!_deps.existsSync(p)) return {};
49
+ const data = JSON.parse(_deps.readFileSync(p, "utf8"));
50
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ function saveConsentStore(store) {
57
+ const p = _deps.storePath();
58
+ _deps.mkdirSync(path.dirname(p), { recursive: true });
59
+ _deps.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
60
+ }
61
+
62
+ /**
63
+ * Coerce a capability set to the canonical shape diffCapabilities needs.
64
+ * `normalizeCapabilities` is NOT idempotent — it re-parses `filesystem`/
65
+ * `credential` through `toList`, so feeding it an already-normalized set (e.g.
66
+ * one read back from the store) corrupts those lists. Detect an already-
67
+ * canonical set and pass it through unchanged; only normalize raw input.
68
+ */
69
+ function canonicalCaps(caps) {
70
+ if (
71
+ caps &&
72
+ typeof caps === "object" &&
73
+ caps.network &&
74
+ typeof caps.network === "object" &&
75
+ "domains" in caps.network &&
76
+ caps.filesystem &&
77
+ "roots" in caps.filesystem &&
78
+ caps.credential &&
79
+ "names" in caps.credential
80
+ ) {
81
+ return caps;
82
+ }
83
+ return normalizeCapabilities(caps);
84
+ }
85
+
86
+ /** True when a capability set grants nothing (all-deny) — nothing to consent. */
87
+ export function capabilitiesAreEmpty(caps) {
88
+ return !diffCapabilities(null, canonicalCaps(caps)).widened;
89
+ }
90
+
91
+ /**
92
+ * PURE consent status: given a plugin's DECLARED capabilities and its stored
93
+ * consent entry, decide whether consent covers them. Re-consent is required on
94
+ * a first request or any WIDENING (new grant) — not on a version bump alone
95
+ * (trust handles versions).
96
+ *
97
+ * @param {object} declaredCaps raw or normalized capabilities
98
+ * @param {object|null} entry stored `{ version, capabilities }` (or null)
99
+ * @returns {{consented:boolean, reason:string, added:string[]}}
100
+ */
101
+ export function capabilityConsentStatus(declaredCaps, entry) {
102
+ const declared = canonicalCaps(declaredCaps);
103
+ if (!diffCapabilities(null, declared).widened) {
104
+ return { consented: true, reason: "no capabilities declared", added: [] };
105
+ }
106
+ if (!entry) {
107
+ return {
108
+ consented: false,
109
+ reason: "capabilities never consented",
110
+ added: diffCapabilities(null, declared).added,
111
+ };
112
+ }
113
+ // Coerce the stored set to canonical too (raw hand-written entry, or an
114
+ // already-normalized one read from the store) before diffing.
115
+ const diff = diffCapabilities(canonicalCaps(entry.capabilities), declared);
116
+ if (diff.widened) {
117
+ return {
118
+ consented: false,
119
+ reason: "capabilities widened since last consent",
120
+ added: diff.added,
121
+ };
122
+ }
123
+ return {
124
+ consented: true,
125
+ reason: "consent covers declared capabilities",
126
+ added: [],
127
+ };
128
+ }
129
+
130
+ /** Load the store and report whether one plugin's declared caps are consented. */
131
+ export function isPluginCapabilityConsented(plugin, declaredCaps) {
132
+ if (!plugin || !plugin.name) return false;
133
+ const entry = loadConsentStore()[consentKey(plugin.scope, plugin.name)];
134
+ return capabilityConsentStatus(declaredCaps, entry).consented;
135
+ }
136
+
137
+ /** Record consent for a plugin's currently-declared capability set. */
138
+ export function consentPluginCapabilities(
139
+ name,
140
+ { scope = "project", version, capabilities } = {},
141
+ ) {
142
+ if (!version) throw new Error("consentPluginCapabilities requires a version");
143
+ const store = loadConsentStore();
144
+ store[consentKey(scope, name)] = {
145
+ version,
146
+ capabilities: canonicalCaps(capabilities),
147
+ consentedAt: _deps.now(),
148
+ };
149
+ saveConsentStore(store);
150
+ return { name, scope, version };
151
+ }
152
+
153
+ /** Revoke capability consent for a plugin at a scope. */
154
+ export function revokeCapabilityConsent(name, { scope = "project" } = {}) {
155
+ const store = loadConsentStore();
156
+ const key = consentKey(scope, name);
157
+ const existed = Object.prototype.hasOwnProperty.call(store, key);
158
+ delete store[key];
159
+ saveConsentStore(store);
160
+ return { name, scope, removed: existed };
161
+ }
162
+
163
+ /** All consent entries (for `cc plugin consent --list`). */
164
+ export function listCapabilityConsent() {
165
+ return Object.entries(loadConsentStore()).map(([key, v]) => {
166
+ const idx = key.indexOf(":");
167
+ return {
168
+ scope: key.slice(0, idx),
169
+ name: key.slice(idx + 1),
170
+ version: v?.version || null,
171
+ consentedAt: v?.consentedAt || null,
172
+ capabilities: describeCapabilities(v?.capabilities),
173
+ };
174
+ });
175
+ }
@@ -25,6 +25,7 @@
25
25
  import { enforcePluginPolicy } from "../plugin-security.js";
26
26
  import { loadManagedSettings } from "../settings-loader.cjs";
27
27
  import { verifyInstalledSignature } from "./signature.js";
28
+ import { isPluginCapabilityConsented } from "./capability-consent.js";
28
29
 
29
30
  // Managed settings are org-admin controlled and effectively static for a
30
31
  // session, and discoverPlugins is called by every collector — memoize the load
@@ -142,6 +143,67 @@ export function filterByManagedPolicy(plugins, managed) {
142
143
  return { kept, dropped };
143
144
  }
144
145
 
146
+ /**
147
+ * Whether capability-consent must be ENFORCED at load time. The consent gate
148
+ * ([[capability-consent.js]]) already exists but was consulted only by `cc
149
+ * plugin consent`/`validate`/`doctor` — a plugin that declared capabilities and
150
+ * was never consented still loaded all six component types. Enforcement is
151
+ * OPT-IN (byte-identical default): an org sets `requirePluginCapabilityConsent`
152
+ * in managed settings, or a single user exports `CC_REQUIRE_PLUGIN_CONSENT=1`.
153
+ *
154
+ * @param {object|null} managed managed settings
155
+ * @param {object} [env] process env (injectable)
156
+ * @returns {boolean}
157
+ */
158
+ export function capabilityConsentRequired(managed, env) {
159
+ const raw = (env || process.env).CC_REQUIRE_PLUGIN_CONSENT;
160
+ if (raw != null && /^(1|true|yes|on)$/i.test(String(raw).trim())) return true;
161
+ return (
162
+ managed?.requirePluginCapabilityConsent === true ||
163
+ managed?.requirePluginCapabilityConsent === "require"
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Drop plugins that DECLARE capabilities but whose current capability SET has
169
+ * not been consented (first request or any widening). Fail-closed: a consent
170
+ * check that throws drops the plugin. Legacy plugins that declare no
171
+ * `permissions` block are UNAFFECTED — the same "未声明 legacy 不受限" rule the
172
+ * declaration audit uses. A plugin whose declared set is empty (all-deny) is
173
+ * auto-consented by `isPluginCapabilityConsented`, so it stays.
174
+ *
175
+ * @param {Array<{name, scope, manifest}>} plugins
176
+ * @returns {{ kept: Array, dropped: Array<{name, reason}> }}
177
+ */
178
+ export function filterByCapabilityConsent(plugins) {
179
+ const kept = [];
180
+ const dropped = [];
181
+ for (const p of plugins) {
182
+ if (!p.manifest?.capabilitiesDeclared) {
183
+ kept.push(p); // legacy plugin, no declaration → not gated
184
+ continue;
185
+ }
186
+ let consented = false;
187
+ try {
188
+ consented = isPluginCapabilityConsented(
189
+ { name: p.name, scope: p.scope },
190
+ p.manifest.capabilities,
191
+ );
192
+ } catch {
193
+ consented = false; // fail closed
194
+ }
195
+ if (consented) {
196
+ kept.push(p);
197
+ } else {
198
+ dropped.push({
199
+ name: p.name,
200
+ reason: `capabilities not consented — run \`cc plugin consent ${p.name} --grant\``,
201
+ });
202
+ }
203
+ }
204
+ return { kept, dropped };
205
+ }
206
+
145
207
  let _warnedDropped = new Set();
146
208
 
147
209
  /** One-time stderr warning naming plugins an org policy blocked from loading. */
@@ -27,6 +27,8 @@ import { parsePluginManifest } from "./manifest.js";
27
27
  import {
28
28
  loadManagedPluginPolicy,
29
29
  filterByManagedPolicy,
30
+ filterByCapabilityConsent,
31
+ capabilityConsentRequired,
30
32
  warnDroppedOnce,
31
33
  } from "./policy.js";
32
34
 
@@ -160,10 +162,24 @@ export function discoverPlugins(opts = {}) {
160
162
  env: opts.env,
161
163
  managedSettingsFile: opts.managedSettingsFile,
162
164
  });
163
- if (!managed) return all;
164
- const { kept, dropped } = filterByManagedPolicy(all, managed);
165
- if (dropped.length > 0) warnDroppedOnce(dropped);
166
- return kept;
165
+ const consentGate = capabilityConsentRequired(managed, opts.env);
166
+ // Default (no managed policy AND consent enforcement off): byte-identical —
167
+ // return every discovered plugin, exactly as before.
168
+ if (!managed && !consentGate) return all;
169
+ let list = all;
170
+ if (managed) {
171
+ const { kept, dropped } = filterByManagedPolicy(list, managed);
172
+ if (dropped.length > 0) warnDroppedOnce(dropped);
173
+ list = kept;
174
+ }
175
+ // Capability-consent enforcement funnels through the SAME chokepoint, so an
176
+ // un-consented plugin loads NONE of its six component types (opt-in).
177
+ if (consentGate) {
178
+ const { kept, dropped } = filterByCapabilityConsent(list);
179
+ if (dropped.length > 0) warnDroppedOnce(dropped);
180
+ list = kept;
181
+ }
182
+ return list;
167
183
  }
168
184
 
169
185
  // ── internals ────────────────────────────────────────────────────────────
@@ -477,4 +477,132 @@ export function loadProjectInstructionsBlock(opts = {}) {
477
477
  }
478
478
  }
479
479
 
480
+ function isDir(fs, p) {
481
+ try {
482
+ return fs.statSync(p).isDirectory();
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Lazy subtree-instruction discovery (module 99 §5.3 "子目录指令按首次访问子树
490
+ * 懒加载"). Startup only loads the instruction chain from the project root DOWN
491
+ * to the cwd (see `findInstructionFiles`); a large monorepo's per-package
492
+ * `cc.md`/`CLAUDE.md`/`AGENTS.md` that sit BELOW the cwd are intentionally NOT
493
+ * injected up front — they cost tokens for subtrees a turn may never touch.
494
+ *
495
+ * This resolver answers "given a path a tool just accessed, which subtree
496
+ * instruction files should now be injected?" — the files in every directory
497
+ * strictly between `baseDir` (the already-loaded cwd) and the accessed path's
498
+ * directory, minus anything already loaded or excluded. Pure + deterministic;
499
+ * the tool-time injection itself is the caller's job.
500
+ *
501
+ * @param {object} opts
502
+ * @param {string} opts.repoRoot excludes are matched relative to this
503
+ * @param {string} opts.baseDir the startup cwd (its own file already loaded)
504
+ * @param {string} opts.accessedPath a file/dir a tool touched (abs or rel to baseDir)
505
+ * @param {Set<string>|string[]} [opts.alreadyLoaded] abs paths already injected
506
+ * @param {string[]} [opts.instructionExcludes]
507
+ * @returns {Array<{path:string, scope:"project", dir:string}>} shallowest-first
508
+ */
509
+ export function resolveSubtreeInstructions(opts = {}) {
510
+ const { fs, path } = resolveDeps(opts);
511
+ const repoRoot = path.resolve(opts.repoRoot || opts.baseDir || process.cwd());
512
+ const baseDir = path.resolve(opts.baseDir || repoRoot);
513
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
514
+ const already =
515
+ opts.alreadyLoaded instanceof Set
516
+ ? opts.alreadyLoaded
517
+ : new Set((opts.alreadyLoaded || []).map((p) => path.resolve(p)));
518
+
519
+ if (!opts.accessedPath) return [];
520
+ // Resolve the accessed path to a directory inside the subtree. A not-yet-
521
+ // created file (about to be written) stats as neither file nor dir → treat it
522
+ // as a file and use its parent.
523
+ let target = path.resolve(baseDir, opts.accessedPath);
524
+ if (!isDir(fs, target)) target = path.dirname(target);
525
+
526
+ // Subtree instructions apply only to DESCENDANTS of baseDir (ancestors were
527
+ // loaded at startup). Reject a target that escapes baseDir or the repo.
528
+ const relFromBase = path.relative(baseDir, target);
529
+ if (
530
+ !relFromBase ||
531
+ relFromBase.startsWith("..") ||
532
+ path.isAbsolute(relFromBase)
533
+ ) {
534
+ return [];
535
+ }
536
+ const relFromRoot = path.relative(repoRoot, target);
537
+ if (relFromRoot.startsWith("..") || path.isAbsolute(relFromRoot)) return [];
538
+
539
+ // Directories strictly between baseDir (exclusive) and target (inclusive).
540
+ const dirs = [];
541
+ let dir = target;
542
+ while (dir !== baseDir) {
543
+ dirs.push(dir);
544
+ const parent = path.dirname(dir);
545
+ if (parent === dir) break; // hit fs root without meeting baseDir
546
+ dir = parent;
547
+ }
548
+ dirs.reverse(); // shallowest first
549
+
550
+ const out = [];
551
+ for (const d of dirs) {
552
+ const file = firstExisting(fs, path, d, PROJECT_FILE_NAMES);
553
+ if (!file) continue;
554
+ const abs = path.resolve(file);
555
+ if (already.has(abs)) continue;
556
+ if (
557
+ excludes.length > 0 &&
558
+ pathIsExcluded(path.relative(repoRoot, abs), excludes)
559
+ ) {
560
+ continue;
561
+ }
562
+ out.push({ path: abs, scope: "project", dir: d });
563
+ }
564
+ return out;
565
+ }
566
+
567
+ /**
568
+ * Stateful companion to `resolveSubtreeInstructions`: tracks which subtree
569
+ * directories have already been injected across a session so a second access to
570
+ * the same subtree is a no-op. `onAccess(path)` returns ONLY the freshly
571
+ * discovered files. Fail-open (any error → []).
572
+ */
573
+ export class SubtreeInstructionLoader {
574
+ constructor(opts = {}) {
575
+ const path = opts.deps?.path || pathDefault;
576
+ this._opts = opts;
577
+ this._loadedDirs = new Set();
578
+ this._loadedFiles = new Set(
579
+ (opts.alreadyLoaded || []).map((p) => path.resolve(p)),
580
+ );
581
+ }
582
+
583
+ /** @returns {Array<{path:string, scope:"project", dir:string}>} newly injected */
584
+ onAccess(accessedPath) {
585
+ try {
586
+ const found = resolveSubtreeInstructions({
587
+ ...this._opts,
588
+ accessedPath,
589
+ alreadyLoaded: this._loadedFiles,
590
+ });
591
+ const fresh = found.filter((f) => !this._loadedDirs.has(f.dir));
592
+ for (const f of fresh) {
593
+ this._loadedDirs.add(f.dir);
594
+ this._loadedFiles.add(f.path);
595
+ }
596
+ return fresh;
597
+ } catch {
598
+ return [];
599
+ }
600
+ }
601
+
602
+ /** Absolute paths injected so far (startup set + every onAccess discovery). */
603
+ loadedFiles() {
604
+ return [...this._loadedFiles];
605
+ }
606
+ }
607
+
480
608
  export const _deps = { fs: fsDefault, path: pathDefault, os: osDefault };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * REPL `/goal <condition>` — a SESSION completion condition for interactive use.
3
+ *
4
+ * Three distinct "goal" concepts, kept separate on purpose:
5
+ * - `cc goal` → long-term, cross-session OKR (goal-store.js).
6
+ * - headless --goal-condition → AUTO re-drives outer turns until met/exhausted.
7
+ * - REPL `/goal <cond>` → THIS: a human is present each turn, so it
8
+ * EVALUATES + REPORTS after every turn whether the condition is met. It does
9
+ * NOT auto-submit follow-ups (that autonomous loop belongs to headless, where
10
+ * no human is watching) — the user decides whether to keep going.
11
+ *
12
+ * This is the pure, clock/dep-injected core over the already-tested
13
+ * goal-condition-engine.js; the agent-repl wiring is thin display + eval glue.
14
+ */
15
+
16
+ import {
17
+ isDeterministicCondition,
18
+ runDeterministicCheck,
19
+ GoalConditionEngine,
20
+ GOAL_DECISION,
21
+ } from "./goal-condition-engine.js";
22
+
23
+ // Interactive sessions are human-paced, so the turn-count budget is generous —
24
+ // it exists only as a backstop (the engine hard-caps at 100 regardless). The
25
+ // meaningful signal in the REPL is met / not-yet-met, not budget exhaustion.
26
+ export const REPL_GOAL_MAX_OUTER_TURNS = 100;
27
+
28
+ /** Parse the `/goal` argument into an intent. Empty → status; a keyword → clear. */
29
+ export function parseGoalCommand(arg) {
30
+ const s = String(arg ?? "").trim();
31
+ if (!s) return { action: "status" };
32
+ if (s === "clear" || s === "off" || s === "stop" || s === "drop")
33
+ return { action: "clear" };
34
+ return { action: "set", spec: s };
35
+ }
36
+
37
+ /**
38
+ * Create the REPL's session goal engine for a spec. Throws (via
39
+ * parseGoalCondition) on a malformed spec so the caller can report it.
40
+ */
41
+ export function createReplGoal(spec, { now = () => 0 } = {}) {
42
+ return new GoalConditionEngine({
43
+ condition: spec,
44
+ budget: { maxOuterTurns: REPL_GOAL_MAX_OUTER_TURNS },
45
+ now,
46
+ });
47
+ }
48
+
49
+ /** Display lines shown when a goal is set. */
50
+ export function renderGoalStart(engine) {
51
+ return [
52
+ `◎ goal set: ${engine.condition.source}`,
53
+ ` evaluated after each turn (≤ ${engine.budget.maxOuterTurns} turns); /goal clear to drop, /goal to check`,
54
+ ];
55
+ }
56
+
57
+ /** Display lines for `/goal` with no argument (status). */
58
+ export function renderGoalStatus(engine) {
59
+ if (!engine)
60
+ return [
61
+ "No session goal. Set one with /goal <condition>",
62
+ " e.g. /goal exit-zero:npm test · /goal file-exists:dist/app.js · /goal contains:ALL GREEN · /goal the tests pass",
63
+ ];
64
+ const s = engine.state;
65
+ return [
66
+ `◎ goal: ${engine.condition.source}`,
67
+ ` progress: outer turn ${s.outerTurns}/${engine.budget.maxOuterTurns}` +
68
+ (isDeterministicCondition(engine.condition)
69
+ ? ` (${engine.condition.kind} check)`
70
+ : " (model-judged)"),
71
+ ];
72
+ }
73
+
74
+ /** Verdict display lines for one evaluation's decision + events. */
75
+ export function renderGoalVerdict(decision, events) {
76
+ const evaluated = (events || []).find((e) => e.type === "goal_evaluated");
77
+ if (decision === GOAL_DECISION.COMPLETE) {
78
+ const done = (events || []).find((e) => e.type === "goal_completed");
79
+ return [
80
+ `✔ goal met: ${done?.reason ?? evaluated?.reason ?? "condition met"}`,
81
+ ];
82
+ }
83
+ if (decision === GOAL_DECISION.EXHAUSTED) {
84
+ const ex = (events || []).find((e) => e.type === "goal_exhausted");
85
+ return [
86
+ `⛔ goal dropped — ${ex?.reason ?? "budget exhausted"}${ex?.limit ? ` (${ex.limit})` : ""}`,
87
+ ];
88
+ }
89
+ return [
90
+ `◎ goal not yet met: ${evaluated?.reason ?? "unmet"} · outer turn ${evaluated?.outerTurns ?? "?"}`,
91
+ ];
92
+ }
93
+
94
+ /**
95
+ * Evaluate the session goal against a completed turn's final text. Deterministic
96
+ * conditions run inline (contains/regex on the text; exit-zero/file-exists via
97
+ * injected `spawnSync`/`existsSync`); a model condition delegates to `judge`.
98
+ *
99
+ * @param {GoalConditionEngine} engine
100
+ * @param {string} finalText the assistant's answer this turn
101
+ * @param {object} deps { cwd, spawnSync, existsSync, judge, tokens, costUsd }
102
+ * judge(condition, {finalText}) => {met, reason, evidence}
103
+ * @returns {Promise<{decision, events, done, met}>}
104
+ */
105
+ export async function evaluateReplGoalTurn(engine, finalText, deps = {}) {
106
+ engine.recordTurnUsage({ tokens: deps.tokens, costUsd: deps.costUsd });
107
+ const cond = engine.condition;
108
+ let evaluation;
109
+ if (isDeterministicCondition(cond)) {
110
+ evaluation = runDeterministicCheck(cond, {
111
+ lastOutput: finalText,
112
+ cwd: deps.cwd,
113
+ spawnSync: deps.spawnSync,
114
+ existsSync: deps.existsSync,
115
+ });
116
+ } else if (typeof deps.judge === "function") {
117
+ evaluation = await deps.judge(cond, { finalText });
118
+ } else {
119
+ evaluation = {
120
+ met: false,
121
+ reason: "no model judge available for this condition",
122
+ evidence: { kind: "model" },
123
+ };
124
+ }
125
+ const { decision, events } = engine.evaluate(evaluation);
126
+ return {
127
+ decision,
128
+ events,
129
+ done: engine.done,
130
+ met: decision === GOAL_DECISION.COMPLETE,
131
+ };
132
+ }
@@ -25,6 +25,13 @@
25
25
  * turn be matched to its pre-mutation snapshot purely from the count.
26
26
  */
27
27
 
28
+ import {
29
+ buildTurnBindingFromMarks,
30
+ resolveRestorePlan,
31
+ RESTORE_SCOPE,
32
+ TURN_COVERAGE,
33
+ } from "./turn-binding.js";
34
+
28
35
  export const DEFAULT_LIST_LIMIT = 10;
29
36
  export const PREVIEW_CHARS = 60;
30
37
 
@@ -219,6 +226,51 @@ export function restoreClearedConversation(messages, marks, cleared) {
219
226
  };
220
227
  }
221
228
 
229
+ /**
230
+ * Coverage-aware restore plan for a rewound turn, derived from the REPL's
231
+ * EXISTING implicit state (`listUserTurns()` output + `{atMessageCount,id,tool}`
232
+ * marks) — no new agent-loop events required. Bridges those into the explicit
233
+ * turn-binding table and asks `resolveRestorePlan` for the honest plan, so
234
+ * `/rewind` can warn when a restore can't be fully promised (side-effects, a
235
+ * missing checkpoint, or a conversation/files drift) instead of silently doing a
236
+ * lossy rewind.
237
+ *
238
+ * @param {Array} messages live conversation (for listUserTurns)
239
+ * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
240
+ * @param {number} turnIndex message index returned by rewindToTurn()
241
+ * @param {string} [scope] one of RESTORE_SCOPE (default BOTH)
242
+ * @returns {{scope, coverage, conversation, files, warnings:string[]}}
243
+ */
244
+ export function buildRewindPlan(
245
+ messages,
246
+ marks,
247
+ turnIndex,
248
+ scope = RESTORE_SCOPE.BOTH,
249
+ ) {
250
+ const turns = listUserTurns(messages, { limit: 1000 });
251
+ const log = buildTurnBindingFromMarks(turns, marks);
252
+ const turn = log.get(`turn-${Number(turnIndex)}`);
253
+ return resolveRestorePlan(turn, scope);
254
+ }
255
+
256
+ /**
257
+ * Human-readable lines for a rewind plan's coverage + warnings, for `/rewind` to
258
+ * print before it truncates. Returns [] for a fully-restorable turn with no
259
+ * warnings (nothing worth interrupting the user for).
260
+ *
261
+ * @param {{coverage:string, warnings:string[]}} plan from buildRewindPlan()
262
+ * @returns {string[]}
263
+ */
264
+ export function renderRewindWarnings(plan) {
265
+ if (!plan) return [];
266
+ const lines = [];
267
+ if (plan.coverage && plan.coverage !== TURN_COVERAGE.FULL) {
268
+ lines.push(` coverage: ${plan.coverage} (restore is not guaranteed)`);
269
+ }
270
+ for (const w of plan.warnings || []) lines.push(` ⚠ ${w}`);
271
+ return lines;
272
+ }
273
+
222
274
  /** Render the picker list (shared by /rewind and double-Esc). */
223
275
  export function renderTurnList(turns) {
224
276
  if (!turns.length) return " (no user turns yet)";
@@ -52,11 +52,27 @@ export function baseFireAt(entry) {
52
52
  return Number.isFinite(n) ? n : null;
53
53
  }
54
54
 
55
+ /**
56
+ * The jitter window that governs an entry: a PER-ENTRY `entry.jitterMs` (a
57
+ * finite, non-negative number stored at create time) is authoritative and wins
58
+ * over the caller's `jitterMs` fallback; an entry without its own field falls
59
+ * back to the passed global. This lets each scheduled task carry its own spread
60
+ * — one noisy cron can jitter without perturbing the rest — while still honoring
61
+ * a daemon-wide default for entries that predate per-entry jitter.
62
+ */
63
+ export function resolveEntryJitterMs(entry, jitterMs = 0) {
64
+ const per = Number(entry?.jitterMs);
65
+ if (Number.isFinite(per) && per >= 0) return per;
66
+ return Number(jitterMs) || 0;
67
+ }
68
+
55
69
  /** Base fire time with the entry's deterministic jitter applied. */
56
70
  export function effectiveFireAt(entry, jitterMs = 0) {
57
71
  const base = baseFireAt(entry);
58
72
  if (base == null) return null;
59
- return base + jitterOffsetMs(entry?.id, jitterMs);
73
+ return (
74
+ base + jitterOffsetMs(entry?.id, resolveEntryJitterMs(entry, jitterMs))
75
+ );
60
76
  }
61
77
 
62
78
  /** Is an entry still pending/active (not in a terminal state)? */
@@ -19,12 +19,18 @@
19
19
  const { collectHooks } = require("./settings-hooks.cjs");
20
20
  const { runHooks } = require("./hook-runner.cjs");
21
21
  const { buildHookEnvelope } = require("./hook-event-bus.cjs");
22
+ const hookEventLog = require("./hook-event-log.cjs");
22
23
 
23
24
  /**
24
25
  * Stamp a unified-bus delivery id (event_id) onto a hook payload (P2 event bus).
25
26
  * Additive — a hook that ignores it is unaffected; a hook that reads it gets a
26
27
  * stable per-delivery id for correlation / `cc hook replay`. traceId/parentId
27
28
  * are threaded when the caller provides them (else null).
29
+ *
30
+ * When `CC_HOOK_EVENT_LOG` is enabled, the FULL envelope is best-effort recorded
31
+ * to the hash-chained hook event log so `cc hook replay <event-id>` has a real
32
+ * source. Recording is opt-in and never throws, so the default firing path is
33
+ * byte-for-byte unchanged.
28
34
  */
29
35
  function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
30
36
  const env = buildHookEnvelope({
@@ -34,6 +40,13 @@ function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
34
40
  traceId: traceId || null,
35
41
  parentId: parentId || null,
36
42
  });
43
+ if (hookEventLog.isHookEventLogEnabled()) {
44
+ try {
45
+ hookEventLog.appendHookEvent(env);
46
+ } catch {
47
+ /* best-effort — recording must not break the turn */
48
+ }
49
+ }
37
50
  return { ...payload, event_id: env.event_id };
38
51
  }
39
52