chainlesschain 0.162.167 → 0.162.169
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-BZJrchIA.js → AIOps-DCeNRK8z.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Bhlrylt7.js → ActionButton-ZfD0HgVe.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-pHtXVQYs.js → Analytics-Ccn40f9E.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-ORpuri4R.js → AppLayout-DyXs4nSt.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-eC77yr1_.js → Artifacts-CYjMh5KR.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B2dZIiqz.js → Audit-DZIhvx88.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-COZiTiwP.js → BackgroundAgents-C1XJsa2d.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-vY_Srl5d.js → Backup-CMXC0oUN.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Dili5BdM.js → BaseInput-CRVmIH1t.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-EzKBf0XL.js → Chat-acBud5Yj.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D1CtRAyy.js → ChatBubbleRenderer-CTjqFhdD.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Beh6BmuW.js → Checkbox-CZem825o.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C3fA6kBU.js → Codegen-B9UjZsww.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CefMqONH.js → Col-Dtu6Frtj.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CBAxFJtB.js → Community-9bV55Huv.js} +1 -1
- package/src/assets/web-panel/assets/{Compact--flHc-sM.js → Compact-g-kQ4c84.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CPeNLvA6.js → Compliance-CQjHzBO1.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bv5yv4BU.js → Cowork-D1Qm_Xhi.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-C22yMEas.js → Cron-992qo8zX.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dsb2bcpu.js → Crosschain-4TiqEwuc.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DAxei4w0.js → DID-BbsXzXCb.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DEUwuVZ8.js → Dashboard-Dx5aO9d3.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BVp4si2j.js → Dropdown-BhzW6i6i.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CyFkybOG.js → EmailListRenderer-qEib4yiq.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BWmmsbVx.js → FamilyGuardDashboard-BZ3N-u3h.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Cxw27A5d.js → Federation-CcOqwzHY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Q9XthzL1.js → FormItemContext-dbi8mohd.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DS5DIdsW.js → GenericCardRenderer-57dqulUe.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BOliHfxP.js → Git-DtSV7C5g.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CD4oxa-W.js → Governance-CLnmH-RD.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CMlcivo8.js → Inference-wXiLeDnk.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BwhERcC4.js → KnowledgeGraph-NGdIX2Gx.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BjVcZR5P.js → Logs-Dz0HQn69.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-CSRZ9da3.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DQLU3E2w.js → Marketplace-DpPDmY3W.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CeK0XkpB.js → McpTools-UMysoRQm.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-DLFkgwaz.js → Memory-DsnBpQnY.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bh6ymXyj.js → MobileBridge-DweooAfG.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-D950tUIe.js → MobileProjects-UdLfZOVA.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Bx5x9_2m.js → Mtc-oXWhJNgC.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-a1-Fexsq.js → MtcAudit-CXTD7qrm.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-eaw0X8X1.js → Multisig-BKt5mJ9O.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BaNsPV4k.js → NLProgramming-C-zcYzje.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-C7pevN2X.js → Notes-CGaZ_jep.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-HDqeoP0r.js → NotificationSettings-BwX-tDbq.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DvzVl5T3.js → OrderTableRenderer-Cc9Se8H-.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D_xHDlGW.js → Organization-8BXdC7Im.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-B5sRnueL.js → Overflow-zCisFuGU.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-C-IRjOPW.js → P2P-BehHH9Ro.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-DVuXYKAR.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-Bm_T1uWw.js → Permissions-BVDATm3p.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DujDllye.js → PersonalDataHub-C43nnYoi.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DyNPOLIG.js → Pipeline-D9E7aSVD.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CYKTW3iK.js → Privacy-CRbIchdL.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CGf03uZ0.js → ProjectInit-CTsy8lhL.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DIec4bTI.js → ProjectSettings-CPEKWWJ5.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BY43-bmi.js → Projects-CNnW0v5N.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-RrnUmgPk.js → Providers-BeZnDL9Y.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BBWfqd4y.js → QrScannerModal-CaydwuIU.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BsICSrFp.js → QuickAsk-qYWbu7kw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B8y0b4ys.js → Recommend-RV_ogHrM.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-0QgdB1Oe.js → RemoteSession-Mah1i41_.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DBJGM0YO.js → Reputation-xZQhGktc.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DtWQ5otv.js → Row-B6cVbPbJ.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-2zBxHd5H.js → RssFeed-C6hKjDPz.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BofliSkZ.js → Search-0jti_-Pe.js} +1 -1
- package/src/assets/web-panel/assets/{Security-ockRBBEL.js → Security-Cy48feGz.js} +3 -3
- package/src/assets/web-panel/assets/{Services-QyvG7Vd0.js → Services-BW3ObFxH.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DwbOndX3.js → Skeleton-CfrZRvAM.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-cHWa63sZ.js → Skills-yoccdk3t.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BHnTEydQ.js → Sla-AKBBdri6.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DE2hrrLb.js → SpeechSettings-DH87GbkH.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CZ9vnbJH.js → SyncSettings-DUuO-ZSw.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-ZTUfOzMZ.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DiR3ad4X.js → Templates-shPrQ4NE.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-NqcW3frV.js → Tenant-C_inPunR.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-iYeqRQZ0.js → Terminal-CvVtx6ie.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C5GOoxFz.js → TimelineRenderer-Bzp_T3Xc.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-82dz0XdU.js → Tokens-kUFRLdtp.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B3JCNrOi.js → Trigger-BsTU2blQ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BehFYsN_.js → Trust-DEKkuAsi.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BfOxJjVf.js → UkeySign-D1KEEgbK.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DdITCCPz.js → VideoEditing-C2yYkm9W.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B0EhkCmD.js → Wallet-BKKod4qb.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DJ4i1f3-.js → WebAuthn-XXUgED6d.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-Dr9XTqIT.js → WorkflowEditor-DnlEyYTi.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Dc2nFyOj.js → chat-BaVjI6nu.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Drw9djBR.js → colors-DQ0yx43H.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BdLXbU52.js → compact-item-DEyWDp0v.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BpQMZiWY.js → createContext-SuPOfylZ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-C_RK48j-.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CbYwawHf.js → hasIn-CQz1JNtC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DO7LD_1t.js → index-3xcB2i6I.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bo_KwJTF.js → index-57GdL9ro.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJQiSQw7.js → index-BHJhyrFr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTeqWdAS.js → index-BIgWgye3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSmZjrFy.js → index-BJEIjYhV.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7Ipfs4F.js → index-BffmUosH.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9_u4Qhe.js → index-ByGF9ev7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyU21xVi.js → index-C-IKjQD3.js} +1 -1
- package/src/assets/web-panel/assets/{index-GG_MXjSL.js → index-C2k8eHyq.js} +3 -3
- package/src/assets/web-panel/assets/{index-Blv7c80V.js → index-C3Vlo45C.js} +1 -1
- package/src/assets/web-panel/assets/index-CDqhisfz.js +1 -0
- package/src/assets/web-panel/assets/{index-C1eo1Qkd.js → index-CEfLMvKf.js} +1 -1
- package/src/assets/web-panel/assets/{index-COoIIMbO.js → index-CGu-b0RE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bu8Vy-mj.js → index-CI5jlfyn.js} +1 -1
- package/src/assets/web-panel/assets/index-CKUoX6Yi.js +1 -0
- package/src/assets/web-panel/assets/{index-BmQlot_u.js → index-COsmMpAO.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWUN8c01.js → index-CZrARoXd.js} +1 -1
- package/src/assets/web-panel/assets/{index-IG3__pGj.js → index-C_fcNCoV.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1ANzVc6.js → index-CfVW-cd_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAq-4KqY.js → index-CgvnudM_.js} +1 -1
- package/src/assets/web-panel/assets/{index-aHrUfrv1.js → index-CnhJU5To.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_lkTnZ0.js → index-CpR6M7eQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWUoc9-s.js → index-D-gXgEsU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZxzdAI0.js → index-D5SY6Lwu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiGDrEt4.js → index-DB8AJ6T-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHMeEu-P.js → index-DNYVGZzG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3Alz2QD.js → index-DWIuZ9bI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKQlF7Pw.js → index-DXczrVij.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKmz3Is-.js → index-Dfh6OJKz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIcEW07n.js → index-DhBeUGCp.js} +1 -1
- package/src/assets/web-panel/assets/{index-UX_Mfyfv.js → index-DjZ6szGM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBwQwtsE.js → index-DpPv8zeU.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpmlTfpv.js → index-XjQfQBXH.js} +1 -1
- package/src/assets/web-panel/assets/{index-_vVucYqC.js → index-ZKOZ-YFJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-XYe6kBmi.js → index-ZMgCYtI7.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5KboakZ.js → index-fGgx2f1y.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3uRI8_y.js → index-fcA6tfBA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBueKSTR.js → index-v8PYYw05.js} +1 -1
- package/src/assets/web-panel/assets/{index-VIyNlXPX.js → index-wBsuJZcz.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DybC85E4.js → initDefaultProps-KetDXPXx.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BNNuyDg7.js → motion-BfAtwo2m.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bm8gSjEN.js → move-BnJvzHgx.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CuHQvzzr.js → mtc-parser-DZxDclhM.js} +1 -1
- package/src/assets/web-panel/assets/{omit-lv4NO7Ng.js → omit-CmqfX2P_.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BoSMfjHq.js → pickAttrs-B2B2ahzC.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BOMDSvgz.js → placementArrow-C1SIgWRj.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C064Ubmu.js → responsiveObserve-Dy_adDTl.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CeqhbKoG.js → slide-hDO9sBPk.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CunFoYww.js → statusUtils-DlpTjRpg.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cxz3wPL7.js → styleChecker-DAv9eo6h.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DJsIaU2S.js → useFlexGapSupport-vfoXaeH8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DnAygceJ.js → useFs-Dzji-S7v.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CrEUY7ta.js → usePersonalDataHub-jKcLN_Gj.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Cq3rN2eT.js → vnode-DAWM5GgY.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-TMzcOx__.js → zoom-q3XgSOUd.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/plugin.js +49 -5
- package/src/data/changelog.json +18 -4
- package/src/harness/worktree-isolator.js +10 -2
- package/src/lib/agent-core.js +1 -0
- package/src/lib/background-agent-phase.js +22 -0
- package/src/lib/background-phase-reporter.js +153 -0
- package/src/lib/hook-runner.cjs +248 -36
- package/src/lib/install-command-policy.js +40 -1
- package/src/lib/lsp/code-intelligence.js +5 -1
- package/src/lib/lsp/workspace-roots.js +115 -0
- package/src/lib/session-lifecycle.js +3 -0
- package/src/lib/settings-hook-events.cjs +16 -3
- package/src/lib/sub-agent-context.js +13 -0
- package/src/runtime/agent-core.js +442 -89
- package/src/runtime/headless-runner.js +75 -8
- package/src/runtime/headless-stream.js +175 -2
- package/src/workers/background-agent-worker.js +22 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-BfRhXdyG.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-BqgUPlcU.js +0 -7
- package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CnfpDzMr.js +0 -1
- package/src/assets/web-panel/assets/index-CYTEB0tb.js +0 -1
- package/src/assets/web-panel/assets/index-CgsEp5bm.js +0 -1
package/src/lib/hook-runner.cjs
CHANGED
|
@@ -26,8 +26,9 @@ const {
|
|
|
26
26
|
buildPowershellArgv,
|
|
27
27
|
loadShellConfig,
|
|
28
28
|
} = require("./shell-selector.cjs");
|
|
29
|
+
const { mergeHookDecisions } = require("./hook-event-bus.cjs");
|
|
29
30
|
|
|
30
|
-
const _deps = { spawnSync: cpDefault.spawnSync };
|
|
31
|
+
const _deps = { spawnSync: cpDefault.spawnSync, spawn: cpDefault.spawn };
|
|
31
32
|
|
|
32
33
|
const HOOK_DECISIONS = Object.freeze({
|
|
33
34
|
BLOCK: "block",
|
|
@@ -116,43 +117,65 @@ function tryParseDecision(stdout) {
|
|
|
116
117
|
* stdout?:string, stderr?:string, additionalContext?:string,
|
|
117
118
|
* nonBlockingError?:boolean, error?:string }}
|
|
118
119
|
*/
|
|
120
|
+
// Circuit breaker (gap 2026-07-11 P2 "Hooks 硬化"): N consecutive non-blocking
|
|
121
|
+
// failures (spawn error / timeout / non-zero non-2 exit) of the SAME command
|
|
122
|
+
// trip an open state — further runs are skipped for a cooldown, then one
|
|
123
|
+
// half-open trial runs; success (or a real block/ask decision) closes it.
|
|
124
|
+
// CC_HOOK_BREAKER_THRESHOLD=0 disables. Extracted so the sync + async runners
|
|
125
|
+
// share ONE breaker implementation.
|
|
126
|
+
function _breakerShortCircuit(command, breaker) {
|
|
127
|
+
if (breaker.threshold <= 0) return null;
|
|
128
|
+
const st = _breaker.get(command);
|
|
129
|
+
if (st && st.fails >= breaker.threshold) {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (now - st.openedAt < breaker.cooldownMs) {
|
|
132
|
+
return {
|
|
133
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
134
|
+
reason: `hook circuit breaker open (${st.fails} consecutive failures) — skipped for cooldown`,
|
|
135
|
+
exitCode: null,
|
|
136
|
+
skipped: true,
|
|
137
|
+
breakerOpen: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// half-open: let this one trial through; the outcome records below.
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _breakerRecord(command, breaker, result) {
|
|
146
|
+
if (breaker.threshold <= 0) return;
|
|
147
|
+
if (result.nonBlockingError) {
|
|
148
|
+
const st = _breaker.get(command) || { fails: 0, openedAt: 0 };
|
|
149
|
+
st.fails += 1;
|
|
150
|
+
if (st.fails >= breaker.threshold) st.openedAt = Date.now();
|
|
151
|
+
_breaker.set(command, st);
|
|
152
|
+
} else {
|
|
153
|
+
_breaker.delete(command); // success / block / ask closes the breaker
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
function runCommandHook(command, input = {}, opts = {}) {
|
|
120
158
|
if (!command) {
|
|
121
159
|
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
|
|
122
160
|
}
|
|
123
|
-
// Circuit breaker (gap 2026-07-11 P2 "Hooks 硬化"): N consecutive
|
|
124
|
-
// non-blocking failures (spawn error / timeout / non-zero non-2 exit) of
|
|
125
|
-
// the SAME command trip an open state — further runs are skipped for a
|
|
126
|
-
// cooldown, then one half-open trial runs; success (or a real block/ask
|
|
127
|
-
// decision) closes it. CC_HOOK_BREAKER_THRESHOLD=0 disables.
|
|
128
161
|
const breaker = hookBreakerConfig();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (st && st.fails >= breaker.threshold) {
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
if (now - st.openedAt < breaker.cooldownMs) {
|
|
134
|
-
return {
|
|
135
|
-
decision: HOOK_DECISIONS.CONTINUE,
|
|
136
|
-
reason: `hook circuit breaker open (${st.fails} consecutive failures) — skipped for cooldown`,
|
|
137
|
-
exitCode: null,
|
|
138
|
-
skipped: true,
|
|
139
|
-
breakerOpen: true,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
// half-open: let this one trial through; the outcome below decides.
|
|
143
|
-
}
|
|
144
|
-
}
|
|
162
|
+
const shortCircuit = _breakerShortCircuit(command, breaker);
|
|
163
|
+
if (shortCircuit) return shortCircuit;
|
|
145
164
|
const result = _runCommandHookInner(command, input, opts);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
165
|
+
_breakerRecord(command, breaker, result);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Async counterpart of runCommandHook (same breaker + protocol). */
|
|
170
|
+
async function runCommandHookAsync(command, input = {}, opts = {}) {
|
|
171
|
+
if (!command) {
|
|
172
|
+
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, exitCode: 0 };
|
|
155
173
|
}
|
|
174
|
+
const breaker = hookBreakerConfig();
|
|
175
|
+
const shortCircuit = _breakerShortCircuit(command, breaker);
|
|
176
|
+
if (shortCircuit) return shortCircuit;
|
|
177
|
+
const result = await _runCommandHookInnerAsync(command, input, opts);
|
|
178
|
+
_breakerRecord(command, breaker, result);
|
|
156
179
|
return result;
|
|
157
180
|
}
|
|
158
181
|
|
|
@@ -230,7 +253,19 @@ function _runCommandHookInner(command, input = {}, opts = {}) {
|
|
|
230
253
|
nonBlockingError: true,
|
|
231
254
|
};
|
|
232
255
|
}
|
|
233
|
-
|
|
256
|
+
return interpretHookOutcome(res);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Interpret a finished hook process — a spawnSync result OR the collected output
|
|
261
|
+
* of an async spawn — into a normalized decision. The exit-code / JSON-stdout
|
|
262
|
+
* protocol lives in ONE place so the sync and async runners can never drift.
|
|
263
|
+
*
|
|
264
|
+
* @param {{error?:Error|null, status:number|null, stdout?:string, stderr?:string}} res
|
|
265
|
+
*/
|
|
266
|
+
function interpretHookOutcome(res) {
|
|
267
|
+
// A surfaced spawn error (timeout / ENOENT, status null) is non-blocking — a
|
|
268
|
+
// broken hook must never wedge the agent; only an explicit block blocks.
|
|
234
269
|
if (res.error) {
|
|
235
270
|
return {
|
|
236
271
|
decision: HOOK_DECISIONS.CONTINUE,
|
|
@@ -287,19 +322,157 @@ function _runCommandHookInner(command, input = {}, opts = {}) {
|
|
|
287
322
|
}
|
|
288
323
|
|
|
289
324
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
325
|
+
* Async single-hook runner — mirrors _runCommandHookInner but uses async spawn
|
|
326
|
+
* so a batch can run CONCURRENTLY (see runHooksParallel). stdin gets the JSON
|
|
327
|
+
* payload; stdout/stderr are collected (bounded); a hard timeout SIGKILLs a hung
|
|
328
|
+
* hook and reports it as a non-blocking error. Interpretation is shared via
|
|
329
|
+
* interpretHookOutcome so the exit-code protocol matches the sync path exactly.
|
|
330
|
+
*/
|
|
331
|
+
async function _runCommandHookInnerAsync(command, input = {}, opts = {}) {
|
|
332
|
+
const { cwd, event } = opts;
|
|
333
|
+
const _rawTimeout = Number(opts.timeout);
|
|
334
|
+
const timeout =
|
|
335
|
+
Number.isFinite(_rawTimeout) && _rawTimeout > 0
|
|
336
|
+
? Math.min(_rawTimeout, 600000)
|
|
337
|
+
: 60000;
|
|
338
|
+
const shellKind = normalizeShellKind(opts.shell);
|
|
339
|
+
const usePowershell = shellKind === "powershell" || shellKind === "pwsh";
|
|
340
|
+
const wd = cwd || process.cwd();
|
|
341
|
+
const inputJson = JSON.stringify({
|
|
342
|
+
...input,
|
|
343
|
+
schema_version: input.schema_version ?? HOOK_PAYLOAD_SCHEMA_VERSION,
|
|
344
|
+
});
|
|
345
|
+
const spawnEnv = {
|
|
346
|
+
...process.env,
|
|
347
|
+
CLAUDE_HOOK_EVENT: event || input.hook_event_name || "",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
let child;
|
|
351
|
+
try {
|
|
352
|
+
if (usePowershell) {
|
|
353
|
+
const { executionPolicy } = loadShellConfig({ cwd: wd });
|
|
354
|
+
const inv = buildPowershellArgv(command, shellKind, { executionPolicy });
|
|
355
|
+
child = _deps.spawn(inv.file, inv.argv, { cwd: wd, env: spawnEnv });
|
|
356
|
+
} else {
|
|
357
|
+
child = _deps.spawn(command, { cwd: wd, env: spawnEnv, shell: true });
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return {
|
|
361
|
+
decision: HOOK_DECISIONS.CONTINUE,
|
|
362
|
+
reason: `hook spawn failed: ${err.message}`,
|
|
363
|
+
exitCode: null,
|
|
364
|
+
error: err.message,
|
|
365
|
+
nonBlockingError: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return await new Promise((resolve) => {
|
|
370
|
+
const MAXB = 8 * 1024 * 1024;
|
|
371
|
+
let stdout = "";
|
|
372
|
+
let stderr = "";
|
|
373
|
+
let settled = false;
|
|
374
|
+
const done = (res) => {
|
|
375
|
+
if (settled) return;
|
|
376
|
+
settled = true;
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
resolve(interpretHookOutcome(res));
|
|
379
|
+
};
|
|
380
|
+
const timer = setTimeout(() => {
|
|
381
|
+
try {
|
|
382
|
+
child.kill("SIGKILL");
|
|
383
|
+
} catch {
|
|
384
|
+
/* already exited */
|
|
385
|
+
}
|
|
386
|
+
done({
|
|
387
|
+
error: new Error(`hook timed out after ${timeout}ms`),
|
|
388
|
+
status: null,
|
|
389
|
+
stdout,
|
|
390
|
+
stderr,
|
|
391
|
+
});
|
|
392
|
+
}, timeout);
|
|
393
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
394
|
+
|
|
395
|
+
child.on("error", (err) =>
|
|
396
|
+
done({ error: err, status: null, stdout, stderr }),
|
|
397
|
+
);
|
|
398
|
+
if (child.stdout) {
|
|
399
|
+
child.stdout.on("data", (d) => {
|
|
400
|
+
if (stdout.length < MAXB) stdout += d.toString("utf8");
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (child.stderr) {
|
|
404
|
+
child.stderr.on("data", (d) => {
|
|
405
|
+
if (stderr.length < MAXB) stderr += d.toString("utf8");
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
child.on("close", (code) =>
|
|
409
|
+
done({ error: null, status: code, stdout, stderr }),
|
|
410
|
+
);
|
|
411
|
+
// Feed the payload to the hook's stdin. A fast-exiting hook may close its
|
|
412
|
+
// end first (EPIPE) — that is harmless, the exit code still governs.
|
|
413
|
+
try {
|
|
414
|
+
if (child.stdin) {
|
|
415
|
+
child.stdin.on("error", () => {});
|
|
416
|
+
child.stdin.write(inputJson);
|
|
417
|
+
child.stdin.end();
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
/* stdin already closed */
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Run a list of command hooks and reduce to one decision.
|
|
427
|
+
*
|
|
428
|
+
* Default (`opts.mergeStrict` falsy): in-order, the first BLOCK **or** ASK
|
|
429
|
+
* short-circuits — byte-identical to the historical behavior.
|
|
430
|
+
*
|
|
431
|
+
* `opts.mergeStrict: true`: run EVERY matching hook (no short-circuit) then take
|
|
432
|
+
* the STRICTEST decision (block > ask > allow > continue) via mergeHookDecisions.
|
|
433
|
+
* This closes the safety gap where an earlier hook's `ask` masks a LATER hook's
|
|
434
|
+
* `block` (the short-circuit path returns `ask` and never runs the blocker).
|
|
435
|
+
* Claude-Code parity: all matching hooks contribute to the merged decision.
|
|
436
|
+
* spawnSync is synchronous so execution is sequential — but the MERGE is
|
|
437
|
+
* order-independent, so the outcome matches a parallel run.
|
|
438
|
+
*
|
|
439
|
+
* @returns {{ decision, reason, hook?, results, contributing? }}
|
|
292
440
|
*/
|
|
293
441
|
function runHooks(commandHooks, input = {}, opts = {}) {
|
|
294
442
|
const results = [];
|
|
295
|
-
|
|
296
|
-
|
|
443
|
+
const hooks = commandHooks || [];
|
|
444
|
+
const runOne = (h) =>
|
|
445
|
+
runCommandHook(h.command, input, {
|
|
297
446
|
...opts,
|
|
298
447
|
timeout: h.timeout != null ? h.timeout * 1000 : opts.timeout,
|
|
299
448
|
// per-hook shell selection (P1 #8): `{ "type":"command", "command":…,
|
|
300
449
|
// "shell":"powershell" }` in the settings hook entry
|
|
301
450
|
shell: h.shell != null ? h.shell : opts.shell,
|
|
302
451
|
});
|
|
452
|
+
|
|
453
|
+
if (opts.mergeStrict) {
|
|
454
|
+
const decisions = [];
|
|
455
|
+
for (const h of hooks) {
|
|
456
|
+
const r = runOne(h);
|
|
457
|
+
results.push({ command: h.command, ...r });
|
|
458
|
+
decisions.push({
|
|
459
|
+
decision: r.decision,
|
|
460
|
+
reason: r.reason,
|
|
461
|
+
hook: h.command,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const merged = mergeHookDecisions(decisions);
|
|
465
|
+
return {
|
|
466
|
+
decision: merged.decision,
|
|
467
|
+
reason: merged.reason,
|
|
468
|
+
hook: merged.hook,
|
|
469
|
+
results,
|
|
470
|
+
contributing: merged.contributing,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const h of hooks) {
|
|
475
|
+
const r = runOne(h);
|
|
303
476
|
results.push({ command: h.command, ...r });
|
|
304
477
|
if (
|
|
305
478
|
r.decision === HOOK_DECISIONS.BLOCK ||
|
|
@@ -316,9 +489,48 @@ function runHooks(commandHooks, input = {}, opts = {}) {
|
|
|
316
489
|
return { decision: HOOK_DECISIONS.CONTINUE, reason: null, results };
|
|
317
490
|
}
|
|
318
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Run every matching hook CONCURRENTLY (async spawn) and reduce to the strictest
|
|
494
|
+
* decision (block > ask > allow > continue) via mergeHookDecisions — the true-
|
|
495
|
+
* parallel form of runHooks({mergeStrict}). Wall-clock is the slowest single hook
|
|
496
|
+
* rather than their sum; the merge is order-independent so the outcome matches
|
|
497
|
+
* the sequential strict merge exactly. Same per-hook timeout/shell resolution as
|
|
498
|
+
* runHooks. Returns the same shape ({decision, reason, hook, results,
|
|
499
|
+
* contributing}) so the decision consumers are unchanged.
|
|
500
|
+
*/
|
|
501
|
+
async function runHooksParallel(commandHooks, input = {}, opts = {}) {
|
|
502
|
+
const hooks = commandHooks || [];
|
|
503
|
+
const settled = await Promise.all(
|
|
504
|
+
hooks.map((h) =>
|
|
505
|
+
runCommandHookAsync(h.command, input, {
|
|
506
|
+
...opts,
|
|
507
|
+
timeout: h.timeout != null ? h.timeout * 1000 : opts.timeout,
|
|
508
|
+
shell: h.shell != null ? h.shell : opts.shell,
|
|
509
|
+
}).then((r) => ({ h, r })),
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
const results = settled.map(({ h, r }) => ({ command: h.command, ...r }));
|
|
513
|
+
const decisions = settled.map(({ h, r }) => ({
|
|
514
|
+
decision: r.decision,
|
|
515
|
+
reason: r.reason,
|
|
516
|
+
hook: h.command,
|
|
517
|
+
}));
|
|
518
|
+
const merged = mergeHookDecisions(decisions);
|
|
519
|
+
return {
|
|
520
|
+
decision: merged.decision,
|
|
521
|
+
reason: merged.reason,
|
|
522
|
+
hook: merged.hook,
|
|
523
|
+
results,
|
|
524
|
+
contributing: merged.contributing,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
319
528
|
module.exports = {
|
|
320
529
|
runCommandHook,
|
|
530
|
+
runCommandHookAsync,
|
|
321
531
|
runHooks,
|
|
532
|
+
runHooksParallel,
|
|
533
|
+
interpretHookOutcome,
|
|
322
534
|
tryParseDecision,
|
|
323
535
|
HOOK_DECISIONS,
|
|
324
536
|
HOOK_PAYLOAD_SCHEMA_VERSION,
|
|
@@ -128,8 +128,18 @@ function unwrapArgv(tokens) {
|
|
|
128
128
|
* @returns {{manager:string, subcommand:string, packages:string[], global:boolean}|null}
|
|
129
129
|
*/
|
|
130
130
|
export function classifyInstallSegment(segment) {
|
|
131
|
-
|
|
131
|
+
let argv = unwrapArgv(tokenizeShellCommand(segment));
|
|
132
132
|
if (argv.length === 0) return null;
|
|
133
|
+
// Module invocation: `python -m pip install …` (and `py -m pip …`) is the
|
|
134
|
+
// canonical way run_code's auto-install — and plenty of humans — invoke pip;
|
|
135
|
+
// treat the module as the binary so it classifies like a direct `pip`.
|
|
136
|
+
if (
|
|
137
|
+
/^(python[0-9.]*|py)$/.test(normalizeBin(argv[0])) &&
|
|
138
|
+
argv[1] === "-m" &&
|
|
139
|
+
argv.length > 2
|
|
140
|
+
) {
|
|
141
|
+
argv = argv.slice(2);
|
|
142
|
+
}
|
|
133
143
|
const bin = normalizeBin(argv[0]);
|
|
134
144
|
const mgr = MANAGERS.find((m) => m.bin.test(bin));
|
|
135
145
|
if (!mgr) return null;
|
|
@@ -269,6 +279,35 @@ export function classifyCodeAcquisition(command) {
|
|
|
269
279
|
};
|
|
270
280
|
}
|
|
271
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Classify a PLUGIN install (`cc plugin add`/`upgrade`) into the unified
|
|
284
|
+
* "downloading and running third-party code" trail. A plugin ships executable
|
|
285
|
+
* components (bin/hooks/LSP/MCP servers run as processes), so installing one
|
|
286
|
+
* is the plugin-runtime sibling of a global package install — same audit
|
|
287
|
+
* file, same opt-in policy. Pure; returns null for a nameless input.
|
|
288
|
+
*/
|
|
289
|
+
export function classifyPluginInstall({
|
|
290
|
+
name,
|
|
291
|
+
version,
|
|
292
|
+
scope,
|
|
293
|
+
source,
|
|
294
|
+
capabilities,
|
|
295
|
+
} = {}) {
|
|
296
|
+
if (!name || typeof name !== "string") return null;
|
|
297
|
+
return {
|
|
298
|
+
manager: "cc-plugin",
|
|
299
|
+
packages: [version ? `${name}@${version}` : name],
|
|
300
|
+
// A user-scope plugin loads in EVERY repo the user runs cc in — the
|
|
301
|
+
// plugin-runtime analogue of `npm install -g`. Project/local scopes stay
|
|
302
|
+
// confined to one repository.
|
|
303
|
+
global: scope === "user",
|
|
304
|
+
...(source ? { source: String(source) } : {}),
|
|
305
|
+
...(Array.isArray(capabilities) && capabilities.length
|
|
306
|
+
? { capabilities }
|
|
307
|
+
: {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
272
311
|
/**
|
|
273
312
|
* Resolve the install-command policy from env/settings (opt-in, so the default
|
|
274
313
|
* shell path is byte-unchanged). `audit` enables the audit trail; `riskFloor`
|
|
@@ -64,7 +64,11 @@ export class CodeIntelligence {
|
|
|
64
64
|
/* plugin LSP contributions are best-effort */
|
|
65
65
|
}
|
|
66
66
|
this.manager =
|
|
67
|
-
opts.manager ||
|
|
67
|
+
opts.manager ||
|
|
68
|
+
new LSPManager({
|
|
69
|
+
projectRoot: opts.projectRoot,
|
|
70
|
+
...(opts.lspOptions || {}),
|
|
71
|
+
});
|
|
68
72
|
this._ownsManager = !opts.manager;
|
|
69
73
|
// One-shot callers (the `cc code-intel` CLI) spawn a cold server per run and
|
|
70
74
|
// must wait for the project to load before querying; the long-lived agent
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-roots — pure helpers for multi-root LSP workspaces (P2 LSP slice,
|
|
3
|
+
* CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"LSP 与 Code Review").
|
|
4
|
+
*
|
|
5
|
+
* A session started with `--add-dir` works across several project roots, but
|
|
6
|
+
* `code_intelligence` always keyed its shared language-server pool on the cwd
|
|
7
|
+
* alone — a query for a file living in an additional root spawned (or reused)
|
|
8
|
+
* the WRONG project's server, and `workspace_symbols` could only ever see the
|
|
9
|
+
* cwd project. These helpers make root selection explicit and testable:
|
|
10
|
+
*
|
|
11
|
+
* - `workspaceRootsFor(cwd, additionalDirectories)` — the deduped, resolved
|
|
12
|
+
* root list (cwd first, mirroring the roots the system prompt advertises).
|
|
13
|
+
* - `pickRootForFile(file, roots)` — the root that CONTAINS the file
|
|
14
|
+
* (deepest match wins so nested roots resolve to the more specific
|
|
15
|
+
* project); falls back to the first root (cwd) for out-of-root files,
|
|
16
|
+
* which is the pre-multi-root behavior.
|
|
17
|
+
* - `mergeWorkspaceSymbolResults(results, roots)` — fold per-root
|
|
18
|
+
* `workspaceSymbols` outcomes into one: available when ANY root answered,
|
|
19
|
+
* symbols concatenated in root order and stamped with their `root` so
|
|
20
|
+
* same-named symbols from different projects stay unambiguous.
|
|
21
|
+
*
|
|
22
|
+
* Pure + IO-free (path math only) so agent-core can consume them without any
|
|
23
|
+
* new export surface of its own.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
|
|
28
|
+
/** Case-fold for prefix comparison on case-insensitive filesystems. */
|
|
29
|
+
function fold(p) {
|
|
30
|
+
return process.platform === "win32" ? p.toLowerCase() : p;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolved, deduped workspace roots — cwd first, then each additional
|
|
35
|
+
* directory. Non-string/empty entries are dropped; never returns empty (falls
|
|
36
|
+
* back to process.cwd()).
|
|
37
|
+
*/
|
|
38
|
+
export function workspaceRootsFor(cwd, additionalDirectories) {
|
|
39
|
+
const out = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const push = (dir) => {
|
|
42
|
+
if (typeof dir !== "string" || dir.trim() === "") return;
|
|
43
|
+
const resolved = path.resolve(dir);
|
|
44
|
+
const key = fold(resolved);
|
|
45
|
+
if (seen.has(key)) return;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
out.push(resolved);
|
|
48
|
+
};
|
|
49
|
+
push(cwd || process.cwd());
|
|
50
|
+
for (const d of Array.isArray(additionalDirectories)
|
|
51
|
+
? additionalDirectories
|
|
52
|
+
: []) {
|
|
53
|
+
push(d);
|
|
54
|
+
}
|
|
55
|
+
if (out.length === 0) out.push(path.resolve(process.cwd()));
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The workspace root that contains `file` — deepest containing root wins
|
|
61
|
+
* (nested roots resolve to the more specific project). Files outside every
|
|
62
|
+
* root fall back to the FIRST root (cwd), preserving the single-root
|
|
63
|
+
* behavior byte-for-byte.
|
|
64
|
+
*/
|
|
65
|
+
export function pickRootForFile(file, roots) {
|
|
66
|
+
const list = Array.isArray(roots) && roots.length > 0 ? roots : null;
|
|
67
|
+
if (!list) return path.resolve(process.cwd());
|
|
68
|
+
if (typeof file !== "string" || file.trim() === "") return list[0];
|
|
69
|
+
const f = fold(path.resolve(file));
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const root of list) {
|
|
72
|
+
const r = fold(path.resolve(root));
|
|
73
|
+
if (f === r || f.startsWith(r + path.sep)) {
|
|
74
|
+
if (best === null || r.length > fold(best).length) {
|
|
75
|
+
best = path.resolve(root);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return best || list[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Merge per-root `workspaceSymbols` results (same order as `roots`).
|
|
84
|
+
* Any available root makes the merged result available; each symbol is
|
|
85
|
+
* stamped with its `root`. All-unavailable folds the reasons together so the
|
|
86
|
+
* caller still sees WHY (first reason wins the summary, all are listed).
|
|
87
|
+
*/
|
|
88
|
+
export function mergeWorkspaceSymbolResults(results, roots = []) {
|
|
89
|
+
const list = Array.isArray(results) ? results : [];
|
|
90
|
+
const symbols = [];
|
|
91
|
+
const reasons = [];
|
|
92
|
+
let anyAvailable = false;
|
|
93
|
+
for (let i = 0; i < list.length; i++) {
|
|
94
|
+
const r = list[i];
|
|
95
|
+
if (r && r.available) {
|
|
96
|
+
anyAvailable = true;
|
|
97
|
+
for (const s of Array.isArray(r.symbols) ? r.symbols : []) {
|
|
98
|
+
symbols.push(roots[i] ? { ...s, root: roots[i] } : s);
|
|
99
|
+
}
|
|
100
|
+
} else if (r) {
|
|
101
|
+
reasons.push(
|
|
102
|
+
roots[i]
|
|
103
|
+
? `${roots[i]}: ${r.reason || "unavailable"}`
|
|
104
|
+
: r.reason || "unavailable",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!anyAvailable) {
|
|
109
|
+
return {
|
|
110
|
+
available: false,
|
|
111
|
+
reason: reasons.join("; ") || "language server unavailable",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { available: true, symbols };
|
|
115
|
+
}
|
|
@@ -86,6 +86,9 @@ const STATE_ALIASES = new Map([
|
|
|
86
86
|
["waiting-permission", SESSION_STATES.WAITING_APPROVAL],
|
|
87
87
|
["waiting_approval", SESSION_STATES.WAITING_APPROVAL],
|
|
88
88
|
["awaiting_approval", SESSION_STATES.WAITING_APPROVAL],
|
|
89
|
+
// A resumed turn with UNKNOWN-outcome side effects blocks on confirmation.
|
|
90
|
+
["uncertain_side_effect", SESSION_STATES.WAITING_APPROVAL],
|
|
91
|
+
["uncertain-side-effect", SESSION_STATES.WAITING_APPROVAL],
|
|
89
92
|
// supervisor liveness — a running process that vanished is a failure.
|
|
90
93
|
["lost", SESSION_STATES.FAILED],
|
|
91
94
|
// IDE connection / transport
|
|
@@ -47,7 +47,15 @@ function withDeliveryId(event, payload, { sessionId, traceId, parentId } = {}) {
|
|
|
47
47
|
/* best-effort — recording must not break the turn */
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// trace_id/parent_id are stamped onto the DELIVERED payload only when the
|
|
51
|
+
// caller actually threaded them (agent-core context) — absent they are
|
|
52
|
+
// omitted entirely, keeping every legacy payload byte-identical.
|
|
53
|
+
return {
|
|
54
|
+
...payload,
|
|
55
|
+
event_id: env.event_id,
|
|
56
|
+
...(env.trace_id ? { trace_id: env.trace_id } : {}),
|
|
57
|
+
...(env.parent_id ? { parent_id: env.parent_id } : {}),
|
|
58
|
+
};
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/**
|
|
@@ -343,7 +351,7 @@ function runObserveHooks(
|
|
|
343
351
|
settingsHooks,
|
|
344
352
|
event,
|
|
345
353
|
payload = {},
|
|
346
|
-
{ cwd, matchTarget } = {},
|
|
354
|
+
{ cwd, matchTarget, traceId, parentId } = {},
|
|
347
355
|
) {
|
|
348
356
|
if (!settingsHooks) return { decision: "continue", results: [] };
|
|
349
357
|
const matched = collectHooks(settingsHooks, event, matchTarget || "");
|
|
@@ -351,12 +359,17 @@ function runObserveHooks(
|
|
|
351
359
|
if (sync.length === 0) return { decision: "continue", results: [] };
|
|
352
360
|
return runHooks(
|
|
353
361
|
sync,
|
|
354
|
-
withDeliveryId(
|
|
362
|
+
withDeliveryId(
|
|
363
|
+
event,
|
|
364
|
+
{ hook_event_name: event, cwd, ...payload },
|
|
365
|
+
{ traceId, parentId },
|
|
366
|
+
),
|
|
355
367
|
{ cwd, event },
|
|
356
368
|
);
|
|
357
369
|
}
|
|
358
370
|
|
|
359
371
|
module.exports = {
|
|
372
|
+
withDeliveryId,
|
|
360
373
|
runUserPromptSubmitHooks,
|
|
361
374
|
runSessionStartHooks,
|
|
362
375
|
runCwdChangedHooks,
|
|
@@ -87,6 +87,10 @@ export class SubAgentContext {
|
|
|
87
87
|
// restricts run_skill/list_skills in this context's loop to those skills.
|
|
88
88
|
this.skillAllowlist =
|
|
89
89
|
options.skillAllowlist != null ? options.skillAllowlist : null;
|
|
90
|
+
// Hook-envelope tracing: the spawning run's id — this child loop stamps it
|
|
91
|
+
// as parent_id on every settings-hook payload it fires (its own runId
|
|
92
|
+
// becomes the trace_id). null → top-level semantics (no parent).
|
|
93
|
+
this._hookParentTraceId = options.hookParentTraceId || null;
|
|
90
94
|
this.cwd = options.cwd || process.cwd();
|
|
91
95
|
this.status = "active";
|
|
92
96
|
this.result = null;
|
|
@@ -98,6 +102,10 @@ export class SubAgentContext {
|
|
|
98
102
|
this._worktreePath = null;
|
|
99
103
|
this._worktreeBranch = null;
|
|
100
104
|
this._repoDir = this.cwd;
|
|
105
|
+
// Optional worktree creation options (large-monorepo): { sparsePaths,
|
|
106
|
+
// symlinkDirectories } — only materialize the needed packages and reuse
|
|
107
|
+
// approved dep dirs. null → full checkout (byte-identical default).
|
|
108
|
+
this._worktreeOptions = options.worktreeOptions || null;
|
|
101
109
|
|
|
102
110
|
// ── Isolated state ──────────────────────────────────────────────
|
|
103
111
|
// Independent message history — never shared with parent
|
|
@@ -254,6 +262,7 @@ export class SubAgentContext {
|
|
|
254
262
|
this.cwd = wtPath;
|
|
255
263
|
return this._runCore(userPrompt, loopOptions);
|
|
256
264
|
},
|
|
265
|
+
this._worktreeOptions || {},
|
|
257
266
|
);
|
|
258
267
|
|
|
259
268
|
// Annotate result with worktree info + diff preview
|
|
@@ -328,6 +337,10 @@ export class SubAgentContext {
|
|
|
328
337
|
subAgentContract: this.subAgentContract,
|
|
329
338
|
// Skill allow-list restricting run_skill/list_skills in this loop.
|
|
330
339
|
skillAllowlist: this.skillAllowlist,
|
|
340
|
+
// Parent trace for hook envelopes (absent → key omitted, no parent_id).
|
|
341
|
+
...(this._hookParentTraceId
|
|
342
|
+
? { hookParentTraceId: this._hookParentTraceId }
|
|
343
|
+
: {}),
|
|
331
344
|
...loopOptions,
|
|
332
345
|
};
|
|
333
346
|
if (this.iterationBudget) {
|