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
@@ -23,9 +23,147 @@ import fs from "fs";
23
23
  import os from "os";
24
24
  import path from "path";
25
25
  import { randomUUID } from "crypto";
26
+ import { isEntryExpired, effectiveFireAt } from "./schedule-planner.js";
27
+ import { SUBAGENT_PERMISSION_MODES } from "./subagent-contract.js";
28
+ import { parseGoalCondition } from "./goal-condition-engine.js";
26
29
 
27
30
  export const SCHEDULE_KINDS = Object.freeze(["wakeup", "cron", "monitor"]);
28
31
 
32
+ /** A valid absolute-epoch-ms expiry, or null (never expires). */
33
+ function normalizeExpiresAt(v) {
34
+ const n = Number(v);
35
+ return Number.isFinite(n) && n > 0 ? n : null;
36
+ }
37
+
38
+ /**
39
+ * A non-negative integer jitter window (ms), or 0. Stored PER entry so each
40
+ * task carries its own deterministic spread — the schedule-planner applies a
41
+ * stable per-id offset within `[0, jitterMs)` so tasks sharing a cron minute
42
+ * fan out instead of firing as a thundering herd. 0 (the default) = no jitter.
43
+ */
44
+ function normalizeJitterMs(v) {
45
+ const n = Math.floor(Number(v));
46
+ return Number.isFinite(n) && n > 0 ? n : 0;
47
+ }
48
+
49
+ /**
50
+ * A per-task execution policy for wakeup/cron entries — how the spawned
51
+ * `cc agent` run should behave when this entry fires. Each field maps to a real
52
+ * `cc agent` flag consumed by `cc agenda run`, so a scheduled task can carry its
53
+ * OWN permission mode, worktree isolation and turn budget instead of inheriting
54
+ * whatever ambient environment `cc agenda run` happens to run in (the gap's
55
+ * "每个定时任务独立预算·权限模式·Worktree"). Returns `null` when nothing is set —
56
+ * the entry then omits `runPolicy` entirely and is byte-identical to before,
57
+ * so the default (unpolicied) task keeps spawning a plain `cc agent -p`.
58
+ *
59
+ * - permissionMode → `--permission-mode <mode>` (validated against the single
60
+ * source SUBAGENT_PERMISSION_MODES; invalid → dropped)
61
+ * - worktree → `--worktree` (run the task in a fresh git worktree)
62
+ * - maxTurns → `--max-turns <n>` (cap the agent loop — a real budget)
63
+ * - goalCondition → `--goal-condition <spec>` (re-drive the run to a
64
+ * completion condition; validated via parseGoalCondition,
65
+ * invalid spec → the whole goal set is dropped) with its
66
+ * own token/cost/time/outer-turn budget:
67
+ * goalMaxTokens → `--goal-max-tokens <n>`
68
+ * goalMaxCost → `--goal-max-cost <usd>`
69
+ * goalMaxTime → `--goal-max-time <ms>`
70
+ * maxOuterTurns → `--max-outer-turns <n>`
71
+ * (the goal budgets are meaningless without a valid condition, so they are
72
+ * only kept when the condition parses.)
73
+ *
74
+ * Monitors run a shell command / watch a file rather than spawning an agent, so
75
+ * a run policy does not apply to them.
76
+ */
77
+ export function normalizeRunPolicy({
78
+ permissionMode = null,
79
+ worktree = false,
80
+ maxTurns = null,
81
+ goalCondition = null,
82
+ goalMaxTokens = null,
83
+ goalMaxCost = null,
84
+ goalMaxTime = null,
85
+ maxOuterTurns = null,
86
+ } = {}) {
87
+ const policy = {};
88
+ if (
89
+ typeof permissionMode === "string" &&
90
+ SUBAGENT_PERMISSION_MODES.includes(permissionMode)
91
+ ) {
92
+ policy.permissionMode = permissionMode;
93
+ }
94
+ if (worktree === true) policy.worktree = true;
95
+ const turns = Math.floor(Number(maxTurns));
96
+ if (Number.isFinite(turns) && turns > 0) policy.maxTurns = turns;
97
+
98
+ // goal-condition + its budgets: run to a completion condition with a per-task
99
+ // token/cost/time/outer-turn budget. Validate the spec up front so a doomed
100
+ // condition never gets scheduled; an invalid spec drops the whole goal set.
101
+ if (typeof goalCondition === "string" && goalCondition.trim()) {
102
+ let validSpec = false;
103
+ try {
104
+ parseGoalCondition(goalCondition);
105
+ validSpec = true;
106
+ } catch {
107
+ // invalid spec → drop goalCondition and its budgets entirely
108
+ }
109
+ if (validSpec) {
110
+ policy.goalCondition = goalCondition.trim();
111
+ const gTokens = Math.floor(Number(goalMaxTokens));
112
+ if (Number.isFinite(gTokens) && gTokens > 0)
113
+ policy.goalMaxTokens = gTokens;
114
+ const gCost = Number(goalMaxCost);
115
+ if (Number.isFinite(gCost) && gCost > 0) policy.goalMaxCost = gCost;
116
+ const gTime = Math.floor(Number(goalMaxTime));
117
+ if (Number.isFinite(gTime) && gTime > 0) policy.goalMaxTime = gTime;
118
+ const outer = Math.floor(Number(maxOuterTurns));
119
+ if (Number.isFinite(outer) && outer > 0) policy.maxOuterTurns = outer;
120
+ }
121
+ }
122
+ return Object.keys(policy).length > 0 ? policy : null;
123
+ }
124
+
125
+ /**
126
+ * Resolve an entry's expiry: an explicit absolute `expiresAt` (epoch-ms) wins;
127
+ * otherwise a positive relative `expiresInMs` is anchored to the store's own
128
+ * `now` (symmetric with `delayMs` on a wakeup, and deterministic under an
129
+ * injected clock). No expiry → null.
130
+ */
131
+ function resolveExpiresAt(expiresAt, expiresInMs, now) {
132
+ if (expiresAt != null) return normalizeExpiresAt(expiresAt);
133
+ const rel = Math.floor(Number(expiresInMs));
134
+ if (Number.isFinite(rel) && rel > 0) return normalizeExpiresAt(now + rel);
135
+ return null;
136
+ }
137
+
138
+ /** Is an entry currently in a schedulable (non-terminal) state? */
139
+ function isSchedulableStatus(entry) {
140
+ return entry.kind === "wakeup"
141
+ ? entry.status === "pending"
142
+ : entry.status === "active";
143
+ }
144
+
145
+ /** Statuses an entry can never leave — safe to prune. */
146
+ export const TERMINAL_STATUSES = Object.freeze([
147
+ "fired",
148
+ "matched",
149
+ "exhausted",
150
+ "expired",
151
+ ]);
152
+ const TERMINAL_SET = new Set(TERMINAL_STATUSES);
153
+
154
+ /** Best available "this entry reached a terminal state at" timestamp. */
155
+ function terminalAt(entry) {
156
+ return (
157
+ entry.firedAt ??
158
+ entry.matchedAt ??
159
+ entry.expiredAt ??
160
+ entry.lastRunAt ??
161
+ entry.lastCheckAt ??
162
+ entry.createdAt ??
163
+ 0
164
+ );
165
+ }
166
+
29
167
  export function agentScheduleDir(homedir = os.homedir()) {
30
168
  return path.join(homedir, ".chainlesschain", "agent-schedule");
31
169
  }
@@ -154,11 +292,23 @@ export class AgentScheduleStore {
154
292
 
155
293
  // ── create ──────────────────────────────────────────────────────────────
156
294
 
157
- scheduleWakeup({ prompt, delayMs = 0, dueAt = null, label = null } = {}) {
295
+ scheduleWakeup(options = {}) {
296
+ const {
297
+ prompt,
298
+ delayMs = 0,
299
+ dueAt = null,
300
+ label = null,
301
+ expiresAt = null,
302
+ expiresInMs = null,
303
+ jitterMs = 0,
304
+ } = options;
158
305
  if (!prompt || typeof prompt !== "string") {
159
306
  throw new Error("wakeup requires a prompt");
160
307
  }
161
308
  const now = this._now();
309
+ // The full options object is passed to normalizeRunPolicy, which reads only
310
+ // the policy fields it knows — new policy knobs need no signature change here.
311
+ const runPolicy = normalizeRunPolicy(options);
162
312
  const entry = {
163
313
  id: randomUUID(),
164
314
  kind: "wakeup",
@@ -166,6 +316,9 @@ export class AgentScheduleStore {
166
316
  label: label || null,
167
317
  dueAt:
168
318
  dueAt != null ? Number(dueAt) : now + Math.max(0, Number(delayMs) || 0),
319
+ expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
320
+ jitterMs: normalizeJitterMs(jitterMs),
321
+ ...(runPolicy ? { runPolicy } : {}),
169
322
  createdAt: now,
170
323
  status: "pending",
171
324
  };
@@ -175,7 +328,15 @@ export class AgentScheduleStore {
175
328
  return entry;
176
329
  }
177
330
 
178
- createCron({ prompt, cron, label = null } = {}) {
331
+ createCron(options = {}) {
332
+ const {
333
+ prompt,
334
+ cron,
335
+ label = null,
336
+ expiresAt = null,
337
+ expiresInMs = null,
338
+ jitterMs = 0,
339
+ } = options;
179
340
  if (!prompt || typeof prompt !== "string") {
180
341
  throw new Error("cron requires a prompt");
181
342
  }
@@ -184,6 +345,8 @@ export class AgentScheduleStore {
184
345
  if (nextAt == null) {
185
346
  throw new Error(`cron "${cron}" has no upcoming match`);
186
347
  }
348
+ // Full options → normalizeRunPolicy (reads only known policy fields).
349
+ const runPolicy = normalizeRunPolicy(options);
187
350
  const entry = {
188
351
  id: randomUUID(),
189
352
  kind: "cron",
@@ -191,6 +354,9 @@ export class AgentScheduleStore {
191
354
  cron,
192
355
  label: label || null,
193
356
  nextAt,
357
+ expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
358
+ jitterMs: normalizeJitterMs(jitterMs),
359
+ ...(runPolicy ? { runPolicy } : {}),
194
360
  createdAt: now,
195
361
  lastRunAt: null,
196
362
  runs: 0,
@@ -204,14 +370,56 @@ export class AgentScheduleStore {
204
370
 
205
371
  createMonitor({
206
372
  command,
373
+ watchFile = null,
374
+ watchUrl = null,
375
+ watchChange = false,
207
376
  intervalMs,
208
377
  stopWhen = null,
209
378
  notify = null,
210
379
  maxChecks = null,
211
380
  label = null,
381
+ expiresAt = null,
382
+ expiresInMs = null,
383
+ jitterMs = 0,
212
384
  } = {}) {
213
- if (!command || typeof command !== "string") {
214
- throw new Error("monitor requires a command");
385
+ // A monitor watches exactly one source:
386
+ // - a shell `command` → match its stdout/stderr
387
+ // - a `watchFile` → match its content, or fire when the file appears
388
+ // - a `watchUrl` → GET it and match the response body, or fire on 2xx
389
+ const hasCommand = typeof command === "string" && command.length > 0;
390
+ const hasFile = typeof watchFile === "string" && watchFile.length > 0;
391
+ const hasUrl = typeof watchUrl === "string" && watchUrl.length > 0;
392
+ const sourceCount = Number(hasCommand) + Number(hasFile) + Number(hasUrl);
393
+ if (sourceCount !== 1) {
394
+ throw new Error(
395
+ "monitor requires exactly one of `command`, `watchFile`, or `watchUrl`",
396
+ );
397
+ }
398
+ if (hasUrl) {
399
+ // Only http(s) endpoints — reject file:// and other schemes up-front so a
400
+ // bad URL errors at creation, not silently at the first poll.
401
+ let parsed;
402
+ try {
403
+ parsed = new URL(watchUrl);
404
+ } catch {
405
+ throw new Error(`monitor watchUrl is not a valid URL: ${watchUrl}`);
406
+ }
407
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
408
+ throw new Error(
409
+ `monitor watchUrl must be http(s), got: ${parsed.protocol}`,
410
+ );
411
+ }
412
+ }
413
+ if (watchChange) {
414
+ // `watchChange` is a file-only mode — fire when the file's mtime advances.
415
+ // It carries its own signal, so it cannot be combined with a stopWhen
416
+ // content pattern (that would be an ambiguous double condition).
417
+ if (!hasFile) {
418
+ throw new Error("monitor watchChange requires a watchFile");
419
+ }
420
+ if (stopWhen != null) {
421
+ throw new Error("monitor watchChange cannot be combined with stopWhen");
422
+ }
215
423
  }
216
424
  const interval = Math.max(1000, Number(intervalMs) || 60000);
217
425
  if (stopWhen != null) {
@@ -221,16 +429,24 @@ export class AgentScheduleStore {
221
429
  new RegExp(stopWhen);
222
430
  }
223
431
  const now = this._now();
432
+ const source = hasFile ? "file" : hasUrl ? "http" : "command";
224
433
  const entry = {
225
434
  id: randomUUID(),
226
435
  kind: "monitor",
227
- command,
436
+ source,
437
+ command: hasCommand ? command : null,
438
+ watchFile: hasFile ? watchFile : null,
439
+ watchUrl: hasUrl ? watchUrl : null,
440
+ watchChange: hasFile ? Boolean(watchChange) : false,
441
+ lastMtimeMs: null,
228
442
  intervalMs: interval,
229
443
  stopWhen: stopWhen || null,
230
444
  notify: notify || null,
231
445
  maxChecks: maxChecks != null ? Number(maxChecks) : null,
232
446
  label: label || null,
233
447
  nextAt: now + interval,
448
+ expiresAt: resolveExpiresAt(expiresAt, expiresInMs, now),
449
+ jitterMs: normalizeJitterMs(jitterMs),
234
450
  createdAt: now,
235
451
  checks: 0,
236
452
  status: "active",
@@ -254,13 +470,45 @@ export class AgentScheduleStore {
254
470
  due(kind = null, atMs = null) {
255
471
  const now = atMs != null ? atMs : this._now();
256
472
  return this.list(kind).filter((entry) => {
257
- if (entry.kind === "wakeup") {
258
- return entry.status === "pending" && entry.dueAt <= now;
259
- }
260
- return entry.status === "active" && entry.nextAt <= now;
473
+ // An entry past its expiry never fires — even if it is not yet retired
474
+ // (defense-in-depth: retireExpired need not have run first).
475
+ if (isEntryExpired(entry, now)) return false;
476
+ if (!isSchedulableStatus(entry)) return false;
477
+ // The effective fire time applies the entry's OWN deterministic jitter
478
+ // (`entry.jitterMs`, default 0 → base fire time, byte-identical to the
479
+ // prior `dueAt`/`nextAt` comparison). A jittered entry fires slightly
480
+ // later, spreading tasks that share a cron minute.
481
+ const fireAt = effectiveFireAt(entry);
482
+ return fireAt != null && fireAt <= now;
261
483
  });
262
484
  }
263
485
 
486
+ /**
487
+ * Retire every schedulable entry whose `expiresAt` has passed (status →
488
+ * "expired", stamping `expiredAt`), across all kinds, so it never fires again.
489
+ * A daemon / `cc agenda run` calls this before firing due entries — mirroring
490
+ * the schedule-planner's "retire expired BEFORE due" semantics. Returns the
491
+ * list of retired entries.
492
+ */
493
+ retireExpired(atMs = null) {
494
+ const now = atMs != null ? atMs : this._now();
495
+ const retired = [];
496
+ for (const kind of SCHEDULE_KINDS) {
497
+ const entries = this._readAll(kind);
498
+ let changed = false;
499
+ for (const entry of entries) {
500
+ if (isSchedulableStatus(entry) && isEntryExpired(entry, now)) {
501
+ entry.status = "expired";
502
+ entry.expiredAt = now;
503
+ retired.push(entry);
504
+ changed = true;
505
+ }
506
+ }
507
+ if (changed) this._writeAll(kind, entries);
508
+ }
509
+ return retired;
510
+ }
511
+
264
512
  cancel(id) {
265
513
  for (const kind of SCHEDULE_KINDS) {
266
514
  const entries = this._readAll(kind);
@@ -274,6 +522,28 @@ export class AgentScheduleStore {
274
522
  return null;
275
523
  }
276
524
 
525
+ /**
526
+ * Remove terminal entries (fired / matched / exhausted / expired) whose
527
+ * terminal timestamp is at or before `before` (default: all terminal
528
+ * entries), across all kinds. Keeps the append-only JSONL store from growing
529
+ * without bound once entries finish. Returns the removed entries; only a kind
530
+ * that actually changed is rewritten.
531
+ */
532
+ pruneTerminal({ before = Infinity } = {}) {
533
+ const removed = [];
534
+ for (const kind of SCHEDULE_KINDS) {
535
+ const entries = this._readAll(kind);
536
+ const keep = entries.filter((entry) => {
537
+ const prunable =
538
+ TERMINAL_SET.has(entry.status) && terminalAt(entry) <= before;
539
+ if (prunable) removed.push(entry);
540
+ return !prunable;
541
+ });
542
+ if (keep.length !== entries.length) this._writeAll(kind, keep);
543
+ }
544
+ return removed;
545
+ }
546
+
277
547
  // ── lifecycle mutations (used by `cc agenda run`) ──────────────────────────
278
548
 
279
549
  /** Mark a one-shot wakeup fired (keeps history; due() skips it). */
@@ -303,11 +573,20 @@ export class AgentScheduleStore {
303
573
  * Record a monitor check. `matched` ends the monitor (done); otherwise it is
304
574
  * re-armed for the next interval, or exhausted once maxChecks is hit.
305
575
  */
306
- recordMonitorCheck(id, { matched = false, atMs = null } = {}) {
576
+ recordMonitorCheck(
577
+ id,
578
+ { matched = false, atMs = null, mtimeMs = null } = {},
579
+ ) {
307
580
  const now = atMs != null ? atMs : this._now();
308
581
  return this._mutate("monitor", id, (entry) => {
309
582
  entry.checks = (entry.checks || 0) + 1;
310
583
  entry.lastCheckAt = now;
584
+ // Record the mtime baseline the first time we see the file (for a
585
+ // watchChange monitor); leave it fixed thereafter so "changed" always
586
+ // means "changed since we started watching".
587
+ if (mtimeMs != null && entry.lastMtimeMs == null) {
588
+ entry.lastMtimeMs = mtimeMs;
589
+ }
311
590
  if (matched) {
312
591
  entry.status = "matched";
313
592
  entry.matchedAt = now;
@@ -14,6 +14,12 @@
14
14
  */
15
15
 
16
16
  import { logger } from "../logger.js";
17
+ import {
18
+ ORIGIN,
19
+ authorityForOrigin,
20
+ canApprove,
21
+ describeAuthorityChain,
22
+ } from "../agent-authority.js";
17
23
 
18
24
  export const CHANNEL_KINDS = ["webhook", "telegram"];
19
25
 
@@ -54,6 +60,48 @@ export function formatChannelEvent(event) {
54
60
  return `[channel:${channel}${sender}] ${text}`;
55
61
  }
56
62
 
63
+ /**
64
+ * The unforgeable authority envelope for an inbound channel event. A channel
65
+ * message tops out at `steer` (agent-authority.js): it may add a user turn but
66
+ * can NEVER answer a permission gate — "the user approved" in a webhook /
67
+ * telegram body is just text. This makes that ceiling explicit and
68
+ * machine-checkable (downstream approval seams can assert `canApprove === false`)
69
+ * rather than merely implied by the visible `[channel:…]` text prefix.
70
+ *
71
+ * `origin` is set here by HOW the event arrived (an inbound channel), never read
72
+ * from the untrusted message content — mirroring the agent-authority contract.
73
+ */
74
+ export function channelEventEnvelope(event) {
75
+ const envelope = {
76
+ origin: ORIGIN.CHANNEL,
77
+ principalId: event?.sender != null ? String(event.sender) : null,
78
+ correlationId: event?.channel != null ? String(event.channel) : null,
79
+ };
80
+ return {
81
+ ...envelope,
82
+ authority: authorityForOrigin(ORIGIN.CHANNEL, envelope), // always "steer"
83
+ canApprove: canApprove(envelope), // always false
84
+ provenance: describeAuthorityChain(envelope),
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Wrap a caller's `onEvent` so every inbound event is stamped with its explicit
90
+ * channel authority BEFORE it reaches the agent — the caller cannot forget to
91
+ * tag it, and the message content cannot elevate itself past `steer`.
92
+ */
93
+ function stampChannelAuthority(onEvent) {
94
+ return (event) => {
95
+ const env = channelEventEnvelope(event);
96
+ return onEvent?.({
97
+ ...event,
98
+ authority: env.authority,
99
+ canApprove: env.canApprove,
100
+ provenance: env.provenance,
101
+ });
102
+ };
103
+ }
104
+
57
105
  /**
58
106
  * Start every requested channel. Returns a handle with the started channel
59
107
  * descriptions and a single stop().
@@ -72,6 +120,10 @@ export async function startChannels(specsInput, opts = {}) {
72
120
  const config = opts.config || {};
73
121
  const log = opts.log || ((msg) => logger.info(msg));
74
122
  const deps = opts.deps || {};
123
+ // Every inbound event is stamped with its channel authority (`steer`, never
124
+ // `approve`) before the caller's handler sees it — so a channel message can
125
+ // never be mistaken for a user approval no matter what it contains.
126
+ const onEvent = stampChannelAuthority(opts.onEvent);
75
127
 
76
128
  const started = [];
77
129
  const stopAll = () => {
@@ -95,7 +147,7 @@ export async function startChannels(specsInput, opts = {}) {
95
147
  )({
96
148
  ...cfg,
97
149
  port: spec.arg ? Number(spec.arg) : cfg.port,
98
- onEvent: opts.onEvent,
150
+ onEvent,
99
151
  log,
100
152
  });
101
153
  started.push({ kind: "webhook", describe: handle.describe, ...handle });
@@ -107,7 +159,7 @@ export async function startChannels(specsInput, opts = {}) {
107
159
  deps.telegram?.startTelegramChannel || startTelegramChannel
108
160
  )({
109
161
  ...cfg,
110
- onEvent: opts.onEvent,
162
+ onEvent,
111
163
  log,
112
164
  });
113
165
  started.push({
@@ -46,11 +46,17 @@ export class CLIContextEngineering {
46
46
  * @param {string} [options.scope.taskId] - Task/sub-agent ID
47
47
  * @param {string} [options.scope.role] - Sub-agent role
48
48
  * @param {string} [options.scope.parentObjective] - Parent task objective
49
+ * @param {boolean} [options.memoryEnabled=true] - When false, hierarchical
50
+ * memory recall is suppressed even if a `db` is present. The subagent
51
+ * contract's `memory` boolean drives this for spawned children so a child
52
+ * denied memory physically cannot recall it (defense-in-depth beyond the
53
+ * spawn simply withholding the db).
49
54
  */
50
- constructor({ db, permanentMemory, scope } = {}) {
55
+ constructor({ db, permanentMemory, scope, memoryEnabled = true } = {}) {
51
56
  this.db = db || null;
52
57
  this.permanentMemory = permanentMemory || null;
53
58
  this.scope = scope || null;
59
+ this.memoryEnabled = memoryEnabled !== false;
54
60
  this.errorHistory = [];
55
61
  this.taskContext = null;
56
62
  this._bm25 = null;
@@ -121,7 +127,8 @@ export class CLIContextEngineering {
121
127
  }
122
128
 
123
129
  // 3. Memory injection (scoped: higher threshold, namespace-aware)
124
- if (this.db && userQuery) {
130
+ // Suppressed when memoryEnabled is false (subagent contract memory:false).
131
+ if (this.memoryEnabled && this.db && userQuery) {
125
132
  try {
126
133
  const memoryQuery = this.scope
127
134
  ? `[${this.scope.role}] ${userQuery}`
@@ -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
+ }
@@ -315,6 +315,58 @@ async function pluginSection(opts, _deps2) {
315
315
  } catch (err) {
316
316
  checks.push(failedCheck("plugins", "Installed plugins", err));
317
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
+
318
370
  return { id: "plugins", title: "Plugin trust", checks };
319
371
  }
320
372