chainlesschain 0.162.167 → 0.162.168
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-CDzzuWZV.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Bhlrylt7.js → ActionButton-Dy-8MmQi.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-pHtXVQYs.js → Analytics-BtJ_VWkx.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-ORpuri4R.js → AppLayout-DkjiI4VS.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-eC77yr1_.js → Artifacts-h8PyDO86.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B2dZIiqz.js → Audit-DiDV0C6l.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-COZiTiwP.js → BackgroundAgents-DTw_aOaf.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-vY_Srl5d.js → Backup-CihFuUO9.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Dili5BdM.js → BaseInput-CqP2QNoR.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-EzKBf0XL.js → Chat-tm-ygX-p.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D1CtRAyy.js → ChatBubbleRenderer-BVKEhj_y.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Beh6BmuW.js → Checkbox-BFMyRN3Q.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-C3fA6kBU.js → Codegen-B1gTeoAS.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CefMqONH.js → Col-1GP8MvKL.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CBAxFJtB.js → Community-BLY938F6.js} +1 -1
- package/src/assets/web-panel/assets/{Compact--flHc-sM.js → Compact-DVt_p36I.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CPeNLvA6.js → Compliance-DxqEBXVG.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bv5yv4BU.js → Cowork-CyWrM-T8.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-C22yMEas.js → Cron-Domu6aI9.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dsb2bcpu.js → Crosschain-C8LKpjHJ.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DAxei4w0.js → DID-XBy15RZ5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DEUwuVZ8.js → Dashboard-DDAyPD-V.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BVp4si2j.js → Dropdown-BHV0MZ13.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CyFkybOG.js → EmailListRenderer-DmFGW4PX.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BWmmsbVx.js → FamilyGuardDashboard-CqYdbKvd.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Cxw27A5d.js → Federation-YP5-Puv5.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Q9XthzL1.js → FormItemContext-BOmcIcTe.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DS5DIdsW.js → GenericCardRenderer-CvTZdoc4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BOliHfxP.js → Git-Q2oxua0L.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CD4oxa-W.js → Governance-D-WpyWEU.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CMlcivo8.js → Inference-NUtsYHQo.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BwhERcC4.js → KnowledgeGraph-BHQLa8fr.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BjVcZR5P.js → Logs-6WsR5Q1G.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BfRhXdyG.js → MarkdownRenderer-DuWoDwMx.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DQLU3E2w.js → Marketplace-CigohRRW.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CeK0XkpB.js → McpTools-jcp6jVLf.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DLFkgwaz.js → Memory-B00NU1ph.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Bh6ymXyj.js → MobileBridge-Cs_p3PzU.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-BFs2WljZ.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Bx5x9_2m.js → Mtc-CSEflwzw.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-a1-Fexsq.js → MtcAudit-dxSiEZ96.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-eaw0X8X1.js → Multisig-CDAFEAyq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BaNsPV4k.js → NLProgramming-HoFpZNiB.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-C7pevN2X.js → Notes-tAoQvVcE.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-HDqeoP0r.js → NotificationSettings-Czo2M9F4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DvzVl5T3.js → OrderTableRenderer-CM1kNTp6.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D_xHDlGW.js → Organization-Cudr0VOC.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-B5sRnueL.js → Overflow-BSXeDKiN.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-C-IRjOPW.js → P2P-Cqv_g-F9.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BqgUPlcU.js → PdhVaultBrowser-7rjprPh8.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-Bm_T1uWw.js → Permissions-97pIDQKE.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DujDllye.js → PersonalDataHub-bparK9eQ.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-DyNPOLIG.js → Pipeline-CJgerM09.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CYKTW3iK.js → Privacy-JqFS3PN7.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CGf03uZ0.js → ProjectInit-DMITd-2c.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DIec4bTI.js → ProjectSettings-1UkdJ8Ii.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BY43-bmi.js → Projects-CYZqegqw.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-RrnUmgPk.js → Providers-ChfXz3U0.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BBWfqd4y.js → QrScannerModal-Rzkka-mf.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BsICSrFp.js → QuickAsk-DEz9HaON.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B8y0b4ys.js → Recommend-EFGP8AVu.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-0QgdB1Oe.js → RemoteSession-6um0lGGy.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DBJGM0YO.js → Reputation-CA1BXTup.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DtWQ5otv.js → Row-stTnDJ9y.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-2zBxHd5H.js → RssFeed-B10qCCoG.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BofliSkZ.js → Search-DSJroZ6p.js} +1 -1
- package/src/assets/web-panel/assets/{Security-ockRBBEL.js → Security-BpXkwiMW.js} +4 -4
- package/src/assets/web-panel/assets/{Services-QyvG7Vd0.js → Services-BPQxjwK6.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DwbOndX3.js → Skeleton-CfbKAMul.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-cHWa63sZ.js → Skills-CttYTfX0.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BHnTEydQ.js → Sla-ByluHich.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DE2hrrLb.js → SpeechSettings-BdZXKsfV.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CZ9vnbJH.js → SyncSettings-xCR7iDzy.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-qTko_INn.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DiR3ad4X.js → Templates-CkIzhgDJ.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-NqcW3frV.js → Tenant-BIaCaaK8.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-B2Qq38XP.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-C5GOoxFz.js → TimelineRenderer-D5eQDnEF.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-82dz0XdU.js → Tokens-DmeJ8NMQ.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B3JCNrOi.js → Trigger-C5sgb3C7.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BehFYsN_.js → Trust-H2vOb3Rb.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BfOxJjVf.js → UkeySign-NmPtHK2u.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DdITCCPz.js → VideoEditing-DHv-Ti7K.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B0EhkCmD.js → Wallet-BYZ3-PRP.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DJ4i1f3-.js → WebAuthn-BaSlP7om.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Dr9XTqIT.js → WorkflowEditor-CZ7IHuIi.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Dc2nFyOj.js → chat-Dd9kY8gn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Drw9djBR.js → colors-yhzo6QSV.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BdLXbU52.js → compact-item-C1JneVdM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BpQMZiWY.js → createContext-C_waVCg2.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DaDlyVDY.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CbYwawHf.js → hasIn-CkVg0Xnh.js} +1 -1
- package/src/assets/web-panel/assets/{index-aHrUfrv1.js → index-1VYhIr6D.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpmlTfpv.js → index-BTMiVIBI.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3uRI8_y.js → index-BYOyb5X2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHMeEu-P.js → index-BjDnI7oM.js} +1 -1
- package/src/assets/web-panel/assets/{index-COoIIMbO.js → index-BlJNvg-2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bu8Vy-mj.js → index-BlzbSUK9.js} +1 -1
- package/src/assets/web-panel/assets/{index-XYe6kBmi.js → index-BrN2hAeE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DO7LD_1t.js → index-BsSsurcn.js} +1 -1
- package/src/assets/web-panel/assets/index-BtsLExcc.js +1 -0
- package/src/assets/web-panel/assets/{index-BIcEW07n.js → index-Bwuu3Bf3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BBueKSTR.js → index-Bx3A6X4J.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWUN8c01.js → index-C-Sscgnv.js} +1 -1
- package/src/assets/web-panel/assets/index-C3qDvvIb.js +1 -0
- package/src/assets/web-panel/assets/{index-D_lkTnZ0.js → index-C4HNdMCt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWUoc9-s.js → index-CBwmwTNa.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1eo1Qkd.js → index-CErewFeT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZxzdAI0.js → index-CGtoL5mR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bo_KwJTF.js → index-CUDjGt_V.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7Ipfs4F.js → index-C_qqYZT7.js} +1 -1
- package/src/assets/web-panel/assets/{index-VIyNlXPX.js → index-C_riAa6d.js} +1 -1
- package/src/assets/web-panel/assets/{index-UX_Mfyfv.js → index-CfEMjbhu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKmz3Is-.js → index-ClQhh6v3.js} +1 -1
- package/src/assets/web-panel/assets/{index-Blv7c80V.js → index-CmiLmG0i.js} +1 -1
- package/src/assets/web-panel/assets/{index-GG_MXjSL.js → index-D1xIbAxQ.js} +3 -3
- package/src/assets/web-panel/assets/{index-_vVucYqC.js → index-D6UXDH2D.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJQiSQw7.js → index-D7kQ_4uL.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTeqWdAS.js → index-DPkqQ2Pr.js} +1 -1
- package/src/assets/web-panel/assets/{index-B9_u4Qhe.js → index-DUA96wEj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyU21xVi.js → index-DXHk9Vof.js} +1 -1
- package/src/assets/web-panel/assets/{index-IG3__pGj.js → index-DapftLUr.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3Alz2QD.js → index-Dez7TUgG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBwQwtsE.js → index-DhZgULsY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAq-4KqY.js → index-Diquc7-l.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiGDrEt4.js → index-DuGtLiTW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSmZjrFy.js → index-U_nLXGIx.js} +1 -1
- package/src/assets/web-panel/assets/{index-B1ANzVc6.js → index-_M8El5iz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5KboakZ.js → index-_VmzCYFn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKQlF7Pw.js → index-_rZxgFtc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmQlot_u.js → index-ewb0-LoH.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DybC85E4.js → initDefaultProps-DNN6qtGa.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BNNuyDg7.js → motion-BcM9exeo.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bm8gSjEN.js → move-DsKJ-YvA.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CuHQvzzr.js → mtc-parser-glgGkxaK.js} +1 -1
- package/src/assets/web-panel/assets/{omit-lv4NO7Ng.js → omit-BwHCU50S.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BoSMfjHq.js → pickAttrs-BVp-bLt7.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BOMDSvgz.js → placementArrow-CZNe0r-w.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C064Ubmu.js → responsiveObserve-DUafj9pU.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CeqhbKoG.js → slide-H4NqXQxu.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CunFoYww.js → statusUtils-UjMzzGZ5.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cxz3wPL7.js → styleChecker-qO5aCjyk.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DJsIaU2S.js → useFlexGapSupport-ChecYwC8.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DnAygceJ.js → useFs-BR0QvwO3.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CrEUY7ta.js → usePersonalDataHub-DitK2Qgz.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-Cq3rN2eT.js → vnode-dxSb40dY.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-TMzcOx__.js → zoom-ekJk8YB2.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/data/changelog.json +5 -4
- package/src/harness/worktree-isolator.js +10 -2
- package/src/lib/agent-core.js +1 -0
- package/src/lib/background-phase-reporter.js +116 -0
- package/src/lib/hook-runner.cjs +248 -36
- package/src/lib/install-command-policy.js +11 -1
- package/src/lib/lsp/code-intelligence.js +5 -1
- package/src/lib/lsp/workspace-roots.js +115 -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 +411 -82
- package/src/runtime/headless-runner.js +42 -8
- package/src/runtime/headless-stream.js +17 -0
- package/src/workers/background-agent-worker.js +8 -0
- package/src/assets/web-panel/assets/MobileProjects-D950tUIe.js +0 -1
- package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +0 -1
- package/src/assets/web-panel/assets/Terminal-iYeqRQZ0.js +0 -3
- 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
|
@@ -163,9 +163,17 @@ export function pruneWorktrees(repoDir) {
|
|
|
163
163
|
return before - after;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
export async function isolateTask(repoDir, taskId, fn) {
|
|
166
|
+
export async function isolateTask(repoDir, taskId, fn, options = {}) {
|
|
167
167
|
const branchName = `agent/${taskId}`;
|
|
168
|
-
|
|
168
|
+
// options (large-monorepo): { sparsePaths, symlinkDirectories } forwarded to
|
|
169
|
+
// createWorktree so an isolated task only materializes the packages it needs
|
|
170
|
+
// and reuses approved dep dirs. Empty/absent → full checkout (byte-identical).
|
|
171
|
+
const { path: worktreePath } = createWorktree(
|
|
172
|
+
repoDir,
|
|
173
|
+
branchName,
|
|
174
|
+
undefined,
|
|
175
|
+
options || {},
|
|
176
|
+
);
|
|
169
177
|
|
|
170
178
|
try {
|
|
171
179
|
const result = await fn(worktreePath);
|
package/src/lib/agent-core.js
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* background-phase-reporter — the missing PRODUCER for the background-agent
|
|
3
|
+
* `waiting_permission` phase (P0 state-machine slice,
|
|
4
|
+
* CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"后台 Agent 状态机").
|
|
5
|
+
*
|
|
6
|
+
* The consuming contract has existed for a while: `background-agent-phase.js`
|
|
7
|
+
* normalizes `waiting_permission` and `pendingApprovals > 0` into the
|
|
8
|
+
* dashboard's "Needs input" group, and `session-lifecycle.js` folds them into
|
|
9
|
+
* `waitingApproval`. But nothing ever WROTE those fields — the worker only
|
|
10
|
+
* emits `turn`/`idle`, so a background agent blocked on a human approval
|
|
11
|
+
* (`--permission-prompt-tool`, `--remote-control`) showed as "Working"
|
|
12
|
+
* indefinitely while a decision was actually pending.
|
|
13
|
+
*
|
|
14
|
+
* The production seam is the agent CHILD process, not the worker: the worker
|
|
15
|
+
* spawns each turn with `CC_BACKGROUND_AGENT_ID` in the environment, and the
|
|
16
|
+
* only places a plain headless run genuinely blocks on a human are the
|
|
17
|
+
* confirmer overrides installed on the ApprovalGate. This module wraps those
|
|
18
|
+
* confirmers: while one is awaiting a decision it patches the shared state
|
|
19
|
+
* file with `phase: "waiting_permission"` + a live `pendingApprovals` count,
|
|
20
|
+
* and restores `phase: "turn"` when every pending approval has settled.
|
|
21
|
+
*
|
|
22
|
+
* Safety properties:
|
|
23
|
+
* - Disabled (no `CC_BACKGROUND_AGENT_ID`) → `wrapConfirmer` returns the
|
|
24
|
+
* SAME function object; zero reads, zero writes, byte-identical behavior
|
|
25
|
+
* for every non-background run.
|
|
26
|
+
* - Never resurrects a terminal session: writes are skipped when the current
|
|
27
|
+
* on-disk status is anything other than absent/"running" (mirrors the
|
|
28
|
+
* worker's own writeHeartbeat guard).
|
|
29
|
+
* - Best-effort throughout: a state-file hiccup must never break an approval.
|
|
30
|
+
* - IO is injectable so tests never touch the real user data dir.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
readBackgroundAgentState,
|
|
35
|
+
writeBackgroundAgentState,
|
|
36
|
+
} from "./background-agent-supervisor.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a phase reporter for the current process.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} [opts]
|
|
42
|
+
* @param {string|null} [opts.agentId] background session id; defaults to
|
|
43
|
+
* `process.env.CC_BACKGROUND_AGENT_ID`. Falsy → disabled no-op reporter.
|
|
44
|
+
* @param {(id:string)=>object|null} [opts.readState] injectable state reader
|
|
45
|
+
* @param {(state:object)=>void} [opts.writeState] injectable state writer
|
|
46
|
+
*/
|
|
47
|
+
export function createBackgroundPhaseReporter(opts = {}) {
|
|
48
|
+
const agentId =
|
|
49
|
+
opts.agentId !== undefined
|
|
50
|
+
? opts.agentId
|
|
51
|
+
: process.env.CC_BACKGROUND_AGENT_ID;
|
|
52
|
+
const readState = opts.readState || readBackgroundAgentState;
|
|
53
|
+
const writeState = opts.writeState || writeBackgroundAgentState;
|
|
54
|
+
const enabled = typeof agentId === "string" && agentId.trim() !== "";
|
|
55
|
+
let pending = 0;
|
|
56
|
+
|
|
57
|
+
function patch(fields) {
|
|
58
|
+
if (!enabled) return false;
|
|
59
|
+
try {
|
|
60
|
+
const current = readState(agentId) || { id: agentId };
|
|
61
|
+
// Terminal states are owned by the worker/stopper — never write a
|
|
62
|
+
// "waiting" phase over a completed/failed/stopped record.
|
|
63
|
+
if (current.status && current.status !== "running") return false;
|
|
64
|
+
writeState({ ...current, id: agentId, ...fields });
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false; // state persistence must never break the approval itself
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
enabled,
|
|
73
|
+
/** Current in-process pending-approval count (for tests/introspection). */
|
|
74
|
+
pendingCount() {
|
|
75
|
+
return pending;
|
|
76
|
+
},
|
|
77
|
+
/** A human approval just started blocking progress. */
|
|
78
|
+
beginApproval() {
|
|
79
|
+
if (!enabled) return;
|
|
80
|
+
pending++;
|
|
81
|
+
patch({
|
|
82
|
+
phase: "waiting_permission",
|
|
83
|
+
pendingApprovals: pending,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
/** A previously reported approval settled (approved, denied, or failed). */
|
|
87
|
+
endApproval() {
|
|
88
|
+
if (!enabled) return;
|
|
89
|
+
pending = Math.max(0, pending - 1);
|
|
90
|
+
if (pending === 0) {
|
|
91
|
+
// Back to a live turn — the worker owns the turn/idle transition, we
|
|
92
|
+
// only hand the phase back to its steady in-turn value.
|
|
93
|
+
patch({ phase: "turn", pendingApprovals: 0 });
|
|
94
|
+
} else {
|
|
95
|
+
patch({ pendingApprovals: pending });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
/**
|
|
99
|
+
* Wrap a (potentially human-blocking) confirmer so the pending window is
|
|
100
|
+
* visible in the background state. Disabled → returns `confirmer`
|
|
101
|
+
* unchanged (same object identity, zero overhead).
|
|
102
|
+
*/
|
|
103
|
+
wrapConfirmer(confirmer) {
|
|
104
|
+
if (!enabled || typeof confirmer !== "function") return confirmer;
|
|
105
|
+
const self = this;
|
|
106
|
+
return async function phaseReportingConfirmer(...args) {
|
|
107
|
+
self.beginApproval();
|
|
108
|
+
try {
|
|
109
|
+
return await confirmer(...args);
|
|
110
|
+
} finally {
|
|
111
|
+
self.endApproval();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
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;
|
|
@@ -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
|
+
}
|
|
@@ -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,
|