chainlesschain 0.162.161 → 0.162.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-DMn528q-.js → AIOps-BQ0kgfA1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-D9ZcDCJL.js → ActionButton-Dr1MDANe.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-A_K-8PX0.js → Analytics-CZ5vsaXW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D4pFW3X6.js → AppLayout-BIBga881.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-Dz46COAd.js → Artifacts-vpXHRU3u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-cbMjbyg0.js → Audit-B8exuoub.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-Dtf88GAj.js → BackgroundAgents-923evRRN.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-C5KRxn52.js → Backup-sr_sswlQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Dd2UK3BN.js → BaseInput-CnW-4XKT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-lptKTFdG.js → Chat-FZCsTZIL.js} +5 -5
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-_yspG6rG.js → Checkbox-DFrxB59H.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BGq_GAha.js → Codegen-_b6WEel8.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CePor71g.js → Col-CkbnnZ-K.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-CLOWhIT7.js → Community-BxM_82zW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CivJt2vT.js → Compact-DYinP45R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Btmo1GGE.js → Compliance-DyXp3sSP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Ct35OlG4.js → Cowork-BEr2DhW7.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-CTc2GmhB.js → Cron-B-Vwo1hN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-ZtiBETk1.js → Crosschain-DbLxKddd.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BNqMQQXw.js → DID-BsQ_LFw9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-BmzZd-9V.js → Dashboard-lVlXQHu5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CMVgvP2M.js → Dropdown-Beo_foO7.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-CcqQFAfT.js → EmailListRenderer-DszHIXED.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CTqB1_4V.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CiKOOzlb.js → Federation-CuJLa2eb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Wk-OT0gr.js → FormItemContext-Cxd1lxhc.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DbEQT6ue.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-0ZvCPSD9.js → Git-WqPKetx6.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CmVuKkPb.js → Governance-CWYtY3ke.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CTWE3Jwg.js → Inference-BnX1iiNV.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-CBEIF00L.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DLhxDwVG.js → Logs-DqLMNntm.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-BqYwEYou.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BgsCBrnc.js → Marketplace-_vGfmhNl.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-ClBUGDS2.js → McpTools-BeJDOkDj.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CnG_ZnYw.js → Memory-Da0EEZDO.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BUOAbR1Z.js → MobileBridge-DSuzcCCy.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-Dnt_9-WB.js → MobileProjects-D0RAk4e8.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-CLjt4kCD.js → Mtc-BE9YgdA0.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-f24U9Aes.js → MtcAudit-fC2BpO5S.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-GrmGQbSq.js → Multisig-D24Xxqw3.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DADaWn-n.js → NLProgramming-Bs3oGvS8.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-xtf5RX6C.js → Notes-BFjmAA-3.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-Cd27Rgtx.js → NotificationSettings-BX_9Bhv0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-fXRxlDjl.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-joCFlNB0.js → Organization-DBcdsVEP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-B1HoURq0.js → Overflow-DDyaF9vC.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-DiwsKbT7.js → P2P-BB78L4he.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Xe-BsW6S.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-C23qi8kl.js → Permissions-INbnWv4j.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BS1syX2f.js → PersonalDataHub-BST-VmTq.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-DRYwwDyh.js → Pipeline-CE7_tS2k.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQKaDHPH.js → Privacy-hK5L8dOB.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BC6F9xiG.js → ProjectInit-BvkkUoDn.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-ClFPc1Nd.js → ProjectSettings-u7_EUtkm.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-5Fmt_8Nw.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-CLWuOUNt.js → Providers-B9eanarR.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-DiqULTxb.js → QrScannerModal-C0dX5N2f.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-9A03P-E_.js → QuickAsk-CKTpG-PZ.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CJZAnbq_.js → Recommend-Px_w_XQM.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C9A1R77h.js → RemoteSession-Di0fnYkM.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BIZc6Rky.js → Reputation-vesnWjXt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-DfIPNtuG.js → Row-exbY6LkT.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-C5znFcQ1.js → RssFeed-B3O3x70t.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-DSs2z3OX.js → Search-uRA0DhNz.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CV6LcrLY.js → Security-CViqdH6K.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-cmhoX0Xj.js → Services-CkXS97WN.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-tCsg2po7.js → Skeleton-DxW5WYnE.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-BK1KUDiy.js → Skills-CLxmGFse.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-yRgyeA82.js → Sla-Cr5fziXq.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-c1mo1mlp.js → SpeechSettings-H0JSb3cp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-koIAfuT3.js → SyncSettings-DMYWMae7.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-DkkreAaB.js → Tasks-CbYyDdF4.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-fy5Ctf9M.js → Templates-Cpc8JjiK.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-Bw70Jf6S.js → Tenant-CRh77Jda.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BKh6Rx-4.js → Terminal-4D8tNBO5.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-Dg54WP0c.js → TimelineRenderer-D41jzzbG.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-BvvD5SxJ.js → Tokens-C_jWEy6b.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-B7Ug9x4e.js → Trigger-Dk1ZUhsy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-BjvuMORY.js → Trust-CLVy4l7m.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-CZqhkx8p.js → UkeySign-C-FeqFDj.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-ClR50lhX.js → VideoEditing-CqVZzyMm.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DlfppYjx.js → Wallet-DX9BW9pu.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-BirgVr8B.js → WebAuthn-7hQDpWw_.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-B8DP_bax.js → WorkflowEditor-6XqUAO0j.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-DA8AUZzL.js → chat-5R_vkaJG.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-C38pLGp1.js → colors-huUPZdC-.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-81rtr98o.js → compact-item-CfJlgisE.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-D34ixq8t.js → createContext-jO8heofr.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-D4bzqTrZ.js → hasIn-DY8Vxqx7.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-E1OJRXs4.js → index-A48PtXw9.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-D3zCCZRS.js → index-B300RPnT.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-Cd3KuNK5.js → index-B74OXFxC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Dmo3dFHb.js → index-BAON7TzU.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Ddk0e_Pi.js → index-BCqxyvqp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-9qoVpPt6.js → index-BHE8OIls.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DBFSo1NF.js → index-BQS-uPOK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DUQLXLQM.js → index-BRXOP09X.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-D6GfvEQG.js → index-BYLBPpt8.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DFyWKOBD.js → index-BfCx4pFT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BP-zkXK8.js → index-BliaT02i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DMgnRwrK.js → index-BmqGerpC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BIP62a6r.js → index-BsBtRWbA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DkHqMnn3.js → index-BtEXSoPL.js} +1 -1
  108. package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
  109. package/src/assets/web-panel/assets/{index-B8mkLM4x.js → index-Bxax9vQ6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-ZRJRItm0.js → index-C2iVSFpy.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CP5alL4t.js → index-CA6sO5VD.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BS3M5H9V.js → index-CA90TZxG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DvfCEJG-.js → index-CLhBh-qE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-tKiC8T-4.js → index-CRElRhyS.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B0QArPQt.js → index-Cbe9G0V4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-COe_cOLJ.js → index-ChvHubX3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BvL_3J_n.js → index-Cis_Ip7H.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-F76w1NiL.js → index-Cjxbu68P.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BlxEphl7.js → index-CzKmteNS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CuS-UPoj.js → index-D0pet5Uy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CbhE3j5j.js → index-D1VLuww0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D_2VYPr1.js → index-DXBcvUnf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DakfFZ-k.js → index-Db1cLWir.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-fI5sXMLg.js → index-DoI2sZm7.js} +1 -1
  125. package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
  126. package/src/assets/web-panel/assets/{index-CoVgZw_b.js → index-KRHroryI.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BvD7b2IK.js → index-d1xkbtNz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BtGxuQrO.js → index-dKx57io7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BqIhLAbA.js → index-j3O2EEJe.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BXd2_LmH.js → index-p85YaaKw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DFHa1HIg.js → index-v3pTaEQm.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-D0Ih7Hwn.js → index-vXs3B8GJ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-ByyvflIR.js → initDefaultProps-MCB7-raZ.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-B3FQm2By.js → motion-UWDF9swV.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-BSfns6Ef.js → move-CsTmgXZU.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-CTG1Ieri.js → mtc-parser-fQy6KGLg.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-DObM_2j6.js → omit-woPnEjd1.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-DOfelQ1t.js → pickAttrs-Bs2dZ1gf.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-CHZILds_.js → placementArrow-C7EVgPkg.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-REbucKSx.js → responsiveObserve-BH2kYCTN.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-C6EZzD7o.js → slide-BFBP2V_e.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BxkWS-g3.js → statusUtils-xdDxDYse.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-Chklyoq6.js → styleChecker-aAWgPPQf.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-CRVwKrrI.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DV24ik_J.js → useFs-BrOih40N.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-C7vIo4Pg.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-By-v5GlF.js → vnode-B8lutNSE.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-B1w9Am3r.js → zoom-CffNre7V.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +254 -23
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/hook.js +181 -7
  154. package/src/commands/plugin.js +239 -1
  155. package/src/commands/review.js +352 -2
  156. package/src/data/changelog.json +14 -4
  157. package/src/gateways/ws/remote-session-protocol.js +39 -0
  158. package/src/gateways/ws/session-protocol.js +5 -2
  159. package/src/lib/agent-schedule-store.js +289 -10
  160. package/src/lib/agents.js +19 -0
  161. package/src/lib/background-agent-supervisor.js +34 -7
  162. package/src/lib/channels/channel-manager.js +54 -2
  163. package/src/lib/cli-context-engineering.js +9 -2
  164. package/src/lib/context-breakdown.js +116 -0
  165. package/src/lib/docs-drift.js +166 -0
  166. package/src/lib/doctor-checkup.js +128 -0
  167. package/src/lib/hook-event-bus.cjs +0 -0
  168. package/src/lib/hook-event-log.cjs +193 -0
  169. package/src/lib/interaction-adapter.js +34 -4
  170. package/src/lib/json-schema-output.js +75 -12
  171. package/src/lib/json-schema-validate.js +831 -0
  172. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  173. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  174. package/src/lib/operation-fingerprint.js +95 -0
  175. package/src/lib/plugin-runtime/capabilities.js +371 -0
  176. package/src/lib/plugin-runtime/capability-consent.js +175 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/plugin-runtime/policy.js +62 -0
  179. package/src/lib/plugin-runtime/scopes.js +20 -4
  180. package/src/lib/project-instructions.js +128 -0
  181. package/src/lib/remote-approval-bridge.js +16 -0
  182. package/src/lib/repl-goal.js +132 -0
  183. package/src/lib/repl-rewind.js +52 -0
  184. package/src/lib/review-pipeline.js +227 -0
  185. package/src/lib/runtime-checkup.js +318 -0
  186. package/src/lib/schedule-planner.js +17 -1
  187. package/src/lib/session-lifecycle.js +305 -0
  188. package/src/lib/settings-hook-events.cjs +52 -13
  189. package/src/lib/settings-loader.cjs +42 -0
  190. package/src/lib/side-effect-ledger-store.js +88 -0
  191. package/src/lib/side-effect-ledger.js +296 -0
  192. package/src/lib/sub-agent-context.js +74 -5
  193. package/src/lib/subagent-contract.js +383 -0
  194. package/src/lib/subagent-inheritance.js +112 -0
  195. package/src/lib/telemetry-ids.js +190 -0
  196. package/src/lib/turn-binding-store.js +119 -0
  197. package/src/lib/turn-binding.js +397 -0
  198. package/src/repl/agent-repl.js +136 -0
  199. package/src/repl/permission-tier.js +28 -0
  200. package/src/runtime/agent-core.js +387 -14
  201. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  202. package/src/runtime/headless-runner.js +267 -0
  203. package/src/runtime/headless-stream.js +60 -1
  204. package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +0 -1
  205. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +0 -1
  206. package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +0 -1
  207. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +0 -1
  208. package/src/assets/web-panel/assets/index-CY1NURWZ.js +0 -1
  209. package/src/assets/web-panel/assets/index-DokUYqpq.js +0 -1
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Plugin capability CONSENT (Phase 3 gap: capability declaration + re-consent).
3
+ *
4
+ * Complements version trust ([[trust.js]]): trust pins the exact CODE a plugin
5
+ * ships (re-consent on version bump); this pins the CAPABILITY SET the plugin is
6
+ * allowed to request (process / network / filesystem / mcp / monitor /
7
+ * credential). The two are orthogonal — a plugin can be trusted-to-run yet still
8
+ * WIDEN what it wants (add `network:*`, a new credential), which must re-prompt.
9
+ *
10
+ * Consent is keyed by the SET, not the version: once the user consents, later
11
+ * versions that keep the same (or a narrower) capability set stay consented; any
12
+ * WIDENING (a new grant) revokes consent until re-granted. This mirrors
13
+ * `consentRequiredForUpgrade` in [[capabilities.js]] exactly.
14
+ *
15
+ * Recorded in the USER data dir (never in the repo — a checked-in project file
16
+ * must not be able to pre-grant itself capabilities), keyed `<scope>:<name>` →
17
+ * { version, capabilities, consentedAt }. Pure over injected IO so unit tests
18
+ * never touch the real user-data dir.
19
+ */
20
+
21
+ import fs from "fs";
22
+ import path from "path";
23
+ import { getElectronUserDataDir } from "../paths.js";
24
+ import {
25
+ normalizeCapabilities,
26
+ diffCapabilities,
27
+ describeCapabilities,
28
+ } from "./capabilities.js";
29
+
30
+ export const _deps = {
31
+ existsSync: fs.existsSync,
32
+ readFileSync: fs.readFileSync,
33
+ writeFileSync: fs.writeFileSync,
34
+ mkdirSync: fs.mkdirSync,
35
+ now: () => new Date().toISOString(),
36
+ // Injectable so unit tests never touch the real user-data dir.
37
+ storePath: () =>
38
+ path.join(getElectronUserDataDir(), "plugin-capability-consent.json"),
39
+ };
40
+
41
+ function consentKey(scope, name) {
42
+ return `${scope}:${name}`;
43
+ }
44
+
45
+ export function loadConsentStore() {
46
+ try {
47
+ const p = _deps.storePath();
48
+ if (!_deps.existsSync(p)) return {};
49
+ const data = JSON.parse(_deps.readFileSync(p, "utf8"));
50
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ function saveConsentStore(store) {
57
+ const p = _deps.storePath();
58
+ _deps.mkdirSync(path.dirname(p), { recursive: true });
59
+ _deps.writeFileSync(p, JSON.stringify(store, null, 2), "utf8");
60
+ }
61
+
62
+ /**
63
+ * Coerce a capability set to the canonical shape diffCapabilities needs.
64
+ * `normalizeCapabilities` is NOT idempotent — it re-parses `filesystem`/
65
+ * `credential` through `toList`, so feeding it an already-normalized set (e.g.
66
+ * one read back from the store) corrupts those lists. Detect an already-
67
+ * canonical set and pass it through unchanged; only normalize raw input.
68
+ */
69
+ function canonicalCaps(caps) {
70
+ if (
71
+ caps &&
72
+ typeof caps === "object" &&
73
+ caps.network &&
74
+ typeof caps.network === "object" &&
75
+ "domains" in caps.network &&
76
+ caps.filesystem &&
77
+ "roots" in caps.filesystem &&
78
+ caps.credential &&
79
+ "names" in caps.credential
80
+ ) {
81
+ return caps;
82
+ }
83
+ return normalizeCapabilities(caps);
84
+ }
85
+
86
+ /** True when a capability set grants nothing (all-deny) — nothing to consent. */
87
+ export function capabilitiesAreEmpty(caps) {
88
+ return !diffCapabilities(null, canonicalCaps(caps)).widened;
89
+ }
90
+
91
+ /**
92
+ * PURE consent status: given a plugin's DECLARED capabilities and its stored
93
+ * consent entry, decide whether consent covers them. Re-consent is required on
94
+ * a first request or any WIDENING (new grant) — not on a version bump alone
95
+ * (trust handles versions).
96
+ *
97
+ * @param {object} declaredCaps raw or normalized capabilities
98
+ * @param {object|null} entry stored `{ version, capabilities }` (or null)
99
+ * @returns {{consented:boolean, reason:string, added:string[]}}
100
+ */
101
+ export function capabilityConsentStatus(declaredCaps, entry) {
102
+ const declared = canonicalCaps(declaredCaps);
103
+ if (!diffCapabilities(null, declared).widened) {
104
+ return { consented: true, reason: "no capabilities declared", added: [] };
105
+ }
106
+ if (!entry) {
107
+ return {
108
+ consented: false,
109
+ reason: "capabilities never consented",
110
+ added: diffCapabilities(null, declared).added,
111
+ };
112
+ }
113
+ // Coerce the stored set to canonical too (raw hand-written entry, or an
114
+ // already-normalized one read from the store) before diffing.
115
+ const diff = diffCapabilities(canonicalCaps(entry.capabilities), declared);
116
+ if (diff.widened) {
117
+ return {
118
+ consented: false,
119
+ reason: "capabilities widened since last consent",
120
+ added: diff.added,
121
+ };
122
+ }
123
+ return {
124
+ consented: true,
125
+ reason: "consent covers declared capabilities",
126
+ added: [],
127
+ };
128
+ }
129
+
130
+ /** Load the store and report whether one plugin's declared caps are consented. */
131
+ export function isPluginCapabilityConsented(plugin, declaredCaps) {
132
+ if (!plugin || !plugin.name) return false;
133
+ const entry = loadConsentStore()[consentKey(plugin.scope, plugin.name)];
134
+ return capabilityConsentStatus(declaredCaps, entry).consented;
135
+ }
136
+
137
+ /** Record consent for a plugin's currently-declared capability set. */
138
+ export function consentPluginCapabilities(
139
+ name,
140
+ { scope = "project", version, capabilities } = {},
141
+ ) {
142
+ if (!version) throw new Error("consentPluginCapabilities requires a version");
143
+ const store = loadConsentStore();
144
+ store[consentKey(scope, name)] = {
145
+ version,
146
+ capabilities: canonicalCaps(capabilities),
147
+ consentedAt: _deps.now(),
148
+ };
149
+ saveConsentStore(store);
150
+ return { name, scope, version };
151
+ }
152
+
153
+ /** Revoke capability consent for a plugin at a scope. */
154
+ export function revokeCapabilityConsent(name, { scope = "project" } = {}) {
155
+ const store = loadConsentStore();
156
+ const key = consentKey(scope, name);
157
+ const existed = Object.prototype.hasOwnProperty.call(store, key);
158
+ delete store[key];
159
+ saveConsentStore(store);
160
+ return { name, scope, removed: existed };
161
+ }
162
+
163
+ /** All consent entries (for `cc plugin consent --list`). */
164
+ export function listCapabilityConsent() {
165
+ return Object.entries(loadConsentStore()).map(([key, v]) => {
166
+ const idx = key.indexOf(":");
167
+ return {
168
+ scope: key.slice(0, idx),
169
+ name: key.slice(idx + 1),
170
+ version: v?.version || null,
171
+ consentedAt: v?.consentedAt || null,
172
+ capabilities: describeCapabilities(v?.capabilities),
173
+ };
174
+ });
175
+ }
@@ -27,6 +27,11 @@
27
27
  import fs from "fs";
28
28
  import path from "path";
29
29
  import semver from "semver";
30
+ import {
31
+ normalizeCapabilities,
32
+ normalizeOptionsSchema,
33
+ auditDeclaredCapabilities,
34
+ } from "./capabilities.js";
30
35
 
31
36
  export const _deps = {
32
37
  existsSync: fs.existsSync,
@@ -73,6 +78,14 @@ export function parsePluginManifest(root) {
73
78
  scope: null,
74
79
  metadata: {},
75
80
  components: emptyComponents(),
81
+ // Declarative security contract (P1): the plugin's declared capabilities
82
+ // (manifest.permissions) + typed config schema (manifest.optionsSchema),
83
+ // normalized by capabilities.js. `capabilitiesDeclared` distinguishes a
84
+ // plugin that declared a permissions block (subject to the declared-vs-
85
+ // actual audit) from a legacy one that declared nothing (unrestricted).
86
+ capabilities: normalizeCapabilities(null),
87
+ capabilitiesDeclared: false,
88
+ optionsSchema: {},
76
89
  errors,
77
90
  warnings,
78
91
  };
@@ -165,6 +178,23 @@ export function parsePluginManifest(root) {
165
178
  c.bin = resolveBin(abs, manifest, safeResolve, warnings);
166
179
  c.settings = resolveSettings(abs, manifest, safeResolve);
167
180
 
181
+ // ── declared capabilities + options schema (P1) ──
182
+ const hasPermissions =
183
+ manifest.permissions &&
184
+ typeof manifest.permissions === "object" &&
185
+ !Array.isArray(manifest.permissions);
186
+ result.capabilities = normalizeCapabilities(manifest.permissions);
187
+ result.capabilitiesDeclared = Boolean(hasPermissions);
188
+ result.optionsSchema = normalizeOptionsSchema(manifest.optionsSchema);
189
+ // Declared-vs-actual audit runs ONLY when a permissions block is present — a
190
+ // legacy plugin that declares nothing is unrestricted by design, so it never
191
+ // gets these warnings (keeps existing manifests warning-free).
192
+ if (hasPermissions) {
193
+ for (const f of auditDeclaredCapabilities(result)) {
194
+ warnings.push(`capability: ${f.reason}`);
195
+ }
196
+ }
197
+
168
198
  result.ok = errors.length === 0;
169
199
  return result;
170
200
  }
@@ -25,6 +25,7 @@
25
25
  import { enforcePluginPolicy } from "../plugin-security.js";
26
26
  import { loadManagedSettings } from "../settings-loader.cjs";
27
27
  import { verifyInstalledSignature } from "./signature.js";
28
+ import { isPluginCapabilityConsented } from "./capability-consent.js";
28
29
 
29
30
  // Managed settings are org-admin controlled and effectively static for a
30
31
  // session, and discoverPlugins is called by every collector — memoize the load
@@ -142,6 +143,67 @@ export function filterByManagedPolicy(plugins, managed) {
142
143
  return { kept, dropped };
143
144
  }
144
145
 
146
+ /**
147
+ * Whether capability-consent must be ENFORCED at load time. The consent gate
148
+ * ([[capability-consent.js]]) already exists but was consulted only by `cc
149
+ * plugin consent`/`validate`/`doctor` — a plugin that declared capabilities and
150
+ * was never consented still loaded all six component types. Enforcement is
151
+ * OPT-IN (byte-identical default): an org sets `requirePluginCapabilityConsent`
152
+ * in managed settings, or a single user exports `CC_REQUIRE_PLUGIN_CONSENT=1`.
153
+ *
154
+ * @param {object|null} managed managed settings
155
+ * @param {object} [env] process env (injectable)
156
+ * @returns {boolean}
157
+ */
158
+ export function capabilityConsentRequired(managed, env) {
159
+ const raw = (env || process.env).CC_REQUIRE_PLUGIN_CONSENT;
160
+ if (raw != null && /^(1|true|yes|on)$/i.test(String(raw).trim())) return true;
161
+ return (
162
+ managed?.requirePluginCapabilityConsent === true ||
163
+ managed?.requirePluginCapabilityConsent === "require"
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Drop plugins that DECLARE capabilities but whose current capability SET has
169
+ * not been consented (first request or any widening). Fail-closed: a consent
170
+ * check that throws drops the plugin. Legacy plugins that declare no
171
+ * `permissions` block are UNAFFECTED — the same "未声明 legacy 不受限" rule the
172
+ * declaration audit uses. A plugin whose declared set is empty (all-deny) is
173
+ * auto-consented by `isPluginCapabilityConsented`, so it stays.
174
+ *
175
+ * @param {Array<{name, scope, manifest}>} plugins
176
+ * @returns {{ kept: Array, dropped: Array<{name, reason}> }}
177
+ */
178
+ export function filterByCapabilityConsent(plugins) {
179
+ const kept = [];
180
+ const dropped = [];
181
+ for (const p of plugins) {
182
+ if (!p.manifest?.capabilitiesDeclared) {
183
+ kept.push(p); // legacy plugin, no declaration → not gated
184
+ continue;
185
+ }
186
+ let consented = false;
187
+ try {
188
+ consented = isPluginCapabilityConsented(
189
+ { name: p.name, scope: p.scope },
190
+ p.manifest.capabilities,
191
+ );
192
+ } catch {
193
+ consented = false; // fail closed
194
+ }
195
+ if (consented) {
196
+ kept.push(p);
197
+ } else {
198
+ dropped.push({
199
+ name: p.name,
200
+ reason: `capabilities not consented — run \`cc plugin consent ${p.name} --grant\``,
201
+ });
202
+ }
203
+ }
204
+ return { kept, dropped };
205
+ }
206
+
145
207
  let _warnedDropped = new Set();
146
208
 
147
209
  /** One-time stderr warning naming plugins an org policy blocked from loading. */
@@ -27,6 +27,8 @@ import { parsePluginManifest } from "./manifest.js";
27
27
  import {
28
28
  loadManagedPluginPolicy,
29
29
  filterByManagedPolicy,
30
+ filterByCapabilityConsent,
31
+ capabilityConsentRequired,
30
32
  warnDroppedOnce,
31
33
  } from "./policy.js";
32
34
 
@@ -160,10 +162,24 @@ export function discoverPlugins(opts = {}) {
160
162
  env: opts.env,
161
163
  managedSettingsFile: opts.managedSettingsFile,
162
164
  });
163
- if (!managed) return all;
164
- const { kept, dropped } = filterByManagedPolicy(all, managed);
165
- if (dropped.length > 0) warnDroppedOnce(dropped);
166
- return kept;
165
+ const consentGate = capabilityConsentRequired(managed, opts.env);
166
+ // Default (no managed policy AND consent enforcement off): byte-identical —
167
+ // return every discovered plugin, exactly as before.
168
+ if (!managed && !consentGate) return all;
169
+ let list = all;
170
+ if (managed) {
171
+ const { kept, dropped } = filterByManagedPolicy(list, managed);
172
+ if (dropped.length > 0) warnDroppedOnce(dropped);
173
+ list = kept;
174
+ }
175
+ // Capability-consent enforcement funnels through the SAME chokepoint, so an
176
+ // un-consented plugin loads NONE of its six component types (opt-in).
177
+ if (consentGate) {
178
+ const { kept, dropped } = filterByCapabilityConsent(list);
179
+ if (dropped.length > 0) warnDroppedOnce(dropped);
180
+ list = kept;
181
+ }
182
+ return list;
167
183
  }
168
184
 
169
185
  // ── internals ────────────────────────────────────────────────────────────
@@ -477,4 +477,132 @@ export function loadProjectInstructionsBlock(opts = {}) {
477
477
  }
478
478
  }
479
479
 
480
+ function isDir(fs, p) {
481
+ try {
482
+ return fs.statSync(p).isDirectory();
483
+ } catch {
484
+ return false;
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Lazy subtree-instruction discovery (module 99 §5.3 "子目录指令按首次访问子树
490
+ * 懒加载"). Startup only loads the instruction chain from the project root DOWN
491
+ * to the cwd (see `findInstructionFiles`); a large monorepo's per-package
492
+ * `cc.md`/`CLAUDE.md`/`AGENTS.md` that sit BELOW the cwd are intentionally NOT
493
+ * injected up front — they cost tokens for subtrees a turn may never touch.
494
+ *
495
+ * This resolver answers "given a path a tool just accessed, which subtree
496
+ * instruction files should now be injected?" — the files in every directory
497
+ * strictly between `baseDir` (the already-loaded cwd) and the accessed path's
498
+ * directory, minus anything already loaded or excluded. Pure + deterministic;
499
+ * the tool-time injection itself is the caller's job.
500
+ *
501
+ * @param {object} opts
502
+ * @param {string} opts.repoRoot excludes are matched relative to this
503
+ * @param {string} opts.baseDir the startup cwd (its own file already loaded)
504
+ * @param {string} opts.accessedPath a file/dir a tool touched (abs or rel to baseDir)
505
+ * @param {Set<string>|string[]} [opts.alreadyLoaded] abs paths already injected
506
+ * @param {string[]} [opts.instructionExcludes]
507
+ * @returns {Array<{path:string, scope:"project", dir:string}>} shallowest-first
508
+ */
509
+ export function resolveSubtreeInstructions(opts = {}) {
510
+ const { fs, path } = resolveDeps(opts);
511
+ const repoRoot = path.resolve(opts.repoRoot || opts.baseDir || process.cwd());
512
+ const baseDir = path.resolve(opts.baseDir || repoRoot);
513
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
514
+ const already =
515
+ opts.alreadyLoaded instanceof Set
516
+ ? opts.alreadyLoaded
517
+ : new Set((opts.alreadyLoaded || []).map((p) => path.resolve(p)));
518
+
519
+ if (!opts.accessedPath) return [];
520
+ // Resolve the accessed path to a directory inside the subtree. A not-yet-
521
+ // created file (about to be written) stats as neither file nor dir → treat it
522
+ // as a file and use its parent.
523
+ let target = path.resolve(baseDir, opts.accessedPath);
524
+ if (!isDir(fs, target)) target = path.dirname(target);
525
+
526
+ // Subtree instructions apply only to DESCENDANTS of baseDir (ancestors were
527
+ // loaded at startup). Reject a target that escapes baseDir or the repo.
528
+ const relFromBase = path.relative(baseDir, target);
529
+ if (
530
+ !relFromBase ||
531
+ relFromBase.startsWith("..") ||
532
+ path.isAbsolute(relFromBase)
533
+ ) {
534
+ return [];
535
+ }
536
+ const relFromRoot = path.relative(repoRoot, target);
537
+ if (relFromRoot.startsWith("..") || path.isAbsolute(relFromRoot)) return [];
538
+
539
+ // Directories strictly between baseDir (exclusive) and target (inclusive).
540
+ const dirs = [];
541
+ let dir = target;
542
+ while (dir !== baseDir) {
543
+ dirs.push(dir);
544
+ const parent = path.dirname(dir);
545
+ if (parent === dir) break; // hit fs root without meeting baseDir
546
+ dir = parent;
547
+ }
548
+ dirs.reverse(); // shallowest first
549
+
550
+ const out = [];
551
+ for (const d of dirs) {
552
+ const file = firstExisting(fs, path, d, PROJECT_FILE_NAMES);
553
+ if (!file) continue;
554
+ const abs = path.resolve(file);
555
+ if (already.has(abs)) continue;
556
+ if (
557
+ excludes.length > 0 &&
558
+ pathIsExcluded(path.relative(repoRoot, abs), excludes)
559
+ ) {
560
+ continue;
561
+ }
562
+ out.push({ path: abs, scope: "project", dir: d });
563
+ }
564
+ return out;
565
+ }
566
+
567
+ /**
568
+ * Stateful companion to `resolveSubtreeInstructions`: tracks which subtree
569
+ * directories have already been injected across a session so a second access to
570
+ * the same subtree is a no-op. `onAccess(path)` returns ONLY the freshly
571
+ * discovered files. Fail-open (any error → []).
572
+ */
573
+ export class SubtreeInstructionLoader {
574
+ constructor(opts = {}) {
575
+ const path = opts.deps?.path || pathDefault;
576
+ this._opts = opts;
577
+ this._loadedDirs = new Set();
578
+ this._loadedFiles = new Set(
579
+ (opts.alreadyLoaded || []).map((p) => path.resolve(p)),
580
+ );
581
+ }
582
+
583
+ /** @returns {Array<{path:string, scope:"project", dir:string}>} newly injected */
584
+ onAccess(accessedPath) {
585
+ try {
586
+ const found = resolveSubtreeInstructions({
587
+ ...this._opts,
588
+ accessedPath,
589
+ alreadyLoaded: this._loadedFiles,
590
+ });
591
+ const fresh = found.filter((f) => !this._loadedDirs.has(f.dir));
592
+ for (const f of fresh) {
593
+ this._loadedDirs.add(f.dir);
594
+ this._loadedFiles.add(f.path);
595
+ }
596
+ return fresh;
597
+ } catch {
598
+ return [];
599
+ }
600
+ }
601
+
602
+ /** Absolute paths injected so far (startup set + every onAccess discovery). */
603
+ loadedFiles() {
604
+ return [...this._loadedFiles];
605
+ }
606
+ }
607
+
480
608
  export const _deps = { fs: fsDefault, path: pathDefault, os: osDefault };
@@ -23,6 +23,10 @@
23
23
 
24
24
  import { randomBytes } from "crypto";
25
25
  import { WsRpcClient } from "./ws-rpc-client.js";
26
+ import {
27
+ approvalFingerprintOk,
28
+ operationFingerprint,
29
+ } from "./operation-fingerprint.js";
26
30
  import {
27
31
  buildDirectPairingUri,
28
32
  pickLanAddress,
@@ -113,6 +117,15 @@ export class RemoteApprovalBridge {
113
117
  const requestId = message.event.requestId || message.event.approvalId;
114
118
  const pending = requestId ? this._pending.get(requestId) : null;
115
119
  if (!pending) return;
120
+ // Confused-deputy guard: a resolve that carries an operation fingerprint
121
+ // must match the operation this ask published. A mismatch (stale card /
122
+ // replayed / swapped body) is rejected — the ask stays pending and fails
123
+ // closed on timeout. Absent fingerprint = legacy device, accepted.
124
+ if (
125
+ !approvalFingerprintOk(pending.fingerprint, message.event.fingerprint)
126
+ ) {
127
+ return;
128
+ }
116
129
  this._pending.delete(requestId);
117
130
  clearTimeout(pending.timer);
118
131
  const answer = message.event.answer ?? message.event.approved;
@@ -150,6 +163,7 @@ export class RemoteApprovalBridge {
150
163
  onRequestId = null,
151
164
  } = {}) {
152
165
  const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
166
+ const fingerprint = operationFingerprint({ tool, action, detail });
153
167
  if (onRequestId) {
154
168
  try {
155
169
  onRequestId(requestId);
@@ -171,6 +185,7 @@ export class RemoteApprovalBridge {
171
185
  if (typeof timer.unref === "function") timer.unref();
172
186
  this._pending.set(requestId, {
173
187
  timer,
188
+ fingerprint,
174
189
  resolve: (decision) => {
175
190
  this._publish({
176
191
  type: "permission.resolved",
@@ -187,6 +202,7 @@ export class RemoteApprovalBridge {
187
202
  tool: tool || null,
188
203
  action,
189
204
  detail,
205
+ fingerprint,
190
206
  askedAt: this._now(),
191
207
  });
192
208
  });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * REPL `/goal <condition>` — a SESSION completion condition for interactive use.
3
+ *
4
+ * Three distinct "goal" concepts, kept separate on purpose:
5
+ * - `cc goal` → long-term, cross-session OKR (goal-store.js).
6
+ * - headless --goal-condition → AUTO re-drives outer turns until met/exhausted.
7
+ * - REPL `/goal <cond>` → THIS: a human is present each turn, so it
8
+ * EVALUATES + REPORTS after every turn whether the condition is met. It does
9
+ * NOT auto-submit follow-ups (that autonomous loop belongs to headless, where
10
+ * no human is watching) — the user decides whether to keep going.
11
+ *
12
+ * This is the pure, clock/dep-injected core over the already-tested
13
+ * goal-condition-engine.js; the agent-repl wiring is thin display + eval glue.
14
+ */
15
+
16
+ import {
17
+ isDeterministicCondition,
18
+ runDeterministicCheck,
19
+ GoalConditionEngine,
20
+ GOAL_DECISION,
21
+ } from "./goal-condition-engine.js";
22
+
23
+ // Interactive sessions are human-paced, so the turn-count budget is generous —
24
+ // it exists only as a backstop (the engine hard-caps at 100 regardless). The
25
+ // meaningful signal in the REPL is met / not-yet-met, not budget exhaustion.
26
+ export const REPL_GOAL_MAX_OUTER_TURNS = 100;
27
+
28
+ /** Parse the `/goal` argument into an intent. Empty → status; a keyword → clear. */
29
+ export function parseGoalCommand(arg) {
30
+ const s = String(arg ?? "").trim();
31
+ if (!s) return { action: "status" };
32
+ if (s === "clear" || s === "off" || s === "stop" || s === "drop")
33
+ return { action: "clear" };
34
+ return { action: "set", spec: s };
35
+ }
36
+
37
+ /**
38
+ * Create the REPL's session goal engine for a spec. Throws (via
39
+ * parseGoalCondition) on a malformed spec so the caller can report it.
40
+ */
41
+ export function createReplGoal(spec, { now = () => 0 } = {}) {
42
+ return new GoalConditionEngine({
43
+ condition: spec,
44
+ budget: { maxOuterTurns: REPL_GOAL_MAX_OUTER_TURNS },
45
+ now,
46
+ });
47
+ }
48
+
49
+ /** Display lines shown when a goal is set. */
50
+ export function renderGoalStart(engine) {
51
+ return [
52
+ `◎ goal set: ${engine.condition.source}`,
53
+ ` evaluated after each turn (≤ ${engine.budget.maxOuterTurns} turns); /goal clear to drop, /goal to check`,
54
+ ];
55
+ }
56
+
57
+ /** Display lines for `/goal` with no argument (status). */
58
+ export function renderGoalStatus(engine) {
59
+ if (!engine)
60
+ return [
61
+ "No session goal. Set one with /goal <condition>",
62
+ " e.g. /goal exit-zero:npm test · /goal file-exists:dist/app.js · /goal contains:ALL GREEN · /goal the tests pass",
63
+ ];
64
+ const s = engine.state;
65
+ return [
66
+ `◎ goal: ${engine.condition.source}`,
67
+ ` progress: outer turn ${s.outerTurns}/${engine.budget.maxOuterTurns}` +
68
+ (isDeterministicCondition(engine.condition)
69
+ ? ` (${engine.condition.kind} check)`
70
+ : " (model-judged)"),
71
+ ];
72
+ }
73
+
74
+ /** Verdict display lines for one evaluation's decision + events. */
75
+ export function renderGoalVerdict(decision, events) {
76
+ const evaluated = (events || []).find((e) => e.type === "goal_evaluated");
77
+ if (decision === GOAL_DECISION.COMPLETE) {
78
+ const done = (events || []).find((e) => e.type === "goal_completed");
79
+ return [
80
+ `✔ goal met: ${done?.reason ?? evaluated?.reason ?? "condition met"}`,
81
+ ];
82
+ }
83
+ if (decision === GOAL_DECISION.EXHAUSTED) {
84
+ const ex = (events || []).find((e) => e.type === "goal_exhausted");
85
+ return [
86
+ `⛔ goal dropped — ${ex?.reason ?? "budget exhausted"}${ex?.limit ? ` (${ex.limit})` : ""}`,
87
+ ];
88
+ }
89
+ return [
90
+ `◎ goal not yet met: ${evaluated?.reason ?? "unmet"} · outer turn ${evaluated?.outerTurns ?? "?"}`,
91
+ ];
92
+ }
93
+
94
+ /**
95
+ * Evaluate the session goal against a completed turn's final text. Deterministic
96
+ * conditions run inline (contains/regex on the text; exit-zero/file-exists via
97
+ * injected `spawnSync`/`existsSync`); a model condition delegates to `judge`.
98
+ *
99
+ * @param {GoalConditionEngine} engine
100
+ * @param {string} finalText the assistant's answer this turn
101
+ * @param {object} deps { cwd, spawnSync, existsSync, judge, tokens, costUsd }
102
+ * judge(condition, {finalText}) => {met, reason, evidence}
103
+ * @returns {Promise<{decision, events, done, met}>}
104
+ */
105
+ export async function evaluateReplGoalTurn(engine, finalText, deps = {}) {
106
+ engine.recordTurnUsage({ tokens: deps.tokens, costUsd: deps.costUsd });
107
+ const cond = engine.condition;
108
+ let evaluation;
109
+ if (isDeterministicCondition(cond)) {
110
+ evaluation = runDeterministicCheck(cond, {
111
+ lastOutput: finalText,
112
+ cwd: deps.cwd,
113
+ spawnSync: deps.spawnSync,
114
+ existsSync: deps.existsSync,
115
+ });
116
+ } else if (typeof deps.judge === "function") {
117
+ evaluation = await deps.judge(cond, { finalText });
118
+ } else {
119
+ evaluation = {
120
+ met: false,
121
+ reason: "no model judge available for this condition",
122
+ evidence: { kind: "model" },
123
+ };
124
+ }
125
+ const { decision, events } = engine.evaluate(evaluation);
126
+ return {
127
+ decision,
128
+ events,
129
+ done: engine.done,
130
+ met: decision === GOAL_DECISION.COMPLETE,
131
+ };
132
+ }