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.
- package/package.json +3 -2
- package/src/assets/web-panel/assets/{AIOps-vAVAFNJ4.js → AIOps-DV0Q9zKL.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BnRHFCKM.js → ActionButton-C6vH8rhL.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BOjwqWqG.js → Analytics-BvPDc2ui.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Dc0D1Txn.js → AppLayout-CWnyqTqY.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-dd_2efaZ.js → Audit-BzenidV4.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-HF1jgm8G.js → Backup-CSl7bNwK.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CCtzmoKe.js → BaseInput-DAY3iHIq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BNfH1c3p.js → Chat-Jyhm9fgk.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DCWFqmI4.js → ChatBubbleRenderer-CwlAnVjy.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BOr-NscK.js → Checkbox-D4rwURAi.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DE058N7-.js → Codegen-DYdjTEfC.js} +1 -1
- package/src/assets/web-panel/assets/{Col-SOREo1XE.js → Col-DsVyZ_fS.js} +1 -1
- package/src/assets/web-panel/assets/{Community-sOvNZo9f.js → Community-CjCpl27Q.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DnBe558D.js → Compact-kt18dsjm.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-o-r6CUbg.js → Compliance-BV5urquU.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D6_k9mHP.js → Cowork-C4SovPWC.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-CEV3Xkrm.js → Cron-uuNs_xzA.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-eJ1lQWKU.js → Crosschain-DR5a65tR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B-WqM9Hp.js → DID-B1KTf2-5.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-ZnKPcsHN.js → Dashboard-Dkj7XgED.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-B8uLWDIP.js → Dropdown-BhXCuJ19.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Jmj2Y7aH.js → EmailListRenderer-DG8365Iv.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Cb2xetG-.js → FamilyGuardDashboard-BdHGPu39.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C_07GXoq.js → Federation-Dwvxl0zR.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-D3kbYrMU.js → FormItemContext-BVmhCVWU.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-9xgqvGPg.js → GenericCardRenderer-DDPjvF2s.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BlwWlMMB.js → Git-foK6WTSr.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DxN3wQZ_.js → Governance-CfqMdu6Y.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-ls7pSw_D.js → Inference-BKrLO4GO.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-_n9hYuPI.js → KnowledgeGraph-6o6Q-mmF.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CvEVY5TK.js → Logs-L5ZIW0Dz.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-C3qvQJT7.js → Marketplace-BWkfEocP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DiwKpnKx.js → McpTools-BPebQbWU.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-CIBPi_da.js → Memory-C0Dq-X3C.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D-v0Se8y.js → MobileBridge-DRBoutTY.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-cP1apTQD.js → MobileProjects-BMP6eLp1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BMFWrI65.js → Mtc-Cj3QPM9p.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-2s8LaHtR.js → MtcAudit-rBQYbfQR.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-dL_nvj7d.js → Multisig-Dbuy4OY4.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BbrJp06R.js → NLProgramming-CMnt1se-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-jR9irwy3.js → Notes-BX9tSCiF.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Dk-STCIX.js → NotificationSettings-BFeirVRq.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CqqfY6zq.js → OrderTableRenderer-ybiMlKQW.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BCK5jylo.js → Organization-kTfRxKqk.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BRAY7Smt.js → Overflow-CtuCAzwV.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BltVRGjb.js → P2P-KfbciaP3.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CV8UbXHe.js → PdhVaultBrowser-bqEUFhgC.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-_tNl47Qh.js → Permissions-BgMypz-z.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Cgc4HjpX.js → PersonalDataHub-C3zUE-1z.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-Bn_QU4mu.js → Pipeline-iX-pYHpC.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-jzJowp5P.js → Privacy-B01uzeFM.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B_1pJ8qd.js → ProjectInit-TsfbzJp7.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CPVZpXzs.js → ProjectSettings-iGvMp8sM.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CQsHOWnT.js → Projects-Be9k29iQ.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CzzMiLC0.js → Providers-C9Pc8dqo.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-MxBKIn9o.js → QuickAsk-DN_yFiVO.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-D8lN6Lis.js → Recommend-CvSNgl7H.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CfYK-IrV.js → Reputation-S6BCz8xH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Bg7NZDP9.js → Row-CTRYCaqP.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BOVNJhj0.js → RssFeed-Cu8_P5ll.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B38qzmhY.js → Search-rZ1Xza_U.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CjqleZpe.js → Security-CF43IJHX.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Bu9JSJap.js → Services-BobNHzne.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B2RvRkaX.js → Skeleton-DWJ2kfuI.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-_h42mxMN.js → Skills-AmEZgHYr.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BssLs56D.js → Sla-DTS-fBiY.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCxFYHsd.js → SpeechSettings-DEr6MHRU.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D2xQuNLE.js → SyncSettings-CVs9alv_.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DhpOGOlo.js → Tasks-BcVDAxdi.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYG-R-aS.js → Templates-CTNjZRKA.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BQRYLsvP.js → Tenant-DPbXg0Pg.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-imKU7N5j.js → Terminal-DhKXcPw2.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BIZzBftk.js → TimelineRenderer-B0DMZOpk.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-uMLH5p_a.js → Tokens-RvWuBXgg.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BzS6XPqx.js → Trigger-2O-BaTQG.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-R4zhHufZ.js → Trust-6qY35L-C.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DATQCoGe.js → UkeySign-DhV1wYtQ.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-ClUmKOtS.js → VideoEditing-DgqA5UZm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzJTbQzD.js → Wallet-DJRYdUAK.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CrXrLmzQ.js → WebAuthn-C2W-x0cg.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CpvZ0Tma.js → WorkflowEditor-BP2tkDHe.js} +1 -1
- package/src/assets/web-panel/assets/{chat-a6wpYmVL.js → chat-CGVfeoTn.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CXJADb1t.js → colors-BmjRolM1.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CL2pohS_.js → compact-item-BvJJkjZE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-xFi_1G5_.js → createContext-DyhlvRYs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CetO0WH0.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bchh1rAi.js → hasIn-BoBMR89s.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2eMYASq.js → index-39VDXdn6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CR3kFPuC.js → index-81tWFqfN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTRd7vkq.js → index-BT1SQ9nj.js} +1 -1
- package/src/assets/web-panel/assets/index-BZVz-WfV.js +1 -0
- package/src/assets/web-panel/assets/{index-D-TT9Swq.js → index-Beh7jDbS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BrbJBnT-.js → index-Bm_MmdwP.js} +1 -1
- package/src/assets/web-panel/assets/{index-dsLc7t6W.js → index-BqGNmoKy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DEYcLAl7.js → index-BuQrONgf.js} +1 -1
- package/src/assets/web-panel/assets/{index-KCib1PTw.js → index-BvvNnWXe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxahxRP7.js → index-ByWpNjTj.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6NehWty.js → index-BycpeGfj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTEu7TSF.js → index-C0xn6hOr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BH9t10pe.js → index-C1t-r7yV.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7wT5VRi.js → index-CDPMHKQi.js} +1 -1
- package/src/assets/web-panel/assets/{index-majCS3s2.js → index-CIaGw7vl.js} +1 -1
- package/src/assets/web-panel/assets/{index-EPERz4Pu.js → index-CQJVedQ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-IkvkNxbc.js → index-CSgbOGaP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ_hw_5P.js → index-Cbh-lCxq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CMybtJY6.js → index-CzDVBBcg.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4zNisy9.js → index-Czsbrn75.js} +1 -1
- package/src/assets/web-panel/assets/{index-M8SZI11a.js → index-D-93XwJd.js} +1 -1
- package/src/assets/web-panel/assets/index-D0-bvFy3.js +1 -0
- package/src/assets/web-panel/assets/{index-TxbHusq2.js → index-D0YToIi_.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7knYOpm.js → index-DIPZ6hbJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGq4HQno.js → index-DeeLHcMY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdU8BwRW.js → index-DgbWSwr5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4yBRKT4.js → index-DtKdCXHW.js} +1 -1
- package/src/assets/web-panel/assets/{index-jMcv1u5o.js → index-DwTgvhOL.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3Tpv7-d.js → index-DyS4I4L-.js} +1 -1
- package/src/assets/web-panel/assets/{index-u8K1y_lh.js → index-FKFT-QTk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cua_P8St.js → index-Te0ruvY_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVo1GJoj.js → index-VXVukhBA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BPH5ESqs.js → index-Y1b8i0NV.js} +3 -3
- package/src/assets/web-panel/assets/{index-DsbMVBj1.js → index-ZNIms1nA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoaRB-4a.js → index-n-N19np-.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmsIKzyu.js → index-vF1pR00A.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuehgDOp.js → index-wLAjVpmJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjdOL159.js → index-xPSzUoWT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BF4xx1_b.js → index-xZdOioVg.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DYn3Gc09.js → initDefaultProps-BLKSE8he.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ZS3eolb9.js → motion-Bb59qqLK.js} +1 -1
- package/src/assets/web-panel/assets/{move-CEw4uqr3.js → move-CB3pYCk6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DlHFZnPp.js → omit-iImQWuU7.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-eZQvV5fA.js → pickAttrs-DRP2Chqo.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B31jQwa-.js → placementArrow-BrlfD4tF.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DAsNmVto.js → responsiveObserve-Cqxkuh5H.js} +1 -1
- package/src/assets/web-panel/assets/{slide-gPQPrYZC.js → slide-nxKEuLMj.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DwWKX5co.js → statusUtils-30E47KSk.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B3VOtXuH.js → styleChecker-Dn2_-5bn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-6ADctM2r.js → useFlexGapSupport-DkZ00X6F.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-6Zx1SSKs.js → useFs-ByrwSCOr.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BzReowln.js → usePersonalDataHub-BDY6jtUD.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C8IpEQbD.js → vnode-BL2q5BLv.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-ruc9vHr0.js → zoom-BSkPKE42.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +31 -0
- package/src/commands/cli-anything.js +14 -6
- package/src/commands/loop.js +450 -0
- package/src/commands/mcp.js +236 -6
- package/src/harness/mcp-client.js +70 -1
- package/src/index.js +2 -0
- package/src/lib/loop.js +198 -0
- package/src/lib/settings-hooks.cjs +1 -0
- package/src/repl/agent-repl.js +57 -20
- package/src/repl/mcp-prompt.js +122 -0
- package/src/runtime/agent-core.js +123 -17
- package/src/runtime/headless-runner.js +34 -9
- package/src/runtime/mcp-config.js +118 -9
- package/src/runtime/policies/agent-policy.js +3 -0
- 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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|