chainlesschain 0.162.161 → 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 (209) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DMn528q-.js → AIOps-BQ0kgfA1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-D9ZcDCJL.js → ActionButton-Dr1MDANe.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-A_K-8PX0.js → Analytics-CZ5vsaXW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D4pFW3X6.js → AppLayout-BIBga881.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-Dz46COAd.js → Artifacts-vpXHRU3u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-cbMjbyg0.js → Audit-B8exuoub.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-Dtf88GAj.js → BackgroundAgents-923evRRN.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-C5KRxn52.js → Backup-sr_sswlQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Dd2UK3BN.js → BaseInput-CnW-4XKT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-lptKTFdG.js → Chat-FZCsTZIL.js} +5 -5
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-_yspG6rG.js → Checkbox-DFrxB59H.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BGq_GAha.js → Codegen-_b6WEel8.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CePor71g.js → Col-CkbnnZ-K.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CLOWhIT7.js → Community-BxM_82zW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CivJt2vT.js → Compact-DYinP45R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Btmo1GGE.js → Compliance-DyXp3sSP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Ct35OlG4.js → Cowork-BEr2DhW7.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-CTc2GmhB.js → Cron-B-Vwo1hN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-ZtiBETk1.js → Crosschain-DbLxKddd.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BNqMQQXw.js → DID-BsQ_LFw9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-BmzZd-9V.js → Dashboard-lVlXQHu5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CMVgvP2M.js → Dropdown-Beo_foO7.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-CcqQFAfT.js → EmailListRenderer-DszHIXED.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CTqB1_4V.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CiKOOzlb.js → Federation-CuJLa2eb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Wk-OT0gr.js → FormItemContext-Cxd1lxhc.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DbEQT6ue.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-0ZvCPSD9.js → Git-WqPKetx6.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CmVuKkPb.js → Governance-CWYtY3ke.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CTWE3Jwg.js → Inference-BnX1iiNV.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-CBEIF00L.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DLhxDwVG.js → Logs-DqLMNntm.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-BqYwEYou.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BgsCBrnc.js → Marketplace-_vGfmhNl.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-ClBUGDS2.js → McpTools-BeJDOkDj.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CnG_ZnYw.js → Memory-Da0EEZDO.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BUOAbR1Z.js → MobileBridge-DSuzcCCy.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-Dnt_9-WB.js → MobileProjects-D0RAk4e8.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-CLjt4kCD.js → Mtc-BE9YgdA0.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-f24U9Aes.js → MtcAudit-fC2BpO5S.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-GrmGQbSq.js → Multisig-D24Xxqw3.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DADaWn-n.js → NLProgramming-Bs3oGvS8.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-xtf5RX6C.js → Notes-BFjmAA-3.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Cd27Rgtx.js → NotificationSettings-BX_9Bhv0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-fXRxlDjl.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-joCFlNB0.js → Organization-DBcdsVEP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-B1HoURq0.js → Overflow-DDyaF9vC.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-DiwsKbT7.js → P2P-BB78L4he.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Xe-BsW6S.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-C23qi8kl.js → Permissions-INbnWv4j.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BS1syX2f.js → PersonalDataHub-BST-VmTq.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-DRYwwDyh.js → Pipeline-CE7_tS2k.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQKaDHPH.js → Privacy-hK5L8dOB.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BC6F9xiG.js → ProjectInit-BvkkUoDn.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-ClFPc1Nd.js → ProjectSettings-u7_EUtkm.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-5Fmt_8Nw.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-CLWuOUNt.js → Providers-B9eanarR.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DiqULTxb.js → QrScannerModal-C0dX5N2f.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-9A03P-E_.js → QuickAsk-CKTpG-PZ.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CJZAnbq_.js → Recommend-Px_w_XQM.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C9A1R77h.js → RemoteSession-Di0fnYkM.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BIZc6Rky.js → Reputation-vesnWjXt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DfIPNtuG.js → Row-exbY6LkT.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C5znFcQ1.js → RssFeed-B3O3x70t.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-DSs2z3OX.js → Search-uRA0DhNz.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CV6LcrLY.js → Security-CViqdH6K.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-cmhoX0Xj.js → Services-CkXS97WN.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-tCsg2po7.js → Skeleton-DxW5WYnE.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-BK1KUDiy.js → Skills-CLxmGFse.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-yRgyeA82.js → Sla-Cr5fziXq.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-c1mo1mlp.js → SpeechSettings-H0JSb3cp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-koIAfuT3.js → SyncSettings-DMYWMae7.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-DkkreAaB.js → Tasks-CbYyDdF4.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-fy5Ctf9M.js → Templates-Cpc8JjiK.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-Bw70Jf6S.js → Tenant-CRh77Jda.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BKh6Rx-4.js → Terminal-4D8tNBO5.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-Dg54WP0c.js → TimelineRenderer-D41jzzbG.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BvvD5SxJ.js → Tokens-C_jWEy6b.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-B7Ug9x4e.js → Trigger-Dk1ZUhsy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BjvuMORY.js → Trust-CLVy4l7m.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CZqhkx8p.js → UkeySign-C-FeqFDj.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-ClR50lhX.js → VideoEditing-CqVZzyMm.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DlfppYjx.js → Wallet-DX9BW9pu.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-BirgVr8B.js → WebAuthn-7hQDpWw_.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-B8DP_bax.js → WorkflowEditor-6XqUAO0j.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-DA8AUZzL.js → chat-5R_vkaJG.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-C38pLGp1.js → colors-huUPZdC-.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-81rtr98o.js → compact-item-CfJlgisE.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-D34ixq8t.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-D4bzqTrZ.js → hasIn-DY8Vxqx7.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-E1OJRXs4.js → index-A48PtXw9.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-D3zCCZRS.js → index-B300RPnT.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-Cd3KuNK5.js → index-B74OXFxC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dmo3dFHb.js → index-BAON7TzU.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Ddk0e_Pi.js → index-BCqxyvqp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-9qoVpPt6.js → index-BHE8OIls.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DBFSo1NF.js → index-BQS-uPOK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DUQLXLQM.js → index-BRXOP09X.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-D6GfvEQG.js → index-BYLBPpt8.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DFyWKOBD.js → index-BfCx4pFT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BP-zkXK8.js → index-BliaT02i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DMgnRwrK.js → index-BmqGerpC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BIP62a6r.js → index-BsBtRWbA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DkHqMnn3.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-B8mkLM4x.js → index-Bxax9vQ6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-ZRJRItm0.js → index-C2iVSFpy.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CP5alL4t.js → index-CA6sO5VD.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BS3M5H9V.js → index-CA90TZxG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DvfCEJG-.js → index-CLhBh-qE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-tKiC8T-4.js → index-CRElRhyS.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B0QArPQt.js → index-Cbe9G0V4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COe_cOLJ.js → index-ChvHubX3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BvL_3J_n.js → index-Cis_Ip7H.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-F76w1NiL.js → index-Cjxbu68P.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BlxEphl7.js → index-CzKmteNS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CuS-UPoj.js → index-D0pet5Uy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CbhE3j5j.js → index-D1VLuww0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D_2VYPr1.js → index-DXBcvUnf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DakfFZ-k.js → index-Db1cLWir.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-fI5sXMLg.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-CoVgZw_b.js → index-KRHroryI.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BvD7b2IK.js → index-d1xkbtNz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BtGxuQrO.js → index-dKx57io7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BqIhLAbA.js → index-j3O2EEJe.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BXd2_LmH.js → index-p85YaaKw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DFHa1HIg.js → index-v3pTaEQm.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D0Ih7Hwn.js → index-vXs3B8GJ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-ByyvflIR.js → initDefaultProps-MCB7-raZ.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-B3FQm2By.js → motion-UWDF9swV.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-BSfns6Ef.js → move-CsTmgXZU.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-CTG1Ieri.js → mtc-parser-fQy6KGLg.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-DObM_2j6.js → omit-woPnEjd1.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-DOfelQ1t.js → pickAttrs-Bs2dZ1gf.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-CHZILds_.js → placementArrow-C7EVgPkg.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-REbucKSx.js → responsiveObserve-BH2kYCTN.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-C6EZzD7o.js → slide-BFBP2V_e.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BxkWS-g3.js → statusUtils-xdDxDYse.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-Chklyoq6.js → styleChecker-aAWgPPQf.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-CRVwKrrI.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DV24ik_J.js → useFs-BrOih40N.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-C7vIo4Pg.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-By-v5GlF.js → vnode-B8lutNSE.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-B1w9Am3r.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 +352 -2
  156. package/src/data/changelog.json +14 -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/agents.js +19 -0
  161. package/src/lib/background-agent-supervisor.js +34 -7
  162. package/src/lib/channels/channel-manager.js +54 -2
  163. package/src/lib/cli-context-engineering.js +9 -2
  164. package/src/lib/context-breakdown.js +116 -0
  165. package/src/lib/docs-drift.js +166 -0
  166. package/src/lib/doctor-checkup.js +128 -0
  167. package/src/lib/hook-event-bus.cjs +0 -0
  168. package/src/lib/hook-event-log.cjs +193 -0
  169. package/src/lib/interaction-adapter.js +34 -4
  170. package/src/lib/json-schema-output.js +75 -12
  171. package/src/lib/json-schema-validate.js +831 -0
  172. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  173. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  174. package/src/lib/operation-fingerprint.js +95 -0
  175. package/src/lib/plugin-runtime/capabilities.js +371 -0
  176. package/src/lib/plugin-runtime/capability-consent.js +175 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/plugin-runtime/policy.js +62 -0
  179. package/src/lib/plugin-runtime/scopes.js +20 -4
  180. package/src/lib/project-instructions.js +128 -0
  181. package/src/lib/remote-approval-bridge.js +16 -0
  182. package/src/lib/repl-goal.js +132 -0
  183. package/src/lib/repl-rewind.js +52 -0
  184. package/src/lib/review-pipeline.js +227 -0
  185. package/src/lib/runtime-checkup.js +318 -0
  186. package/src/lib/schedule-planner.js +17 -1
  187. package/src/lib/session-lifecycle.js +305 -0
  188. package/src/lib/settings-hook-events.cjs +52 -13
  189. package/src/lib/settings-loader.cjs +42 -0
  190. package/src/lib/side-effect-ledger-store.js +88 -0
  191. package/src/lib/side-effect-ledger.js +296 -0
  192. package/src/lib/sub-agent-context.js +74 -5
  193. package/src/lib/subagent-contract.js +383 -0
  194. package/src/lib/subagent-inheritance.js +112 -0
  195. package/src/lib/telemetry-ids.js +190 -0
  196. package/src/lib/turn-binding-store.js +119 -0
  197. package/src/lib/turn-binding.js +397 -0
  198. package/src/repl/agent-repl.js +136 -0
  199. package/src/repl/permission-tier.js +28 -0
  200. package/src/runtime/agent-core.js +387 -14
  201. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  202. package/src/runtime/headless-runner.js +267 -0
  203. package/src/runtime/headless-stream.js +60 -1
  204. package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +0 -1
  205. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +0 -1
  206. package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +0 -1
  207. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +0 -1
  208. package/src/assets/web-panel/assets/index-CY1NURWZ.js +0 -1
  209. package/src/assets/web-panel/assets/index-DokUYqpq.js +0 -1
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Per-source token attribution for `cc context --sources` — the P1
3
+ * large-monorepo "/context 按来源分解" lever. The base `cc context` view buckets
4
+ * a session's stored messages by ROLE; this adds a BY-SOURCE view that also
5
+ * attributes the project-memory instruction files (cc.md / CLAUDE.md / rules /
6
+ * @imports) that would be loaded for the current working directory, each with
7
+ * its own token cost and provenance — so a user can see WHICH instruction file
8
+ * (or message class) fills the context window.
9
+ *
10
+ * Pure: token counting for instruction files is delegated to the injected
11
+ * estimator (the same CJK-aware `estimateTokens` the window sizing uses);
12
+ * message-role buckets already arrive pre-counted. No I/O.
13
+ *
14
+ * Scope note: skills and MCP tool schemas are runtime tool definitions loaded
15
+ * when an agent actually runs — they are NOT part of a persisted transcript nor
16
+ * of the project-instruction set, so they are intentionally not attributed here.
17
+ */
18
+
19
+ import path from "path";
20
+
21
+ /**
22
+ * Display path for an instruction source: relative-to-cwd when the file lives
23
+ * inside cwd, else the absolute path (a `~/.claude/…` user-scope file, or an
24
+ * @import that escaped the tree, is clearer shown in full).
25
+ */
26
+ export function relativizeInstructionPath(absPath, cwd) {
27
+ if (!absPath) return "";
28
+ if (!cwd) return absPath;
29
+ try {
30
+ const rel = path.relative(cwd, absPath);
31
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return absPath;
32
+ return rel.split(path.sep).join("/");
33
+ } catch {
34
+ return absPath;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Per-file instruction sources from `loadProjectInstructions().files`.
40
+ *
41
+ * @param {Array<{path,scope,content,truncated}>} files
42
+ * @param {(t:string)=>number} estimateTokens
43
+ * @param {string} [cwd]
44
+ * @returns {{ sources: Array<{scope,source,path,tokens,truncated}>, total:number }}
45
+ */
46
+ export function breakdownInstructionSources(files, estimateTokens, cwd) {
47
+ const est = typeof estimateTokens === "function" ? estimateTokens : () => 0;
48
+ const list = Array.isArray(files) ? files : [];
49
+ const sources = list.map((f) => ({
50
+ scope: f?.scope || "project",
51
+ source: relativizeInstructionPath(f?.path, cwd),
52
+ path: f?.path || "",
53
+ tokens: est(typeof f?.content === "string" ? f.content : ""),
54
+ truncated: Boolean(f?.truncated),
55
+ }));
56
+ const total = sources.reduce((s, x) => s + x.tokens, 0);
57
+ return { sources, total };
58
+ }
59
+
60
+ // role bucket key → { label, kind } for the ranked source list.
61
+ const MESSAGE_SOURCE_ROWS = [
62
+ ["system", "system messages", "message"],
63
+ ["user", "user messages", "message"],
64
+ ["assistant", "assistant messages", "message"],
65
+ ["tool", "tool results", "message"],
66
+ ["toolCalls", "tool calls", "tool_calls"],
67
+ ];
68
+
69
+ /**
70
+ * Merge the project-memory instruction total with the per-role message buckets
71
+ * into ONE ranked source list (largest first) with each source's share of the
72
+ * combined by-source total. The message buckets are the same ones
73
+ * `categorizeContext` produces; instructions are folded in as a single group so
74
+ * the ranked view stays readable (per-file detail lives in the instructions
75
+ * breakdown).
76
+ *
77
+ * @param {object} args
78
+ * @param {number} [args.instructionTotal=0] Σ instruction-file tokens
79
+ * @param {object} [args.buckets={}] categorizeContext buckets
80
+ * @param {object} [args.counts={}] categorizeContext counts
81
+ * @returns {{ sources: Array<{kind,source,tokens,count,share}>, total:number }}
82
+ */
83
+ export function rankContextSources({
84
+ instructionTotal = 0,
85
+ buckets = {},
86
+ counts = {},
87
+ } = {}) {
88
+ const rows = [];
89
+ if (instructionTotal > 0) {
90
+ rows.push({
91
+ kind: "instructions",
92
+ source: "project memory (instructions)",
93
+ tokens: instructionTotal,
94
+ count: null,
95
+ });
96
+ }
97
+ for (const [key, label, kind] of MESSAGE_SOURCE_ROWS) {
98
+ const tok = buckets[key] || 0;
99
+ if (tok > 0) {
100
+ rows.push({
101
+ kind,
102
+ source: label,
103
+ tokens: tok,
104
+ count: counts[key] ?? null,
105
+ });
106
+ }
107
+ }
108
+ const total = rows.reduce((s, x) => s + x.tokens, 0);
109
+ // Stable sort: tokens desc, then source name for deterministic ties.
110
+ rows.sort((a, b) => b.tokens - a.tokens || a.source.localeCompare(b.source));
111
+ const sources = rows.map((r) => ({
112
+ ...r,
113
+ share: total ? r.tokens / total : 0,
114
+ }));
115
+ return { sources, total };
116
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Auto-generated CLI reference + doc-drift detection (P2 "文档").
3
+ *
4
+ * The gap: commands, agent tools, flags, exit codes, and protocol fields are
5
+ * documented by hand and silently drift from the code. `command-manifest.json`
6
+ * (regenerated + CI-guarded) and the `AGENT_TOOLS` registry are the authoritative
7
+ * data sources; nothing turns them into a reference doc or checks a hand-written
8
+ * doc against them.
9
+ *
10
+ * This is the PURE core: build a canonical reference from the manifest + tool
11
+ * list, render it, and diff a documentation surface against the authoritative
12
+ * sets to catch drift in BOTH directions (documented-but-gone / present-but-
13
+ * undocumented). No I/O — the caller reads the manifest, tool names, and doc text.
14
+ */
15
+
16
+ /** Normalize the command-manifest.json commands into a stable reference list. */
17
+ export function extractCommands(manifest = {}) {
18
+ const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
19
+ return commands
20
+ .map((c) => ({
21
+ name: String(c?.name || "").trim(),
22
+ aliases: Array.isArray(c?.aliases)
23
+ ? c.aliases.map((a) => String(a).trim()).filter(Boolean)
24
+ : [],
25
+ summary: String(c?.summary || "").trim(),
26
+ }))
27
+ .filter((c) => c.name)
28
+ .sort((a, b) => a.name.localeCompare(b.name));
29
+ }
30
+
31
+ /**
32
+ * Normalize the AGENT_TOOLS registry into a sorted, de-duped name list. Accepts
33
+ * plain strings, `{ name }` contracts, and OpenAI-style `{ function: { name } }`
34
+ * function-tool definitions.
35
+ */
36
+ export function extractToolNames(tools = []) {
37
+ const names = (Array.isArray(tools) ? tools : [])
38
+ .map((t) => {
39
+ if (typeof t === "string") return t.trim();
40
+ const n = t?.name ?? t?.function?.name;
41
+ return String(n || "").trim();
42
+ })
43
+ .filter(Boolean);
44
+ return [...new Set(names)].sort();
45
+ }
46
+
47
+ /** Every command token a user can invoke: primary names + aliases. */
48
+ export function commandTokens(commands) {
49
+ const set = new Set();
50
+ for (const c of commands) {
51
+ set.add(c.name);
52
+ for (const a of c.aliases) set.add(a);
53
+ }
54
+ return set;
55
+ }
56
+
57
+ /**
58
+ * Build the canonical reference object from the authoritative sources. This is
59
+ * the data a generator serializes; `renderReferenceMarkdown` renders it.
60
+ */
61
+ export function buildCliReference({ manifest = {}, tools = [] } = {}) {
62
+ const commands = extractCommands(manifest);
63
+ const toolNames = extractToolNames(tools);
64
+ return {
65
+ commandCount: commands.length,
66
+ toolCount: toolNames.length,
67
+ commands,
68
+ tools: toolNames,
69
+ };
70
+ }
71
+
72
+ /** Deterministic markdown render of the reference (stable output for diffing). */
73
+ export function renderReferenceMarkdown(reference) {
74
+ const lines = [];
75
+ lines.push("# CLI Reference (generated)");
76
+ lines.push("");
77
+ lines.push(
78
+ "<!-- Generated from command-manifest.json + AGENT_TOOLS. Do not edit by hand. -->",
79
+ );
80
+ lines.push("");
81
+ lines.push(`## Commands (${reference.commandCount})`);
82
+ lines.push("");
83
+ for (const c of reference.commands) {
84
+ const alias = c.aliases.length ? ` (aliases: ${c.aliases.join(", ")})` : "";
85
+ lines.push(`- \`${c.name}\`${alias}${c.summary ? ` — ${c.summary}` : ""}`);
86
+ }
87
+ lines.push("");
88
+ lines.push(`## Agent tools (${reference.toolCount})`);
89
+ lines.push("");
90
+ for (const name of reference.tools) lines.push(`- \`${name}\``);
91
+ lines.push("");
92
+ return lines.join("\n");
93
+ }
94
+
95
+ /**
96
+ * Which authoritative command/tool tokens appear as literal `cc <token>` /
97
+ * backticked `<token>` mentions in the doc text. We look for word-boundary
98
+ * mentions; a token is "documented" if it appears anywhere in the doc.
99
+ */
100
+ function isMentioned(doc, token) {
101
+ if (!token) return false;
102
+ const esc = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
+ return new RegExp(`(^|[^\\w-])${esc}([^\\w-]|$)`).test(doc);
104
+ }
105
+
106
+ /**
107
+ * Diff a documentation surface against the authoritative sets. Reports drift both
108
+ * ways so neither a NEW undocumented command nor a REMOVED-but-still-documented
109
+ * one slips through.
110
+ *
111
+ * @param {object} args
112
+ * manifest — command-manifest.json object
113
+ * tools — AGENT_TOOLS array
114
+ * doc — the documentation text to check
115
+ * knownTokens — optional extra tokens to treat as authoritative (e.g. flags,
116
+ * exit codes, protocol fields) to catch their drift too
117
+ * @returns {{ undocumentedCommands, undocumentedTools, staleCommandMentions,
118
+ * ok, counts }}
119
+ */
120
+ export function detectDocDrift({
121
+ manifest = {},
122
+ tools = [],
123
+ doc = "",
124
+ knownTokens = [],
125
+ } = {}) {
126
+ const commands = extractCommands(manifest);
127
+ const toolNames = extractToolNames(tools);
128
+ const text = String(doc || "");
129
+
130
+ const undocumentedCommands = commands
131
+ .filter((c) => !isMentioned(text, c.name))
132
+ .map((c) => c.name);
133
+ const undocumentedTools = toolNames.filter((n) => !isMentioned(text, n));
134
+
135
+ // Tokens the doc claims as commands (in a `cc <token>` construct) that are not
136
+ // in the manifest → stale references to removed/renamed commands.
137
+ const known = commandTokens(commands);
138
+ for (const t of knownTokens) known.add(String(t));
139
+ const staleCommandMentions = [];
140
+ const seen = new Set();
141
+ const ccRe = /\bcc\s+([a-z][a-z0-9-]*)\b/g;
142
+ let m;
143
+ while ((m = ccRe.exec(text)) !== null) {
144
+ const tok = m[1];
145
+ if (!known.has(tok) && !seen.has(tok)) {
146
+ seen.add(tok);
147
+ staleCommandMentions.push(tok);
148
+ }
149
+ }
150
+
151
+ const counts = {
152
+ undocumentedCommands: undocumentedCommands.length,
153
+ undocumentedTools: undocumentedTools.length,
154
+ staleCommandMentions: staleCommandMentions.length,
155
+ };
156
+ return {
157
+ undocumentedCommands,
158
+ undocumentedTools,
159
+ staleCommandMentions,
160
+ counts,
161
+ ok:
162
+ counts.undocumentedCommands === 0 &&
163
+ counts.undocumentedTools === 0 &&
164
+ counts.staleCommandMentions === 0,
165
+ };
166
+ }
@@ -26,6 +26,7 @@ import {
26
26
  import { execSync } from "node:child_process";
27
27
  import { join, basename } from "node:path";
28
28
  import { getHomeDir, getConfigPath } from "./paths.js";
29
+ import { checkAgenda, checkInstructionFiles } from "./runtime-checkup.js";
29
30
 
30
31
  export const CHECK_LEVELS = Object.freeze({
31
32
  OK: "ok",
@@ -314,6 +315,58 @@ async function pluginSection(opts, _deps2) {
314
315
  } catch (err) {
315
316
  checks.push(failedCheck("plugins", "Installed plugins", err));
316
317
  }
318
+
319
+ // Broken manifests + un-consented capabilities (Phase 3 capability gap).
320
+ // Distinct from the signature-lock check above: re-discover to get each
321
+ // plugin's parsed manifest (listInstalled drops it) so we can flag an invalid
322
+ // manifest and any DECLARED capability set the user has not consented to.
323
+ try {
324
+ const { discoverPlugins } = await import("./plugin-runtime/scopes.js");
325
+ const { isPluginCapabilityConsented } =
326
+ await import("./plugin-runtime/capability-consent.js");
327
+ const { checkPluginsAndLsp } = await import("./runtime-checkup.js");
328
+ const discovered =
329
+ discoverPlugins({ cwd: opts.cwd || process.cwd(), skipPolicy: true }) ||
330
+ [];
331
+ // Broken manifests → warn (shared runtime-checkup evaluator).
332
+ for (const f of checkPluginsAndLsp(
333
+ discovered.map((p) => ({
334
+ id: p.name,
335
+ healthy: p.manifest?.ok !== false,
336
+ kind: "plugin",
337
+ })),
338
+ )) {
339
+ checks.push(
340
+ check(
341
+ f.id,
342
+ `plugin ${f.ref}`,
343
+ CHECK_LEVELS.WARN,
344
+ `${f.message} — ${f.remediation}`,
345
+ ),
346
+ );
347
+ }
348
+ // Declared capabilities not yet consented → warn.
349
+ for (const p of discovered) {
350
+ if (!p.manifest?.capabilitiesDeclared) continue;
351
+ const consented = isPluginCapabilityConsented(
352
+ { name: p.name, scope: p.scope, version: p.version },
353
+ p.manifest.capabilities,
354
+ );
355
+ if (!consented) {
356
+ checks.push(
357
+ check(
358
+ "plugin-capability-unconsented",
359
+ `plugin ${p.name}`,
360
+ CHECK_LEVELS.WARN,
361
+ `declares capabilities not yet consented — run \`cc plugin consent ${p.name}\``,
362
+ ),
363
+ );
364
+ }
365
+ }
366
+ } catch (err) {
367
+ checks.push(failedCheck("plugin-capabilities", "plugin capabilities", err));
368
+ }
369
+
317
370
  return { id: "plugins", title: "Plugin trust", checks };
318
371
  }
319
372
 
@@ -540,6 +593,80 @@ async function worktreeSection(opts, deps) {
540
593
  return { id: "worktrees", title: "Git worktrees", checks };
541
594
  }
542
595
 
596
+ // ── runtime checkup: agenda expiry + verbose instruction files ──────────────
597
+ // Only the gaps the OTHER sections don't already cover. Stale sessions,
598
+ // worktrees, orphan processes and lost background agents are handled by
599
+ // transcriptSection / worktreeSection / backgroundSection — surfacing them here
600
+ // too would double-report, so this section deliberately covers just the agenda
601
+ // schedule store and the instruction files (cc.md / AGENTS.md / CLAUDE.md).
602
+ const SEVERITY_TO_LEVEL = {
603
+ error: CHECK_LEVELS.ERR,
604
+ warn: CHECK_LEVELS.WARN,
605
+ info: CHECK_LEVELS.INFO,
606
+ };
607
+
608
+ async function runtimeSection(opts, deps) {
609
+ const checks = [];
610
+ const now = deps.now();
611
+
612
+ // Agenda schedule store — overdue/never-fired wakeups, retirable leftovers.
613
+ try {
614
+ const { AgentScheduleStore } = await import("./agent-schedule-store.js");
615
+ const store = new AgentScheduleStore();
616
+ const entries = store.list().map((e) => ({
617
+ id: e.id,
618
+ dueAt: e.kind === "wakeup" ? e.dueAt : e.nextAt,
619
+ recurring: e.kind !== "wakeup",
620
+ }));
621
+ for (const f of checkAgenda(entries, now)) {
622
+ checks.push(
623
+ check(
624
+ f.id,
625
+ `agenda ${f.ref}`,
626
+ SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
627
+ `${f.message} — ${f.remediation}`,
628
+ ),
629
+ );
630
+ }
631
+ } catch (err) {
632
+ checks.push(failedCheck("agenda", "agenda schedule store", err));
633
+ }
634
+
635
+ // Instruction files — flag verbose ones (byte size only; a derivability
636
+ // heuristic would need code analysis and isn't computed here).
637
+ try {
638
+ const cwd = opts.cwd || process.cwd();
639
+ const files = [];
640
+ for (const name of ["cc.md", "AGENTS.md", "CLAUDE.md"]) {
641
+ const p = join(cwd, name);
642
+ if (deps.existsSync(p)) {
643
+ try {
644
+ files.push({ path: name, bytes: deps.statSync(p).size });
645
+ } catch {
646
+ // raced/unreadable — skip
647
+ }
648
+ }
649
+ }
650
+ for (const f of checkInstructionFiles(files)) {
651
+ checks.push(
652
+ check(
653
+ f.id,
654
+ f.ref,
655
+ SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
656
+ `${f.message} — ${f.remediation}`,
657
+ ),
658
+ );
659
+ }
660
+ } catch (err) {
661
+ checks.push(failedCheck("instructions", "instruction files", err));
662
+ }
663
+
664
+ if (checks.length === 0) {
665
+ checks.push(check("runtime-clean", "runtime hygiene", CHECK_LEVELS.OK, ""));
666
+ }
667
+ return { id: "runtime", title: "Runtime checkup", checks };
668
+ }
669
+
543
670
  /**
544
671
  * Collect all checkup sections. Never throws — a failing subsystem becomes an
545
672
  * `err` check inside its section.
@@ -557,6 +684,7 @@ export async function collectCheckupSections(opts = {}) {
557
684
  transcriptSection,
558
685
  backgroundSection,
559
686
  worktreeSection,
687
+ runtimeSection,
560
688
  ]) {
561
689
  try {
562
690
  sections.push(await build(opts, deps));
Binary file
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * hook-event-log — a persistent, hash-chained, replay-safe log of the hook
5
+ * delivery envelopes produced by the unified event bus ([[hook-event-bus.cjs]]).
6
+ *
7
+ * The event bus already stamps every settings-hook delivery with a stable
8
+ * `event_id` (see [[settings-hook-events.cjs]] `withDeliveryId`), but until now
9
+ * the full envelope was discarded — so there was nothing to look up when a user
10
+ * wants `cc hook replay <event-id>`. This store persists each envelope to a
11
+ * tamper-evident JSONL log so the replay command has a real source.
12
+ *
13
+ * Design:
14
+ * - OPT-IN: recording only happens when `CC_HOOK_EVENT_LOG` is enabled, so the
15
+ * default hook-firing path is byte-for-byte unchanged (no new IO per turn).
16
+ * - Hash chain: each line is `{envelope, prevHash, hash}` where
17
+ * `hash = sha256(prevHash + stableStringify(envelope))`. Editing / deleting /
18
+ * reordering any recorded line breaks the chain (mirrors the session
19
+ * transcript integrity design), which `verifyHookEventChain` detects.
20
+ * - Size guard: the log rotates to `<file>.1` once it exceeds MAX_LOG_BYTES so
21
+ * an always-on session can't grow it without bound.
22
+ * - Pure over injected IO (`_deps.fs` / `_deps.os`) so it is fully unit-testable
23
+ * without touching the real home directory.
24
+ */
25
+
26
+ const fs = require("fs");
27
+ const os = require("os");
28
+ const path = require("path");
29
+ const crypto = require("crypto");
30
+ const { stableStringify } = require("./hook-event-bus.cjs");
31
+
32
+ const _deps = { fs, os };
33
+
34
+ /** Cap the always-on log at 5 MiB, then rotate to `<file>.1`. */
35
+ const MAX_LOG_BYTES = 5 * 1024 * 1024;
36
+
37
+ /** Default log location: `~/.chainlesschain/hook-events.jsonl`. */
38
+ function hookEventLogPath(homedir) {
39
+ const home = homedir || _deps.os.homedir();
40
+ return path.join(home, ".chainlesschain", "hook-events.jsonl");
41
+ }
42
+
43
+ /** Recording is opt-in — `CC_HOOK_EVENT_LOG=1` (or `true`) turns it on. */
44
+ function isHookEventLogEnabled(env = process.env) {
45
+ const v = String(env.CC_HOOK_EVENT_LOG || "").toLowerCase();
46
+ return v === "1" || v === "true" || v === "yes" || v === "on";
47
+ }
48
+
49
+ /** Hash one envelope against the previous chain hash (deterministic). */
50
+ function computeChainHash(prevHash, envelope) {
51
+ return crypto
52
+ .createHash("sha256")
53
+ .update(`${prevHash || "genesis"}\n${stableStringify(envelope)}`, "utf-8")
54
+ .digest("hex");
55
+ }
56
+
57
+ /** The hash the NEXT record must chain from (last parseable line's `hash`). */
58
+ function latestChainHash(text) {
59
+ const lines = String(text || "").split("\n");
60
+ for (let i = lines.length - 1; i >= 0; i--) {
61
+ if (!lines[i].trim()) continue;
62
+ try {
63
+ const rec = JSON.parse(lines[i]);
64
+ return typeof rec?.hash === "string" ? rec.hash : null;
65
+ } catch {
66
+ // half-written tail line — chain from the previous intact record
67
+ continue;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Append a hook envelope to the chained log. Best-effort: never throws — a
75
+ * failure to record must not break the turn that fired the hook.
76
+ *
77
+ * @param {object} envelope a full envelope from buildHookEnvelope
78
+ * @param {object} [opts] { filePath, homedir }
79
+ * @returns {{ok:boolean, event_id?:string, hash?:string, reason?:string}}
80
+ */
81
+ function appendHookEvent(envelope, { filePath, homedir } = {}) {
82
+ if (!envelope || typeof envelope !== "object" || !envelope.event_id) {
83
+ return { ok: false, reason: "invalid envelope (no event_id)" };
84
+ }
85
+ const f = _deps.fs;
86
+ const fp = filePath || hookEventLogPath(homedir);
87
+ try {
88
+ const dir = path.dirname(fp);
89
+ if (!f.existsSync(dir)) f.mkdirSync(dir, { recursive: true });
90
+ let prevHash = null;
91
+ if (f.existsSync(fp)) {
92
+ let rotated = false;
93
+ try {
94
+ if (f.statSync(fp).size > MAX_LOG_BYTES) {
95
+ f.renameSync(fp, `${fp}.1`);
96
+ rotated = true;
97
+ }
98
+ } catch {
99
+ /* stat/rename best-effort */
100
+ }
101
+ if (!rotated) prevHash = latestChainHash(f.readFileSync(fp, "utf-8"));
102
+ }
103
+ const hash = computeChainHash(prevHash, envelope);
104
+ const line = `${JSON.stringify({ envelope, prevHash, hash })}\n`;
105
+ f.appendFileSync(fp, line, "utf-8");
106
+ return { ok: true, event_id: envelope.event_id, hash };
107
+ } catch (e) {
108
+ return { ok: false, reason: e.message };
109
+ }
110
+ }
111
+
112
+ /** Read every recorded record (skips malformed / half-written lines). */
113
+ function readHookEventRecords({ filePath, homedir } = {}) {
114
+ const f = _deps.fs;
115
+ const fp = filePath || hookEventLogPath(homedir);
116
+ if (!f.existsSync(fp)) return [];
117
+ const out = [];
118
+ for (const line of f.readFileSync(fp, "utf-8").split("\n")) {
119
+ if (!line.trim()) continue;
120
+ try {
121
+ out.push(JSON.parse(line));
122
+ } catch {
123
+ /* skip malformed / partial line */
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Look up the envelope for a delivery id. The LATEST matching record wins (an
131
+ * event_id can recur across processes; the most recent delivery is what a user
132
+ * replaying "the last one" means).
133
+ */
134
+ function findHookEvent(eventId, { filePath, homedir } = {}) {
135
+ if (!eventId) return null;
136
+ const records = readHookEventRecords({ filePath, homedir });
137
+ for (let i = records.length - 1; i >= 0; i--) {
138
+ if (records[i]?.envelope?.event_id === eventId) return records[i].envelope;
139
+ }
140
+ return null;
141
+ }
142
+
143
+ /** The most recent `limit` envelopes (newest last), for `cc hook events-log`. */
144
+ function listHookEvents({ limit = 20, filePath, homedir } = {}) {
145
+ const records = readHookEventRecords({ filePath, homedir });
146
+ const n = Math.max(0, Number(limit) || 0);
147
+ return records.slice(n ? -n : 0).map((r) => r.envelope);
148
+ }
149
+
150
+ /**
151
+ * Recompute the chain and report the first break, if any.
152
+ * @returns {{ok:boolean, length:number, brokenAt:number, reason?:string}}
153
+ */
154
+ function verifyHookEventChain({ filePath, homedir } = {}) {
155
+ const records = readHookEventRecords({ filePath, homedir });
156
+ let prev = null;
157
+ for (let i = 0; i < records.length; i++) {
158
+ const { envelope, prevHash, hash } = records[i] || {};
159
+ if ((prevHash ?? null) !== prev) {
160
+ return {
161
+ ok: false,
162
+ length: records.length,
163
+ brokenAt: i,
164
+ reason: "prevHash does not match the previous record's hash",
165
+ };
166
+ }
167
+ const expect = computeChainHash(prev, envelope);
168
+ if (expect !== hash) {
169
+ return {
170
+ ok: false,
171
+ length: records.length,
172
+ brokenAt: i,
173
+ reason: "record hash does not match its envelope (tampered)",
174
+ };
175
+ }
176
+ prev = hash;
177
+ }
178
+ return { ok: true, length: records.length, brokenAt: -1 };
179
+ }
180
+
181
+ module.exports = {
182
+ MAX_LOG_BYTES,
183
+ hookEventLogPath,
184
+ isHookEventLogEnabled,
185
+ computeChainHash,
186
+ latestChainHash,
187
+ appendHookEvent,
188
+ readHookEventRecords,
189
+ findHookEvent,
190
+ listHookEvents,
191
+ verifyHookEventChain,
192
+ _deps,
193
+ };