chainlesschain 0.162.36 → 0.162.38

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 (160) hide show
  1. package/package.json +3 -2
  2. package/src/assets/web-panel/assets/{AIOps-vAVAFNJ4.js → AIOps-DV0Q9zKL.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BnRHFCKM.js → ActionButton-C6vH8rhL.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BOjwqWqG.js → Analytics-BvPDc2ui.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-Dc0D1Txn.js → AppLayout-CWnyqTqY.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-dd_2efaZ.js → Audit-BzenidV4.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-HF1jgm8G.js → Backup-CSl7bNwK.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-CCtzmoKe.js → BaseInput-DAY3iHIq.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-BNfH1c3p.js → Chat-Jyhm9fgk.js} +6 -6
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DCWFqmI4.js → ChatBubbleRenderer-CwlAnVjy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-BOr-NscK.js → Checkbox-D4rwURAi.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-DE058N7-.js → Codegen-DYdjTEfC.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-SOREo1XE.js → Col-DsVyZ_fS.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-sOvNZo9f.js → Community-CjCpl27Q.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DnBe558D.js → Compact-kt18dsjm.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-o-r6CUbg.js → Compliance-BV5urquU.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-D6_k9mHP.js → Cowork-C4SovPWC.js} +3 -3
  18. package/src/assets/web-panel/assets/{Cron-CEV3Xkrm.js → Cron-uuNs_xzA.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-eJ1lQWKU.js → Crosschain-DR5a65tR.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-B-WqM9Hp.js → DID-B1KTf2-5.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-ZnKPcsHN.js → Dashboard-Dkj7XgED.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-B8uLWDIP.js → Dropdown-BhXCuJ19.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-Jmj2Y7aH.js → EmailListRenderer-DG8365Iv.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cb2xetG-.js → FamilyGuardDashboard-BdHGPu39.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-C_07GXoq.js → Federation-Dwvxl0zR.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-D3kbYrMU.js → FormItemContext-BVmhCVWU.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-9xgqvGPg.js → GenericCardRenderer-DDPjvF2s.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-BlwWlMMB.js → Git-foK6WTSr.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-DxN3wQZ_.js → Governance-CfqMdu6Y.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-ls7pSw_D.js → Inference-BKrLO4GO.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-_n9hYuPI.js → KnowledgeGraph-6o6Q-mmF.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CvEVY5TK.js → Logs-L5ZIW0Dz.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-C3qvQJT7.js → Marketplace-BWkfEocP.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-DiwKpnKx.js → McpTools-BPebQbWU.js} +4 -4
  35. package/src/assets/web-panel/assets/{Memory-CIBPi_da.js → Memory-C0Dq-X3C.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-D-v0Se8y.js → MobileBridge-DRBoutTY.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-cP1apTQD.js → MobileProjects-BMP6eLp1.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-BMFWrI65.js → Mtc-Cj3QPM9p.js} +4 -4
  39. package/src/assets/web-panel/assets/{MtcAudit-2s8LaHtR.js → MtcAudit-rBQYbfQR.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-dL_nvj7d.js → Multisig-Dbuy4OY4.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BbrJp06R.js → NLProgramming-CMnt1se-.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-jR9irwy3.js → Notes-BX9tSCiF.js} +4 -4
  43. package/src/assets/web-panel/assets/{NotificationSettings-Dk-STCIX.js → NotificationSettings-BFeirVRq.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-CqqfY6zq.js → OrderTableRenderer-ybiMlKQW.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-BCK5jylo.js → Organization-kTfRxKqk.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-BRAY7Smt.js → Overflow-CtuCAzwV.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BltVRGjb.js → P2P-KfbciaP3.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-CV8UbXHe.js → PdhVaultBrowser-bqEUFhgC.js} +5 -5
  49. package/src/assets/web-panel/assets/{Permissions-_tNl47Qh.js → Permissions-BgMypz-z.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-Cgc4HjpX.js → PersonalDataHub-C3zUE-1z.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-Bn_QU4mu.js → Pipeline-iX-pYHpC.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-jzJowp5P.js → Privacy-B01uzeFM.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-B_1pJ8qd.js → ProjectInit-TsfbzJp7.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-CPVZpXzs.js → ProjectSettings-iGvMp8sM.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-CQsHOWnT.js → Projects-Be9k29iQ.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-CzzMiLC0.js → Providers-C9Pc8dqo.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-MxBKIn9o.js → QuickAsk-DN_yFiVO.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-D8lN6Lis.js → Recommend-CvSNgl7H.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-CfYK-IrV.js → Reputation-S6BCz8xH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-Bg7NZDP9.js → Row-CTRYCaqP.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-BOVNJhj0.js → RssFeed-Cu8_P5ll.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-B38qzmhY.js → Search-rZ1Xza_U.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-CjqleZpe.js → Security-CF43IJHX.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-Bu9JSJap.js → Services-BobNHzne.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-B2RvRkaX.js → Skeleton-DWJ2kfuI.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-_h42mxMN.js → Skills-AmEZgHYr.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-BssLs56D.js → Sla-DTS-fBiY.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-DCxFYHsd.js → SpeechSettings-DEr6MHRU.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-D2xQuNLE.js → SyncSettings-CVs9alv_.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-DhpOGOlo.js → Tasks-BcVDAxdi.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-CYG-R-aS.js → Templates-CTNjZRKA.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-BQRYLsvP.js → Tenant-DPbXg0Pg.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-imKU7N5j.js → Terminal-DhKXcPw2.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-BIZzBftk.js → TimelineRenderer-B0DMZOpk.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-uMLH5p_a.js → Tokens-RvWuBXgg.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-BzS6XPqx.js → Trigger-2O-BaTQG.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-R4zhHufZ.js → Trust-6qY35L-C.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-DATQCoGe.js → UkeySign-DhV1wYtQ.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-ClUmKOtS.js → VideoEditing-DgqA5UZm.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-DzJTbQzD.js → Wallet-DJRYdUAK.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-CrXrLmzQ.js → WebAuthn-C2W-x0cg.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CpvZ0Tma.js → WorkflowEditor-BP2tkDHe.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-a6wpYmVL.js → chat-CGVfeoTn.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-CXJADb1t.js → colors-BmjRolM1.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-CL2pohS_.js → compact-item-BvJJkjZE.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-xFi_1G5_.js → createContext-DyhlvRYs.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-CetO0WH0.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-Bchh1rAi.js → hasIn-BoBMR89s.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-C2eMYASq.js → index-39VDXdn6.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-CR3kFPuC.js → index-81tWFqfN.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CTRd7vkq.js → index-BT1SQ9nj.js} +1 -1
  92. package/src/assets/web-panel/assets/index-BZVz-WfV.js +1 -0
  93. package/src/assets/web-panel/assets/{index-D-TT9Swq.js → index-Beh7jDbS.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BrbJBnT-.js → index-Bm_MmdwP.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-dsLc7t6W.js → index-BqGNmoKy.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DEYcLAl7.js → index-BuQrONgf.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-KCib1PTw.js → index-BvvNnWXe.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DxahxRP7.js → index-ByWpNjTj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-B6NehWty.js → index-BycpeGfj.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DTEu7TSF.js → index-C0xn6hOr.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BH9t10pe.js → index-C1t-r7yV.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-B7wT5VRi.js → index-CDPMHKQi.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-majCS3s2.js → index-CIaGw7vl.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-EPERz4Pu.js → index-CQJVedQ3.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-IkvkNxbc.js → index-CSgbOGaP.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DQ_hw_5P.js → index-Cbh-lCxq.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CMybtJY6.js → index-CzDVBBcg.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-B4zNisy9.js → index-Czsbrn75.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-M8SZI11a.js → index-D-93XwJd.js} +1 -1
  110. package/src/assets/web-panel/assets/index-D0-bvFy3.js +1 -0
  111. package/src/assets/web-panel/assets/{index-TxbHusq2.js → index-D0YToIi_.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-B7knYOpm.js → index-DIPZ6hbJ.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CGq4HQno.js → index-DeeLHcMY.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CdU8BwRW.js → index-DgbWSwr5.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C4yBRKT4.js → index-DtKdCXHW.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-jMcv1u5o.js → index-DwTgvhOL.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-B3Tpv7-d.js → index-DyS4I4L-.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-u8K1y_lh.js → index-FKFT-QTk.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-Cua_P8St.js → index-Te0ruvY_.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DVo1GJoj.js → index-VXVukhBA.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BPH5ESqs.js → index-Y1b8i0NV.js} +3 -3
  122. package/src/assets/web-panel/assets/{index-DsbMVBj1.js → index-ZNIms1nA.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BoaRB-4a.js → index-n-N19np-.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BmsIKzyu.js → index-vF1pR00A.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CuehgDOp.js → index-wLAjVpmJ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DjdOL159.js → index-xPSzUoWT.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BF4xx1_b.js → index-xZdOioVg.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-DYn3Gc09.js → initDefaultProps-BLKSE8he.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-ZS3eolb9.js → motion-Bb59qqLK.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-CEw4uqr3.js → move-CB3pYCk6.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-DlHFZnPp.js → omit-iImQWuU7.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-eZQvV5fA.js → pickAttrs-DRP2Chqo.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-B31jQwa-.js → placementArrow-BrlfD4tF.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-DAsNmVto.js → responsiveObserve-Cqxkuh5H.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-gPQPrYZC.js → slide-nxKEuLMj.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-DwWKX5co.js → statusUtils-30E47KSk.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-B3VOtXuH.js → styleChecker-Dn2_-5bn.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-6ADctM2r.js → useFlexGapSupport-DkZ00X6F.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-6Zx1SSKs.js → useFs-ByrwSCOr.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-BzReowln.js → usePersonalDataHub-BDY6jtUD.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-C8IpEQbD.js → vnode-BL2q5BLv.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-ruc9vHr0.js → zoom-BSkPKE42.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +31 -0
  145. package/src/commands/cli-anything.js +14 -6
  146. package/src/commands/loop.js +450 -0
  147. package/src/commands/mcp.js +236 -6
  148. package/src/harness/mcp-client.js +70 -1
  149. package/src/index.js +2 -0
  150. package/src/lib/loop.js +198 -0
  151. package/src/lib/settings-hooks.cjs +1 -0
  152. package/src/repl/agent-repl.js +57 -20
  153. package/src/repl/mcp-prompt.js +122 -0
  154. package/src/runtime/agent-core.js +123 -17
  155. package/src/runtime/headless-runner.js +34 -9
  156. package/src/runtime/mcp-config.js +118 -9
  157. package/src/runtime/policies/agent-policy.js +3 -0
  158. package/src/assets/web-panel/assets/devWarning-BtmELbtB.js +0 -1
  159. package/src/assets/web-panel/assets/index-B4l4vLTB.js +0 -1
  160. package/src/assets/web-panel/assets/index-B7Ek5iiY.js +0 -1
@@ -6,6 +6,7 @@
6
6
  import chalk from "chalk";
7
7
  import { logger } from "../lib/logger.js";
8
8
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
9
+ import { withQuietStdout } from "../runtime/quiet-stdout.js";
9
10
  import {
10
11
  ensureCliAnythingTables,
11
12
  detectPython,
@@ -17,6 +18,13 @@ import {
17
18
  listTools,
18
19
  } from "../lib/cli-anything-bridge.js";
19
20
 
21
+ // bootstrap()/shutdown() log "[AppConfig] …" and "[DatabaseManager] …" via
22
+ // console.info, which Node writes to stdout. That chatter corrupts the JSON
23
+ // emitted by `--json` subcommands. Divert it to stderr (the diagnostic
24
+ // channel) so stdout stays a pristine machine-readable payload.
25
+ const quietBootstrap = (opts) => withQuietStdout(() => bootstrap(opts));
26
+ const quietShutdown = () => withQuietStdout(() => shutdown());
27
+
20
28
  export function registerCliAnythingCommand(program) {
21
29
  const cliAny = program
22
30
  .command("cli-anything")
@@ -135,7 +143,7 @@ export function registerCliAnythingCommand(program) {
135
143
  .option("--json", "Output as JSON")
136
144
  .action(async (name, opts) => {
137
145
  try {
138
- const ctx = await bootstrap({ verbose: program.opts().verbose });
146
+ const ctx = await quietBootstrap({ verbose: program.opts().verbose });
139
147
  if (!ctx.db) {
140
148
  logger.error(
141
149
  "Database not available. Run `chainlesschain setup` first.",
@@ -154,7 +162,7 @@ export function registerCliAnythingCommand(program) {
154
162
  force: opts.force,
155
163
  });
156
164
 
157
- await shutdown();
165
+ await quietShutdown();
158
166
 
159
167
  if (opts.json) {
160
168
  console.log(JSON.stringify(result, null, 2));
@@ -188,7 +196,7 @@ export function registerCliAnythingCommand(program) {
188
196
  .option("--json", "Output as JSON")
189
197
  .action(async (opts) => {
190
198
  try {
191
- const ctx = await bootstrap({ verbose: program.opts().verbose });
199
+ const ctx = await quietBootstrap({ verbose: program.opts().verbose });
192
200
  if (!ctx.db) {
193
201
  logger.error("Database not available.");
194
202
  process.exit(1);
@@ -197,7 +205,7 @@ export function registerCliAnythingCommand(program) {
197
205
  ensureCliAnythingTables(db);
198
206
 
199
207
  const tools = listTools(db);
200
- await shutdown();
208
+ await quietShutdown();
201
209
 
202
210
  if (opts.json) {
203
211
  console.log(JSON.stringify(tools, null, 2));
@@ -241,7 +249,7 @@ export function registerCliAnythingCommand(program) {
241
249
  .option("--json", "Output as JSON")
242
250
  .action(async (name, opts) => {
243
251
  try {
244
- const ctx = await bootstrap({ verbose: program.opts().verbose });
252
+ const ctx = await quietBootstrap({ verbose: program.opts().verbose });
245
253
  if (!ctx.db) {
246
254
  logger.error("Database not available.");
247
255
  process.exit(1);
@@ -250,7 +258,7 @@ export function registerCliAnythingCommand(program) {
250
258
  ensureCliAnythingTables(db);
251
259
 
252
260
  const result = removeTool(db, name);
253
- await shutdown();
261
+ await quietShutdown();
254
262
 
255
263
  if (opts.json) {
256
264
  console.log(JSON.stringify(result, null, 2));
@@ -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
+ }