chainlesschain 0.162.186 → 0.162.187
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-DTXiO-TB.js → AIOps-CETaO3s8.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BbmDW-Pz.js → ActionButton-CAe7E9TW.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CSfgXJuD.js → Analytics-BGr2mzwO.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BS0FFLb7.js → AppLayout-BEoY-NcT.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B19WFPK2.js → Artifacts-DUZivCgt.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CQhZBi7n.js → Audit-C3yBs68k.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BOLZCpGH.js → BackgroundAgents-C9KwiY3O.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Dz3R_ePD.js → Backup-BoSWD_C8.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CYfgMpax.js → BaseInput-Dp09GkxL.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-5CymMr1j.js → Chat-5t9d-Ja0.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DmVku4GH.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CDXHVYcu.js → Checkbox-8QJFlkqI.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-uq9Z9oiS.js → Codegen-BJVFQ3lL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BuW1K_65.js → Col-6hdLD0oB.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Bbg-PvRY.js → Community-B8opo0uY.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-co0pPPTw.js → Compact-Dv8HudiN.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-B-pFAv7z.js → Compliance-DhxuCS5N.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-54ZQS4gC.js → Cowork-Dggy6VIL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B1BF2Seq.js → Cron-DSmDqJTT.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Dey6qQTj.js → Crosschain-7bZq0j5f.js} +1 -1
- package/src/assets/web-panel/assets/{DID-ltERvfAj.js → DID-BWY4kh-H.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DuWxOcqN.js → Dashboard-bOzPZxlu.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrZ8ekTo.js → Dropdown-BTFJHqQu.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-fIyHXhUS.js → EmailListRenderer-BC3MOdtT.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BtRyrRPm.js → FamilyGuardDashboard-CFHAdTTY.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-EUC6Wydj.js → Federation-DZ_4aF_n.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BOpka2G-.js → FormItemContext-BZje6UTj.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BXtrrYHn.js → GenericCardRenderer-D7QfCvjN.js} +1 -1
- package/src/assets/web-panel/assets/{Git-8WMb7PiR.js → Git-DuG7qANG.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DNWOeDTV.js → Governance-BDmYAgTh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Bne9b_Lb.js → Inference-DSXAfc1Q.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cp-6Us9b.js → KnowledgeGraph-DusleniP.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ck-uSPAx.js → Logs-CpImmA8p.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DxjsdAXw.js → MarkdownRenderer-BZ6LJBkk.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-Vdq7brrJ.js → Marketplace-DXS8CXkE.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ggLnw4hY.js → McpTools-D151UfUI.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BKIjNVCU.js → Memory-DKDxnoA1.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BElCY25a.js → MobileBridge-BFZ95iKM.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-CDAR-grF.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-bNdrF_Zh.js → Mtc-OGJ95xJs.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-Dw8w1Cqk.js → MtcAudit-BmGTo1XC.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CfGKvlqc.js → Multisig-XumJRqOa.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-D6OJf7YN.js → NLProgramming-DHhhRZed.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B3n_LzIS.js → Notes-B9rGxNNr.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Uu1V_ICM.js → NotificationSettings-DClLbMYV.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-aGyWwWcp.js → OrderTableRenderer-I8OohT7N.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-qjFFmuwK.js → Organization-M6WzaYVa.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-J4ZgnwZz.js → Overflow-Cb3B5KtM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-YEZF-uVH.js → P2P-BGk57kya.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-yqx7fwqk.js → PdhVaultBrowser-CXcKyPqK.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DdEjlZ02.js → Permissions-hsfJ-Y0e.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DbAPYvns.js → PersonalDataHub-BSBii4js.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Bxlk_ZJU.js → Pipeline-BNw52rey.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CpE-05tz.js → Privacy-DUpfue4E.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DWlhcGHT.js → ProjectInit-C68NsP3h.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BnH6BiTQ.js → ProjectSettings-BJ2It9v4.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CHw2fXDB.js → Projects-D2FHLUa-.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Bdz_sTyb.js → Providers-EnU4VWwS.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BHh2b3pH.js → QrScannerModal-BRt5-B_I.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D84M09pe.js → QuickAsk-Jd5L9UL2.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Czc30K6M.js → Recommend-BNDhoVIu.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-q_TU5qxt.js → RemoteSession-BII41Mel.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-CnTce4WM.js → Reputation-D1wQPUNv.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C60UFLay.js → Row-DnjRegul.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOUFEu9_.js → RssFeed-C_T4I-nF.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D4__blE-.js → Search-C4lrxlLh.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BoOm7_JK.js → Security-ByYT-VzP.js} +4 -4
- package/src/assets/web-panel/assets/{Services-AvzrW6HA.js → Services-BHpyEqB5.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BZlrHozi.js → Skeleton-C0I23vG-.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CKaMLuk0.js → Skills-DGZXz22K.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DYCNJF8h.js → Sla-DL_urP9A.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-6x-hUNQ_.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-B6tW8dVO.js → SyncSettings-Crk5TCXq.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-oNC_2NVz.js → Tasks-rZGq5PZs.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DvLPvvjL.js → Templates-DT5el8Q8.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BDt9UBn_.js → Tenant-mlgeyMK8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-ddCUnH-X.js → Terminal-C8HEA-lM.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CA4sDL47.js → TimelineRenderer-DvlqBayB.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-By03hnVs.js → Tokens-C7lDcNmT.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dz2mH-ZU.js → Trigger-CM8nCnIJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DShSWHeJ.js → Trust-BQDLUQ9x.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-JN8lP3gt.js → UkeySign-CVqM0OFE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BgPy4gV3.js → VideoEditing-BvAcQpzT.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CQGEsXw_.js → Wallet-zFVFaAZ2.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-D8xR-ds_.js → WebAuthn-Cc522ums.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Df61_p1p.js → WorkflowEditor-Bg_wxpDo.js} +1 -1
- package/src/assets/web-panel/assets/{chat-fdPby427.js → chat-jg0QwC8K.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Dimqhufo.js → colors-ALzYz1gO.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DHayKcqZ.js → compact-item-DXHYUoqi.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-hZUhypRH.js → createContext-BsGsBM5m.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Dy1GJA4S.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dw7zC_dV.js → hasIn-BccMLzxA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIxxvaGN.js → index-08diKcTm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVHdkz8F.js → index-2VstcImq.js} +17 -17
- package/src/assets/web-panel/assets/index-8zbMbbuf.js +1 -0
- package/src/assets/web-panel/assets/{index-DAjvkQ4K.js → index-BBbbr1YB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DX1DwDFI.js → index-BTTeAiOP.js} +1 -1
- package/src/assets/web-panel/assets/{index-JE6Udz2r.js → index-BcJzphrP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYdNwhkv.js → index-BdCmJ_bG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTiVrnpR.js → index-BfF0OGKU.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1yMYKi-.js → index-BfV8RDlg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxoPzPHy.js → index-BvsII10i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyEhrvpm.js → index-C0AbXsDy.js} +1 -1
- package/src/assets/web-panel/assets/{index-lPjBQJCy.js → index-C3Rf8wIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CPZ3U5iH.js → index-C3k3ZwkQ.js} +1 -1
- package/src/assets/web-panel/assets/index-C97f71oi.js +1 -0
- package/src/assets/web-panel/assets/{index-DJP7TjWm.js → index-CBVIFX-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSlda4-6.js → index-CJg6Wsjc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzJXkoZs.js → index-CLzPzN75.js} +1 -1
- package/src/assets/web-panel/assets/{index-cEnkyHtX.js → index-CQ1ORdp-.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bhmpp3cm.js → index-CSFESurl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BwnUluxu.js → index-CWLWBgfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIOE8tt6.js → index-CahDve8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-CBQK-LEF.js → index-Cg9cOVe9.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7jQEwB6.js → index-Cmg_Npx5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuMSdwno.js → index-CrZHUsOZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnwDYBnG.js → index-Cz_s25m3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVM_KgzF.js → index-D9oV2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{index--DVpUpz3.js → index-DgjQ-kdT.js} +1 -1
- package/src/assets/web-panel/assets/{index-y5LHQ5Ck.js → index-Dmk7dD-T.js} +1 -1
- package/src/assets/web-panel/assets/{index-Urt2R5aE.js → index-Dxqiu9-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvKHpaao.js → index-JS2LXdd0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CsCo_A0I.js → index-TeCof1s3.js} +1 -1
- package/src/assets/web-panel/assets/{index-ulvBurtw.js → index-UQXedWQz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BM5BNqp9.js → index-koT8Q86Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHJUptSx.js → index-mIQUoUx1.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8XXPz2n.js → index-oWltafBn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC2R4zvM.js → index-rIy96PeM.js} +1 -1
- package/src/assets/web-panel/assets/{index-j0pxzOu7.js → index-rcwgNMc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuaVa8tX.js → index-srqW5_-X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CcpDboUd.js → index-uzykuL_R.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-B4pR3px5.js → initDefaultProps-xafkqrW3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CE7dWPPs.js → motion-BrtyQXLk.js} +1 -1
- package/src/assets/web-panel/assets/{move-Hw8cUObc.js → move-BunUyg59.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CvRjE4Fa.js → mtc-parser-BPwjBeuE.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CkFeLVre.js → omit-BpyqZlNO.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-5ZMqmbbb.js → pickAttrs-DDrV0XjE.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BZ4oeZ0q.js → placementArrow-1A18Pcl2.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-lwoiX_Us.js → responsiveObserve-B8Fnlmso.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D_as37l8.js → slide-x-Fwb2Zm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BsA-4Bs7.js → statusUtils-aAG5yJl_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BVar4sLQ.js → styleChecker-BRQSTMRL.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-j1IwVpGd.js → useFlexGapSupport-D5X_KozY.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-7Q9bEnUp.js → useFs-DN_YIjP-.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BBwTIRMd.js → usePersonalDataHub-BizxAvjw.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C7Dk-M8w.js → vnode-B5oAxN93.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D3UHaTFB.js → zoom-DJhvyzKv.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/team.js +1805 -473
- package/src/data/changelog.json +13 -0
- package/src/harness/worktree-isolator.js +671 -79
- package/src/lib/agent-team/task-lease.js +378 -78
- package/src/lib/agent-team/team-agent-stream.js +488 -0
- package/src/lib/agent-team/team-budget.js +216 -20
- package/src/lib/agent-team/team-mailbox.js +389 -15
- package/src/lib/agent-team/team-run-state-lock.js +209 -0
- package/src/lib/agent-team/team-runner.js +547 -63
- package/src/lib/agent-team/team-scope-lock.js +409 -0
- package/src/lib/agent-team/team-task-contract.js +277 -0
- package/src/lib/agent-team/team-worktree.js +1171 -62
- package/src/lib/agent-worktree.js +19 -20
- package/src/lib/collaboration-run-store.js +983 -68
- package/src/lib/with-file-lock.js +190 -24
- package/src/lib/worktree-isolator.js +3 -0
- package/src/lib/worktree-sparse.js +23 -0
- package/src/runtime/headless-stream.js +6 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B2A_TJtl.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DpLgovdH.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-BvCSU6d2.js +0 -1
- package/src/assets/web-panel/assets/devWarning-ePxoBp92.js +0 -1
- package/src/assets/web-panel/assets/index-BTBDdCoo.js +0 -1
- package/src/assets/web-panel/assets/index-CfOGx8Ns.js +0 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, in-memory scope ownership for large Agent Teams.
|
|
3
|
+
*
|
|
4
|
+
* A lock key (normally a task or agent id) owns one or more repository-relative
|
|
5
|
+
* paths. Directory-prefix overlap is intentionally conservative: owning `src`
|
|
6
|
+
* also owns `src/a.js`. An empty scope list means the whole workspace.
|
|
7
|
+
*
|
|
8
|
+
* The implementation does not inspect the filesystem. That keeps planning,
|
|
9
|
+
* scheduling, persistence validation, and tests deterministic across platforms.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const TEAM_SCOPE_LOCK_ERROR_CODES = Object.freeze({
|
|
13
|
+
INVALID_KEY: "TEAM_SCOPE_LOCK_INVALID_KEY",
|
|
14
|
+
INVALID_SCOPES: "TEAM_SCOPE_LOCK_INVALID_SCOPES",
|
|
15
|
+
INVALID_SCOPE_PATH: "TEAM_SCOPE_LOCK_INVALID_SCOPE_PATH",
|
|
16
|
+
SCOPE_CONFLICT: "TEAM_SCOPE_LOCK_SCOPE_CONFLICT",
|
|
17
|
+
KEY_ALREADY_HELD: "TEAM_SCOPE_LOCK_KEY_ALREADY_HELD",
|
|
18
|
+
NOT_HELD: "TEAM_SCOPE_LOCK_NOT_HELD",
|
|
19
|
+
INVALID_SNAPSHOT: "TEAM_SCOPE_LOCK_INVALID_SNAPSHOT",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const TEAM_SCOPE_LOCK_SNAPSHOT_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
export class TeamScopeLockError extends Error {
|
|
25
|
+
constructor(code, message, details = {}) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "TeamScopeLockError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
Object.assign(this, details);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function codedError(code, message, details = {}) {
|
|
34
|
+
return new TeamScopeLockError(code, message, details);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compareStrings(left, right) {
|
|
38
|
+
if (left < right) return -1;
|
|
39
|
+
if (left > right) return 1;
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeKey(value) {
|
|
44
|
+
if (typeof value !== "string") {
|
|
45
|
+
throw codedError(
|
|
46
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_KEY,
|
|
47
|
+
"scope lock key must be a non-empty string",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const key = value.trim();
|
|
51
|
+
if (!key || key.includes("\0")) {
|
|
52
|
+
throw codedError(
|
|
53
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_KEY,
|
|
54
|
+
"scope lock key must be a non-empty string without NUL bytes",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return key;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Canonicalize a relative POSIX or Windows path to slash-separated POSIX form.
|
|
62
|
+
*
|
|
63
|
+
* Absolute paths, drive-qualified paths, parent traversal, NUL bytes, and paths
|
|
64
|
+
* that normalize to empty are rejected. `.` and duplicate separators are
|
|
65
|
+
* removed without resolving against a working directory.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeTeamScopePath(value) {
|
|
68
|
+
if (typeof value !== "string") {
|
|
69
|
+
throw codedError(
|
|
70
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
71
|
+
"scope path must be a non-empty string",
|
|
72
|
+
{ scope: value },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const input = value.trim();
|
|
77
|
+
if (!input || input.includes("\0")) {
|
|
78
|
+
throw codedError(
|
|
79
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
80
|
+
"scope path must be non-empty and must not contain NUL bytes",
|
|
81
|
+
{ scope: value },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const withoutLeadingPresentationSpace = value.trimStart();
|
|
85
|
+
if (
|
|
86
|
+
withoutLeadingPresentationSpace.length !== input.length &&
|
|
87
|
+
!input.endsWith("/") &&
|
|
88
|
+
!input.endsWith("\\")
|
|
89
|
+
) {
|
|
90
|
+
throw codedError(
|
|
91
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
92
|
+
"scope path must not end in ambiguous whitespace",
|
|
93
|
+
{ scope: value },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Reject POSIX absolute paths, Windows root/UNC paths, and both rooted and
|
|
98
|
+
// drive-relative forms (`C:\x`, `C:/x`, and `C:x`).
|
|
99
|
+
if (
|
|
100
|
+
input.startsWith("/") ||
|
|
101
|
+
input.startsWith("\\") ||
|
|
102
|
+
/^[A-Za-z]:/.test(input)
|
|
103
|
+
) {
|
|
104
|
+
throw codedError(
|
|
105
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
106
|
+
"scope path must be repository-relative",
|
|
107
|
+
{ scope: value },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const parts = [];
|
|
112
|
+
for (const rawPart of input.replaceAll("\\", "/").split("/")) {
|
|
113
|
+
if (!rawPart || rawPart === ".") continue;
|
|
114
|
+
if (rawPart === "..") {
|
|
115
|
+
throw codedError(
|
|
116
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
117
|
+
"scope path must not contain parent traversal",
|
|
118
|
+
{ scope: value },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Use one conservative cross-platform identity. Windows treats case,
|
|
122
|
+
// trailing dots/spaces, ADS colons, and device names specially; rejecting
|
|
123
|
+
// the ambiguous spellings and case-folding everywhere may serialize a few
|
|
124
|
+
// safe Linux paths, but cannot miss a Windows collision.
|
|
125
|
+
if (
|
|
126
|
+
rawPart.endsWith(".") ||
|
|
127
|
+
rawPart.endsWith(" ") ||
|
|
128
|
+
rawPart.includes(":") ||
|
|
129
|
+
/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i.test(rawPart)
|
|
130
|
+
) {
|
|
131
|
+
throw codedError(
|
|
132
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
133
|
+
"scope path contains a cross-platform ambiguous segment",
|
|
134
|
+
{ scope: value },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
parts.push(rawPart.normalize("NFC").toLowerCase());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parts.length === 0) {
|
|
141
|
+
throw codedError(
|
|
142
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPE_PATH,
|
|
143
|
+
"scope path must identify a repository-relative location",
|
|
144
|
+
{ scope: value },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return parts.join("/");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function containsScope(parent, candidate) {
|
|
152
|
+
return candidate === parent || candidate.startsWith(`${parent}/`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Canonicalize, sort, de-duplicate, and compact a scope list.
|
|
157
|
+
*
|
|
158
|
+
* Descendants are redundant when an ancestor is present, so
|
|
159
|
+
* `["src", "src/a.js"]` canonicalizes to `["src"]`. `[]` is preserved and
|
|
160
|
+
* denotes the whole workspace.
|
|
161
|
+
*/
|
|
162
|
+
export function normalizeTeamScopes(scopes = []) {
|
|
163
|
+
if (!Array.isArray(scopes)) {
|
|
164
|
+
throw codedError(
|
|
165
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPES,
|
|
166
|
+
"scopes must be an array of repository-relative paths",
|
|
167
|
+
{ scopes },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sorted = [
|
|
172
|
+
...new Set(scopes.map((scope) => normalizeTeamScopePath(scope))),
|
|
173
|
+
].sort(compareStrings);
|
|
174
|
+
const compact = [];
|
|
175
|
+
for (const scope of sorted) {
|
|
176
|
+
if (compact.some((ancestor) => containsScope(ancestor, scope))) continue;
|
|
177
|
+
compact.push(scope);
|
|
178
|
+
}
|
|
179
|
+
return compact;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether two normalized-or-raw scope lists overlap.
|
|
184
|
+
*
|
|
185
|
+
* Empty scopes mean whole-workspace ownership and therefore overlap every
|
|
186
|
+
* other lock, including another whole-workspace lock.
|
|
187
|
+
*/
|
|
188
|
+
export function teamScopesOverlap(leftScopes, rightScopes) {
|
|
189
|
+
const left = normalizeTeamScopes(leftScopes);
|
|
190
|
+
const right = normalizeTeamScopes(rightScopes);
|
|
191
|
+
if (left.length === 0 || right.length === 0) return true;
|
|
192
|
+
|
|
193
|
+
return left.some((leftScope) =>
|
|
194
|
+
right.some(
|
|
195
|
+
(rightScope) =>
|
|
196
|
+
containsScope(leftScope, rightScope) ||
|
|
197
|
+
containsScope(rightScope, leftScope),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sameScopes(left, right) {
|
|
203
|
+
return (
|
|
204
|
+
left.length === right.length &&
|
|
205
|
+
left.every((scope, index) => scope === right[index])
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function cloneLock(key, scopes) {
|
|
210
|
+
return {
|
|
211
|
+
key,
|
|
212
|
+
scopes: [...scopes],
|
|
213
|
+
workspace: scopes.length === 0,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function failure(error, fallbackCode) {
|
|
218
|
+
if (error instanceof TeamScopeLockError) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
code: error.code,
|
|
222
|
+
message: error.message,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
code: fallbackCode,
|
|
228
|
+
message: error instanceof Error ? error.message : String(error),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export class TeamScopeLock {
|
|
233
|
+
constructor() {
|
|
234
|
+
this._locks = new Map();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate whether `key` can own `scopes` without mutating lock state.
|
|
239
|
+
*
|
|
240
|
+
* Re-acquiring the same canonical scopes under the same key is idempotent.
|
|
241
|
+
* A key cannot silently replace its current ownership with different scopes;
|
|
242
|
+
* callers must release it first.
|
|
243
|
+
*/
|
|
244
|
+
canAcquire(key, scopes = []) {
|
|
245
|
+
let canonicalKey;
|
|
246
|
+
let canonicalScopes;
|
|
247
|
+
try {
|
|
248
|
+
canonicalKey = normalizeKey(key);
|
|
249
|
+
canonicalScopes = normalizeTeamScopes(scopes);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return failure(error, TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SCOPES);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const current = this._locks.get(canonicalKey);
|
|
255
|
+
if (current) {
|
|
256
|
+
if (sameScopes(current, canonicalScopes)) {
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
key: canonicalKey,
|
|
260
|
+
scopes: [...canonicalScopes],
|
|
261
|
+
alreadyHeld: true,
|
|
262
|
+
conflicts: [],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
code: TEAM_SCOPE_LOCK_ERROR_CODES.KEY_ALREADY_HELD,
|
|
268
|
+
message: `scope lock key "${canonicalKey}" already owns different scopes`,
|
|
269
|
+
key: canonicalKey,
|
|
270
|
+
scopes: [...canonicalScopes],
|
|
271
|
+
heldScopes: [...current],
|
|
272
|
+
conflicts: [],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const conflicts = [];
|
|
277
|
+
for (const [heldKey, heldScopes] of this._locks) {
|
|
278
|
+
if (teamScopesOverlap(canonicalScopes, heldScopes)) {
|
|
279
|
+
conflicts.push(cloneLock(heldKey, heldScopes));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
conflicts.sort((left, right) => compareStrings(left.key, right.key));
|
|
283
|
+
|
|
284
|
+
if (conflicts.length > 0) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
code: TEAM_SCOPE_LOCK_ERROR_CODES.SCOPE_CONFLICT,
|
|
288
|
+
message: `requested scopes conflict with ${conflicts.length} active lock(s)`,
|
|
289
|
+
key: canonicalKey,
|
|
290
|
+
scopes: [...canonicalScopes],
|
|
291
|
+
conflicts,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
key: canonicalKey,
|
|
298
|
+
scopes: [...canonicalScopes],
|
|
299
|
+
alreadyHeld: false,
|
|
300
|
+
conflicts: [],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
acquire(key, scopes = []) {
|
|
305
|
+
const check = this.canAcquire(key, scopes);
|
|
306
|
+
if (!check.ok) return check;
|
|
307
|
+
if (check.alreadyHeld) {
|
|
308
|
+
return {
|
|
309
|
+
...check,
|
|
310
|
+
acquired: false,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this._locks.set(check.key, [...check.scopes]);
|
|
315
|
+
return {
|
|
316
|
+
...check,
|
|
317
|
+
acquired: true,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
release(key) {
|
|
322
|
+
let canonicalKey;
|
|
323
|
+
try {
|
|
324
|
+
canonicalKey = normalizeKey(key);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return failure(error, TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_KEY);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const scopes = this._locks.get(canonicalKey);
|
|
330
|
+
if (!scopes) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
code: TEAM_SCOPE_LOCK_ERROR_CODES.NOT_HELD,
|
|
334
|
+
message: `scope lock key "${canonicalKey}" does not own a lock`,
|
|
335
|
+
key: canonicalKey,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this._locks.delete(canonicalKey);
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
released: true,
|
|
343
|
+
...cloneLock(canonicalKey, scopes),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
status() {
|
|
348
|
+
const locks = [...this._locks]
|
|
349
|
+
.sort(([left], [right]) => compareStrings(left, right))
|
|
350
|
+
.map(([key, scopes]) => cloneLock(key, scopes));
|
|
351
|
+
return {
|
|
352
|
+
count: locks.length,
|
|
353
|
+
locks,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
snapshot() {
|
|
358
|
+
return {
|
|
359
|
+
version: TEAM_SCOPE_LOCK_SNAPSHOT_VERSION,
|
|
360
|
+
locks: this.status().locks.map(({ key, scopes }) => ({
|
|
361
|
+
key,
|
|
362
|
+
scopes,
|
|
363
|
+
})),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Restore only snapshots that could have been produced by a valid lock set.
|
|
369
|
+
* Corrupt entries, duplicate keys, and mutually-overlapping owners fail
|
|
370
|
+
* closed with one stable snapshot error code.
|
|
371
|
+
*/
|
|
372
|
+
static restore(snapshot) {
|
|
373
|
+
if (
|
|
374
|
+
!snapshot ||
|
|
375
|
+
typeof snapshot !== "object" ||
|
|
376
|
+
Array.isArray(snapshot) ||
|
|
377
|
+
snapshot.version !== TEAM_SCOPE_LOCK_SNAPSHOT_VERSION ||
|
|
378
|
+
!Array.isArray(snapshot.locks)
|
|
379
|
+
) {
|
|
380
|
+
throw codedError(
|
|
381
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SNAPSHOT,
|
|
382
|
+
"invalid team scope lock snapshot",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const restored = new TeamScopeLock();
|
|
387
|
+
for (const entry of snapshot.locks) {
|
|
388
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
389
|
+
throw codedError(
|
|
390
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SNAPSHOT,
|
|
391
|
+
"team scope lock snapshot contains an invalid lock entry",
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const result = restored.acquire(entry.key, entry.scopes);
|
|
396
|
+
if (!result.ok || result.alreadyHeld) {
|
|
397
|
+
throw codedError(
|
|
398
|
+
TEAM_SCOPE_LOCK_ERROR_CODES.INVALID_SNAPSHOT,
|
|
399
|
+
"team scope lock snapshot contains invalid or conflicting ownership",
|
|
400
|
+
{
|
|
401
|
+
causeCode:
|
|
402
|
+
result.code ?? TEAM_SCOPE_LOCK_ERROR_CODES.KEY_ALREADY_HELD,
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return restored;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure task-level contract resolution for `cc team`.
|
|
3
|
+
*
|
|
4
|
+
* A task may request execution settings in `task.agent` or `task.policy`.
|
|
5
|
+
* Policy has precedence over agent when both declare the same field. The
|
|
6
|
+
* parent contract is always the security/resource ceiling:
|
|
7
|
+
*
|
|
8
|
+
* - permission mode may only become more restrictive;
|
|
9
|
+
* - numeric budgets may only become smaller;
|
|
10
|
+
* - required checkpoint/worktree isolation may not be disabled.
|
|
11
|
+
*
|
|
12
|
+
* Invalid child values inherit the parent value. No I/O, clocks, environment
|
|
13
|
+
* reads, or mutation are performed here, so the resolver is safe to share
|
|
14
|
+
* between planning, execution, resume validation, and UI projection.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
SUBAGENT_PERMISSION_MODES,
|
|
19
|
+
capBudget,
|
|
20
|
+
tightenPermissionMode,
|
|
21
|
+
} from "../subagent-contract.js";
|
|
22
|
+
|
|
23
|
+
const hasOwn = (value, key) =>
|
|
24
|
+
value != null && Object.prototype.hasOwnProperty.call(value, key);
|
|
25
|
+
|
|
26
|
+
const asObject = (value) =>
|
|
27
|
+
value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
28
|
+
|
|
29
|
+
function finitePositiveNumber(value) {
|
|
30
|
+
const number = Number(value);
|
|
31
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function finitePositiveInteger(value) {
|
|
35
|
+
const number = Number(value);
|
|
36
|
+
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeModel(value) {
|
|
40
|
+
if (typeof value !== "string") return null;
|
|
41
|
+
const model = value.trim();
|
|
42
|
+
return model || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeRequired(value) {
|
|
46
|
+
return value === true || value === false ? value : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Pick a task request. `task.policy` deliberately wins over `task.agent`.
|
|
51
|
+
* An explicit-but-invalid policy value therefore inherits the parent instead
|
|
52
|
+
* of falling through to a less-authoritative agent declaration.
|
|
53
|
+
*/
|
|
54
|
+
function requestedField(task, field) {
|
|
55
|
+
const policy = asObject(task?.policy);
|
|
56
|
+
if (hasOwn(policy, field)) {
|
|
57
|
+
return { specified: true, source: "task.policy", raw: policy[field] };
|
|
58
|
+
}
|
|
59
|
+
const agent = asObject(task?.agent);
|
|
60
|
+
if (hasOwn(agent, field)) {
|
|
61
|
+
return { specified: true, source: "task.agent", raw: agent[field] };
|
|
62
|
+
}
|
|
63
|
+
return { specified: false, source: null, raw: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addAdjustment(adjustments, request, field, effective, reason) {
|
|
67
|
+
adjustments.push({
|
|
68
|
+
field,
|
|
69
|
+
source: request.source,
|
|
70
|
+
requested: request.raw,
|
|
71
|
+
effective,
|
|
72
|
+
reason,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve one task's effective execution contract.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} [options]
|
|
80
|
+
* @param {object} [options.parent] Trusted run/team defaults and ceilings.
|
|
81
|
+
* @param {object} [options.task] Task containing optional `agent` / `policy`.
|
|
82
|
+
* @returns {{
|
|
83
|
+
* permissionMode:string,
|
|
84
|
+
* maxTurns:number|null,
|
|
85
|
+
* maxBudgetUsd:number|null,
|
|
86
|
+
* maxTokens:number|null,
|
|
87
|
+
* maxWallMs:number|null,
|
|
88
|
+
* checkpointRequired:boolean,
|
|
89
|
+
* worktreeRequired:boolean,
|
|
90
|
+
* model:string|null,
|
|
91
|
+
* adjustments:Array<{
|
|
92
|
+
* field:string,
|
|
93
|
+
* source:string,
|
|
94
|
+
* requested:unknown,
|
|
95
|
+
* effective:unknown,
|
|
96
|
+
* reason:string
|
|
97
|
+
* }>
|
|
98
|
+
* }}
|
|
99
|
+
*/
|
|
100
|
+
export function resolveTeamTaskContract({ parent = {}, task = {} } = {}) {
|
|
101
|
+
const defaults = asObject(parent);
|
|
102
|
+
const work = asObject(task);
|
|
103
|
+
const adjustments = [];
|
|
104
|
+
|
|
105
|
+
const parentPermission = tightenPermissionMode(defaults.permissionMode, null);
|
|
106
|
+
const permissionRequest = requestedField(work, "permissionMode");
|
|
107
|
+
const requestedPermission =
|
|
108
|
+
permissionRequest.specified &&
|
|
109
|
+
SUBAGENT_PERMISSION_MODES.includes(permissionRequest.raw)
|
|
110
|
+
? permissionRequest.raw
|
|
111
|
+
: null;
|
|
112
|
+
const permissionMode = tightenPermissionMode(
|
|
113
|
+
parentPermission,
|
|
114
|
+
requestedPermission,
|
|
115
|
+
);
|
|
116
|
+
if (permissionRequest.specified) {
|
|
117
|
+
if (requestedPermission == null) {
|
|
118
|
+
addAdjustment(
|
|
119
|
+
adjustments,
|
|
120
|
+
permissionRequest,
|
|
121
|
+
"permissionMode",
|
|
122
|
+
permissionMode,
|
|
123
|
+
"invalid-value-inherited",
|
|
124
|
+
);
|
|
125
|
+
} else if (permissionMode !== requestedPermission) {
|
|
126
|
+
addAdjustment(
|
|
127
|
+
adjustments,
|
|
128
|
+
permissionRequest,
|
|
129
|
+
"permissionMode",
|
|
130
|
+
permissionMode,
|
|
131
|
+
"parent-permission-ceiling",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parentMaxTurns = finitePositiveInteger(defaults.maxTurns);
|
|
137
|
+
const turnsRequest = requestedField(work, "maxTurns");
|
|
138
|
+
const requestedMaxTurns = turnsRequest.specified
|
|
139
|
+
? finitePositiveInteger(turnsRequest.raw)
|
|
140
|
+
: null;
|
|
141
|
+
const maxTurns =
|
|
142
|
+
requestedMaxTurns == null
|
|
143
|
+
? parentMaxTurns
|
|
144
|
+
: parentMaxTurns == null
|
|
145
|
+
? requestedMaxTurns
|
|
146
|
+
: Math.min(parentMaxTurns, requestedMaxTurns);
|
|
147
|
+
if (turnsRequest.specified) {
|
|
148
|
+
if (requestedMaxTurns == null) {
|
|
149
|
+
addAdjustment(
|
|
150
|
+
adjustments,
|
|
151
|
+
turnsRequest,
|
|
152
|
+
"maxTurns",
|
|
153
|
+
maxTurns,
|
|
154
|
+
"invalid-value-inherited",
|
|
155
|
+
);
|
|
156
|
+
} else if (maxTurns !== requestedMaxTurns) {
|
|
157
|
+
addAdjustment(
|
|
158
|
+
adjustments,
|
|
159
|
+
turnsRequest,
|
|
160
|
+
"maxTurns",
|
|
161
|
+
maxTurns,
|
|
162
|
+
"parent-budget-ceiling",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const budgetFields = [
|
|
168
|
+
{
|
|
169
|
+
field: "maxBudgetUsd",
|
|
170
|
+
budgetKey: "costUsd",
|
|
171
|
+
normalize: finitePositiveNumber,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
field: "maxTokens",
|
|
175
|
+
budgetKey: "tokens",
|
|
176
|
+
normalize: finitePositiveInteger,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
field: "maxWallMs",
|
|
180
|
+
budgetKey: "timeMs",
|
|
181
|
+
normalize: finitePositiveInteger,
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
const parentBudget = {};
|
|
185
|
+
const requestedBudget = {};
|
|
186
|
+
const budgetRequests = new Map();
|
|
187
|
+
for (const spec of budgetFields) {
|
|
188
|
+
parentBudget[spec.budgetKey] = spec.normalize(defaults[spec.field]);
|
|
189
|
+
const request = requestedField(work, spec.field);
|
|
190
|
+
budgetRequests.set(spec.field, request);
|
|
191
|
+
requestedBudget[spec.budgetKey] = request.specified
|
|
192
|
+
? spec.normalize(request.raw)
|
|
193
|
+
: null;
|
|
194
|
+
}
|
|
195
|
+
const effectiveBudget = capBudget(requestedBudget, parentBudget) || {
|
|
196
|
+
tokens: null,
|
|
197
|
+
costUsd: null,
|
|
198
|
+
timeMs: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
for (const spec of budgetFields) {
|
|
202
|
+
const request = budgetRequests.get(spec.field);
|
|
203
|
+
if (!request.specified) continue;
|
|
204
|
+
const normalized = requestedBudget[spec.budgetKey];
|
|
205
|
+
const effective = effectiveBudget[spec.budgetKey] ?? null;
|
|
206
|
+
if (normalized == null) {
|
|
207
|
+
addAdjustment(
|
|
208
|
+
adjustments,
|
|
209
|
+
request,
|
|
210
|
+
spec.field,
|
|
211
|
+
effective,
|
|
212
|
+
"invalid-value-inherited",
|
|
213
|
+
);
|
|
214
|
+
} else if (effective !== normalized) {
|
|
215
|
+
addAdjustment(
|
|
216
|
+
adjustments,
|
|
217
|
+
request,
|
|
218
|
+
spec.field,
|
|
219
|
+
effective,
|
|
220
|
+
"parent-budget-ceiling",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const resolveRequirement = (field) => {
|
|
226
|
+
const parentRequired = normalizeRequired(defaults[field]) === true;
|
|
227
|
+
const request = requestedField(work, field);
|
|
228
|
+
const requested = request.specified ? normalizeRequired(request.raw) : null;
|
|
229
|
+
const effective =
|
|
230
|
+
requested == null ? parentRequired : parentRequired || requested;
|
|
231
|
+
if (request.specified) {
|
|
232
|
+
if (requested == null) {
|
|
233
|
+
addAdjustment(
|
|
234
|
+
adjustments,
|
|
235
|
+
request,
|
|
236
|
+
field,
|
|
237
|
+
effective,
|
|
238
|
+
"invalid-value-inherited",
|
|
239
|
+
);
|
|
240
|
+
} else if (parentRequired && requested === false) {
|
|
241
|
+
addAdjustment(adjustments, request, field, true, "parent-requirement");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return effective;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const checkpointRequired = resolveRequirement("checkpointRequired");
|
|
248
|
+
const worktreeRequired = resolveRequirement("worktreeRequired");
|
|
249
|
+
|
|
250
|
+
const parentModel = normalizeModel(defaults.model);
|
|
251
|
+
const modelRequest = requestedField(work, "model");
|
|
252
|
+
const requestedModel = modelRequest.specified
|
|
253
|
+
? normalizeModel(modelRequest.raw)
|
|
254
|
+
: null;
|
|
255
|
+
const model = requestedModel ?? parentModel;
|
|
256
|
+
if (modelRequest.specified && requestedModel == null) {
|
|
257
|
+
addAdjustment(
|
|
258
|
+
adjustments,
|
|
259
|
+
modelRequest,
|
|
260
|
+
"model",
|
|
261
|
+
model,
|
|
262
|
+
"invalid-value-inherited",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
permissionMode,
|
|
268
|
+
maxTurns,
|
|
269
|
+
maxBudgetUsd: effectiveBudget.costUsd ?? null,
|
|
270
|
+
maxTokens: effectiveBudget.tokens ?? null,
|
|
271
|
+
maxWallMs: effectiveBudget.timeMs ?? null,
|
|
272
|
+
checkpointRequired,
|
|
273
|
+
worktreeRequired,
|
|
274
|
+
model,
|
|
275
|
+
adjustments,
|
|
276
|
+
};
|
|
277
|
+
}
|