chainlesschain 0.162.154 → 0.162.156
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/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CIpoXyVI.js → AIOps-DjBXlypQ.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Ch_QXIPk.js → ActionButton-Cv9er9bp.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CvaBYShv.js → Analytics-CLKDjSNs.js} +3 -3
- package/src/assets/web-panel/assets/AppLayout-CfyZNzmJ.js +9 -0
- package/src/assets/web-panel/assets/{Audit-Bng8eHCW.js → Audit-CXafD178.js} +1 -1
- package/src/assets/web-panel/assets/BackgroundAgents-CP5c-GSd.js +1 -0
- package/src/assets/web-panel/assets/BackgroundAgents-Dq2340Kl.css +1 -0
- package/src/assets/web-panel/assets/{Backup-DvBs8-lm.js → Backup-BbYLQfyt.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DKwajm0g.js → BaseInput-Bqe0MwZL.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BoGpZ3uO.js → Chat-jNA_yUPH.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-mDQl_PXO.js → ChatBubbleRenderer-BOFLF01s.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BMJEmxFQ.js → Checkbox-Dlzz-zD1.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-Bsoag5CV.js → Codegen-BAsDQjGf.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CDjH9-sp.js → Col-Ju0HrAuS.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Cg2QHPQ6.js → Community-DynJd43Z.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BU2BaqP-.js → Compact-CabbZVww.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-D6ZYMCqZ.js → Compliance-DW4BmAoh.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DaZ8VW-I.js → Cowork-Bk1S_7X-.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-DQY1RH9v.js → Cron-By4sh_NM.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BVR28vk9.js → Crosschain-DdZdbS1C.js} +1 -1
- package/src/assets/web-panel/assets/{DID-N44KvTP-.js → DID-CkL-sQyS.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Bjhlx6wS.js → Dashboard-g_mOpumg.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CpLYxpy1.js → Dropdown-DtHyE7Ao.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-srEITSPI.js → EmailListRenderer-Kpvov5Wr.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-juOC_fX2.js → FamilyGuardDashboard-C4kZoePR.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-nRaQtaXW.js → Federation-BAwWoK2N.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DflhTj09.js → FormItemContext-DeI0i5f8.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DulI_3NI.js → GenericCardRenderer-CkcKRtos.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BxOPefXn.js → Git-kX6jjI9y.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-C6G_Wnwi.js → Governance-BXJCBm6x.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BnFYa2BF.js → Inference-ujD2JSKo.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-TqQLEabm.js → KnowledgeGraph-3Dnuf1Wh.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BH4AVh7s.js → Logs-lVAZCtUB.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DoWLTaEE.js → Marketplace-qnzNPm-A.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D6yC7OG_.js → McpTools-CqjzefRU.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-DLCeHj21.js → Memory-BFiPAxk6.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BZBE0GRl.js → MobileBridge-D0O43VwF.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DJA15G1J.js → MobileProjects-B3now48t.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CyTKJDHn.js → Mtc-DybhtaCS.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-DZKjgEVc.js → MtcAudit-D7MmY4F0.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-2rMPNhSO.js → Multisig-wZvGJnB8.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-VeMk4Fx-.js → NLProgramming-BNPpgfvn.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-jr2GHMhh.js → Notes-B6FS3KBj.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-WN9VZyFb.js → NotificationSettings-CK0yla9U.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CXfPHIpy.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DdjVHuzT.js → Organization-geqgwN1T.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-l8vBsGGq.js → Overflow-BWJYZFbC.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-BlgL9440.js → OverrideContext-C0w0P4A3.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CVQDHHwq.js → P2P-DnU1505F.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CTsNh8qE.js → PdhVaultBrowser-BGY74KnC.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DLW87uyK.js → Permissions-CgArrWtQ.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-N8OvASTx.js → PersonalDataHub-C2vYE61a.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BPZmdlz2.js → Pipeline-CBYLI4yW.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-MFZw_GNh.js → Privacy-B-PdW9aN.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-zW0pEsfU.js → ProjectInit-CuWQegbV.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CppVHr_o.js → ProjectSettings-4d32m_cS.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BTglkJO2.js → Projects-VLA7ttwd.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CPHZ6JKT.js → Providers-B_Izb_oa.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DO4klNZ2.js → QrScannerModal-C6B_ndXV.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B37TTqUa.js → QuickAsk-C6tavL-y.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-n7KIfK7y.js → Recommend-CF-LaZ6x.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-SjVoS9mm.js → RemoteSession-DBqTTQ9_.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Dt6RTeHi.js → Reputation-B01FALUi.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dty6jz5V.js → Row-DO0fd48A.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DDLhqw5j.js → RssFeed-BV5LDzFR.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DrYbXi-W.js → Search-CTti4_MP.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Fc9Z6fbB.js → Security-DwSrWRCw.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BAhPgjSM.js → Services-BEf1QyM4.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-8JF7EPfE.js → Skeleton-LZXTpovy.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DL39dY0t.js → Skills-SKgc0Zug.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CSvjcATc.js → Sla-C_QVOtkN.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Demx7Gnx.js → SpeechSettings-v5TW2V-X.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CKLnRWJr.js → SyncSettings-CQPjHdqU.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DCd-uYcI.js +1 -0
- package/src/assets/web-panel/assets/{Templates-AjQI5rxD.js → Templates-62VDIjtj.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-dbtEh1yR.js → Tenant-BnFQ0pNy.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DX-9jjfk.js → Terminal-D3v0MGSj.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-C11EDZRt.js → TimelineRenderer-BI1Ka-cu.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-GiA9fiZn.js → Tokens-BwX1jXYP.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BPcnclJn.js → Trigger-CSA8TqNx.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-C-DVY8nq.js → Trust-Bn3IjCx7.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DxodSwME.js → UkeySign-pUbOAqNi.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-D_WV0O8O.js → VideoEditing-Db2RercF.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DSeNlVSU.js → Wallet-CpR1BVAl.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Bwuuvpz1.js → WebAuthn-Yxtkn7Zp.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-8M96EU1N.js → WorkflowEditor-CYLe9a2E.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D5S6SmLm.js → chat-gsOGdak0.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-DVfhbsdP.js → collapseMotion-CqM1QOwM.js} +1 -1
- package/src/assets/web-panel/assets/{colors-5fbnGkAP.js → colors-D9fN299a.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DaSuuMlW.js → compact-item-vkd7s-ay.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BNOHcXAF.js → createContext-C7YkbMyy.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DEcR2h6k.js +1 -0
- package/src/assets/web-panel/assets/{echarts-CL0F2SnX.js → echarts-C61Vn3mP.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-DONwZUra.js → hasIn-ig7reRSU.js} +1 -1
- package/src/assets/web-panel/assets/{icons-BsDmGpcT.js → icons-DEmcnxBf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJp4kRjw.js → index-9TxdMYju.js} +1 -1
- package/src/assets/web-panel/assets/{index-MOgO5Yvx.js → index-B7QRpKif.js} +1 -1
- package/src/assets/web-panel/assets/index-BEibanjg.js +1 -0
- package/src/assets/web-panel/assets/{index-CAYmc5Nh.js → index-BHHN_KTS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcWMZqb4.js → index-BMw48d19.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_yDD9Lm.js → index-BYe9HRSt.js} +1 -1
- package/src/assets/web-panel/assets/{index-xo2f4DZR.js → index-BfpVcXKa.js} +1 -1
- package/src/assets/web-panel/assets/{index-DbdQBn4n.js → index-BhftELHI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dd7xwGcD.js → index-BkP17Z95.js} +1 -1
- package/src/assets/web-panel/assets/{index-RkInlwV0.js → index-BpGlLbRv.js} +1 -1
- package/src/assets/web-panel/assets/{index-DM3Ug61e.js → index-Bt-9ZhIu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrkCFohi.js → index-BvDEukXe.js} +1 -1
- package/src/assets/web-panel/assets/{index-_6S41ikV.js → index-C7AY39sg.js} +1 -1
- package/src/assets/web-panel/assets/{index-uR5KTNZA.js → index-C8HZMygt.js} +1 -1
- package/src/assets/web-panel/assets/{index-5_fj5gVd.js → index-CAkz1f1r.js} +1 -1
- package/src/assets/web-panel/assets/{index-BLzMuz38.js → index-CDWTycrg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEO5f7Vf.js → index-CGzGe5tv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEzd5m5a.js → index-Co2NYyew.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcqOK7Lv.js → index-CspGymmg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DP2-3A_j.js → index-D2yhtk6a.js} +1 -1
- package/src/assets/web-panel/assets/{index-LRIt-AFl.js → index-D6s_kbjt.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4L3gsNA.js → index-DFM1oNe1.js} +1 -1
- package/src/assets/web-panel/assets/{index-KwV9g_cf.js → index-DFfxF7Vt.js} +1 -1
- package/src/assets/web-panel/assets/{index-BK2N5yVW.js → index-DPguq3X7.js} +4 -4
- package/src/assets/web-panel/assets/{index-Cf2OCa8q.js → index-DQ8U7kzC.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7gs_3Ck.js → index-DZAxCnjS.js} +1 -1
- package/src/assets/web-panel/assets/{index-PsMErAB1.js → index-DhdXbsXX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTZeOnzO.js → index-DuwbKHoS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dm5A-MN5.js → index-DvCJXmrx.js} +1 -1
- package/src/assets/web-panel/assets/{index-vYtprlNs.js → index-Dy_TKZ9h.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHx2lhHW.js → index-FzGr6CVk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cn5LtyIH.js → index-PkH39qsp.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmUOjyL6.js → index-SDrcQXO1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CztESwB4.js → index-W52sHRdr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSfYJqXt.js → index-bVbgYBhv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrAJSu-v.js → index-gj8kxcAi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLXf-8ek.js → index-gpurplyq.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClGqO-2X.js → index-nX8YE6cj.js} +1 -1
- package/src/assets/web-panel/assets/index-z_25tU3p.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-CxwLynp9.js → initDefaultProps-BrgfVRg9.js} +1 -1
- package/src/assets/web-panel/assets/{motion-XbPvGJbW.js → motion-D_3xmwac.js} +1 -1
- package/src/assets/web-panel/assets/{move-D1u5pcgS.js → move-DDDJdlxw.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BDxhTBMH.js → mtc-parser-BK2uEWwn.js} +1 -1
- package/src/assets/web-panel/assets/{omit-EtIFGg5k.js → omit-BPHKbDcf.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D33sWb0l.js → pickAttrs-LNnTscuz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BwFRzaGo.js → placementArrow-CvmTZll5.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CBk2WqvQ.js → responsiveObserve-eBj7qi2J.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B52HtrKn.js → slide-D68hHkrM.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D4qxqWnH.js → statusUtils-daTlaeo0.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BxaCFlOL.js → styleChecker-DtyKMtHB.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-ekFwFs9h.js +1 -0
- package/src/assets/web-panel/assets/{useFs-C6VAU1_6.js → useFs-jRaSEoF4.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-Dc9sO2d7.js → useMergedState-DXbJa_zE.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-rEG6oXG8.js → usePersonalDataHub-DD5nGzjA.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-CAeFRzPf.js → useRefs-BrCfMLMc.js} +1 -1
- package/src/assets/web-panel/assets/{useState-LwExUYDO.js → useState-CnxtbLoC.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-BMxUs6UX.js → vendor-Bn1JzzAn.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DOTafkEi.js → vnode-KcgLUiom.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BuwwN5K2.js → zoom-Cyk2PQ-s.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/command-manifest.json +50 -1
- package/src/commands/agenda.js +228 -0
- package/src/commands/agent.js +23 -2
- package/src/commands/agents.js +62 -1
- package/src/commands/auto-mode.js +87 -0
- package/src/commands/background-session.js +499 -0
- package/src/commands/batch.js +345 -0
- package/src/commands/command.js +4 -1
- package/src/commands/permissions.js +49 -0
- package/src/commands/remote-control.js +407 -0
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +337 -0
- package/src/gateways/ws/message-dispatcher.js +10 -0
- package/src/gateways/ws/remote-session-protocol.js +102 -1
- package/src/gateways/ws/ws-server.js +61 -0
- package/src/harness/mcp-client.js +8 -0
- package/src/index.js +22 -0
- package/src/lib/agent-batch.js +235 -0
- package/src/lib/agent-notify.js +66 -0
- package/src/lib/agent-schedule-store.js +330 -0
- package/src/lib/auto-mode-config.js +396 -0
- package/src/lib/background-agent-supervisor.js +202 -17
- package/src/lib/background-session-transport.js +315 -0
- package/src/lib/permission-denial-store.js +159 -0
- package/src/lib/remote-approval-bridge.js +360 -0
- package/src/lib/remote-control.js +270 -0
- package/src/lib/repl-denials.js +26 -0
- package/src/lib/shell-approval.js +4 -0
- package/src/lib/ws-rpc-client.js +150 -0
- package/src/repl/agent-repl.js +208 -16
- package/src/repl/permission-tier.js +31 -0
- package/src/runtime/agent-core.js +161 -0
- package/src/runtime/coding-agent-contract-shared.cjs +104 -0
- package/src/runtime/coding-agent-policy.cjs +28 -0
- package/src/runtime/headless-runner.js +126 -8
- package/src/runtime/headless-stream.js +87 -1
- package/src/runtime/mcp-config.js +11 -0
- package/src/runtime/mcp-tool-search.js +0 -0
- package/src/runtime/policies/agent-policy.js +17 -0
- package/src/workers/background-agent-worker.js +218 -37
- package/src/assets/web-panel/assets/AppLayout-B6Q6olcr.js +0 -9
- package/src/assets/web-panel/assets/OrderTableRenderer-CjNO-e9T.js +0 -1
- package/src/assets/web-panel/assets/Tasks-mhITRxjv.js +0 -1
- package/src/assets/web-panel/assets/devWarning-K_woVElh.js +0 -1
- package/src/assets/web-panel/assets/index-3ILvaBpo.js +0 -1
- package/src/assets/web-panel/assets/index-JpB-FoZq.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-B1lWlqXN.js +0 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-batch — dynamic worktree batch core (gap-analysis 第四阶段 #4).
|
|
3
|
+
*
|
|
4
|
+
* Takes a large task already decomposed into independent UNITS and runs each in
|
|
5
|
+
* its OWN git worktree, in parallel (bounded concurrency), then aggregates the
|
|
6
|
+
* outcome: per-unit agent status, test result, diff stat, and — on integration
|
|
7
|
+
* — a merge-conflict preview against base. This is the `/batch` shape from
|
|
8
|
+
* Claude Code, built on the same worktree isolation `cc team --worktree` uses.
|
|
9
|
+
*
|
|
10
|
+
* Pure over injected deps (worktree lifecycle, agent/test spawns, git) so the
|
|
11
|
+
* whole fan-out + aggregation is unit-testable without a real repo or LLM.
|
|
12
|
+
*
|
|
13
|
+
* A `unit` is `{ key, prompt, test? }`. Concurrency is a simple worker pool.
|
|
14
|
+
* Integration is SEQUENTIAL (like TeamWorktreeCoordinator.integrate) so a later
|
|
15
|
+
* branch that conflicts with an already-merged one is reported, not clobbered.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Run `items` through `worker` with at most `limit` in flight; preserves order. */
|
|
19
|
+
async function mapPool(items, limit, worker) {
|
|
20
|
+
const results = new Array(items.length);
|
|
21
|
+
let next = 0;
|
|
22
|
+
const runners = new Array(Math.max(1, Math.min(limit, items.length)))
|
|
23
|
+
.fill(0)
|
|
24
|
+
.map(async () => {
|
|
25
|
+
while (true) {
|
|
26
|
+
const i = next++;
|
|
27
|
+
if (i >= items.length) return;
|
|
28
|
+
results[i] = await worker(items[i], i);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
await Promise.all(runners);
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeUnits(units) {
|
|
36
|
+
if (!Array.isArray(units) || units.length === 0) {
|
|
37
|
+
throw new Error("batch requires a non-empty `units` array");
|
|
38
|
+
}
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
return units.map((u, i) => {
|
|
41
|
+
const key = String(u.key || `unit-${i + 1}`);
|
|
42
|
+
if (seen.has(key)) throw new Error(`duplicate unit key "${key}"`);
|
|
43
|
+
seen.add(key);
|
|
44
|
+
if (!u.prompt || typeof u.prompt !== "string") {
|
|
45
|
+
throw new Error(`unit "${key}" has no prompt`);
|
|
46
|
+
}
|
|
47
|
+
return { key, prompt: u.prompt, test: u.test || null };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} opts
|
|
53
|
+
* @param {Array<{key,prompt,test?}>} opts.units
|
|
54
|
+
* @param {number} [opts.concurrency=4]
|
|
55
|
+
* @param {string} [opts.test] default test command for units without one
|
|
56
|
+
* @param {boolean} [opts.merge] merge clean branches back to base
|
|
57
|
+
* @param {(msg:object)=>void} [opts.onEvent] progress callback
|
|
58
|
+
* @param {object} deps injected effectful ops (see command wiring)
|
|
59
|
+
* @returns {Promise<{units:Array, summary:object}>}
|
|
60
|
+
*/
|
|
61
|
+
export async function runBatch(opts, deps) {
|
|
62
|
+
const units = normalizeUnits(opts.units);
|
|
63
|
+
const concurrency = Math.max(1, Number(opts.concurrency) || 4);
|
|
64
|
+
const onEvent = opts.onEvent || (() => {});
|
|
65
|
+
const {
|
|
66
|
+
createWorktree,
|
|
67
|
+
removeWorktree,
|
|
68
|
+
runAgent,
|
|
69
|
+
runTest,
|
|
70
|
+
diffStat,
|
|
71
|
+
commit,
|
|
72
|
+
previewMerge,
|
|
73
|
+
mergeBranch,
|
|
74
|
+
branchFor = (key) => `batch/${key}`,
|
|
75
|
+
} = deps;
|
|
76
|
+
|
|
77
|
+
onEvent({ type: "batch:start", units: units.length, concurrency });
|
|
78
|
+
|
|
79
|
+
const results = await mapPool(units, concurrency, async (unit) => {
|
|
80
|
+
const branch = branchFor(unit.key);
|
|
81
|
+
const record = {
|
|
82
|
+
key: unit.key,
|
|
83
|
+
branch,
|
|
84
|
+
status: "pending",
|
|
85
|
+
filesChanged: 0,
|
|
86
|
+
insertions: 0,
|
|
87
|
+
deletions: 0,
|
|
88
|
+
testCommand: unit.test || opts.test || null,
|
|
89
|
+
testPassed: null,
|
|
90
|
+
committed: false,
|
|
91
|
+
error: null,
|
|
92
|
+
};
|
|
93
|
+
let worktreePath = null;
|
|
94
|
+
try {
|
|
95
|
+
worktreePath = createWorktree(unit.key, branch);
|
|
96
|
+
onEvent({ type: "unit:start", key: unit.key, branch });
|
|
97
|
+
await runAgent(unit.prompt, worktreePath);
|
|
98
|
+
|
|
99
|
+
const stat = diffStat(worktreePath);
|
|
100
|
+
record.filesChanged = stat.filesChanged;
|
|
101
|
+
record.insertions = stat.insertions;
|
|
102
|
+
record.deletions = stat.deletions;
|
|
103
|
+
|
|
104
|
+
const testCmd = unit.test || opts.test || null;
|
|
105
|
+
if (testCmd) {
|
|
106
|
+
try {
|
|
107
|
+
await runTest(testCmd, worktreePath);
|
|
108
|
+
record.testPassed = true;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
record.testPassed = false;
|
|
111
|
+
record.testError = err.message;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (stat.filesChanged > 0) {
|
|
116
|
+
record.committed = commit(worktreePath, `batch: ${unit.key}`);
|
|
117
|
+
}
|
|
118
|
+
// A unit whose tests FAILED is reported failed even though its worktree
|
|
119
|
+
// committed — integration below skips non-passing units so a red unit
|
|
120
|
+
// never merges into base.
|
|
121
|
+
record.status =
|
|
122
|
+
record.testPassed === false
|
|
123
|
+
? "test-failed"
|
|
124
|
+
: record.filesChanged === 0
|
|
125
|
+
? "no-changes"
|
|
126
|
+
: "done";
|
|
127
|
+
} catch (err) {
|
|
128
|
+
record.status = "error";
|
|
129
|
+
record.error = err.message;
|
|
130
|
+
}
|
|
131
|
+
onEvent({
|
|
132
|
+
type: "unit:done",
|
|
133
|
+
key: unit.key,
|
|
134
|
+
status: record.status,
|
|
135
|
+
testPassed: record.testPassed,
|
|
136
|
+
filesChanged: record.filesChanged,
|
|
137
|
+
});
|
|
138
|
+
return { record, worktreePath };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Sequential integration: only units that DID change, committed, and did not
|
|
142
|
+
// fail tests are eligible. Preview each; merge clean ones when --merge.
|
|
143
|
+
for (const { record } of results) {
|
|
144
|
+
if (!record.committed || record.status === "test-failed") {
|
|
145
|
+
record.integration = { eligible: false, reason: record.status };
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
let preview;
|
|
149
|
+
try {
|
|
150
|
+
preview = previewMerge(record.branch);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
record.integration = {
|
|
153
|
+
eligible: true,
|
|
154
|
+
clean: false,
|
|
155
|
+
merged: false,
|
|
156
|
+
conflicts: [],
|
|
157
|
+
error: err.message,
|
|
158
|
+
};
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const clean = preview?.success === true;
|
|
162
|
+
const conflicts = preview?.conflicts || [];
|
|
163
|
+
let merged = false;
|
|
164
|
+
if (clean && opts.merge) {
|
|
165
|
+
let mergeResult;
|
|
166
|
+
try {
|
|
167
|
+
mergeResult = mergeBranch(record.branch, `Merge batch ${record.key}`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
mergeResult = { success: false, message: err.message };
|
|
170
|
+
}
|
|
171
|
+
merged = mergeResult?.success === true;
|
|
172
|
+
if (!merged && (!conflicts || conflicts.length === 0)) {
|
|
173
|
+
record.integration = {
|
|
174
|
+
eligible: true,
|
|
175
|
+
clean,
|
|
176
|
+
merged,
|
|
177
|
+
conflicts: mergeResult?.conflicts || [],
|
|
178
|
+
error: mergeResult?.message || null,
|
|
179
|
+
};
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
record.integration = { eligible: true, clean, merged, conflicts };
|
|
184
|
+
onEvent({
|
|
185
|
+
type: "unit:integrated",
|
|
186
|
+
key: record.key,
|
|
187
|
+
clean,
|
|
188
|
+
merged,
|
|
189
|
+
conflicts: conflicts.length,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Best-effort teardown of every worktree we created (branches persist for the
|
|
194
|
+
// user to inspect/merge later unless already merged).
|
|
195
|
+
if (typeof removeWorktree === "function") {
|
|
196
|
+
for (const { record, worktreePath } of results) {
|
|
197
|
+
if (!worktreePath) continue;
|
|
198
|
+
try {
|
|
199
|
+
removeWorktree(worktreePath, {
|
|
200
|
+
deleteBranch: record.integration?.merged === true,
|
|
201
|
+
});
|
|
202
|
+
} catch {
|
|
203
|
+
// best-effort — a leftover worktree is recoverable via `git worktree`
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const records = results.map((r) => r.record);
|
|
209
|
+
const summary = summarize(records);
|
|
210
|
+
onEvent({ type: "batch:done", ...summary });
|
|
211
|
+
return { units: records, summary };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function summarize(records) {
|
|
215
|
+
const summary = {
|
|
216
|
+
total: records.length,
|
|
217
|
+
done: 0,
|
|
218
|
+
testFailed: 0,
|
|
219
|
+
noChanges: 0,
|
|
220
|
+
errored: 0,
|
|
221
|
+
merged: 0,
|
|
222
|
+
conflicted: 0,
|
|
223
|
+
};
|
|
224
|
+
for (const r of records) {
|
|
225
|
+
if (r.status === "done") summary.done += 1;
|
|
226
|
+
else if (r.status === "test-failed") summary.testFailed += 1;
|
|
227
|
+
else if (r.status === "no-changes") summary.noChanges += 1;
|
|
228
|
+
else if (r.status === "error") summary.errored += 1;
|
|
229
|
+
if (r.integration?.merged) summary.merged += 1;
|
|
230
|
+
if (r.integration?.conflicts?.length) summary.conflicted += 1;
|
|
231
|
+
}
|
|
232
|
+
return summary;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const _internal = { mapPool, normalizeUnits, summarize };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-notify — shared backing for the `notify` agent tool + `cc agenda`
|
|
3
|
+
* monitor firing (gap-analysis 第四阶段 #3).
|
|
4
|
+
*
|
|
5
|
+
* Fans a notification out over the user's configured multi-channel notifiers
|
|
6
|
+
* (Telegram / WeCom / DingTalk / Feishu, via NotificationManager.fromEnv) and,
|
|
7
|
+
* when a paired-device push descriptor exists, the mobile push bridge. Pure
|
|
8
|
+
* over injected deps so it is unit-testable without any network.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NotificationManager } from "./notifiers/index.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{title:string, body?:string, level?:"info"|"success"|"failure"}} msg
|
|
15
|
+
* @param {{env?:object, manager?:object}} [deps]
|
|
16
|
+
* @returns {Promise<{delivered:string[], failed:string[], channels:number, results:Array}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function sendAgentNotification(msg, deps = {}) {
|
|
19
|
+
const title = String(msg?.title || "").trim();
|
|
20
|
+
if (!title) throw new Error("notify requires a title");
|
|
21
|
+
const body = msg?.body ? String(msg.body) : "";
|
|
22
|
+
const level = ["info", "success", "failure"].includes(msg?.level)
|
|
23
|
+
? msg.level
|
|
24
|
+
: "info";
|
|
25
|
+
|
|
26
|
+
const manager =
|
|
27
|
+
deps.manager ||
|
|
28
|
+
NotificationManager.fromEnv({ env: deps.env || process.env });
|
|
29
|
+
if (!manager.isConfigured) {
|
|
30
|
+
return {
|
|
31
|
+
delivered: [],
|
|
32
|
+
failed: [],
|
|
33
|
+
channels: 0,
|
|
34
|
+
results: [],
|
|
35
|
+
note: "No notification channels configured (set TELEGRAM_BOT_TOKEN / WECOM_WEBHOOK_URL / DINGTALK_WEBHOOK_URL / FEISHU_WEBHOOK_URL).",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const summary = {
|
|
40
|
+
taskId: msg?.taskId || null,
|
|
41
|
+
description: title,
|
|
42
|
+
detail: body,
|
|
43
|
+
message: body || title,
|
|
44
|
+
title,
|
|
45
|
+
};
|
|
46
|
+
const method =
|
|
47
|
+
level === "success"
|
|
48
|
+
? "notifySuccess"
|
|
49
|
+
: level === "failure"
|
|
50
|
+
? "notifyFailure"
|
|
51
|
+
: "notifyStart";
|
|
52
|
+
const results = await manager[method](summary);
|
|
53
|
+
|
|
54
|
+
const delivered = [];
|
|
55
|
+
const failed = [];
|
|
56
|
+
for (const r of results || []) {
|
|
57
|
+
if (r && r.ok !== false) delivered.push(r.channel);
|
|
58
|
+
else failed.push(r?.channel || "unknown");
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
delivered,
|
|
62
|
+
failed,
|
|
63
|
+
channels: (manager.activeChannels || []).length,
|
|
64
|
+
results: results || [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentScheduleStore — persistent backing for the agent-callable Monitor /
|
|
3
|
+
* Cron / Wakeup tools (gap-analysis 第四阶段 #3).
|
|
4
|
+
*
|
|
5
|
+
* A one-shot `cc agent` turn cannot itself keep a timer alive, so these tools
|
|
6
|
+
* PERSIST their intent to a JSONL store under
|
|
7
|
+
* `~/.chainlesschain/agent-schedule/<kind>.jsonl`; a supervising loop / cron
|
|
8
|
+
* (`cc agenda run`) reads due entries and acts. This mirrors how Claude Code's
|
|
9
|
+
* Monitor/CronCreate/ScheduleWakeup outlive a single turn.
|
|
10
|
+
*
|
|
11
|
+
* Three entry kinds share one store:
|
|
12
|
+
* - wakeup one-shot: run `prompt` (a `cc agent -p`) once at/after `dueAt`.
|
|
13
|
+
* - cron recurring: run `prompt` on a cron schedule; `nextAt` advances.
|
|
14
|
+
* - monitor recurring watch: run `command` (shell) every `intervalMs`;
|
|
15
|
+
* fire the `notify` payload when `stopWhen` regex matches output.
|
|
16
|
+
*
|
|
17
|
+
* Pure over an injected clock + directory so it is fully unit-testable. Cron
|
|
18
|
+
* parsing is a compact 5-field evaluator (min hour dom mon dow) supporting
|
|
19
|
+
* `* , - /` — enough for the scheduling the agent actually asks for.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from "fs";
|
|
23
|
+
import os from "os";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { randomUUID } from "crypto";
|
|
26
|
+
|
|
27
|
+
export const SCHEDULE_KINDS = Object.freeze(["wakeup", "cron", "monitor"]);
|
|
28
|
+
|
|
29
|
+
export function agentScheduleDir(homedir = os.homedir()) {
|
|
30
|
+
return path.join(homedir, ".chainlesschain", "agent-schedule");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── cron (5-field) ────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const CRON_RANGES = [
|
|
36
|
+
[0, 59], // minute
|
|
37
|
+
[0, 23], // hour
|
|
38
|
+
[1, 31], // day of month
|
|
39
|
+
[1, 12], // month
|
|
40
|
+
[0, 6], // day of week (0/7 = Sunday)
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function parseCronField(field, [min, max]) {
|
|
44
|
+
const values = new Set();
|
|
45
|
+
for (const part of String(field).split(",")) {
|
|
46
|
+
const [rangePart, stepPart] = part.split("/");
|
|
47
|
+
const step = stepPart ? parseInt(stepPart, 10) : 1;
|
|
48
|
+
if (!Number.isFinite(step) || step < 1) {
|
|
49
|
+
throw new Error(`invalid cron step in "${field}"`);
|
|
50
|
+
}
|
|
51
|
+
let lo = min;
|
|
52
|
+
let hi = max;
|
|
53
|
+
if (rangePart !== "*" && rangePart !== "") {
|
|
54
|
+
const bounds = rangePart.split("-");
|
|
55
|
+
lo = parseInt(bounds[0], 10);
|
|
56
|
+
hi = bounds.length > 1 ? parseInt(bounds[1], 10) : lo;
|
|
57
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
58
|
+
throw new Error(`invalid cron range in "${field}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (let v = lo; v <= hi; v += step) {
|
|
62
|
+
if (v < min || v > max) throw new Error(`cron value out of range: ${v}`);
|
|
63
|
+
values.add(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return values;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parse a 5-field cron expression into per-field value sets. Throws on junk. */
|
|
70
|
+
export function parseCron(expr) {
|
|
71
|
+
const fields = String(expr).trim().split(/\s+/);
|
|
72
|
+
if (fields.length !== 5) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`cron expression must have 5 fields (min hour dom mon dow), got ${fields.length}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return fields.map((field, i) => parseCronField(field, CRON_RANGES[i]));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cronMatches(sets, date) {
|
|
81
|
+
const dow = date.getDay(); // 0-6, Sun=0
|
|
82
|
+
return (
|
|
83
|
+
sets[0].has(date.getMinutes()) &&
|
|
84
|
+
sets[1].has(date.getHours()) &&
|
|
85
|
+
sets[2].has(date.getDate()) &&
|
|
86
|
+
sets[3].has(date.getMonth() + 1) &&
|
|
87
|
+
(sets[4].has(dow) || (sets[4].has(7) && dow === 0))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Next epoch-ms at which `expr` fires strictly after `fromMs` (local time,
|
|
93
|
+
* minute granularity). Scans up to ~366 days then gives up (returns null) so a
|
|
94
|
+
* never-matching expression can't loop forever.
|
|
95
|
+
*/
|
|
96
|
+
export function nextCronTime(expr, fromMs) {
|
|
97
|
+
const sets = parseCron(expr);
|
|
98
|
+
const cursor = new Date(fromMs);
|
|
99
|
+
cursor.setSeconds(0, 0);
|
|
100
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
101
|
+
const limit = 366 * 24 * 60;
|
|
102
|
+
for (let i = 0; i < limit; i += 1) {
|
|
103
|
+
if (cronMatches(sets, cursor)) return cursor.getTime();
|
|
104
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── store ───────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export class AgentScheduleStore {
|
|
112
|
+
constructor({ dir = null, now = () => Date.now() } = {}) {
|
|
113
|
+
this.dir = dir || agentScheduleDir();
|
|
114
|
+
this._now = typeof now === "function" ? now : () => now;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_file(kind) {
|
|
118
|
+
return path.join(this.dir, `${kind}.jsonl`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_ensureDir() {
|
|
122
|
+
fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_readAll(kind) {
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = fs.readFileSync(this._file(kind), "utf-8");
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const entries = [];
|
|
133
|
+
for (const line of raw.split("\n")) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
if (!trimmed) continue;
|
|
136
|
+
try {
|
|
137
|
+
entries.push(JSON.parse(trimmed));
|
|
138
|
+
} catch {
|
|
139
|
+
// Skip a corrupt line rather than throwing — one bad append must not
|
|
140
|
+
// poison the whole store (per-row resilience, like the JSON.parse sweep).
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return entries;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_writeAll(kind, entries) {
|
|
147
|
+
this._ensureDir();
|
|
148
|
+
const body = entries.map((entry) => JSON.stringify(entry)).join("\n");
|
|
149
|
+
fs.writeFileSync(this._file(kind), body ? body + "\n" : "", {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
mode: 0o600,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── create ──────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
scheduleWakeup({ prompt, delayMs = 0, dueAt = null, label = null } = {}) {
|
|
158
|
+
if (!prompt || typeof prompt !== "string") {
|
|
159
|
+
throw new Error("wakeup requires a prompt");
|
|
160
|
+
}
|
|
161
|
+
const now = this._now();
|
|
162
|
+
const entry = {
|
|
163
|
+
id: randomUUID(),
|
|
164
|
+
kind: "wakeup",
|
|
165
|
+
prompt,
|
|
166
|
+
label: label || null,
|
|
167
|
+
dueAt:
|
|
168
|
+
dueAt != null ? Number(dueAt) : now + Math.max(0, Number(delayMs) || 0),
|
|
169
|
+
createdAt: now,
|
|
170
|
+
status: "pending",
|
|
171
|
+
};
|
|
172
|
+
const entries = this._readAll("wakeup");
|
|
173
|
+
entries.push(entry);
|
|
174
|
+
this._writeAll("wakeup", entries);
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
createCron({ prompt, cron, label = null } = {}) {
|
|
179
|
+
if (!prompt || typeof prompt !== "string") {
|
|
180
|
+
throw new Error("cron requires a prompt");
|
|
181
|
+
}
|
|
182
|
+
const now = this._now();
|
|
183
|
+
const nextAt = nextCronTime(cron, now); // validates the expression too
|
|
184
|
+
if (nextAt == null) {
|
|
185
|
+
throw new Error(`cron "${cron}" has no upcoming match`);
|
|
186
|
+
}
|
|
187
|
+
const entry = {
|
|
188
|
+
id: randomUUID(),
|
|
189
|
+
kind: "cron",
|
|
190
|
+
prompt,
|
|
191
|
+
cron,
|
|
192
|
+
label: label || null,
|
|
193
|
+
nextAt,
|
|
194
|
+
createdAt: now,
|
|
195
|
+
lastRunAt: null,
|
|
196
|
+
runs: 0,
|
|
197
|
+
status: "active",
|
|
198
|
+
};
|
|
199
|
+
const entries = this._readAll("cron");
|
|
200
|
+
entries.push(entry);
|
|
201
|
+
this._writeAll("cron", entries);
|
|
202
|
+
return entry;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
createMonitor({
|
|
206
|
+
command,
|
|
207
|
+
intervalMs,
|
|
208
|
+
stopWhen = null,
|
|
209
|
+
notify = null,
|
|
210
|
+
maxChecks = null,
|
|
211
|
+
label = null,
|
|
212
|
+
} = {}) {
|
|
213
|
+
if (!command || typeof command !== "string") {
|
|
214
|
+
throw new Error("monitor requires a command");
|
|
215
|
+
}
|
|
216
|
+
const interval = Math.max(1000, Number(intervalMs) || 60000);
|
|
217
|
+
if (stopWhen != null) {
|
|
218
|
+
// Validate the regex up-front so a bad pattern errors at creation, not
|
|
219
|
+
// silently at the first check.
|
|
220
|
+
// eslint-disable-next-line no-new
|
|
221
|
+
new RegExp(stopWhen);
|
|
222
|
+
}
|
|
223
|
+
const now = this._now();
|
|
224
|
+
const entry = {
|
|
225
|
+
id: randomUUID(),
|
|
226
|
+
kind: "monitor",
|
|
227
|
+
command,
|
|
228
|
+
intervalMs: interval,
|
|
229
|
+
stopWhen: stopWhen || null,
|
|
230
|
+
notify: notify || null,
|
|
231
|
+
maxChecks: maxChecks != null ? Number(maxChecks) : null,
|
|
232
|
+
label: label || null,
|
|
233
|
+
nextAt: now + interval,
|
|
234
|
+
createdAt: now,
|
|
235
|
+
checks: 0,
|
|
236
|
+
status: "active",
|
|
237
|
+
};
|
|
238
|
+
const entries = this._readAll("monitor");
|
|
239
|
+
entries.push(entry);
|
|
240
|
+
this._writeAll("monitor", entries);
|
|
241
|
+
return entry;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── query ─────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
list(kind = null) {
|
|
247
|
+
const kinds = kind ? [kind] : SCHEDULE_KINDS;
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const k of kinds) out.push(...this._readAll(k));
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Entries whose time has come (pending wakeup / active cron|monitor). */
|
|
254
|
+
due(kind = null, atMs = null) {
|
|
255
|
+
const now = atMs != null ? atMs : this._now();
|
|
256
|
+
return this.list(kind).filter((entry) => {
|
|
257
|
+
if (entry.kind === "wakeup") {
|
|
258
|
+
return entry.status === "pending" && entry.dueAt <= now;
|
|
259
|
+
}
|
|
260
|
+
return entry.status === "active" && entry.nextAt <= now;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cancel(id) {
|
|
265
|
+
for (const kind of SCHEDULE_KINDS) {
|
|
266
|
+
const entries = this._readAll(kind);
|
|
267
|
+
const idx = entries.findIndex((entry) => entry.id === id);
|
|
268
|
+
if (idx >= 0) {
|
|
269
|
+
const [removed] = entries.splice(idx, 1);
|
|
270
|
+
this._writeAll(kind, entries);
|
|
271
|
+
return removed;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── lifecycle mutations (used by `cc agenda run`) ──────────────────────────
|
|
278
|
+
|
|
279
|
+
/** Mark a one-shot wakeup fired (keeps history; due() skips it). */
|
|
280
|
+
markWakeupFired(id, atMs = null) {
|
|
281
|
+
return this._mutate("wakeup", id, (entry) => {
|
|
282
|
+
entry.status = "fired";
|
|
283
|
+
entry.firedAt = atMs != null ? atMs : this._now();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Advance a cron entry to its next fire time after running once. */
|
|
288
|
+
advanceCron(id, atMs = null) {
|
|
289
|
+
const now = atMs != null ? atMs : this._now();
|
|
290
|
+
return this._mutate("cron", id, (entry) => {
|
|
291
|
+
entry.lastRunAt = now;
|
|
292
|
+
entry.runs = (entry.runs || 0) + 1;
|
|
293
|
+
const next = nextCronTime(entry.cron, now);
|
|
294
|
+
if (next == null) {
|
|
295
|
+
entry.status = "exhausted";
|
|
296
|
+
} else {
|
|
297
|
+
entry.nextAt = next;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Record a monitor check. `matched` ends the monitor (done); otherwise it is
|
|
304
|
+
* re-armed for the next interval, or exhausted once maxChecks is hit.
|
|
305
|
+
*/
|
|
306
|
+
recordMonitorCheck(id, { matched = false, atMs = null } = {}) {
|
|
307
|
+
const now = atMs != null ? atMs : this._now();
|
|
308
|
+
return this._mutate("monitor", id, (entry) => {
|
|
309
|
+
entry.checks = (entry.checks || 0) + 1;
|
|
310
|
+
entry.lastCheckAt = now;
|
|
311
|
+
if (matched) {
|
|
312
|
+
entry.status = "matched";
|
|
313
|
+
entry.matchedAt = now;
|
|
314
|
+
} else if (entry.maxChecks != null && entry.checks >= entry.maxChecks) {
|
|
315
|
+
entry.status = "exhausted";
|
|
316
|
+
} else {
|
|
317
|
+
entry.nextAt = now + entry.intervalMs;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_mutate(kind, id, fn) {
|
|
323
|
+
const entries = this._readAll(kind);
|
|
324
|
+
const entry = entries.find((e) => e.id === id);
|
|
325
|
+
if (!entry) return null;
|
|
326
|
+
fn(entry);
|
|
327
|
+
this._writeAll(kind, entries);
|
|
328
|
+
return entry;
|
|
329
|
+
}
|
|
330
|
+
}
|