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
@@ -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;
package/src/lib/agents.js CHANGED
@@ -15,6 +15,21 @@
15
15
  * model model override for runs of this agent
16
16
  * maxTurns per-run iteration cap for this agent (positive integer)
17
17
  * isolation "worktree" → run in a fresh git worktree (gap 2026-07-11)
18
+ * permission-mode child permission mode (tighten-only vs parent)
19
+ * skills capability allow-list — skills the child may use
20
+ * mcp-servers capability allow-list — MCP servers the child may use
21
+ * hooks capability allow-list — hooks the child may run
22
+ * memory whether the child may read permanent memory
23
+ * effort reasoning effort (low|medium|high|xhigh)
24
+ * background run detached
25
+ * context "fresh" (default, isolated) | "fork" (inherit parent)
26
+ * max-depth/max-children ceilings this agent imposes on its own descendants
27
+ * budget { tokens, cost, time } caps
28
+ *
29
+ * The extended P1 contract (permission/skills/mcp/hooks/memory/effort/
30
+ * background/context/depth/children/budget) is normalized into a `contract`
31
+ * field via subagent-contract.js; the top-level tools/maxTurns/isolation are
32
+ * kept as-is for the existing spawn path.
18
33
  *
19
34
  * Project scope shadows personal on a name clash. Discovery + parse are pure
20
35
  * (inject fs/path/home) so the whole thing is unit-testable.
@@ -26,6 +41,7 @@ import { homedir } from "node:os";
26
41
  import yaml from "js-yaml";
27
42
  import { projectRootBase } from "./project-root.cjs";
28
43
  import { discoverPluginAgentLayers } from "./plugin-runtime/agents.js";
44
+ import { normalizeSubagentContract } from "./subagent-contract.js";
29
45
 
30
46
  const _deps = { fs: fsDefault, path: pathDefault };
31
47
 
@@ -147,6 +163,9 @@ export function parseAgentFile(file, scope, opts = {}) {
147
163
  maxTurns:
148
164
  Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : null,
149
165
  isolation: data.isolation === "worktree" ? "worktree" : null,
166
+ // Extended P1 subagent contract (permission/skills/mcp/hooks/memory/effort/
167
+ // background/context/depth/children/budget), normalized + validated.
168
+ contract: normalizeSubagentContract(data),
150
169
  systemPrompt: body || "",
151
170
  };
152
171
  }
@@ -15,6 +15,7 @@ import { randomBytes } from "node:crypto";
15
15
  import { join } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { getHomeDir } from "./paths.js";
18
+ import { deriveSessionState } from "./session-lifecycle.js";
18
19
 
19
20
  export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
20
21
  export const DEFAULT_HEARTBEAT_STALE_MS = 120000;
@@ -347,6 +348,26 @@ export function normalizeBackgroundAgentTitle(title) {
347
348
  return value.slice(0, 160);
348
349
  }
349
350
 
351
+ /**
352
+ * The canonical unified lifecycle state (session-lifecycle.js) for a session —
353
+ * folds the supervisor `status` + worker `phase` + `pendingApprovals` into one
354
+ * of the 10 canonical states so every consumer (dashboard, `cc daemon list
355
+ * --json`, an IDE bridge) reasons about a single vocabulary. Pure; call it at a
356
+ * DISPLAY boundary only. It is deliberately NOT baked into
357
+ * effectiveBackgroundAgentState because that function's output is spread back
358
+ * into writeBackgroundAgentState by the rename/pin/stop read-modify-write
359
+ * paths — a derived field there would leak into the on-disk schema.
360
+ */
361
+ export function sessionLifecycleState(state) {
362
+ return deriveSessionState(state);
363
+ }
364
+
365
+ /** A display copy of `state` with the derived `lifecycleState` attached. */
366
+ function withLifecycleState(state) {
367
+ if (!state) return state;
368
+ return { ...state, lifecycleState: deriveSessionState(state) };
369
+ }
370
+
350
371
  export function effectiveBackgroundAgentState(state, options = {}) {
351
372
  if (!state) return null;
352
373
  if (state.status !== "running") return state;
@@ -403,13 +424,19 @@ export function effectiveBackgroundAgentState(state, options = {}) {
403
424
  }
404
425
 
405
426
  export function listBackgroundAgents(options = {}) {
406
- return readdirSync(backgroundAgentsDir())
407
- .filter((name) => name.endsWith(".json") && !name.includes(".job."))
408
- .map((name) => readBackgroundAgentState(name.slice(0, -5)))
409
- .filter(Boolean)
410
- .map((state) => effectiveBackgroundAgentState(state, options))
411
- .filter((state) => options.all || state.status === "running")
412
- .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
427
+ return (
428
+ readdirSync(backgroundAgentsDir())
429
+ .filter((name) => name.endsWith(".json") && !name.includes(".job."))
430
+ .map((name) => readBackgroundAgentState(name.slice(0, -5)))
431
+ .filter(Boolean)
432
+ .map((state) => effectiveBackgroundAgentState(state, options))
433
+ .filter((state) => options.all || state.status === "running")
434
+ .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))
435
+ // Display-only enrichment: attach the canonical unified lifecycle state.
436
+ // This is a list feed (never written back to disk), so the derived field
437
+ // cannot leak into the on-disk schema the way it would in the mutate paths.
438
+ .map(withLifecycleState)
439
+ );
413
440
  }
414
441
 
415
442
  function sleepSyncMs(ms) {
@@ -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}`