chainlesschain 0.162.36 → 0.162.37
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-vAVAFNJ4.js → AIOps-_oxz4VHy.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BnRHFCKM.js → ActionButton-uaeqFuDj.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BOjwqWqG.js → Analytics-BPVV0OUf.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Dc0D1Txn.js → AppLayout-ppCYKm3I.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-dd_2efaZ.js → Audit-DFAY6umk.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-HF1jgm8G.js → Backup-pAPBFDyP.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CCtzmoKe.js → BaseInput-BbBl0uT2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BNfH1c3p.js → Chat-Ct22JUnT.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DCWFqmI4.js → ChatBubbleRenderer-DPlsLl22.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BOr-NscK.js → Checkbox-DEkCollc.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DE058N7-.js → Codegen-Tor-de39.js} +1 -1
- package/src/assets/web-panel/assets/{Col-SOREo1XE.js → Col-ojNrLQU7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-sOvNZo9f.js → Community-CLOGhqMF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DnBe558D.js → Compact-CYKNlSZ4.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-o-r6CUbg.js → Compliance-C5E6ABuA.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D6_k9mHP.js → Cowork-CHeEsZ3W.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-CEV3Xkrm.js → Cron-B4e1n2e7.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-eJ1lQWKU.js → Crosschain-DbNV8P9R.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B-WqM9Hp.js → DID-C5_Tk3nC.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-ZnKPcsHN.js → Dashboard-BhdV_c4N.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B8uLWDIP.js → Dropdown-CEi5AMtM.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Jmj2Y7aH.js → EmailListRenderer-DOhPiYng.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cb2xetG-.js → FamilyGuardDashboard-fu4NRP3X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C_07GXoq.js → Federation-B7BtIWKL.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D3kbYrMU.js → FormItemContext-BmPWZVLP.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-9xgqvGPg.js → GenericCardRenderer-hsOPNJq8.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlwWlMMB.js → Git-Bi_EFBUH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DxN3wQZ_.js → Governance-emf2ubDK.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-ls7pSw_D.js → Inference-B7KjKzkI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-_n9hYuPI.js → KnowledgeGraph-uAaBK0F3.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CvEVY5TK.js → Logs-utK7hNpj.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-C3qvQJT7.js → Marketplace-CzQe6n3z.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DiwKpnKx.js → McpTools-CuAaJr51.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-CIBPi_da.js → Memory-CRuZZJ75.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D-v0Se8y.js → MobileBridge-Cp06wunh.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-cP1apTQD.js → MobileProjects-DJEdUwhr.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BMFWrI65.js → Mtc-8YY4dR7g.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-2s8LaHtR.js → MtcAudit-BmPJYHar.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-dL_nvj7d.js → Multisig-d-ydyVdq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BbrJp06R.js → NLProgramming-DA_ikw_n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-jR9irwy3.js → Notes-DIyF-fRe.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Dk-STCIX.js → NotificationSettings-CzPZXEtK.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BiLtg-LY.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BCK5jylo.js → Organization-DdDZ_Ap6.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BRAY7Smt.js → Overflow-BnMBkttv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BltVRGjb.js → P2P-Es1050f-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CV8UbXHe.js → PdhVaultBrowser-CKkRmyn9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-_tNl47Qh.js → Permissions-zU9n9cAD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Cgc4HjpX.js → PersonalDataHub-BZi5Xwas.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-Bn_QU4mu.js → Pipeline-CRfeGiFc.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-jzJowp5P.js → Privacy-CQA_IgLA.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B_1pJ8qd.js → ProjectInit-C9hmEvoT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CPVZpXzs.js → ProjectSettings-yXA72ws4.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CQsHOWnT.js → Projects-BpWS-qam.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CzzMiLC0.js → Providers-Cxe55dRD.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-MxBKIn9o.js → QuickAsk-Do0aUTQr.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-D8lN6Lis.js → Recommend--ysZHjyA.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CfYK-IrV.js → Reputation-BOBU8JrH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Bg7NZDP9.js → Row-C6X7bRKE.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOVNJhj0.js → RssFeed-D8AwqlkQ.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B38qzmhY.js → Search-Bi3rCZD4.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CjqleZpe.js → Security-DxUDVrtY.js} +3 -3
- package/src/assets/web-panel/assets/{Services-Bu9JSJap.js → Services-BXXN7yC1.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B2RvRkaX.js → Skeleton-B3BR34tZ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-_h42mxMN.js → Skills-BjYu8OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BssLs56D.js → Sla-DDkCtD8w.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCxFYHsd.js → SpeechSettings-CGhYzP7V.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D2xQuNLE.js → SyncSettings-CYNKVAHA.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DhpOGOlo.js → Tasks-DavmlJpd.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYG-R-aS.js → Templates-CQuYFf2C.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BQRYLsvP.js → Tenant-DdzZh8vE.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-D75WeG9d.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-BIZzBftk.js → TimelineRenderer-DKOARnc_.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-uMLH5p_a.js → Tokens-D7QRNG8y.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BzS6XPqx.js → Trigger-BCsqLZl4.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-R4zhHufZ.js → Trust-BarGUa6p.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DATQCoGe.js → UkeySign-pHrg5a8E.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-ClUmKOtS.js → VideoEditing-Dug3m1py.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzJTbQzD.js → Wallet-BfK3Z_Ez.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CrXrLmzQ.js → WebAuthn-CYRdl9td.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CpvZ0Tma.js → WorkflowEditor-DTW5AcqM.js} +1 -1
- package/src/assets/web-panel/assets/{chat-a6wpYmVL.js → chat-CCXz4j38.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CXJADb1t.js → colors-BJBOhAqa.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CL2pohS_.js → compact-item-E9M6BQcM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-xFi_1G5_.js → createContext-Cg9CAws4.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BrsbTJUv.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bchh1rAi.js → hasIn-DhVtqv5L.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2eMYASq.js → index--7o5YdL6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmsIKzyu.js → index-4N5lNXGP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuehgDOp.js → index-6-04M2Nx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BH9t10pe.js → index-B111fZ21.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoaRB-4a.js → index-B4NBF4Sa.js} +1 -1
- package/src/assets/web-panel/assets/{index-KCib1PTw.js → index-B8bjEHrQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-majCS3s2.js → index-BAB0nGP7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsbMVBj1.js → index-BFZPRd0T.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7wT5VRi.js → index-B_SMPD4L.js} +1 -1
- package/src/assets/web-panel/assets/{index-TxbHusq2.js → index-BxSzyly9.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4zNisy9.js → index-ByazO4Q9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cua_P8St.js → index-C-2dUIli.js} +1 -1
- package/src/assets/web-panel/assets/{index-EPERz4Pu.js → index-CFarAlXj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6NehWty.js → index-CFp-wdrQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTRd7vkq.js → index-CJ8nNT8h.js} +1 -1
- package/src/assets/web-panel/assets/{index-CR3kFPuC.js → index-CSiyjCYi.js} +1 -1
- package/src/assets/web-panel/assets/{index-u8K1y_lh.js → index-CUp_c8Le.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxahxRP7.js → index-CVR_s-pT.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4yBRKT4.js → index-Ca8BYV1g.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7knYOpm.js → index-CeRlLp3F.js} +1 -1
- package/src/assets/web-panel/assets/{index-jMcv1u5o.js → index-ChsSljaN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGq4HQno.js → index-CkTeBHI9.js} +1 -1
- package/src/assets/web-panel/assets/{index-M8SZI11a.js → index-Cm1m7BJh.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-TT9Swq.js → index-ComyTKz-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPH5ESqs.js → index-CznfPnOx.js} +3 -3
- package/src/assets/web-panel/assets/{index-dsLc7t6W.js → index-D5yC2Ps8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjdOL159.js → index-D7DXdf7x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVo1GJoj.js → index-DDcJO27F.js} +1 -1
- package/src/assets/web-panel/assets/{index-IkvkNxbc.js → index-DSQazU6J.js} +1 -1
- package/src/assets/web-panel/assets/index-DSTQDO-Y.js +1 -0
- package/src/assets/web-panel/assets/{index-CMybtJY6.js → index-DaFe1aqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3Tpv7-d.js → index-DdhnGez0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BF4xx1_b.js → index-Di5LBXcE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrbJBnT-.js → index-Dwvewrul.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ_hw_5P.js → index-MdXEhfdJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEYcLAl7.js → index-_PNqQ5mE.js} +1 -1
- package/src/assets/web-panel/assets/index-c2U6LV3Q.js +1 -0
- package/src/assets/web-panel/assets/{index-CdU8BwRW.js → index-kz1oXl1a.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTEu7TSF.js → index-wkt-o5q5.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DYn3Gc09.js → initDefaultProps-iyBaePF-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ZS3eolb9.js → motion-RWtj4rgu.js} +1 -1
- package/src/assets/web-panel/assets/{move-CEw4uqr3.js → move-CqPRVzpH.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DlHFZnPp.js → omit-DsvJze25.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-eZQvV5fA.js → pickAttrs-B4tfZBhc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B31jQwa-.js → placementArrow-KvHUwXMA.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DAsNmVto.js → responsiveObserve-DGdJ-b7W.js} +1 -1
- package/src/assets/web-panel/assets/{slide-gPQPrYZC.js → slide-Cd6ebRmw.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DwWKX5co.js → statusUtils-Bg9GcIAn.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B3VOtXuH.js → styleChecker-MQjKsG84.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-6ADctM2r.js → useFlexGapSupport-C241WujP.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-6Zx1SSKs.js → useFs-CMpy7RS4.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BzReowln.js → usePersonalDataHub-BLHtapKb.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C8IpEQbD.js → vnode-DmcTV67c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-ruc9vHr0.js → zoom-DHL8_0Y8.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/cli-anything.js +14 -6
- package/src/commands/loop.js +450 -0
- package/src/index.js +2 -0
- package/src/lib/loop.js +198 -0
- package/src/repl/agent-repl.js +5 -0
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/OrderTableRenderer-CqqfY6zq.js +0 -1
- package/src/assets/web-panel/assets/Terminal-imKU7N5j.js +0 -3
- package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +0 -1
- package/src/assets/web-panel/assets/index-B4l4vLTB.js +0 -1
- package/src/assets/web-panel/assets/index-B7Ek5iiY.js +0 -1
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc loop — repeatedly run a command or agent prompt on a fixed interval
|
|
3
|
+
* (Claude-Code `/loop` parity, MVP). Lightweight by design: unlike `cc ccron`
|
|
4
|
+
* (in-memory profile governance, runs nothing) or `cc automation` (DB-backed
|
|
5
|
+
* flow/trigger engine), this just re-runs ONE thing on a timer until a stop
|
|
6
|
+
* condition fires or you Ctrl-C.
|
|
7
|
+
*
|
|
8
|
+
* cc loop "check if CI passed, summarize failures" # wraps `cc agent -p`
|
|
9
|
+
* cc loop --every 30s -- npm test # external command
|
|
10
|
+
* cc loop --every 1m --max-iterations 10 -- npm test
|
|
11
|
+
* cc loop --until-exit-zero --every 30s -- npm test # stop when it passes
|
|
12
|
+
* cc loop --until "DONE" --every 1m "poll the deploy"
|
|
13
|
+
* cc loop "review the diff" --think --provider openai # extra flags → cc agent
|
|
14
|
+
* cc loop --dynamic "watch the deploy; stop when it's live" # agent self-paces
|
|
15
|
+
* cc loop --save ci-watch --every 1m -- npm test # persist a resumable loop
|
|
16
|
+
* cc loop --resume ci-watch --max-iterations 20 # continue it (cumulative)
|
|
17
|
+
*
|
|
18
|
+
* Two modes, disambiguated by the literal `--` separator:
|
|
19
|
+
* - no `--` → the single operand is a PROMPT, run via `cc agent -p <prompt>`
|
|
20
|
+
* - with `--` → the operands after it are an EXTERNAL command (shell-resolved)
|
|
21
|
+
*
|
|
22
|
+
* The loop driver lives in src/lib/loop.js (pure, clock-injected). This layer
|
|
23
|
+
* only builds the concrete iteration (spawn + tee output) and wires SIGINT.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import chalk from "chalk";
|
|
29
|
+
import { logger } from "../lib/logger.js";
|
|
30
|
+
import {
|
|
31
|
+
runLoop,
|
|
32
|
+
parseDuration,
|
|
33
|
+
formatDuration,
|
|
34
|
+
makeSleep,
|
|
35
|
+
parseLoopDirectives,
|
|
36
|
+
summarizeLoopEvents,
|
|
37
|
+
} from "../lib/loop.js";
|
|
38
|
+
import {
|
|
39
|
+
startSession,
|
|
40
|
+
appendEvent,
|
|
41
|
+
readEvents,
|
|
42
|
+
sessionExists,
|
|
43
|
+
} from "../harness/jsonl-session-store.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Appended to the prompt under `--dynamic` so the model can self-pace: it ends
|
|
47
|
+
* its reply with at most one control directive the loop parses (parseLoopDirectives).
|
|
48
|
+
*/
|
|
49
|
+
const DYNAMIC_PROMPT_SUFFIX = `
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
You are running inside a \`cc loop --dynamic\` controller. After deciding what happens next, end your reply with EXACTLY ONE control directive alone on the final line:
|
|
53
|
+
[[loop:next <interval>]] run me again after <interval> (e.g. 30s, 5m, 1h)
|
|
54
|
+
[[loop:stop]] the task is complete — stop looping
|
|
55
|
+
Emit neither and the loop falls back to its default --every interval.`;
|
|
56
|
+
|
|
57
|
+
/** Absolute path to this CLI's bin entry, for self-spawning the prompt mode. */
|
|
58
|
+
const BIN_PATH = fileURLToPath(
|
|
59
|
+
new URL("../../bin/chainlesschain.js", import.meta.url),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run one child process to completion. Tees stdout/stderr to the parent (so
|
|
64
|
+
* the user sees live output) while capturing it, so `--until <regex>` can match
|
|
65
|
+
* against what was printed. Resolves with { exitCode, output }.
|
|
66
|
+
*/
|
|
67
|
+
function spawnIteration(cmd, args, { shell, onChild, capture }) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const child = spawn(cmd, args, {
|
|
70
|
+
shell,
|
|
71
|
+
stdio: capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
72
|
+
env: process.env,
|
|
73
|
+
});
|
|
74
|
+
if (onChild) onChild(child);
|
|
75
|
+
|
|
76
|
+
let output = "";
|
|
77
|
+
if (capture) {
|
|
78
|
+
child.stdout?.on("data", (d) => {
|
|
79
|
+
output += d;
|
|
80
|
+
process.stdout.write(d);
|
|
81
|
+
});
|
|
82
|
+
child.stderr?.on("data", (d) => {
|
|
83
|
+
output += d;
|
|
84
|
+
process.stderr.write(d);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// `close` (not `exit`) so piped stdio is fully drained before we resolve.
|
|
89
|
+
child.on("close", (code, signal) => {
|
|
90
|
+
resolve({ exitCode: code == null ? null : code, output, signal });
|
|
91
|
+
});
|
|
92
|
+
child.on("error", (err) => {
|
|
93
|
+
resolve({
|
|
94
|
+
exitCode: 127,
|
|
95
|
+
output: String(err.message || err),
|
|
96
|
+
signal: null,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the concrete child invocation from the resolved operands + mode.
|
|
104
|
+
* Shared by fresh runs and `--resume` (which reconstructs it from saved config).
|
|
105
|
+
* exec mode → shell-run the joined operands (resolves Windows .cmd shims).
|
|
106
|
+
* prompt mode → `cc agent -p <prompt>` with operands up to the first flag as
|
|
107
|
+
* the prompt and the rest forwarded verbatim to `cc agent`.
|
|
108
|
+
* Returns { cmd, args, shell, label }.
|
|
109
|
+
*/
|
|
110
|
+
function buildInvocation({ operands, execMode, dynamic }) {
|
|
111
|
+
if (execMode) {
|
|
112
|
+
const cmd = operands.join(" ");
|
|
113
|
+
return { cmd, args: [], shell: true, label: cmd };
|
|
114
|
+
}
|
|
115
|
+
const flagIdx = operands.findIndex((p) => p.startsWith("-"));
|
|
116
|
+
const promptParts = flagIdx === -1 ? operands : operands.slice(0, flagIdx);
|
|
117
|
+
const agentFlags = flagIdx === -1 ? [] : operands.slice(flagIdx);
|
|
118
|
+
let prompt = promptParts.join(" ");
|
|
119
|
+
if (dynamic) prompt += DYNAMIC_PROMPT_SUFFIX;
|
|
120
|
+
const label =
|
|
121
|
+
`cc agent -p ${chalk.italic(promptParts.join(" "))}` +
|
|
122
|
+
(agentFlags.length ? ` ${chalk.gray(agentFlags.join(" "))}` : "");
|
|
123
|
+
return {
|
|
124
|
+
cmd: process.execPath,
|
|
125
|
+
args: [BIN_PATH, "agent", "-p", prompt, ...agentFlags],
|
|
126
|
+
shell: false,
|
|
127
|
+
label,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function registerLoopCommand(program) {
|
|
132
|
+
program
|
|
133
|
+
.command("loop [parts...]")
|
|
134
|
+
.description(
|
|
135
|
+
"Repeatedly run an agent prompt or `-- <command>` on a fixed interval",
|
|
136
|
+
)
|
|
137
|
+
.option(
|
|
138
|
+
"--every <dur>",
|
|
139
|
+
"Interval between iterations (e.g. 30s, 5m, 1.5h; bare number = seconds)",
|
|
140
|
+
"5m",
|
|
141
|
+
)
|
|
142
|
+
.option("-n, --max-iterations <n>", "Stop after N iterations")
|
|
143
|
+
.option(
|
|
144
|
+
"--until-exit-zero",
|
|
145
|
+
"Stop once an iteration exits with code 0 (e.g. tests pass)",
|
|
146
|
+
)
|
|
147
|
+
.option(
|
|
148
|
+
"--until <regex>",
|
|
149
|
+
"Stop once an iteration's output matches this JS regex",
|
|
150
|
+
)
|
|
151
|
+
.option(
|
|
152
|
+
"--dynamic",
|
|
153
|
+
"Let each iteration self-pace via [[loop:next <dur>]] / [[loop:stop]] directives (prompt mode augments the prompt)",
|
|
154
|
+
)
|
|
155
|
+
.option(
|
|
156
|
+
"--save [id]",
|
|
157
|
+
"Persist this loop to a resumable session (auto-generates an id if omitted)",
|
|
158
|
+
)
|
|
159
|
+
.option("--resume <id>", "Continue a previously --save'd loop session")
|
|
160
|
+
.option("--json", "Print a JSON summary when the loop ends")
|
|
161
|
+
.allowUnknownOption(true) // pass-through flags for the wrapped agent/command
|
|
162
|
+
.action(async (parts, options, command) => {
|
|
163
|
+
try {
|
|
164
|
+
// Was an option explicitly given on the command line (vs a default)?
|
|
165
|
+
// Used so --resume inherits the saved config but still honors flags the
|
|
166
|
+
// user re-passes (e.g. extend --max-iterations).
|
|
167
|
+
const fromCli = (name) =>
|
|
168
|
+
command?.getOptionValueSource?.(name) === "cli";
|
|
169
|
+
|
|
170
|
+
// --- resolve session: --resume loads saved config; --save persists ---
|
|
171
|
+
let sessionId = null;
|
|
172
|
+
let persist = false;
|
|
173
|
+
let startIndex = 0;
|
|
174
|
+
let savedConfig = null;
|
|
175
|
+
if (options.resume) {
|
|
176
|
+
if (!sessionExists(options.resume)) {
|
|
177
|
+
logger.error(chalk.red(`no such loop session: ${options.resume}`));
|
|
178
|
+
logger.log(chalk.gray(" list sessions with: cc session list"));
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const s = summarizeLoopEvents(readEvents(options.resume));
|
|
183
|
+
if (!s.config) {
|
|
184
|
+
logger.error(
|
|
185
|
+
chalk.red(`session ${options.resume} has no loop to resume`),
|
|
186
|
+
);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
savedConfig = s.config;
|
|
191
|
+
startIndex = s.completedIterations;
|
|
192
|
+
sessionId = options.resume;
|
|
193
|
+
persist = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- resolve mode / operands (saved config wins on resume) ---
|
|
197
|
+
let execMode;
|
|
198
|
+
let operands;
|
|
199
|
+
let dynamic;
|
|
200
|
+
if (savedConfig) {
|
|
201
|
+
execMode = Boolean(savedConfig.execMode);
|
|
202
|
+
operands = savedConfig.operands || [];
|
|
203
|
+
dynamic = fromCli("dynamic")
|
|
204
|
+
? Boolean(options.dynamic)
|
|
205
|
+
: Boolean(savedConfig.dynamic);
|
|
206
|
+
} else {
|
|
207
|
+
// `--` is the unambiguous signal for external-command mode. Commander
|
|
208
|
+
// folds the post-`--` operands into `parts`, so we sniff the parsed
|
|
209
|
+
// argv for the literal separator. `rawArgs` is what Commander actually
|
|
210
|
+
// parsed (process.argv in prod, the explicit array under test).
|
|
211
|
+
const argv = command?.parent?.rawArgs || process.argv;
|
|
212
|
+
execMode = argv.includes("--");
|
|
213
|
+
operands = (parts || []).filter((p) => p !== "--");
|
|
214
|
+
dynamic = Boolean(options.dynamic);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (operands.length === 0) {
|
|
218
|
+
logger.error(
|
|
219
|
+
chalk.red(
|
|
220
|
+
'nothing to loop: pass a prompt ("...") or a command after `--`',
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
logger.log(chalk.gray(' cc loop --every 5m "check CI"'));
|
|
224
|
+
logger.log(chalk.gray(" cc loop --every 30s -- npm test"));
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- resolve interval (CLI overrides saved on resume) ---
|
|
230
|
+
const everyRaw =
|
|
231
|
+
savedConfig && !fromCli("every") ? savedConfig.every : options.every;
|
|
232
|
+
let intervalMs;
|
|
233
|
+
try {
|
|
234
|
+
intervalMs = parseDuration(everyRaw);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
logger.error(chalk.red(e.message));
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- resolve stop conditions (CLI overrides saved on resume) ---
|
|
242
|
+
const maxRaw =
|
|
243
|
+
savedConfig && !fromCli("maxIterations")
|
|
244
|
+
? savedConfig.maxIterations
|
|
245
|
+
: options.maxIterations;
|
|
246
|
+
let maxIterations;
|
|
247
|
+
if (maxRaw != null) {
|
|
248
|
+
maxIterations = Number(maxRaw);
|
|
249
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
250
|
+
logger.error(
|
|
251
|
+
chalk.red("--max-iterations must be a positive integer"),
|
|
252
|
+
);
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const untilRaw =
|
|
258
|
+
savedConfig && !fromCli("until") ? savedConfig.until : options.until;
|
|
259
|
+
let untilRegex = null;
|
|
260
|
+
if (untilRaw) {
|
|
261
|
+
try {
|
|
262
|
+
untilRegex = new RegExp(untilRaw);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
logger.error(chalk.red(`invalid --until regex: ${e.message}`));
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const untilExitZero =
|
|
270
|
+
savedConfig && !fromCli("untilExitZero")
|
|
271
|
+
? Boolean(savedConfig.untilExitZero)
|
|
272
|
+
: Boolean(options.untilExitZero);
|
|
273
|
+
|
|
274
|
+
// --- build the child invocation (shared with resume) ---
|
|
275
|
+
const { cmd, args, shell, label } = buildInvocation({
|
|
276
|
+
operands,
|
|
277
|
+
execMode,
|
|
278
|
+
dynamic,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// --- --save creates a fresh session + writes the loop_config once ---
|
|
282
|
+
if (options.save != null && !options.resume) {
|
|
283
|
+
persist = true;
|
|
284
|
+
sessionId = startSession(
|
|
285
|
+
typeof options.save === "string" && options.save
|
|
286
|
+
? options.save
|
|
287
|
+
: null,
|
|
288
|
+
{ title: `loop: ${operands.join(" ")}`.slice(0, 80) },
|
|
289
|
+
);
|
|
290
|
+
appendEvent(sessionId, "loop_config", {
|
|
291
|
+
execMode,
|
|
292
|
+
operands,
|
|
293
|
+
dynamic,
|
|
294
|
+
every: everyRaw,
|
|
295
|
+
maxIterations: maxIterations ?? null,
|
|
296
|
+
untilExitZero,
|
|
297
|
+
until: untilRaw || null,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- SIGINT → graceful stop after the current iteration ---
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
let activeChild = null;
|
|
304
|
+
let interrupted = false;
|
|
305
|
+
const onSigint = () => {
|
|
306
|
+
interrupted = true;
|
|
307
|
+
controller.abort();
|
|
308
|
+
if (activeChild && activeChild.exitCode == null) {
|
|
309
|
+
try {
|
|
310
|
+
activeChild.kill("SIGINT");
|
|
311
|
+
} catch {
|
|
312
|
+
/* already gone */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
logger.log(chalk.yellow("\n⏹ stopping after current iteration…"));
|
|
316
|
+
};
|
|
317
|
+
process.on("SIGINT", onSigint);
|
|
318
|
+
|
|
319
|
+
// Capture output when we need to read it: regex matching or --dynamic
|
|
320
|
+
// directive parsing.
|
|
321
|
+
const capture = Boolean(untilRegex) || dynamic;
|
|
322
|
+
logger.log(
|
|
323
|
+
chalk.cyan(
|
|
324
|
+
`↻ loop: ${label} ${chalk.gray(
|
|
325
|
+
`(${dynamic ? "dynamic, fallback " : "every "}${formatDuration(
|
|
326
|
+
intervalMs,
|
|
327
|
+
)}${maxIterations ? `, max ${maxIterations}` : ""}${
|
|
328
|
+
startIndex ? `, resuming from ${startIndex}` : ""
|
|
329
|
+
}${persist ? `, session ${sessionId}` : ""})`,
|
|
330
|
+
)}`,
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const startedAt = Date.now();
|
|
335
|
+
let summary;
|
|
336
|
+
try {
|
|
337
|
+
summary = await runLoop({
|
|
338
|
+
intervalMs,
|
|
339
|
+
maxIterations,
|
|
340
|
+
untilExitZero,
|
|
341
|
+
untilRegex,
|
|
342
|
+
startIndex,
|
|
343
|
+
sleep: makeSleep(controller.signal),
|
|
344
|
+
shouldStop: () => controller.signal.aborted,
|
|
345
|
+
onIteration: (n, res) => {
|
|
346
|
+
const tag =
|
|
347
|
+
res.exitCode === 0
|
|
348
|
+
? chalk.green(`exit 0`)
|
|
349
|
+
: chalk.red(`exit ${res.exitCode}`);
|
|
350
|
+
logger.log(chalk.gray(` ↳ iteration ${n} done (${tag})`));
|
|
351
|
+
// Persist a compact record per round (no output body — keeps the
|
|
352
|
+
// session small; resume only needs the count + config).
|
|
353
|
+
if (persist) {
|
|
354
|
+
appendEvent(sessionId, "loop_iteration", {
|
|
355
|
+
n,
|
|
356
|
+
exitCode: res.exitCode,
|
|
357
|
+
durationMs: res.durationMs ?? null,
|
|
358
|
+
done: Boolean(res.done),
|
|
359
|
+
nextDelayMs: res.nextDelayMs ?? null,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
runIteration: async (n) => {
|
|
364
|
+
logger.log(chalk.gray(`\n▸ iteration ${n} — ${label}`));
|
|
365
|
+
const t0 = Date.now();
|
|
366
|
+
const res = await spawnIteration(cmd, args, {
|
|
367
|
+
shell,
|
|
368
|
+
capture,
|
|
369
|
+
onChild: (c) => {
|
|
370
|
+
activeChild = c;
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
res.durationMs = Date.now() - t0;
|
|
374
|
+
// --dynamic: read the iteration's [[loop:next]] / [[loop:stop]]
|
|
375
|
+
// directive and surface it to runLoop as done / nextDelayMs.
|
|
376
|
+
if (options.dynamic) {
|
|
377
|
+
const d = parseLoopDirectives(res.output);
|
|
378
|
+
res.done = d.done;
|
|
379
|
+
if (d.nextDelayMs != null) res.nextDelayMs = d.nextDelayMs;
|
|
380
|
+
if (d.done) {
|
|
381
|
+
logger.log(chalk.gray(` ↺ directive: stop`));
|
|
382
|
+
} else if (d.nextDelayMs != null) {
|
|
383
|
+
logger.log(
|
|
384
|
+
chalk.gray(
|
|
385
|
+
` ↺ directive: next in ${formatDuration(d.nextDelayMs)}`,
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return res;
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
} finally {
|
|
394
|
+
process.removeListener("SIGINT", onSigint);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const elapsed = formatDuration(Date.now() - startedAt);
|
|
398
|
+
const lastExit =
|
|
399
|
+
summary.results.length > 0
|
|
400
|
+
? summary.results[summary.results.length - 1].exitCode
|
|
401
|
+
: null;
|
|
402
|
+
const stoppedBy = interrupted ? "signal" : summary.stoppedBy;
|
|
403
|
+
|
|
404
|
+
if (persist) {
|
|
405
|
+
appendEvent(sessionId, "loop_end", {
|
|
406
|
+
stoppedBy,
|
|
407
|
+
iterations: summary.iterations,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (options.json) {
|
|
412
|
+
logger.log(
|
|
413
|
+
JSON.stringify(
|
|
414
|
+
{
|
|
415
|
+
iterations: summary.iterations,
|
|
416
|
+
stoppedBy,
|
|
417
|
+
lastExitCode: lastExit,
|
|
418
|
+
elapsed,
|
|
419
|
+
...(persist ? { sessionId } : {}),
|
|
420
|
+
},
|
|
421
|
+
null,
|
|
422
|
+
2,
|
|
423
|
+
),
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
logger.log(
|
|
427
|
+
chalk.cyan(
|
|
428
|
+
`\n✔ loop ended — ${summary.iterations} iteration(s), stopped by ${chalk.bold(
|
|
429
|
+
stoppedBy,
|
|
430
|
+
)} ${chalk.gray(`(${elapsed})`)}`,
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
if (persist) {
|
|
434
|
+
logger.log(
|
|
435
|
+
chalk.gray(
|
|
436
|
+
` session saved — resume with: cc loop --resume ${sessionId}`,
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Exit code mirrors the last iteration when we stopped on a condition;
|
|
443
|
+
// an interrupt is a clean stop (0).
|
|
444
|
+
if (!interrupted && lastExit != null) process.exitCode = lastExit;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
logger.error(chalk.red(`loop failed: ${err.message}`));
|
|
447
|
+
process.exitCode = 1;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
package/src/index.js
CHANGED
|
@@ -62,6 +62,7 @@ import { registerCheckpointCommand } from "./commands/checkpoint.js";
|
|
|
62
62
|
import { registerGoalCommand } from "./commands/goal.js";
|
|
63
63
|
import { registerCommandCommand } from "./commands/command.js";
|
|
64
64
|
import { registerCompactCommand } from "./commands/compact.js";
|
|
65
|
+
import { registerLoopCommand } from "./commands/loop.js";
|
|
65
66
|
import { registerPermissionsCommand } from "./commands/permissions.js";
|
|
66
67
|
import { registerOutputStyleCommand } from "./commands/output-style.js";
|
|
67
68
|
import { registerStatuslineCommand } from "./commands/statusline.js";
|
|
@@ -464,6 +465,7 @@ export function createProgram(opts = {}) {
|
|
|
464
465
|
registerGoalCommand(program);
|
|
465
466
|
registerCommandCommand(program);
|
|
466
467
|
registerCompactCommand(program);
|
|
468
|
+
registerLoopCommand(program);
|
|
467
469
|
registerPermissionsCommand(program);
|
|
468
470
|
registerOutputStyleCommand(program);
|
|
469
471
|
registerStatuslineCommand(program);
|
package/src/lib/loop.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc loop — core driver (pure, dependency-injected) for the fixed-interval
|
|
3
|
+
* loop runner. The command layer (src/commands/loop.js) supplies a concrete
|
|
4
|
+
* `runIteration` (spawns a child process) plus a real clock; everything here
|
|
5
|
+
* is side-effect-free and clock-injected so the loop semantics — iteration
|
|
6
|
+
* counting, stop conditions, between-run delay — are deterministically
|
|
7
|
+
* testable without timers or subprocesses.
|
|
8
|
+
*
|
|
9
|
+
* Claude-Code `/loop` parity (fixed-interval MVP): run a command or agent
|
|
10
|
+
* prompt repeatedly until a stop condition fires (max iterations / exit 0 /
|
|
11
|
+
* output match) or the caller aborts (Ctrl-C).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Multipliers for the duration suffixes we accept. */
|
|
15
|
+
const DURATION_UNITS = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a human interval ("30s", "5m", "1.5h", "500ms") into milliseconds.
|
|
19
|
+
* A bare number is interpreted as SECONDS (the natural unit for an interval),
|
|
20
|
+
* so `--every 30` === `--every 30s`. Throws on anything unparseable.
|
|
21
|
+
*/
|
|
22
|
+
export function parseDuration(input) {
|
|
23
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
24
|
+
return Math.max(0, Math.round(input));
|
|
25
|
+
}
|
|
26
|
+
const s = String(input ?? "")
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase();
|
|
29
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/);
|
|
30
|
+
if (!m) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`invalid duration: "${input}" (use 30s, 5m, 1.5h, or 500ms)`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const value = parseFloat(m[1]);
|
|
36
|
+
const unit = m[2] || "s"; // bare number → seconds
|
|
37
|
+
return Math.round(value * DURATION_UNITS[unit]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Render a millisecond duration back to a compact human string. */
|
|
41
|
+
export function formatDuration(ms) {
|
|
42
|
+
if (ms < 1000) return `${ms}ms`;
|
|
43
|
+
if (ms < 60000) return `${trim(ms / 1000)}s`;
|
|
44
|
+
if (ms < 3600000) return `${trim(ms / 60000)}m`;
|
|
45
|
+
return `${trim(ms / 3600000)}h`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function trim(n) {
|
|
49
|
+
// Strip trailing ".0" so 5.0 → "5" but 1.5 stays "1.5".
|
|
50
|
+
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse the `--dynamic` control directives an iteration may print so it can
|
|
55
|
+
* self-pace. An iteration ends its output with at most one of:
|
|
56
|
+
* [[loop:next <interval>]] schedule the next run after <interval>
|
|
57
|
+
* [[loop:stop]] the task is done; stop looping
|
|
58
|
+
* Returns { done, nextDelayMs }. `stop` wins over `next` (done short-circuits
|
|
59
|
+
* before the next sleep). A malformed interval is ignored (falls back to the
|
|
60
|
+
* fixed `--every`). Lives here so the protocol is unit-testable in isolation.
|
|
61
|
+
*/
|
|
62
|
+
export function parseLoopDirectives(output) {
|
|
63
|
+
const text = String(output || "");
|
|
64
|
+
const result = { done: false, nextDelayMs: null };
|
|
65
|
+
if (/\[\[\s*loop:stop\s*\]\]/i.test(text)) result.done = true;
|
|
66
|
+
const m = text.match(/\[\[\s*loop:next\s+([0-9.]+\s*(?:ms|s|m|h)?)\s*\]\]/i);
|
|
67
|
+
if (m) {
|
|
68
|
+
try {
|
|
69
|
+
result.nextDelayMs = parseDuration(m[1]);
|
|
70
|
+
} catch {
|
|
71
|
+
/* malformed interval → leave null, caller falls back to --every */
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reduce a persisted loop session's events into the state needed to resume it:
|
|
79
|
+
* the original `loop_config`, how many iterations already completed, and the
|
|
80
|
+
* last recorded exit code. Pure (operates on the event array, no fs) so the
|
|
81
|
+
* resume reconstruction is unit-testable without the session store.
|
|
82
|
+
*/
|
|
83
|
+
export function summarizeLoopEvents(events) {
|
|
84
|
+
let config = null;
|
|
85
|
+
let completedIterations = 0;
|
|
86
|
+
let lastExitCode = null;
|
|
87
|
+
for (const e of events || []) {
|
|
88
|
+
if (e?.type === "loop_config") {
|
|
89
|
+
config = e.data || null;
|
|
90
|
+
} else if (e?.type === "loop_iteration") {
|
|
91
|
+
completedIterations += 1;
|
|
92
|
+
if (e.data && typeof e.data.exitCode !== "undefined") {
|
|
93
|
+
lastExitCode = e.data.exitCode;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { config, completedIterations, lastExitCode };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Default abortable sleep — resolves early if the signal aborts. */
|
|
101
|
+
export function makeSleep(signal) {
|
|
102
|
+
return (ms) =>
|
|
103
|
+
new Promise((resolve) => {
|
|
104
|
+
if (signal?.aborted || ms <= 0) return resolve();
|
|
105
|
+
// NB: do NOT unref() — the pending interval timer is what keeps the
|
|
106
|
+
// process alive between rounds. Under a TTY the active stdin would mask
|
|
107
|
+
// an unref'd timer, but headless (piped stdin / CI / cron) the loop would
|
|
108
|
+
// exit after the first round. SIGINT aborts the wait via the signal.
|
|
109
|
+
const t = setTimeout(resolve, ms);
|
|
110
|
+
signal?.addEventListener(
|
|
111
|
+
"abort",
|
|
112
|
+
() => {
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
resolve();
|
|
115
|
+
},
|
|
116
|
+
{ once: true },
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Drive the loop. Calls `runIteration(n)` once per round (1-based), evaluates
|
|
123
|
+
* the stop conditions AFTER each round, and sleeps `intervalMs` between rounds
|
|
124
|
+
* (never after the final round). Returns a summary describing why it stopped.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} opts
|
|
127
|
+
* @param {(n:number)=>Promise<{exitCode?:number, output?:string}>} opts.runIteration
|
|
128
|
+
* @param {number} opts.intervalMs default delay between iterations (>= 0)
|
|
129
|
+
* @param {number} [opts.maxIterations] stop after N rounds (>= 1)
|
|
130
|
+
* @param {boolean} [opts.untilExitZero] stop once a round exits with code 0
|
|
131
|
+
* @param {RegExp} [opts.untilRegex] stop once a round's output matches
|
|
132
|
+
* @param {(ms:number)=>Promise<void>} [opts.sleep] injectable delay
|
|
133
|
+
* @param {()=>boolean} [opts.shouldStop] external stop probe (e.g. SIGINT)
|
|
134
|
+
* @param {(n:number, res:object)=>void} [opts.onIteration] per-round hook
|
|
135
|
+
* @param {number} [opts.startIndex] iterations already done (resume)
|
|
136
|
+
* @returns {Promise<{iterations:number, stoppedBy:string, results:object[]}>}
|
|
137
|
+
* `iterations` is cumulative (startIndex + rounds run this call);
|
|
138
|
+
* `results` holds only this call's rounds.
|
|
139
|
+
*/
|
|
140
|
+
export async function runLoop({
|
|
141
|
+
runIteration,
|
|
142
|
+
intervalMs,
|
|
143
|
+
maxIterations,
|
|
144
|
+
untilExitZero = false,
|
|
145
|
+
untilRegex = null,
|
|
146
|
+
sleep,
|
|
147
|
+
shouldStop,
|
|
148
|
+
onIteration,
|
|
149
|
+
startIndex = 0,
|
|
150
|
+
}) {
|
|
151
|
+
if (typeof runIteration !== "function") {
|
|
152
|
+
throw new Error("runLoop requires a runIteration function");
|
|
153
|
+
}
|
|
154
|
+
const delay = sleep || makeSleep();
|
|
155
|
+
const results = [];
|
|
156
|
+
// Iterations already completed in a prior (resumed) run. `i` continues from
|
|
157
|
+
// here so the displayed/persisted round numbers are cumulative and
|
|
158
|
+
// `maxIterations` counts across resume.
|
|
159
|
+
let i = startIndex;
|
|
160
|
+
|
|
161
|
+
while (true) {
|
|
162
|
+
if (shouldStop && shouldStop()) {
|
|
163
|
+
return { iterations: i, stoppedBy: "signal", results };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
i += 1;
|
|
167
|
+
const res = (await runIteration(i)) || {};
|
|
168
|
+
results.push(res);
|
|
169
|
+
if (onIteration) onIteration(i, res);
|
|
170
|
+
|
|
171
|
+
// Stop conditions, most-specific first. Evaluated after the round so the
|
|
172
|
+
// work always runs at least once before any condition can end the loop.
|
|
173
|
+
// `res.done` is the iteration's own explicit stop (e.g. a --dynamic
|
|
174
|
+
// [[loop:stop]] directive) and wins over everything else.
|
|
175
|
+
if (res.done) {
|
|
176
|
+
return { iterations: i, stoppedBy: "done", results };
|
|
177
|
+
}
|
|
178
|
+
if (untilExitZero && res.exitCode === 0) {
|
|
179
|
+
return { iterations: i, stoppedBy: "exit-zero", results };
|
|
180
|
+
}
|
|
181
|
+
if (untilRegex && untilRegex.test(res.output || "")) {
|
|
182
|
+
return { iterations: i, stoppedBy: "match", results };
|
|
183
|
+
}
|
|
184
|
+
if (maxIterations && i >= maxIterations) {
|
|
185
|
+
return { iterations: i, stoppedBy: "max-iterations", results };
|
|
186
|
+
}
|
|
187
|
+
if (shouldStop && shouldStop()) {
|
|
188
|
+
return { iterations: i, stoppedBy: "signal", results };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// An iteration may set its own next interval (--dynamic [[loop:next]]);
|
|
192
|
+
// otherwise fall back to the fixed --every delay.
|
|
193
|
+
const nextMs = Number.isFinite(res.nextDelayMs)
|
|
194
|
+
? res.nextDelayMs
|
|
195
|
+
: intervalMs;
|
|
196
|
+
await delay(nextMs);
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -630,6 +630,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
630
630
|
mcpConfigPath: options.mcpConfig || null,
|
|
631
631
|
db: db?.getDatabase?.() || null,
|
|
632
632
|
includeRegistered: options.useRegisteredMcp !== false,
|
|
633
|
+
// IDE bridge: auto-connect a running editor's MCP server when inside
|
|
634
|
+
// an IDE integrated terminal. --ide forces it, --no-ide disables it
|
|
635
|
+
// (parity with headless; auto-detect already works via process.env).
|
|
636
|
+
ide: options.ide,
|
|
637
|
+
cwd: process.cwd(),
|
|
633
638
|
},
|
|
634
639
|
{ writeErr: (s) => process.stderr.write(s) },
|
|
635
640
|
);
|
|
@@ -35,6 +35,9 @@ export function resolveAgentPolicy({
|
|
|
35
35
|
fallbackModel: overrides.fallbackModel || null,
|
|
36
36
|
mcpConfig: overrides.mcpConfig || null,
|
|
37
37
|
useRegisteredMcp: overrides.useRegisteredMcp !== false,
|
|
38
|
+
// IDE bridge tri-state (undefined=auto / true=--ide / false=--no-ide); the
|
|
39
|
+
// REPL forwards it to resolveAgentMcp so --ide/--no-ide work interactively.
|
|
40
|
+
ide: overrides.ide,
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
|