chainlesschain 0.162.160 → 0.162.161

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 (179) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-DMn528q-.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-D9ZcDCJL.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-A_K-8PX0.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-D4pFW3X6.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-Dz46COAd.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-cbMjbyg0.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-Dtf88GAj.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-C5KRxn52.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-Dd2UK3BN.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-lptKTFdG.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-_yspG6rG.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-BGq_GAha.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-CePor71g.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-CLOWhIT7.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-CivJt2vT.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-Btmo1GGE.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Ct35OlG4.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-CTc2GmhB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-ZtiBETk1.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-BNqMQQXw.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-BmzZd-9V.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-CMVgvP2M.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-CcqQFAfT.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-CiKOOzlb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Wk-OT0gr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-D7xRjPjZ.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-0ZvCPSD9.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-CmVuKkPb.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CTWE3Jwg.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-CBEIF00L.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-DLhxDwVG.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BgsCBrnc.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-ClBUGDS2.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-CnG_ZnYw.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-BUOAbR1Z.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Dnt_9-WB.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-CLjt4kCD.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-f24U9Aes.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-GrmGQbSq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-DADaWn-n.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-xtf5RX6C.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-Cd27Rgtx.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-fXRxlDjl.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-joCFlNB0.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-B1HoURq0.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-DiwsKbT7.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-C23qi8kl.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BS1syX2f.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DRYwwDyh.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BQKaDHPH.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-BC6F9xiG.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-ClFPc1Nd.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-SS9uZKCl.js → Projects-Dn-f5aYe.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-CLWuOUNt.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-DiqULTxb.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-9A03P-E_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-CJZAnbq_.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C9A1R77h.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BIZc6Rky.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-DfIPNtuG.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-C5znFcQ1.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DSs2z3OX.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-CV6LcrLY.js} +3 -3
  69. package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-cmhoX0Xj.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-tCsg2po7.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-BK1KUDiy.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-yRgyeA82.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-c1mo1mlp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-koIAfuT3.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-DkkreAaB.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-fy5Ctf9M.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-Bw70Jf6S.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-BKh6Rx-4.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-Dg54WP0c.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-BvvD5SxJ.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-B7Ug9x4e.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-BjvuMORY.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-CZqhkx8p.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-ClR50lhX.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-DlfppYjx.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-BirgVr8B.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-B8DP_bax.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-DA8AUZzL.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-C38pLGp1.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-81rtr98o.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-D34ixq8t.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-D4bzqTrZ.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-9qoVpPt6.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-B0QArPQt.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-B8mkLM4x.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-BIP62a6r.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-BP-zkXK8.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-BS3M5H9V.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BXd2_LmH.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-BlxEphl7.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-BqIhLAbA.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-BtGxuQrO.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-BvD7b2IK.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BvL_3J_n.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BeBv896m.js → index-COe_cOLJ.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-CP5alL4t.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
  109. package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-CbhE3j5j.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-Cd3KuNK5.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-CoVgZw_b.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-CuS-UPoj.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-D0Ih7Hwn.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-D3zCCZRS.js} +3 -3
  115. package/src/assets/web-panel/assets/{index-BdyokEau.js → index-D6GfvEQG.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-DBFSo1NF.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-DFHa1HIg.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-DFyWKOBD.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-DMgnRwrK.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DUQLXLQM.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-D_2VYPr1.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DakfFZ-k.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-Ddk0e_Pi.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-DkHqMnn3.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-Dmo3dFHb.js} +1 -1
  126. package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
  127. package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DvfCEJG-.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-E1OJRXs4.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-F76w1NiL.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-ZRJRItm0.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-fI5sXMLg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-tKiC8T-4.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-ByyvflIR.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-B3FQm2By.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-BSfns6Ef.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-CTG1Ieri.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-DObM_2j6.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-DOfelQ1t.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-CHZILds_.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-REbucKSx.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-C6EZzD7o.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BxkWS-g3.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-Chklyoq6.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DV24ik_J.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-By-v5GlF.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-B1w9Am3r.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +15 -1
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +16 -2
  153. package/src/data/changelog.json +7 -2
  154. package/src/gateways/ws/background-agent-protocol.js +88 -14
  155. package/src/lib/agent-authority.js +251 -0
  156. package/src/lib/background-agent-phase.js +100 -0
  157. package/src/lib/background-agent-supervisor.js +288 -8
  158. package/src/lib/backpressure-policy.js +105 -0
  159. package/src/lib/capability-negotiation.js +212 -0
  160. package/src/lib/credential-guard.js +11 -0
  161. package/src/lib/credential-proxy.js +182 -0
  162. package/src/lib/event-seq-replay.js +167 -0
  163. package/src/lib/goal-condition-engine.js +369 -0
  164. package/src/lib/headless-manifest.js +13 -0
  165. package/src/lib/ide-context.js +53 -14
  166. package/src/lib/project-instructions.js +98 -1
  167. package/src/lib/remote-path-mapping.js +229 -0
  168. package/src/lib/schedule-planner.js +136 -0
  169. package/src/repl/bg-dashboard.js +13 -12
  170. package/src/runtime/agent-core.js +11 -4
  171. package/src/runtime/headless-stream.js +195 -11
  172. package/src/runtime/system-prompt.js +3 -0
  173. package/src/workers/background-agent-worker.js +37 -2
  174. package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +0 -1
  175. package/src/assets/web-panel/assets/MarkdownRenderer-BqaPmC0W.js +0 -1
  176. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
  177. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
  178. package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
  179. package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Session-level completion-condition engine (P1 §"会话级完成条件引擎").
3
+ *
4
+ * Distinct from `cc goal` (long-term, cross-session OKR progress) and from
5
+ * `cc loop` (a timer that re-runs ONE command): this drives the agent's OUTER
6
+ * turns. After each outer turn an independent evaluator judges a completion
7
+ * CONDITION; if unmet, the runner starts the next outer turn — until the
8
+ * condition is met (`completed`) or a budget is exhausted (`exhausted`).
9
+ *
10
+ * The engine is a pure reducer over usage + per-turn evaluations, so it is
11
+ * deterministically testable without a model or the real agent loop. It:
12
+ * - supports model-judged conditions AND deterministic checkers
13
+ * (exit-zero / file-exists / contains / regex),
14
+ * - enforces max_outer_turns and token / cost / time budgets,
15
+ * - emits the SDK event taxonomy (goal_started / goal_evaluated /
16
+ * goal_completed / goal_exhausted) with structured reason, evidence and
17
+ * turn/token/cost/time, and
18
+ * - snapshots/restores so a condition survives `--resume`.
19
+ *
20
+ * The clock is injected (`now`) and NEVER read implicitly, so snapshots replay
21
+ * identically and tests are deterministic.
22
+ */
23
+
24
+ /** SDK event taxonomy for the completion-condition loop. */
25
+ export const GOAL_EVENTS = Object.freeze({
26
+ STARTED: "goal_started",
27
+ EVALUATED: "goal_evaluated",
28
+ COMPLETED: "goal_completed",
29
+ EXHAUSTED: "goal_exhausted",
30
+ });
31
+
32
+ /** Terminal decisions the reducer can return alongside `continue`. */
33
+ export const GOAL_DECISION = Object.freeze({
34
+ CONTINUE: "continue",
35
+ COMPLETE: "complete",
36
+ EXHAUSTED: "exhausted",
37
+ });
38
+
39
+ /** Default budget — a bounded outer-turn count; other axes unbounded (null). */
40
+ export const DEFAULT_GOAL_BUDGET = Object.freeze({
41
+ maxOuterTurns: 10,
42
+ maxTokens: null,
43
+ maxCostUsd: null,
44
+ maxTimeMs: null,
45
+ });
46
+
47
+ // A hard ceiling so a misconfigured condition can never spin forever.
48
+ const OUTER_TURN_HARD_CAP = 100;
49
+
50
+ function positiveIntOrNull(value, cap) {
51
+ const n = Number(value);
52
+ if (!Number.isFinite(n) || n <= 0) return null;
53
+ const floored = Math.floor(n);
54
+ return cap ? Math.min(floored, cap) : floored;
55
+ }
56
+
57
+ function positiveNumOrNull(value) {
58
+ const n = Number(value);
59
+ return Number.isFinite(n) && n > 0 ? n : null;
60
+ }
61
+
62
+ /** Normalize a budget: sane defaults, positive values, hard outer-turn cap. */
63
+ export function normalizeGoalBudget(input = {}) {
64
+ const maxOuterTurns =
65
+ positiveIntOrNull(input.maxOuterTurns, OUTER_TURN_HARD_CAP) ??
66
+ DEFAULT_GOAL_BUDGET.maxOuterTurns;
67
+ return {
68
+ maxOuterTurns,
69
+ maxTokens: positiveIntOrNull(input.maxTokens),
70
+ maxCostUsd: positiveNumOrNull(input.maxCostUsd),
71
+ maxTimeMs: positiveIntOrNull(input.maxTimeMs),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Parse a `--goal-condition` spec into a condition descriptor. A `kind:` prefix
77
+ * selects a deterministic checker; anything else (or an explicit `model:`) is
78
+ * model-judged natural language.
79
+ *
80
+ * exit-zero:npm test → run the command, met iff exit 0
81
+ * file-exists:dist/app.js → met iff the path exists
82
+ * contains:ALL GREEN → met iff the last output contains the text
83
+ * regex:^BUILD OK$ → met iff the last output matches
84
+ * model:the tests pass → a model judges the transcript
85
+ * the tests pass → (bare) model-judged
86
+ */
87
+ export function parseGoalCondition(spec) {
88
+ const raw = String(spec ?? "").trim();
89
+ if (!raw) throw new Error("goal condition is required");
90
+ const idx = raw.indexOf(":");
91
+ const prefix = idx === -1 ? "" : raw.slice(0, idx).trim().toLowerCase();
92
+ const rest = idx === -1 ? "" : raw.slice(idx + 1).trim();
93
+ switch (prefix) {
94
+ case "exit-zero":
95
+ case "exit0":
96
+ if (!rest) throw new Error("exit-zero condition needs a command");
97
+ return { kind: "exit-zero", command: rest, source: raw };
98
+ case "file-exists":
99
+ case "file":
100
+ if (!rest) throw new Error("file-exists condition needs a path");
101
+ return { kind: "file-exists", path: rest, source: raw };
102
+ case "contains":
103
+ if (!rest) throw new Error("contains condition needs text");
104
+ return { kind: "contains", text: rest, source: raw };
105
+ case "regex":
106
+ if (!rest) throw new Error("regex condition needs a pattern");
107
+ return { kind: "regex", pattern: rest, source: raw };
108
+ case "model":
109
+ if (!rest) throw new Error("model condition needs a description");
110
+ return { kind: "model", text: rest, source: raw };
111
+ default:
112
+ return { kind: "model", text: raw, source: raw };
113
+ }
114
+ }
115
+
116
+ /** True when a condition is a deterministic checker (not model-judged). */
117
+ export function isDeterministicCondition(condition) {
118
+ return Boolean(condition) && condition.kind !== "model";
119
+ }
120
+
121
+ /**
122
+ * Evaluate a deterministic condition against gathered evidence — PURE.
123
+ * `evidence` supplies `output` (last turn text), `exitCode`, and a
124
+ * `fileExists(path) => boolean` predicate. Returns `{met, reason, evidence}`.
125
+ */
126
+ export function checkDeterministicCondition(condition, evidence = {}) {
127
+ const output = String(evidence.output ?? "");
128
+ switch (condition?.kind) {
129
+ case "contains": {
130
+ const met = output.includes(condition.text);
131
+ return {
132
+ met,
133
+ reason: met
134
+ ? `output contains "${condition.text}"`
135
+ : `output does not contain "${condition.text}"`,
136
+ evidence: { kind: "contains", matched: met },
137
+ };
138
+ }
139
+ case "regex": {
140
+ let re;
141
+ try {
142
+ re = new RegExp(condition.pattern, condition.flags || "m");
143
+ } catch (err) {
144
+ return {
145
+ met: false,
146
+ reason: `invalid regex: ${err.message}`,
147
+ evidence: { kind: "regex", error: true },
148
+ };
149
+ }
150
+ const met = re.test(output);
151
+ return {
152
+ met,
153
+ reason: met
154
+ ? `output matches /${condition.pattern}/`
155
+ : `output does not match /${condition.pattern}/`,
156
+ evidence: { kind: "regex", matched: met },
157
+ };
158
+ }
159
+ case "exit-zero": {
160
+ const met = Number(evidence.exitCode) === 0;
161
+ return {
162
+ met,
163
+ reason: `\`${condition.command}\` exited ${evidence.exitCode}`,
164
+ evidence: { kind: "exit-zero", exitCode: evidence.exitCode ?? null },
165
+ };
166
+ }
167
+ case "file-exists": {
168
+ const fx = typeof evidence.fileExists === "function";
169
+ const met = fx ? evidence.fileExists(condition.path) === true : false;
170
+ return {
171
+ met,
172
+ reason: met
173
+ ? `${condition.path} exists`
174
+ : `${condition.path} does not exist`,
175
+ evidence: { kind: "file-exists", path: condition.path, met },
176
+ };
177
+ }
178
+ default:
179
+ return {
180
+ met: false,
181
+ reason: `not a deterministic condition (${condition?.kind})`,
182
+ evidence: { kind: condition?.kind ?? "unknown" },
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Gather evidence and run a deterministic check. Deps (`spawnSync`,
189
+ * `existsSync`) are injected for tests; `lastOutput` feeds contains/regex.
190
+ */
191
+ export function runDeterministicCheck(condition, deps = {}) {
192
+ const evidence = { output: deps.lastOutput ?? "" };
193
+ if (condition.kind === "exit-zero") {
194
+ const spawnSync = deps.spawnSync;
195
+ if (typeof spawnSync !== "function") {
196
+ return {
197
+ met: false,
198
+ reason: "no spawnSync available for exit-zero",
199
+ evidence: { kind: "exit-zero", error: true },
200
+ };
201
+ }
202
+ const res = spawnSync(condition.command, {
203
+ cwd: deps.cwd,
204
+ shell: true,
205
+ encoding: "utf8",
206
+ windowsHide: true,
207
+ });
208
+ evidence.exitCode = res?.status ?? (res?.error ? 1 : null);
209
+ } else if (condition.kind === "file-exists") {
210
+ const existsSync = deps.existsSync;
211
+ evidence.fileExists = (p) =>
212
+ typeof existsSync === "function" ? existsSync(p) === true : false;
213
+ }
214
+ return checkDeterministicCondition(condition, evidence);
215
+ }
216
+
217
+ function usageSnapshot(state, nowMs) {
218
+ return {
219
+ outerTurns: state.outerTurns,
220
+ tokens: state.tokens,
221
+ costUsd: state.costUsd,
222
+ elapsedMs: Math.max(0, nowMs - state.startedAtMs),
223
+ };
224
+ }
225
+
226
+ function whichLimitHit(state, budget, nowMs) {
227
+ if (budget.maxOuterTurns != null && state.outerTurns >= budget.maxOuterTurns)
228
+ return "max_outer_turns";
229
+ if (budget.maxTokens != null && state.tokens >= budget.maxTokens)
230
+ return "max_tokens";
231
+ if (budget.maxCostUsd != null && state.costUsd >= budget.maxCostUsd)
232
+ return "max_cost";
233
+ if (
234
+ budget.maxTimeMs != null &&
235
+ Math.max(0, nowMs - state.startedAtMs) >= budget.maxTimeMs
236
+ )
237
+ return "max_time";
238
+ return null;
239
+ }
240
+
241
+ /**
242
+ * The pure decision step. Given the accumulated `state`, this turn's
243
+ * `evaluation` ({met, reason, evidence}), the `budget`, and the current time,
244
+ * decide `complete | continue | exhausted` and produce events. Always emits a
245
+ * `goal_evaluated`; adds a terminal `goal_completed` / `goal_exhausted` when
246
+ * the loop stops. Does NOT mutate `state` — returns the next one.
247
+ */
248
+ export function evaluateGoalStep(state, evaluation, budget, opts = {}) {
249
+ const nowMs = Number(opts.now ?? state.startedAtMs);
250
+ const next = {
251
+ ...state,
252
+ outerTurns: state.outerTurns + 1,
253
+ };
254
+ const usage = usageSnapshot(next, nowMs);
255
+ const met = evaluation?.met === true;
256
+ const evaluated = {
257
+ type: GOAL_EVENTS.EVALUATED,
258
+ met,
259
+ reason: evaluation?.reason ?? null,
260
+ evidence: evaluation?.evidence ?? null,
261
+ ...usage,
262
+ };
263
+ if (met) {
264
+ return {
265
+ state: { ...next, done: true, outcome: "completed" },
266
+ decision: GOAL_DECISION.COMPLETE,
267
+ events: [
268
+ evaluated,
269
+ {
270
+ type: GOAL_EVENTS.COMPLETED,
271
+ reason: evaluation?.reason ?? "condition met",
272
+ evidence: evaluation?.evidence ?? null,
273
+ ...usage,
274
+ },
275
+ ],
276
+ };
277
+ }
278
+ const limit = whichLimitHit(next, budget, nowMs);
279
+ if (limit) {
280
+ return {
281
+ state: { ...next, done: true, outcome: "exhausted", limit },
282
+ decision: GOAL_DECISION.EXHAUSTED,
283
+ events: [
284
+ evaluated,
285
+ {
286
+ type: GOAL_EVENTS.EXHAUSTED,
287
+ limit,
288
+ reason: `budget exhausted (${limit})`,
289
+ ...usage,
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ return { state: next, decision: GOAL_DECISION.CONTINUE, events: [evaluated] };
295
+ }
296
+
297
+ /**
298
+ * Stateful wrapper over the reducer for the runner. Inject `now` (a `() => ms`
299
+ * clock) so behavior is deterministic and snapshot-replayable.
300
+ */
301
+ export class GoalConditionEngine {
302
+ constructor({ condition, budget, now } = {}) {
303
+ this.condition =
304
+ typeof condition === "string" ? parseGoalCondition(condition) : condition;
305
+ this.budget = normalizeGoalBudget(budget);
306
+ this._now = typeof now === "function" ? now : () => 0;
307
+ this.state = {
308
+ outerTurns: 0,
309
+ tokens: 0,
310
+ costUsd: 0,
311
+ startedAtMs: this._now(),
312
+ done: false,
313
+ outcome: null,
314
+ };
315
+ }
316
+
317
+ /** Emit the opening event; call once before the first outer turn. */
318
+ start() {
319
+ return {
320
+ type: GOAL_EVENTS.STARTED,
321
+ condition: this.condition,
322
+ budget: this.budget,
323
+ startedAtMs: this.state.startedAtMs,
324
+ };
325
+ }
326
+
327
+ /** Accumulate a turn's usage before evaluating it. */
328
+ recordTurnUsage({ tokens = 0, costUsd = 0 } = {}) {
329
+ this.state = {
330
+ ...this.state,
331
+ tokens: this.state.tokens + (Number(tokens) || 0),
332
+ costUsd: this.state.costUsd + (Number(costUsd) || 0),
333
+ };
334
+ return this.state;
335
+ }
336
+
337
+ /** Evaluate this outer turn; returns `{decision, events}`. */
338
+ evaluate(evaluation) {
339
+ const result = evaluateGoalStep(this.state, evaluation, this.budget, {
340
+ now: this._now(),
341
+ });
342
+ this.state = result.state;
343
+ return { decision: result.decision, events: result.events };
344
+ }
345
+
346
+ get done() {
347
+ return this.state.done === true;
348
+ }
349
+
350
+ /** Serialize for `--resume`. startedAtMs is preserved so time keeps counting. */
351
+ snapshot() {
352
+ return {
353
+ condition: this.condition,
354
+ budget: this.budget,
355
+ state: { ...this.state },
356
+ };
357
+ }
358
+
359
+ /** Restore a prior run (same clock injection contract). */
360
+ static fromSnapshot(snap, { now } = {}) {
361
+ const engine = new GoalConditionEngine({
362
+ condition: snap.condition,
363
+ budget: snap.budget,
364
+ now,
365
+ });
366
+ engine.state = { ...snap.state };
367
+ return engine;
368
+ }
369
+ }
@@ -16,6 +16,7 @@
16
16
 
17
17
  import crypto from "node:crypto";
18
18
  import { createRequire } from "node:module";
19
+ import { PROTOCOL_MIN_VERSION } from "./capability-negotiation.js";
19
20
 
20
21
  export const STREAM_PROTOCOL_VERSION = 1;
21
22
 
@@ -137,6 +138,10 @@ export function buildAgentCapabilities() {
137
138
  name: "chainlesschain-cli",
138
139
  version,
139
140
  protocol_version: STREAM_PROTOCOL_VERSION,
141
+ // Oldest wire version this CLI can still negotiate down to (the N-1 in the
142
+ // capability handshake, agent-sdk docs/PROTOCOL.md §1.3). At v1 there is no
143
+ // older line shape, so min === current.
144
+ min_protocol_version: PROTOCOL_MIN_VERSION,
140
145
  agent_tools: contract.listCodingAgentToolNames(),
141
146
  permission_modes: [
142
147
  "default",
@@ -162,6 +167,14 @@ export function buildAgentCapabilities() {
162
167
  interactive_approvals: true,
163
168
  permission_prompt_tool: true,
164
169
  remote_control: true,
170
+ // Additive protocol-v1 stream fields (agent-sdk docs/PROTOCOL.md §1.2.1):
171
+ // every stream-json stdout line carries a monotonic `seq` and a
172
+ // run-scoped `trace_id`, and tool_use/tool_result lines carry a pairing
173
+ // `id` ("tu-<n>"). `trace_id` is caller-injectable (--trace-id /
174
+ // CC_TRACE_ID) for end-to-end IDE ↔ CLI correlation.
175
+ tool_use_id: true,
176
+ event_seq: true,
177
+ trace_id: true,
165
178
  mcp: {
166
179
  config_file: true,
167
180
  registry: true,
@@ -22,6 +22,7 @@ import {
22
22
  filterIdeFileEntry,
23
23
  redactSecretsInText,
24
24
  } from "./ide-context-redaction.js";
25
+ import { normalizeIdePathForCli, detectWsl } from "./remote-path-mapping.js";
25
26
 
26
27
  /** Hard cap on the selected text we inline into the prompt. */
27
28
  const SELECTION_TEXT_CAP = 2000;
@@ -95,24 +96,55 @@ export function parseToolResultJson(result) {
95
96
  // through read_file), so credential files and secret-shaped text are filtered
96
97
  // HERE, at the collection boundary, before anything reaches the prompt.
97
98
  // CC_IDE_CONTEXT_REDACTION=0 disables (the filters below become pass-through).
99
+ //
100
+ // The same boundary also folds a remote file representation (a vscode-remote://
101
+ // URI, a \\wsl.localhost\… UNC, or a Windows drive path while cc runs in WSL)
102
+ // into the path THIS host can open (see [[remote-path-mapping.js]]) — otherwise
103
+ // the agent, reading the inlined path, would target a file cc can't see. The
104
+ // credential filter runs on the ORIGINAL value (basename is preserved by the
105
+ // map, so the verdict is unchanged). CC_IDE_PATH_MAP=0 disables it.
106
+
107
+ /** Env gate: CC_IDE_PATH_MAP=0|false|off disables remote path normalization. */
108
+ export function idePathMapEnabled(env = process.env) {
109
+ const v = String(env?.CC_IDE_PATH_MAP ?? "").toLowerCase();
110
+ return !(v === "0" || v === "false" || v === "off");
111
+ }
112
+
113
+ /** Fold one IDE-reported file value into a path cc can open on this host. */
114
+ function mapIdeFile(file, env) {
115
+ if (typeof file !== "string" || !idePathMapEnabled(env)) return file;
116
+ return normalizeIdePathForCli(file, {
117
+ platform: process.platform,
118
+ wsl: detectWsl(env || {}),
119
+ });
120
+ }
98
121
 
99
122
  /** Drop the selection when it lives in a credential file; scrub its text. */
100
123
  function sanitizeIdeSelection(sel, env) {
101
124
  if (!sel) return sel;
102
125
  if (sel.file && filterIdeFileEntry(sel.file, { env }) === null) return null;
103
- if (typeof sel.text === "string" && sel.text.length > 0) {
104
- const scrubbed = redactSecretsInText(sel.text, { env });
105
- if (scrubbed !== sel.text) return { ...sel, text: scrubbed };
126
+ let out = sel;
127
+ const mappedFile = mapIdeFile(sel.file, env);
128
+ if (mappedFile !== sel.file) out = { ...out, file: mappedFile };
129
+ if (typeof out.text === "string" && out.text.length > 0) {
130
+ const scrubbed = redactSecretsInText(out.text, { env });
131
+ if (scrubbed !== out.text) out = { ...out, text: scrubbed };
106
132
  }
107
- return sel;
133
+ return out;
108
134
  }
109
135
 
110
- /** Drop open-editor entries whose file is a credential file. */
136
+ /** Drop open-editor entries whose file is a credential file; map the rest. */
111
137
  function sanitizeIdeEditors(editors, env) {
112
138
  if (!Array.isArray(editors)) return editors;
113
- return editors.filter(
114
- (e) => !(e && e.file && filterIdeFileEntry(e.file, { env }) === null),
115
- );
139
+ return editors
140
+ .filter(
141
+ (e) => !(e && e.file && filterIdeFileEntry(e.file, { env }) === null),
142
+ )
143
+ .map((e) => {
144
+ if (!e || typeof e.file !== "string") return e;
145
+ const mapped = mapIdeFile(e.file, env);
146
+ return mapped === e.file ? e : { ...e, file: mapped };
147
+ });
116
148
  }
117
149
 
118
150
  /** Scrub secret-shaped text out of terminal commands and their output. */
@@ -131,18 +163,25 @@ function sanitizeIdeTerminals(terms, env) {
131
163
  });
132
164
  }
133
165
 
134
- /** Drop diagnostics for credential files; scrub their message text. */
166
+ /** Drop diagnostics for credential files; map their file + scrub message text. */
135
167
  function sanitizeIdeDiagnostics(diags, env) {
136
168
  if (!Array.isArray(diags)) return diags;
137
169
  return diags
138
170
  .filter(
139
171
  (d) => !(d && d.file && filterIdeFileEntry(d.file, { env }) === null),
140
172
  )
141
- .map((d) =>
142
- d && typeof d.message === "string"
143
- ? { ...d, message: redactSecretsInText(d.message, { env }) }
144
- : d,
145
- );
173
+ .map((d) => {
174
+ if (!d) return d;
175
+ let out = d;
176
+ if (typeof d.file === "string") {
177
+ const mapped = mapIdeFile(d.file, env);
178
+ if (mapped !== d.file) out = { ...out, file: mapped };
179
+ }
180
+ if (typeof out.message === "string") {
181
+ out = { ...out, message: redactSecretsInText(out.message, { env }) };
182
+ }
183
+ return out;
184
+ });
146
185
  }
147
186
 
148
187
  /** Resolve to the promise's value, or null after `ms` / on rejection. */
@@ -44,6 +44,83 @@ export const PROJECT_FILE_NAMES = ["cc.md", "CLAUDE.md", "AGENTS.md"];
44
44
  /** Local (gitignored) companion names, first match wins. */
45
45
  export const LOCAL_FILE_NAMES = ["cc.local.md", "CLAUDE.local.md"];
46
46
 
47
+ /**
48
+ * `instructionExcludes` — a large-monorepo reduction lever (P1). Subtrees whose
49
+ * instruction / rule / @import files should NEVER load (legacy, vendored,
50
+ * generated). Opt-in: empty by default so behavior is byte-identical unless the
51
+ * caller (config `instructionExcludes`) sets it. Patterns are matched against a
52
+ * path RELATIVE to the project root, with forward slashes:
53
+ * - a bare name (`node_modules`, `dist`) matches that segment ANYWHERE;
54
+ * - a slashed prefix (`packages/legacy`) matches that dir and everything under;
55
+ * - a glob (`**​/generated/**`, `vendor/*`) matches via `*`/`**`/`?`.
56
+ */
57
+ export function normalizeInstructionExcludes(list) {
58
+ if (!Array.isArray(list)) return [];
59
+ return [
60
+ ...new Set(
61
+ list
62
+ .map((s) =>
63
+ String(s || "")
64
+ .trim()
65
+ .replace(/\\/g, "/")
66
+ .replace(/\/+$/, ""),
67
+ )
68
+ .filter(Boolean),
69
+ ),
70
+ ];
71
+ }
72
+
73
+ function globToRegExp(glob) {
74
+ let re = "";
75
+ for (let i = 0; i < glob.length; i++) {
76
+ const c = glob[i];
77
+ if (c === "*") {
78
+ if (glob[i + 1] === "*") {
79
+ re += ".*";
80
+ i++;
81
+ if (glob[i + 1] === "/") i++; // `**/` also swallows the slash
82
+ } else {
83
+ re += "[^/]*";
84
+ }
85
+ } else if (c === "?") {
86
+ re += "[^/]";
87
+ } else {
88
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
89
+ }
90
+ }
91
+ return new RegExp("^" + re + "$");
92
+ }
93
+
94
+ /**
95
+ * True when a repo-relative path falls under an excluded subtree. A file is
96
+ * excluded when it, or any of its ancestor directories, matches a pattern.
97
+ */
98
+ export function pathIsExcluded(relPath, excludes) {
99
+ if (!excludes || excludes.length === 0) return false;
100
+ const rel = String(relPath || "")
101
+ .replace(/\\/g, "/")
102
+ .replace(/^\.\/?/, "")
103
+ .replace(/\/+$/, "");
104
+ if (!rel) return false;
105
+ const segs = rel.split("/");
106
+ for (const pat of excludes) {
107
+ if (/[*?[\]]/.test(pat)) {
108
+ const re = globToRegExp(pat);
109
+ if (re.test(rel)) return true;
110
+ let acc = "";
111
+ for (const s of segs) {
112
+ acc = acc ? `${acc}/${s}` : s;
113
+ if (re.test(acc)) return true;
114
+ }
115
+ } else {
116
+ if (rel === pat || rel.startsWith(`${pat}/`)) return true;
117
+ // a bare (slash-free) name matches that path segment anywhere
118
+ if (!pat.includes("/") && segs.includes(pat)) return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
47
124
  // Same boundary rule as file-ref-expander: `@` at start / after whitespace or
48
125
  // an opening bracket-quote, so emails and decorative @ never match.
49
126
  const IMPORT_TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
@@ -100,13 +177,21 @@ export function findInstructionFiles(opts = {}) {
100
177
  const { fs, path, os } = resolveDeps(opts);
101
178
  const cwd = path.resolve(opts.cwd || process.cwd());
102
179
  const home = opts.home || os.homedir() || "";
180
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
103
181
 
104
182
  const seen = new Set();
105
183
  const out = [];
184
+ const root = findProjectRoot(cwd, opts) || cwd;
185
+ // A discovered file under an excluded subtree (relative to the project root)
186
+ // never loads. The user-scope file lives outside the repo, so it is exempt.
187
+ const excluded = (abs) =>
188
+ excludes.length > 0 &&
189
+ pathIsExcluded(path.relative(root, path.resolve(abs)), excludes);
106
190
  const push = (p, scope) => {
107
191
  if (!p) return;
108
192
  const abs = path.resolve(p);
109
193
  if (seen.has(abs) || !isFile(fs, abs)) return;
194
+ if (scope !== "user" && excluded(abs)) return;
110
195
  seen.add(abs);
111
196
  out.push({ path: abs, scope });
112
197
  };
@@ -121,7 +206,6 @@ export function findInstructionFiles(opts = {}) {
121
206
  );
122
207
  }
123
208
 
124
- const root = findProjectRoot(cwd, opts) || cwd;
125
209
  const chain = [];
126
210
  let dir = cwd;
127
211
  for (;;) {
@@ -274,6 +358,10 @@ export function loadProjectInstructions(opts = {}) {
274
358
  : DEFAULT_MAX_TOTAL_BYTES;
275
359
 
276
360
  const roots = findInstructionFiles(opts);
361
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
362
+ const projectRoot =
363
+ findProjectRoot(path.resolve(opts.cwd || process.cwd()), opts) ||
364
+ path.resolve(opts.cwd || process.cwd());
277
365
  const visited = new Set(roots.map((r) => r.path));
278
366
  const out = [];
279
367
  const warnings = [];
@@ -322,6 +410,15 @@ export function loadProjectInstructions(opts = {}) {
322
410
  const resolved = path.resolve(baseDir, target);
323
411
  if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
324
412
  // non-files are decorative @tokens (npm scopes, emails), not imports.
413
+ // An @import that resolves under an excluded subtree does not load — the
414
+ // reduction lever must not be bypassed by an import pointing into it.
415
+ if (
416
+ excludes.length > 0 &&
417
+ pathIsExcluded(path.relative(projectRoot, resolved), excludes)
418
+ ) {
419
+ visited.add(resolved);
420
+ continue;
421
+ }
325
422
  // Refuse to import a credential / secret file. `cc agent` auto-loads
326
423
  // project memory, so a cloned/untrusted repo's cc.md must NOT be able to
327
424
  // pull `@~/.ssh/id_rsa` / `@../.env` into the LLM-bound system prompt