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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-DMn528q-.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-D9ZcDCJL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-A_K-8PX0.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-D4pFW3X6.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-Dz46COAd.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-cbMjbyg0.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-Dtf88GAj.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-C5KRxn52.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-Dd2UK3BN.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-lptKTFdG.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-_yspG6rG.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-BGq_GAha.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-CePor71g.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-CLOWhIT7.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-CivJt2vT.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-Btmo1GGE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Ct35OlG4.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-CTc2GmhB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-ZtiBETk1.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-BNqMQQXw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-BmzZd-9V.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-CMVgvP2M.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-CcqQFAfT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-CTqB1_4V.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-CiKOOzlb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Wk-OT0gr.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-D7xRjPjZ.js → GenericCardRenderer-DbEQT6ue.js} +1 -1
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-0ZvCPSD9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-CmVuKkPb.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CTWE3Jwg.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-CBEIF00L.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-DLhxDwVG.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BgsCBrnc.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-ClBUGDS2.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-CnG_ZnYw.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-BUOAbR1Z.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Dnt_9-WB.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-CLjt4kCD.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-f24U9Aes.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-GrmGQbSq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-DADaWn-n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-xtf5RX6C.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-Cd27Rgtx.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-fXRxlDjl.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-joCFlNB0.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-B1HoURq0.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-DiwsKbT7.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Xe-BsW6S.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-C23qi8kl.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BS1syX2f.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DRYwwDyh.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BQKaDHPH.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-BC6F9xiG.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-ClFPc1Nd.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-SS9uZKCl.js → Projects-Dn-f5aYe.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-CLWuOUNt.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-DiqULTxb.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-9A03P-E_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-CJZAnbq_.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C9A1R77h.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BIZc6Rky.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-DfIPNtuG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-C5znFcQ1.js} +2 -2
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DSs2z3OX.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-CV6LcrLY.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-cmhoX0Xj.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-tCsg2po7.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-BK1KUDiy.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-yRgyeA82.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-c1mo1mlp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-koIAfuT3.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-DkkreAaB.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-fy5Ctf9M.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-Bw70Jf6S.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-BKh6Rx-4.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-Dg54WP0c.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-BvvD5SxJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-B7Ug9x4e.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-BjvuMORY.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-CZqhkx8p.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-ClR50lhX.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-DlfppYjx.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-BirgVr8B.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-B8DP_bax.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-DA8AUZzL.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-C38pLGp1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-81rtr98o.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-D34ixq8t.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-D4bzqTrZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-9qoVpPt6.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-B0QArPQt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-B8mkLM4x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-BIP62a6r.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-BP-zkXK8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-BS3M5H9V.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BXd2_LmH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-BlxEphl7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-BqIhLAbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-BtGxuQrO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-BvD7b2IK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BvL_3J_n.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-COe_cOLJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-CP5alL4t.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1NURWZ.js +1 -0
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-CbhE3j5j.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-Cd3KuNK5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-CoVgZw_b.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-CuS-UPoj.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-D0Ih7Hwn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-D3zCCZRS.js} +3 -3
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-D6GfvEQG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-DBFSo1NF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-DFHa1HIg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-DFyWKOBD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-DMgnRwrK.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DUQLXLQM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-D_2VYPr1.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DakfFZ-k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-Ddk0e_Pi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-DkHqMnn3.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-Dmo3dFHb.js} +1 -1
- package/src/assets/web-panel/assets/index-DokUYqpq.js +1 -0
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DvfCEJG-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-E1OJRXs4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-F76w1NiL.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-ZRJRItm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-fI5sXMLg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-tKiC8T-4.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-ByyvflIR.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-B3FQm2By.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-BSfns6Ef.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-CTG1Ieri.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-DObM_2j6.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-DOfelQ1t.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-CHZILds_.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-REbucKSx.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-C6EZzD7o.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BxkWS-g3.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-Chklyoq6.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-CRVwKrrI.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DV24ik_J.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-C7vIo4Pg.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-By-v5GlF.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-B1w9Am3r.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/data/changelog.json +7 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +288 -8
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +11 -4
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BqaPmC0W.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- 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,
|
package/src/lib/ide-context.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
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;
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|