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.
Files changed (154) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-vAVAFNJ4.js → AIOps-_oxz4VHy.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BnRHFCKM.js → ActionButton-uaeqFuDj.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BOjwqWqG.js → Analytics-BPVV0OUf.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Dc0D1Txn.js → AppLayout-ppCYKm3I.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-dd_2efaZ.js → Audit-DFAY6umk.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-HF1jgm8G.js → Backup-pAPBFDyP.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-CCtzmoKe.js → BaseInput-BbBl0uT2.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-BNfH1c3p.js → Chat-Ct22JUnT.js} +6 -6
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DCWFqmI4.js → ChatBubbleRenderer-DPlsLl22.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-BOr-NscK.js → Checkbox-DEkCollc.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-DE058N7-.js → Codegen-Tor-de39.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-SOREo1XE.js → Col-ojNrLQU7.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-sOvNZo9f.js → Community-CLOGhqMF.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DnBe558D.js → Compact-CYKNlSZ4.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-o-r6CUbg.js → Compliance-C5E6ABuA.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-D6_k9mHP.js → Cowork-CHeEsZ3W.js} +3 -3
  18. package/src/assets/web-panel/assets/{Cron-CEV3Xkrm.js → Cron-B4e1n2e7.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-eJ1lQWKU.js → Crosschain-DbNV8P9R.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-B-WqM9Hp.js → DID-C5_Tk3nC.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-ZnKPcsHN.js → Dashboard-BhdV_c4N.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-B8uLWDIP.js → Dropdown-CEi5AMtM.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-Jmj2Y7aH.js → EmailListRenderer-DOhPiYng.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cb2xetG-.js → FamilyGuardDashboard-fu4NRP3X.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-C_07GXoq.js → Federation-B7BtIWKL.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-D3kbYrMU.js → FormItemContext-BmPWZVLP.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-9xgqvGPg.js → GenericCardRenderer-hsOPNJq8.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-BlwWlMMB.js → Git-Bi_EFBUH.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-DxN3wQZ_.js → Governance-emf2ubDK.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-ls7pSw_D.js → Inference-B7KjKzkI.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-_n9hYuPI.js → KnowledgeGraph-uAaBK0F3.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CvEVY5TK.js → Logs-utK7hNpj.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-C3qvQJT7.js → Marketplace-CzQe6n3z.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-DiwKpnKx.js → McpTools-CuAaJr51.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-CIBPi_da.js → Memory-CRuZZJ75.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-D-v0Se8y.js → MobileBridge-Cp06wunh.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-cP1apTQD.js → MobileProjects-DJEdUwhr.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-BMFWrI65.js → Mtc-8YY4dR7g.js} +4 -4
  39. package/src/assets/web-panel/assets/{MtcAudit-2s8LaHtR.js → MtcAudit-BmPJYHar.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-dL_nvj7d.js → Multisig-d-ydyVdq.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BbrJp06R.js → NLProgramming-DA_ikw_n.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-jR9irwy3.js → Notes-DIyF-fRe.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-Dk-STCIX.js → NotificationSettings-CzPZXEtK.js} +1 -1
  44. package/src/assets/web-panel/assets/OrderTableRenderer-BiLtg-LY.js +1 -0
  45. package/src/assets/web-panel/assets/{Organization-BCK5jylo.js → Organization-DdDZ_Ap6.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-BRAY7Smt.js → Overflow-BnMBkttv.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BltVRGjb.js → P2P-Es1050f-.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-CV8UbXHe.js → PdhVaultBrowser-CKkRmyn9.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-_tNl47Qh.js → Permissions-zU9n9cAD.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-Cgc4HjpX.js → PersonalDataHub-BZi5Xwas.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-Bn_QU4mu.js → Pipeline-CRfeGiFc.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-jzJowp5P.js → Privacy-CQA_IgLA.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-B_1pJ8qd.js → ProjectInit-C9hmEvoT.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-CPVZpXzs.js → ProjectSettings-yXA72ws4.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-CQsHOWnT.js → Projects-BpWS-qam.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-CzzMiLC0.js → Providers-Cxe55dRD.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-MxBKIn9o.js → QuickAsk-Do0aUTQr.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-D8lN6Lis.js → Recommend--ysZHjyA.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-CfYK-IrV.js → Reputation-BOBU8JrH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-Bg7NZDP9.js → Row-C6X7bRKE.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-BOVNJhj0.js → RssFeed-D8AwqlkQ.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-B38qzmhY.js → Search-Bi3rCZD4.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-CjqleZpe.js → Security-DxUDVrtY.js} +3 -3
  64. package/src/assets/web-panel/assets/{Services-Bu9JSJap.js → Services-BXXN7yC1.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-B2RvRkaX.js → Skeleton-B3BR34tZ.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-_h42mxMN.js → Skills-BjYu8OQ1.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-BssLs56D.js → Sla-DDkCtD8w.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-DCxFYHsd.js → SpeechSettings-CGhYzP7V.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-D2xQuNLE.js → SyncSettings-CYNKVAHA.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-DhpOGOlo.js → Tasks-DavmlJpd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-CYG-R-aS.js → Templates-CQuYFf2C.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-BQRYLsvP.js → Tenant-DdzZh8vE.js} +1 -1
  73. package/src/assets/web-panel/assets/Terminal-D75WeG9d.js +3 -0
  74. package/src/assets/web-panel/assets/{TimelineRenderer-BIZzBftk.js → TimelineRenderer-DKOARnc_.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-uMLH5p_a.js → Tokens-D7QRNG8y.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-BzS6XPqx.js → Trigger-BCsqLZl4.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-R4zhHufZ.js → Trust-BarGUa6p.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-DATQCoGe.js → UkeySign-pHrg5a8E.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-ClUmKOtS.js → VideoEditing-Dug3m1py.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-DzJTbQzD.js → Wallet-BfK3Z_Ez.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-CrXrLmzQ.js → WebAuthn-CYRdl9td.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CpvZ0Tma.js → WorkflowEditor-DTW5AcqM.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-a6wpYmVL.js → chat-CCXz4j38.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-CXJADb1t.js → colors-BJBOhAqa.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-CL2pohS_.js → compact-item-E9M6BQcM.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-xFi_1G5_.js → createContext-Cg9CAws4.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BrsbTJUv.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-Bchh1rAi.js → hasIn-DhVtqv5L.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-C2eMYASq.js → index--7o5YdL6.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BmsIKzyu.js → index-4N5lNXGP.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CuehgDOp.js → index-6-04M2Nx.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-BH9t10pe.js → index-B111fZ21.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-BoaRB-4a.js → index-B4NBF4Sa.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-KCib1PTw.js → index-B8bjEHrQ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-majCS3s2.js → index-BAB0nGP7.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DsbMVBj1.js → index-BFZPRd0T.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-B7wT5VRi.js → index-B_SMPD4L.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-TxbHusq2.js → index-BxSzyly9.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B4zNisy9.js → index-ByazO4Q9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Cua_P8St.js → index-C-2dUIli.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-EPERz4Pu.js → index-CFarAlXj.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-B6NehWty.js → index-CFp-wdrQ.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CTRd7vkq.js → index-CJ8nNT8h.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CR3kFPuC.js → index-CSiyjCYi.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-u8K1y_lh.js → index-CUp_c8Le.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DxahxRP7.js → index-CVR_s-pT.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C4yBRKT4.js → index-Ca8BYV1g.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-B7knYOpm.js → index-CeRlLp3F.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-jMcv1u5o.js → index-ChsSljaN.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CGq4HQno.js → index-CkTeBHI9.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-M8SZI11a.js → index-Cm1m7BJh.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D-TT9Swq.js → index-ComyTKz-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BPH5ESqs.js → index-CznfPnOx.js} +3 -3
  114. package/src/assets/web-panel/assets/{index-dsLc7t6W.js → index-D5yC2Ps8.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DjdOL159.js → index-D7DXdf7x.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DVo1GJoj.js → index-DDcJO27F.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-IkvkNxbc.js → index-DSQazU6J.js} +1 -1
  118. package/src/assets/web-panel/assets/index-DSTQDO-Y.js +1 -0
  119. package/src/assets/web-panel/assets/{index-CMybtJY6.js → index-DaFe1aqY.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B3Tpv7-d.js → index-DdhnGez0.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BF4xx1_b.js → index-Di5LBXcE.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BrbJBnT-.js → index-Dwvewrul.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DQ_hw_5P.js → index-MdXEhfdJ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DEYcLAl7.js → index-_PNqQ5mE.js} +1 -1
  125. package/src/assets/web-panel/assets/index-c2U6LV3Q.js +1 -0
  126. package/src/assets/web-panel/assets/{index-CdU8BwRW.js → index-kz1oXl1a.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DTEu7TSF.js → index-wkt-o5q5.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-DYn3Gc09.js → initDefaultProps-iyBaePF-.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-ZS3eolb9.js → motion-RWtj4rgu.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-CEw4uqr3.js → move-CqPRVzpH.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-DlHFZnPp.js → omit-DsvJze25.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-eZQvV5fA.js → pickAttrs-B4tfZBhc.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-B31jQwa-.js → placementArrow-KvHUwXMA.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-DAsNmVto.js → responsiveObserve-DGdJ-b7W.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-gPQPrYZC.js → slide-Cd6ebRmw.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-DwWKX5co.js → statusUtils-Bg9GcIAn.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-B3VOtXuH.js → styleChecker-MQjKsG84.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-6ADctM2r.js → useFlexGapSupport-C241WujP.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-6Zx1SSKs.js → useFs-CMpy7RS4.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-BzReowln.js → usePersonalDataHub-BLHtapKb.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-C8IpEQbD.js → vnode-DmcTV67c.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-ruc9vHr0.js → zoom-DHL8_0Y8.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/cli-anything.js +14 -6
  145. package/src/commands/loop.js +450 -0
  146. package/src/index.js +2 -0
  147. package/src/lib/loop.js +198 -0
  148. package/src/repl/agent-repl.js +5 -0
  149. package/src/runtime/policies/agent-policy.js +3 -0
  150. package/src/assets/web-panel/assets/OrderTableRenderer-CqqfY6zq.js +0 -1
  151. package/src/assets/web-panel/assets/Terminal-imKU7N5j.js +0 -3
  152. package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +0 -1
  153. package/src/assets/web-panel/assets/index-B4l4vLTB.js +0 -1
  154. 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);
@@ -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
+ }
@@ -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