chainlesschain 0.162.163 → 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 (213) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.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-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
  49. package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-jO8heofr.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-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BliaT02i.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-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-dKx57io7.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-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +71 -14
  151. package/src/commands/doctor.js +85 -1
  152. package/src/commands/plugin.js +226 -9
  153. package/src/commands/review.js +17 -1
  154. package/src/commands/session.js +191 -0
  155. package/src/commands/team.js +19 -1
  156. package/src/data/changelog.json +5 -4
  157. package/src/gateways/ws/session-protocol.js +50 -0
  158. package/src/gateways/ws/ws-agent-handler.js +50 -0
  159. package/src/harness/jsonl-session-store.js +60 -0
  160. package/src/harness/worktree-isolator.js +140 -5
  161. package/src/lib/agent-schedule-store.js +30 -1
  162. package/src/lib/agent-team/team-worktree.js +37 -2
  163. package/src/lib/agent-tool-admission.js +125 -0
  164. package/src/lib/app-preview.js +360 -0
  165. package/src/lib/async-hook-supervisor.cjs +54 -0
  166. package/src/lib/capability-manifest.js +319 -0
  167. package/src/lib/capability-negotiation.js +23 -12
  168. package/src/lib/diagnostic-bundle.js +230 -0
  169. package/src/lib/doctor-checkup.js +223 -4
  170. package/src/lib/execution-location.js +286 -0
  171. package/src/lib/headless-manifest.js +13 -40
  172. package/src/lib/hook-stats-store.cjs +197 -0
  173. package/src/lib/idempotency.js +109 -0
  174. package/src/lib/jsonl-session-store.js +1 -0
  175. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  176. package/src/lib/lsp/lsp-manager.js +56 -0
  177. package/src/lib/monitor-event.js +290 -0
  178. package/src/lib/operation-fingerprint.js +217 -0
  179. package/src/lib/plugin-runtime/bin.js +37 -0
  180. package/src/lib/plugin-runtime/capabilities.js +19 -0
  181. package/src/lib/plugin-runtime/capability-consent.js +28 -0
  182. package/src/lib/plugin-runtime/hooks.js +37 -0
  183. package/src/lib/plugin-runtime/lsp.js +38 -0
  184. package/src/lib/plugin-runtime/mcp.js +40 -0
  185. package/src/lib/plugin-runtime/monitors.js +37 -0
  186. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  187. package/src/lib/pr-automation-policy.js +224 -0
  188. package/src/lib/remote-approval-bridge.js +66 -13
  189. package/src/lib/repl-rewind.js +101 -0
  190. package/src/lib/review-comment-anchor.js +241 -0
  191. package/src/lib/review-pipeline.js +46 -7
  192. package/src/lib/runtime-checkup.js +221 -0
  193. package/src/lib/secret-scan.js +409 -0
  194. package/src/lib/session-branch.js +0 -0
  195. package/src/lib/side-effect-ledger.js +105 -0
  196. package/src/lib/skill-loader.js +32 -0
  197. package/src/lib/skill-path-scope.js +63 -0
  198. package/src/lib/sub-agent-context.js +36 -4
  199. package/src/lib/sub-agent-registry.js +19 -0
  200. package/src/lib/telemetry/span-recorder.js +17 -4
  201. package/src/lib/terminal-context.js +195 -0
  202. package/src/lib/unattended-action-policy.js +182 -0
  203. package/src/lib/worktree-cleanup-safety.js +138 -0
  204. package/src/lib/worktree-isolator.js +8 -0
  205. package/src/lib/worktree-sparse.js +131 -0
  206. package/src/repl/agent-repl.js +152 -38
  207. package/src/runtime/agent-core.js +191 -13
  208. package/src/runtime/diagnostics.js +60 -0
  209. package/src/runtime/headless-runner.js +126 -1
  210. package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
  211. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
  212. package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
  213. package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
@@ -17,9 +17,36 @@
17
17
  import fs from "fs";
18
18
  import { discoverPlugins } from "./scopes.js";
19
19
  import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
20
+ import { componentCapabilityDenial } from "./capabilities.js";
20
21
 
21
22
  export const _deps = { readFileSync: fs.readFileSync };
22
23
 
24
+ // One-time stderr notice when a plugin's hooks are refused at the COMPONENT
25
+ // level because the plugin opted into the capability model but did not declare
26
+ // the `process` capability its shell hooks need. Distinct from the trust gate:
27
+ // these plugins ARE trusted, but their hook component is denied.
28
+ const _capabilityDenied = new Set();
29
+ function warnHookCapabilityDeniedOnce(entries) {
30
+ if (!entries || entries.length === 0) return;
31
+ if (_capabilityDenied.has("hook-capability")) return;
32
+ _capabilityDenied.add("hook-capability");
33
+ const list = entries.map((e) => `${e.name} (${e.reason})`).join("; ");
34
+ try {
35
+ process.stderr.write(
36
+ `[plugins] refused hook(s) from plugin(s) that declared a permissions ` +
37
+ `block but did not declare the 'process' capability their hooks need: ${list}\n` +
38
+ ` add 'process' to the plugin's permissions block to enable them.\n`,
39
+ );
40
+ } catch {
41
+ /* stderr notice is best-effort */
42
+ }
43
+ }
44
+
45
+ /** Test hook: reset the one-time capability-denied warning guard. */
46
+ export function _resetHookWarnings() {
47
+ _capabilityDenied.clear();
48
+ }
49
+
23
50
  /** Accept either `{ hooks: {Event:[...]} }` (plugin wrap) or `{Event:[...]}`. */
24
51
  function normalizeHookMap(parsed) {
25
52
  if (
@@ -57,10 +84,19 @@ export function collectPluginHooks(opts = {}) {
57
84
  "hooks",
58
85
  );
59
86
  const merged = {};
87
+ const denied = [];
60
88
  for (const p of trusted) {
61
89
  if (!p.manifest || p.manifest.ok !== true) continue;
62
90
  const h = p.manifest.components?.hooks;
63
91
  if (!h) continue;
92
+ // Component-level capability gate: a plugin that declared a permissions
93
+ // block but under-declared the `process` capability its shell hooks need
94
+ // gets its hooks refused here (mirrors the MCP collector's denial).
95
+ const denial = componentCapabilityDenial(p.manifest, ["process"]);
96
+ if (denial) {
97
+ denied.push({ name: p.name, reason: denial.reason });
98
+ continue;
99
+ }
64
100
  let parsed;
65
101
  if (h.absPath) {
66
102
  try {
@@ -89,6 +125,7 @@ export function collectPluginHooks(opts = {}) {
89
125
  merged[event] = (merged[event] || []).concat(entries);
90
126
  }
91
127
  }
128
+ warnHookCapabilityDeniedOnce(denied);
92
129
  return merged;
93
130
  }
94
131
 
@@ -16,9 +16,31 @@ import path from "path";
16
16
  import { discoverPlugins } from "./scopes.js";
17
17
  import { registerLanguageServer } from "../lsp/lsp-server-registry.js";
18
18
  import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
19
+ import { componentCapabilityDenial } from "./capabilities.js";
19
20
 
20
21
  const _loadedRoots = new Set();
21
22
 
23
+ // One-time stderr notice when a plugin's LSP servers are refused at the
24
+ // COMPONENT level because the plugin opted into the capability model but did not
25
+ // declare the `process` capability the servers need. Distinct from the trust
26
+ // gate: these plugins ARE trusted, but their LSP component is denied.
27
+ const _capabilityDenied = new Set();
28
+ function warnLspCapabilityDeniedOnce(entries) {
29
+ if (!entries || entries.length === 0) return;
30
+ if (_capabilityDenied.has("lsp-capability")) return;
31
+ _capabilityDenied.add("lsp-capability");
32
+ const list = entries.map((e) => `${e.name} (${e.reason})`).join("; ");
33
+ try {
34
+ process.stderr.write(
35
+ `[plugins] refused language server(s) from plugin(s) that declared a ` +
36
+ `permissions block but did not declare the 'process' capability: ${list}\n` +
37
+ ` add 'process' to the plugin's permissions block to enable them.\n`,
38
+ );
39
+ } catch {
40
+ /* stderr notice is best-effort */
41
+ }
42
+ }
43
+
22
44
  /**
23
45
  * Register every installed plugin's LSP servers for `cwd` (once per root).
24
46
  * Never throws — a broken plugin manifest must not break code navigation.
@@ -49,8 +71,18 @@ export function ensurePluginLspServers(opts = {}) {
49
71
  "language servers",
50
72
  );
51
73
 
74
+ const denied = [];
52
75
  for (const p of trusted) {
53
76
  const servers = p.manifest?.components?.lsp || [];
77
+ if (servers.length === 0) continue;
78
+ // Component-level capability gate: a plugin that declared a permissions
79
+ // block but did not declare the `process` capability its language servers
80
+ // need gets them refused here (mirrors the MCP collector's denial).
81
+ const denial = componentCapabilityDenial(p.manifest, ["process"]);
82
+ if (denial) {
83
+ denied.push({ name: p.name, reason: denial.reason });
84
+ continue;
85
+ }
54
86
  for (const s of servers) {
55
87
  try {
56
88
  registerLanguageServer({
@@ -66,6 +98,7 @@ export function ensurePluginLspServers(opts = {}) {
66
98
  }
67
99
  }
68
100
  }
101
+ warnLspCapabilityDeniedOnce(denied);
69
102
  return { registered };
70
103
  }
71
104
 
@@ -73,3 +106,8 @@ export function ensurePluginLspServers(opts = {}) {
73
106
  export function _resetPluginLspLoadState() {
74
107
  _loadedRoots.clear();
75
108
  }
109
+
110
+ /** Test hook: reset the one-time capability-denied warning guard. */
111
+ export function _resetLspWarnings() {
112
+ _capabilityDenied.clear();
113
+ }
@@ -17,9 +17,38 @@
17
17
  import fs from "fs";
18
18
  import { discoverPlugins } from "./scopes.js";
19
19
  import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
20
+ import { componentCapabilityDenial } from "./capabilities.js";
20
21
 
21
22
  export const _deps = { readFileSync: fs.readFileSync };
22
23
 
24
+ // One-time stderr notice when a plugin's MCP server is refused at the COMPONENT
25
+ // level because the plugin opted into the capability model but did not declare
26
+ // the `mcp` capability its server needs. Distinct from the trust gate: these
27
+ // plugins ARE trusted, but their MCP component is denied. (Network is enforced
28
+ // separately at connection time by the egress/network policy, so a local stdio
29
+ // server is not denied for lacking a `network` declaration.)
30
+ const _capabilityDenied = new Set();
31
+ function warnMcpCapabilityDeniedOnce(entries) {
32
+ if (!entries || entries.length === 0) return;
33
+ if (_capabilityDenied.has("mcp-capability")) return;
34
+ _capabilityDenied.add("mcp-capability");
35
+ const list = entries.map((e) => `${e.name} (${e.reason})`).join("; ");
36
+ try {
37
+ process.stderr.write(
38
+ `[plugins] refused MCP server(s) from plugin(s) that declared a permissions ` +
39
+ `block but did not declare the 'mcp' capability their server needs: ${list}\n` +
40
+ ` add 'mcp' to the plugin's permissions block to enable them.\n`,
41
+ );
42
+ } catch {
43
+ /* stderr notice is best-effort */
44
+ }
45
+ }
46
+
47
+ /** Test hook: reset the one-time capability-denied warning guard. */
48
+ export function _resetMcpWarnings() {
49
+ _capabilityDenied.clear();
50
+ }
51
+
23
52
  /** Accept `{ mcpServers: {...} }`, `{ servers: {...} }`, or a bare map. */
24
53
  function normalizeServerMap(parsed) {
25
54
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -52,10 +81,20 @@ export function collectPluginMcpServers(opts = {}) {
52
81
  );
53
82
  const servers = {};
54
83
  const sources = [];
84
+ const denied = [];
55
85
  for (const p of trusted) {
56
86
  if (!p.manifest || p.manifest.ok !== true) continue;
57
87
  const m = p.manifest.components?.mcp;
58
88
  if (!m) continue;
89
+ // Component-level capability gate: a plugin that declared a permissions
90
+ // block but under-declared the `mcp` capability its server needs gets its
91
+ // MCP component refused here (mirrors the hooks/monitors/bin/lsp denial).
92
+ // Legacy plugins (no permissions block) are unrestricted and never denied.
93
+ const denial = componentCapabilityDenial(p.manifest, ["mcp"]);
94
+ if (denial) {
95
+ denied.push({ name: p.name, reason: denial.reason });
96
+ continue;
97
+ }
59
98
  let parsed = null;
60
99
  if (m.absPath) {
61
100
  try {
@@ -85,5 +124,6 @@ export function collectPluginMcpServers(opts = {}) {
85
124
  }
86
125
  if (added > 0 && m.absPath) sources.push(m.absPath);
87
126
  }
127
+ warnMcpCapabilityDeniedOnce(denied);
88
128
  return { servers, sources };
89
129
  }
@@ -21,11 +21,38 @@
21
21
  import fs from "fs";
22
22
  import { discoverPlugins } from "./scopes.js";
23
23
  import { partitionByTrust, warnUntrustedOnce } from "./trust.js";
24
+ import { componentCapabilityDenial } from "./capabilities.js";
24
25
 
25
26
  export const _deps = { readFileSync: fs.readFileSync };
26
27
 
27
28
  const VALID_MODES = new Set(["interval", "longRunning"]);
28
29
 
30
+ // One-time stderr notice when a plugin's monitors are refused at the COMPONENT
31
+ // level because the plugin opted into the capability model but did not declare
32
+ // the `monitor` capability. Distinct from the trust gate: these plugins ARE
33
+ // trusted, but their monitor component is denied.
34
+ const _capabilityDenied = new Set();
35
+ function warnMonitorCapabilityDeniedOnce(entries) {
36
+ if (!entries || entries.length === 0) return;
37
+ if (_capabilityDenied.has("monitor-capability")) return;
38
+ _capabilityDenied.add("monitor-capability");
39
+ const list = entries.map((e) => `${e.name} (${e.reason})`).join("; ");
40
+ try {
41
+ process.stderr.write(
42
+ `[plugins] refused monitor(s) from plugin(s) that declared a permissions ` +
43
+ `block but did not declare the 'monitor' capability: ${list}\n` +
44
+ ` add 'monitor' to the plugin's permissions block to enable them.\n`,
45
+ );
46
+ } catch {
47
+ /* stderr notice is best-effort */
48
+ }
49
+ }
50
+
51
+ /** Test hook: reset the one-time capability-denied warning guard. */
52
+ export function _resetMonitorWarnings() {
53
+ _capabilityDenied.clear();
54
+ }
55
+
29
56
  /** Normalize one raw monitor entry into a validated descriptor, or null. */
30
57
  function normalizeMonitor(plugin, raw) {
31
58
  if (!raw || typeof raw !== "object") return null;
@@ -76,10 +103,19 @@ export function collectPluginMonitors(opts = {}) {
76
103
  "monitors",
77
104
  );
78
105
  const out = [];
106
+ const denied = [];
79
107
  for (const p of trusted) {
80
108
  if (!p.manifest || p.manifest.ok !== true) continue;
81
109
  const m = p.manifest.components?.monitors;
82
110
  if (!m) continue;
111
+ // Component-level capability gate: a plugin that declared a permissions
112
+ // block but did not declare the `monitor` capability gets its monitors
113
+ // refused here (mirrors the MCP collector's denial).
114
+ const denial = componentCapabilityDenial(p.manifest, ["monitor"]);
115
+ if (denial) {
116
+ denied.push({ name: p.name, reason: denial.reason });
117
+ continue;
118
+ }
83
119
  let parsed;
84
120
  if (m.absPath) {
85
121
  try {
@@ -112,5 +148,6 @@ export function collectPluginMonitors(opts = {}) {
112
148
  if (desc) out.push(desc);
113
149
  }
114
150
  }
151
+ warnMonitorCapabilityDeniedOnce(denied);
115
152
  return out;
116
153
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Plugin OPTIONS resolution (Phase 3 gap: typed optionsSchema + sensitive
3
+ * project-scope gate).
4
+ *
5
+ * A plugin declares `optionsSchema` (typed config with `scope` and `sensitive`
6
+ * in [[capabilities.js]]). Users supply option VALUES at two scopes:
7
+ * - USER scope → user data dir `plugin-options.json` (may hold secrets)
8
+ * - PROJECT scope → repo-local `.chainlesschain/plugin-options.json`
9
+ * (checked-in-able → MUST NOT hold secrets)
10
+ *
11
+ * The security-critical rule (already enforced by `validateOptions`): a
12
+ * SENSITIVE option can never come from project config. `resolvePluginOptions`
13
+ * layers defaults < project (non-sensitive only) < user, DROPS + WARNS on any
14
+ * sensitive/user-only option that appears in project config, and returns the
15
+ * merged values with a redacted view for logging.
16
+ *
17
+ * Store IO is injected (`_deps`) so unit tests never touch the real dirs; the
18
+ * resolver itself is pure.
19
+ */
20
+
21
+ import fs from "fs";
22
+ import path from "path";
23
+ import { getElectronUserDataDir } from "../paths.js";
24
+ import {
25
+ optionDefaults,
26
+ validateOptions,
27
+ redactSensitiveOptions,
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
+ userStorePath: () =>
36
+ path.join(getElectronUserDataDir(), "plugin-options.json"),
37
+ projectStorePath: (cwd) =>
38
+ path.join(cwd || process.cwd(), ".chainlesschain", "plugin-options.json"),
39
+ };
40
+
41
+ function readJsonObject(p) {
42
+ try {
43
+ if (!_deps.existsSync(p)) return {};
44
+ const data = JSON.parse(_deps.readFileSync(p, "utf8"));
45
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ function writeJsonObject(p, obj) {
52
+ _deps.mkdirSync(path.dirname(p), { recursive: true });
53
+ _deps.writeFileSync(p, JSON.stringify(obj, null, 2), "utf8");
54
+ }
55
+
56
+ /** Load a plugin's option VALUES at the given scope (`user` | `project`). */
57
+ export function loadPluginOptionValues(name, scope, { cwd } = {}) {
58
+ const p =
59
+ scope === "project" ? _deps.projectStorePath(cwd) : _deps.userStorePath();
60
+ const store = readJsonObject(p);
61
+ const v = store[name];
62
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
63
+ }
64
+
65
+ /** Persist a plugin's option VALUES at a scope (replaces the plugin's entry). */
66
+ export function setPluginOptionValues(name, values, scope, { cwd } = {}) {
67
+ const p =
68
+ scope === "project" ? _deps.projectStorePath(cwd) : _deps.userStorePath();
69
+ const store = readJsonObject(p);
70
+ store[name] = values && typeof values === "object" ? values : {};
71
+ writeJsonObject(p, store);
72
+ return { name, scope, path: p };
73
+ }
74
+
75
+ /** Provided keys of `values` (that exist in schema) accepted at `scope`. */
76
+ function acceptedKeys(schema, values, scope) {
77
+ const res = validateOptions(schema, values, { scope });
78
+ const keys = new Set();
79
+ for (const key of Object.keys(values || {})) {
80
+ if (!Object.prototype.hasOwnProperty.call(schema, key)) continue; // unknown → ignored
81
+ // A provided key was accepted iff no error names it. validateOptions always
82
+ // quotes the offending key as `"<key>"`, so this is an exact match.
83
+ if (!res.errors.some((e) => e.includes(`"${key}"`))) keys.add(key);
84
+ }
85
+ return { res, keys };
86
+ }
87
+
88
+ /**
89
+ * PURE: resolve effective option values from defaults + project + user config.
90
+ *
91
+ * Precedence: defaults < project (non-sensitive, non-user-only) < user.
92
+ * A sensitive or user-only option present in project config is DROPPED and a
93
+ * warning is recorded — a checked-in project file can never inject a secret.
94
+ *
95
+ * @returns {{ options:object, redacted:object, warnings:string[],
96
+ * sources:Record<string,'default'|'project'|'user'>,
97
+ * droppedFromProject:string[] }}
98
+ */
99
+ export function resolvePluginOptions(
100
+ schema,
101
+ { userValues, projectValues } = {},
102
+ ) {
103
+ const s = schema || {};
104
+ const options = optionDefaults(s);
105
+ const sources = {};
106
+ for (const key of Object.keys(options)) sources[key] = "default";
107
+
108
+ const warnings = [];
109
+ const droppedFromProject = [];
110
+
111
+ // Project layer first (lower precedence). validateOptions errors on sensitive/
112
+ // user-only keys at project scope — those are the drops we surface + refuse.
113
+ const proj = acceptedKeys(s, projectValues, "project");
114
+ for (const err of proj.res.errors) {
115
+ if (/sensitive|user-scoped/.test(err)) {
116
+ const m = err.match(/"([^"]+)"/);
117
+ if (m) droppedFromProject.push(m[1]);
118
+ }
119
+ warnings.push(`project: ${err}`);
120
+ }
121
+ for (const w of proj.res.warnings) warnings.push(`project: ${w}`);
122
+ for (const key of proj.keys) {
123
+ options[key] = proj.res.normalized[key];
124
+ sources[key] = "project";
125
+ }
126
+
127
+ // User layer on top (highest precedence; may carry secrets).
128
+ const user = acceptedKeys(s, userValues, "user");
129
+ for (const err of user.res.errors) warnings.push(`user: ${err}`);
130
+ for (const w of user.res.warnings) warnings.push(`user: ${w}`);
131
+ for (const key of user.keys) {
132
+ options[key] = user.res.normalized[key];
133
+ sources[key] = "user";
134
+ }
135
+
136
+ return {
137
+ options,
138
+ redacted: redactSensitiveOptions(s, options),
139
+ warnings,
140
+ sources,
141
+ droppedFromProject: [...new Set(droppedFromProject)],
142
+ };
143
+ }
144
+
145
+ /** Load both scopes' values from disk and resolve against a schema. */
146
+ export function getResolvedPluginOptions(name, schema, { cwd } = {}) {
147
+ return resolvePluginOptions(schema, {
148
+ userValues: loadPluginOptionValues(name, "user", { cwd }),
149
+ projectValues: loadPluginOptionValues(name, "project", { cwd }),
150
+ });
151
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * PR/CI automation policy — the fail-closed decision engine behind "P1-4 PR/CI
3
+ * Monitor、Auto-fix 与受控合并" of CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-
4
+ * 07-13.md.
5
+ *
6
+ * Letting an agent iterate on CI failures and merge PRs is useful and dangerous.
7
+ * The gap doc pins the guardrails precisely: each failing check may be auto-
8
+ * fixed at most N times within a token/time budget; a fix must read only the
9
+ * CURRENT commit's logs (never chase a stale failure); auto-merge is OFF by
10
+ * default and, when on, requires branch protection, all required checks, an
11
+ * approving review and zero pending approvals — and never pushes a protected
12
+ * branch directly or bypasses protection.
13
+ *
14
+ * This module is those decisions as pure predicates. It holds no state and does
15
+ * no I/O — a caller feeds the observed PR/CI signals and gets an allow/deny with
16
+ * a named reason. Everything defaults to DENY: an unknown or missing signal
17
+ * blocks the automation rather than assuming it is safe.
18
+ */
19
+
20
+ /** Canonical CI check states. */
21
+ export const CHECK_STATE = Object.freeze({
22
+ PASSED: "passed",
23
+ FAILED: "failed",
24
+ PENDING: "pending",
25
+ NEUTRAL: "neutral",
26
+ ERROR: "error",
27
+ });
28
+
29
+ const CHECK_ALIASES = new Map([
30
+ ["passed", CHECK_STATE.PASSED],
31
+ ["pass", CHECK_STATE.PASSED],
32
+ ["success", CHECK_STATE.PASSED],
33
+ ["succeeded", CHECK_STATE.PASSED],
34
+ ["green", CHECK_STATE.PASSED],
35
+ ["failed", CHECK_STATE.FAILED],
36
+ ["fail", CHECK_STATE.FAILED],
37
+ ["failure", CHECK_STATE.FAILED],
38
+ ["red", CHECK_STATE.FAILED],
39
+ ["pending", CHECK_STATE.PENDING],
40
+ ["queued", CHECK_STATE.PENDING],
41
+ ["in_progress", CHECK_STATE.PENDING],
42
+ ["running", CHECK_STATE.PENDING],
43
+ ["neutral", CHECK_STATE.NEUTRAL],
44
+ ["skipped", CHECK_STATE.NEUTRAL],
45
+ ["cancelled", CHECK_STATE.NEUTRAL],
46
+ ["canceled", CHECK_STATE.NEUTRAL],
47
+ ["error", CHECK_STATE.ERROR],
48
+ ["timed_out", CHECK_STATE.ERROR],
49
+ ["action_required", CHECK_STATE.ERROR],
50
+ ]);
51
+
52
+ /** Normalize any provider's check conclusion/status to a canonical state. */
53
+ export function normalizeCheckState(value) {
54
+ if (typeof value !== "string") return null;
55
+ return CHECK_ALIASES.get(value.trim().toLowerCase()) || null;
56
+ }
57
+
58
+ /** A failing state that auto-fix could act on. */
59
+ function isFailing(state) {
60
+ return state === CHECK_STATE.FAILED || state === CHECK_STATE.ERROR;
61
+ }
62
+
63
+ /**
64
+ * Summarize a set of checks. `allPassed` is true only when there is at least one
65
+ * check and every one is passed or neutral (a bare PR with no checks is NOT
66
+ * "all passed" — required checks must actually exist and run).
67
+ *
68
+ * @param {Array<{name?:string, state?:string, conclusion?:string, status?:string}>} checks
69
+ */
70
+ export function summarizeChecks(checks = []) {
71
+ const list = Array.isArray(checks) ? checks : [];
72
+ let passed = 0;
73
+ let failed = 0;
74
+ let pending = 0;
75
+ const byName = new Map();
76
+ for (const c of list) {
77
+ const state =
78
+ normalizeCheckState(c?.state) ??
79
+ normalizeCheckState(c?.conclusion) ??
80
+ normalizeCheckState(c?.status);
81
+ if (c?.name != null) byName.set(String(c.name), state);
82
+ if (state === CHECK_STATE.PASSED || state === CHECK_STATE.NEUTRAL) passed++;
83
+ else if (isFailing(state)) failed++;
84
+ else pending++; // pending / unknown → not yet decided
85
+ }
86
+ return {
87
+ total: list.length,
88
+ passed,
89
+ failed,
90
+ pending,
91
+ anyFailed: failed > 0,
92
+ allPassed: list.length > 0 && failed === 0 && pending === 0,
93
+ byName,
94
+ };
95
+ }
96
+
97
+ function budgetExhausted(budget) {
98
+ if (!budget || typeof budget !== "object") return false;
99
+ const over = (spent, cap) =>
100
+ cap != null && Number.isFinite(Number(cap)) && Number(spent) >= Number(cap);
101
+ return (
102
+ over(budget.tokensSpent, budget.tokenBudget) ||
103
+ over(budget.timeSpentMs, budget.timeBudgetMs)
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Decide whether an agent may auto-fix ONE failing check right now. Fail-closed:
109
+ * anything unproven denies.
110
+ *
111
+ * @param {object} params
112
+ * @param {object} params.check {name, state|conclusion, logCommitSha}
113
+ * @param {string} params.headCommitSha the PR head the fix would target
114
+ * @param {number} params.iteration fixes already attempted for this check
115
+ * @param {number} params.maxIterations cap (default 3)
116
+ * @param {object} [params.budget] {tokensSpent, tokenBudget, timeSpentMs, timeBudgetMs}
117
+ * @returns {{allow:boolean, reason:string}}
118
+ */
119
+ export function autoFixDecision(params = {}) {
120
+ const check = params.check || {};
121
+ const state =
122
+ normalizeCheckState(check.state) ?? normalizeCheckState(check.conclusion);
123
+ if (!isFailing(state)) {
124
+ return { allow: false, reason: "check-not-failing" };
125
+ }
126
+
127
+ const maxIterations = Number.isFinite(Number(params.maxIterations))
128
+ ? Number(params.maxIterations)
129
+ : 3;
130
+ const iteration = Number(params.iteration) || 0;
131
+ if (iteration >= maxIterations) {
132
+ return { allow: false, reason: "max-iterations-reached" };
133
+ }
134
+
135
+ // Only fix the CURRENT failure: the log we would read must belong to the head
136
+ // commit. A missing/older log sha means we cannot prove freshness → deny.
137
+ const logSha = check.logCommitSha == null ? "" : String(check.logCommitSha);
138
+ const headSha =
139
+ params.headCommitSha == null ? "" : String(params.headCommitSha);
140
+ if (!logSha || !headSha) {
141
+ return { allow: false, reason: "commit-sha-unverifiable" };
142
+ }
143
+ if (logSha !== headSha) {
144
+ return { allow: false, reason: "stale-failure-log" };
145
+ }
146
+
147
+ if (budgetExhausted(params.budget)) {
148
+ return { allow: false, reason: "budget-exhausted" };
149
+ }
150
+
151
+ return { allow: true, reason: "ok" };
152
+ }
153
+
154
+ /**
155
+ * Decide whether a PR may be auto-merged. Fail-closed and exhaustive: every
156
+ * unmet requirement is collected, and merge is allowed ONLY when the list is
157
+ * empty. Auto-merge is off unless `enabled === true`.
158
+ *
159
+ * @param {object} params
160
+ * @param {boolean} params.enabled auto-merge explicitly turned on
161
+ * @param {boolean} params.hasOpenPr a real PR exists (no direct-to-branch)
162
+ * @param {boolean} params.branchProtectionSatisfied
163
+ * @param {boolean} params.reviewApproved
164
+ * @param {number} params.pendingApprovals permission approvals still waiting
165
+ * @param {string[]} params.requiredChecks names that must be present AND passed
166
+ * @param {Array} params.checks observed checks (summarizeChecks input)
167
+ * @param {string} [params.targetBranch]
168
+ * @returns {{allow:boolean, reason:string, unmet:string[]}}
169
+ */
170
+ export function autoMergeDecision(params = {}) {
171
+ const unmet = [];
172
+
173
+ if (params.enabled !== true) unmet.push("auto-merge-disabled");
174
+ if (params.hasOpenPr !== true) unmet.push("no-open-pr");
175
+ if (params.branchProtectionSatisfied !== true) {
176
+ unmet.push("branch-protection-unsatisfied");
177
+ }
178
+ if (params.reviewApproved !== true) unmet.push("review-not-approved");
179
+ if (Number(params.pendingApprovals) > 0) unmet.push("pending-approvals");
180
+
181
+ const summary = summarizeChecks(params.checks);
182
+ if (summary.anyFailed) unmet.push("checks-failing");
183
+ if (summary.pending > 0) unmet.push("checks-pending");
184
+
185
+ // Every REQUIRED check must be present and passed — a required check that
186
+ // never ran is a block, not a pass.
187
+ const required = Array.isArray(params.requiredChecks)
188
+ ? params.requiredChecks
189
+ : [];
190
+ for (const name of required) {
191
+ const state = summary.byName.get(String(name));
192
+ if (state !== CHECK_STATE.PASSED && state !== CHECK_STATE.NEUTRAL) {
193
+ unmet.push(`required-check-missing:${name}`);
194
+ }
195
+ }
196
+
197
+ return {
198
+ allow: unmet.length === 0,
199
+ reason: unmet.length === 0 ? "ok" : unmet[0],
200
+ unmet,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * A compact, log-safe status line for the PR status bar (P1-4 "Session 顶部提供
206
+ * PR 状态条"). Carries only coordinates + a checks tally, never a token.
207
+ *
208
+ * @param {object} params {branch, prNumber, checks, reviewApproved, mergeable}
209
+ * @returns {string}
210
+ */
211
+ export function describePrStatusBar(params = {}) {
212
+ const parts = [];
213
+ if (params.branch) parts.push(`branch:${params.branch}`);
214
+ if (params.prNumber != null) parts.push(`PR#${params.prNumber}`);
215
+ const s = summarizeChecks(params.checks);
216
+ parts.push(`checks:${s.passed}/${s.total}${s.anyFailed ? " ✗" : ""}`);
217
+ parts.push(
218
+ `review:${params.reviewApproved === true ? "approved" : "pending"}`,
219
+ );
220
+ if (params.mergeable != null) {
221
+ parts.push(`merge:${params.mergeable ? "ready" : "blocked"}`);
222
+ }
223
+ return parts.join(" · ");
224
+ }