@loops-adk/core 0.1.0

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +486 -0
  3. package/bin/loops.mjs +16 -0
  4. package/dist/App-3YQS6DXA.js +461 -0
  5. package/dist/App-3YQS6DXA.js.map +1 -0
  6. package/dist/agent-sdk-RF5VJZAT.js +95 -0
  7. package/dist/agent-sdk-RF5VJZAT.js.map +1 -0
  8. package/dist/anthropic-api-XJY6Y4T2.js +131 -0
  9. package/dist/anthropic-api-XJY6Y4T2.js.map +1 -0
  10. package/dist/api.d.ts +949 -0
  11. package/dist/api.js +898 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/chunk-33YIGWNU.js +63 -0
  14. package/dist/chunk-33YIGWNU.js.map +1 -0
  15. package/dist/chunk-3BPU34DE.js +2163 -0
  16. package/dist/chunk-3BPU34DE.js.map +1 -0
  17. package/dist/chunk-CXEPZHSR.js +86 -0
  18. package/dist/chunk-CXEPZHSR.js.map +1 -0
  19. package/dist/chunk-I3STY7U6.js +61 -0
  20. package/dist/chunk-I3STY7U6.js.map +1 -0
  21. package/dist/chunk-JFTXJ7I2.js +18 -0
  22. package/dist/chunk-JFTXJ7I2.js.map +1 -0
  23. package/dist/chunk-XC46B4FD.js +9 -0
  24. package/dist/chunk-XC46B4FD.js.map +1 -0
  25. package/dist/chunk-Y2SD7GBL.js +30 -0
  26. package/dist/chunk-Y2SD7GBL.js.map +1 -0
  27. package/dist/claude-cli-U7WEVAOL.js +124 -0
  28. package/dist/claude-cli-U7WEVAOL.js.map +1 -0
  29. package/dist/codex-6I5UZ2HM.js +60 -0
  30. package/dist/codex-6I5UZ2HM.js.map +1 -0
  31. package/dist/env/command.d.ts +53 -0
  32. package/dist/env/command.js +3 -0
  33. package/dist/env/command.js.map +1 -0
  34. package/dist/env/docker.d.ts +38 -0
  35. package/dist/env/docker.js +33 -0
  36. package/dist/env/docker.js.map +1 -0
  37. package/dist/env/sst.d.ts +39 -0
  38. package/dist/env/sst.js +20 -0
  39. package/dist/env/sst.js.map +1 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +620 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types-B4wGVpqo.d.ts +898 -0
  44. package/package.json +100 -0
  45. package/skills/author-loop/SKILL.md +121 -0
@@ -0,0 +1,39 @@
1
+ import { W as Workspace, E as Environment } from '../types-B4wGVpqo.js';
2
+ import { Cmd } from './command.js';
3
+
4
+ /**
5
+ * `sstEnvironment` — a per-branch sst stage as an Environment, a thin preset over
6
+ * `commandEnvironment`. `sst deploy --stage <slug(branch)>` on up,
7
+ * `sst remove --stage …` on down. Each worktree-team gets its own stage named
8
+ * after its branch — the personal-stack convention, generalised per-branch.
9
+ *
10
+ * The exact sst flags vary by sst version, so deploy/outputs/destroy are all
11
+ * overridable, and the CONSUMER supplies `map` (which output is the URL / how
12
+ * outputs become env vars) since the output shape is app-specific. By default no
13
+ * outputs are read (the deploy still runs); set `outputs` + `map` to surface a
14
+ * URL. This adds no dependency — it shells out to the `sst` CLI on PATH.
15
+ */
16
+
17
+ interface SstEnvConfig {
18
+ /** App dir (where sst.config.ts lives). Default: the workspace dir. */
19
+ cwd?: (ws: Workspace) => string;
20
+ /** Stage name. Default: a slug of the workspace branch. */
21
+ stage?: (ws: Workspace) => string;
22
+ /** Override the deploy command. Default: `sst deploy --stage <stage>`. */
23
+ deploy?: (stage: string, ws: Workspace) => Cmd;
24
+ /** Command to read outputs as JSON. Omitted by default (version-specific). */
25
+ outputs?: (stage: string, ws: Workspace) => Cmd;
26
+ /** Override the destroy command. Default: `sst remove --stage <stage>`. */
27
+ destroy?: (stage: string, ws: Workspace) => Cmd;
28
+ /** Map outputs → { url, env }. Required to surface a URL to the gate. */
29
+ map?: (outputs: Record<string, unknown>, stage: string, raw: string) => {
30
+ url?: string;
31
+ env?: Record<string, string>;
32
+ };
33
+ /** The sst binary. Default 'sst'. */
34
+ binary?: string;
35
+ timeoutMs?: number;
36
+ }
37
+ declare function sstEnvironment(config?: SstEnvConfig): Environment;
38
+
39
+ export { type SstEnvConfig, sstEnvironment };
@@ -0,0 +1,20 @@
1
+ import { commandEnvironment } from '../chunk-33YIGWNU.js';
2
+
3
+ // src/env/sst.ts
4
+ function sstEnvironment(config = {}) {
5
+ const bin = config.binary ?? "sst";
6
+ return commandEnvironment({
7
+ name: "sst",
8
+ cwd: config.cwd,
9
+ stage: config.stage,
10
+ deploy: config.deploy ?? ((stage) => ({ cmd: bin, args: ["deploy", "--stage", stage] })),
11
+ outputs: config.outputs,
12
+ destroy: config.destroy ?? ((stage) => ({ cmd: bin, args: ["remove", "--stage", stage] })),
13
+ map: config.map,
14
+ timeoutMs: config.timeoutMs
15
+ });
16
+ }
17
+
18
+ export { sstEnvironment };
19
+ //# sourceMappingURL=sst.js.map
20
+ //# sourceMappingURL=sst.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/env/sst.ts"],"names":[],"mappings":";;;AAuCO,SAAS,cAAA,CAAe,MAAA,GAAuB,EAAC,EAAgB;AACrE,EAAA,MAAM,GAAA,GAAM,OAAO,MAAA,IAAU,KAAA;AAC7B,EAAA,OAAO,kBAAA,CAAmB;AAAA,IACxB,IAAA,EAAM,KAAA;AAAA,IACN,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,MAAA,EACE,MAAA,CAAO,MAAA,KACN,CAAC,KAAA,MAAW,EAAE,GAAA,EAAK,GAAA,EAAK,IAAA,EAAM,CAAC,QAAA,EAAU,SAAA,EAAW,KAAK,CAAA,EAAE,CAAA,CAAA;AAAA,IAC9D,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,OAAA,EACE,MAAA,CAAO,OAAA,KACN,CAAC,KAAA,MAAW,EAAE,GAAA,EAAK,GAAA,EAAK,IAAA,EAAM,CAAC,QAAA,EAAU,SAAA,EAAW,KAAK,CAAA,EAAE,CAAA,CAAA;AAAA,IAC9D,KAAK,MAAA,CAAO,GAAA;AAAA,IACZ,WAAW,MAAA,CAAO;AAAA,GACnB,CAAA;AACH","file":"sst.js","sourcesContent":["/**\n * `sstEnvironment` — a per-branch sst stage as an Environment, a thin preset over\n * `commandEnvironment`. `sst deploy --stage <slug(branch)>` on up,\n * `sst remove --stage …` on down. Each worktree-team gets its own stage named\n * after its branch — the personal-stack convention, generalised per-branch.\n *\n * The exact sst flags vary by sst version, so deploy/outputs/destroy are all\n * overridable, and the CONSUMER supplies `map` (which output is the URL / how\n * outputs become env vars) since the output shape is app-specific. By default no\n * outputs are read (the deploy still runs); set `outputs` + `map` to surface a\n * URL. This adds no dependency — it shells out to the `sst` CLI on PATH.\n */\n\nimport type { Workspace } from '../core/types.ts';\nimport type { Environment } from './environment.ts';\nimport { commandEnvironment, type Cmd } from './command.ts';\n\nexport interface SstEnvConfig {\n /** App dir (where sst.config.ts lives). Default: the workspace dir. */\n cwd?: (ws: Workspace) => string;\n /** Stage name. Default: a slug of the workspace branch. */\n stage?: (ws: Workspace) => string;\n /** Override the deploy command. Default: `sst deploy --stage <stage>`. */\n deploy?: (stage: string, ws: Workspace) => Cmd;\n /** Command to read outputs as JSON. Omitted by default (version-specific). */\n outputs?: (stage: string, ws: Workspace) => Cmd;\n /** Override the destroy command. Default: `sst remove --stage <stage>`. */\n destroy?: (stage: string, ws: Workspace) => Cmd;\n /** Map outputs → { url, env }. Required to surface a URL to the gate. */\n map?: (\n outputs: Record<string, unknown>,\n stage: string,\n raw: string,\n ) => { url?: string; env?: Record<string, string> };\n /** The sst binary. Default 'sst'. */\n binary?: string;\n timeoutMs?: number;\n}\n\nexport function sstEnvironment(config: SstEnvConfig = {}): Environment {\n const bin = config.binary ?? 'sst';\n return commandEnvironment({\n name: 'sst',\n cwd: config.cwd,\n stage: config.stage,\n deploy:\n config.deploy ??\n ((stage) => ({ cmd: bin, args: ['deploy', '--stage', stage] })),\n outputs: config.outputs,\n destroy:\n config.destroy ??\n ((stage) => ({ cmd: bin, args: ['remove', '--stage', stage] })),\n map: config.map,\n timeoutMs: config.timeoutMs,\n });\n}\n"]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env node
2
+ import { renderPlan, jobMeta, run, exitCodeFor, loop, agentJob, agentCheck, bodyPassed, gateJob } from './chunk-3BPU34DE.js';
3
+ import './chunk-JFTXJ7I2.js';
4
+ import './chunk-XC46B4FD.js';
5
+ import './chunk-Y2SD7GBL.js';
6
+ import './chunk-I3STY7U6.js';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { pathToFileURL } from 'url';
10
+ import 'react';
11
+ import { Command } from 'commander';
12
+ import { z } from 'zod';
13
+ import pc from 'picocolors';
14
+ import ms from 'ms';
15
+ import { jsx } from 'react/jsx-runtime';
16
+
17
+ // src/runtime/hub.ts
18
+ function createHub() {
19
+ const listeners = /* @__PURE__ */ new Set();
20
+ return {
21
+ emit(event) {
22
+ for (const listener of listeners) listener(event);
23
+ },
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => listeners.delete(listener);
27
+ }
28
+ };
29
+ }
30
+
31
+ // src/runtime/signals.ts
32
+ function installSignalHandlers() {
33
+ const controller = new AbortController();
34
+ let hits = 0;
35
+ const onSignal = () => {
36
+ hits += 1;
37
+ if (hits === 1) controller.abort();
38
+ else process.exit(130);
39
+ };
40
+ process.on("SIGINT", onSignal);
41
+ process.on("SIGTERM", onSignal);
42
+ return {
43
+ controller,
44
+ dispose() {
45
+ process.off("SIGINT", onSignal);
46
+ process.off("SIGTERM", onSignal);
47
+ }
48
+ };
49
+ }
50
+ var indent = (path2) => " ".repeat(Math.max(0, path2.length - 1));
51
+ var statusColor = (status, text) => status === "pass" ? pc.green(text) : status === "fail" ? pc.red(text) : status === "exhausted" ? pc.yellow(text) : status === "paused" ? pc.cyan(text) : pc.gray(text);
52
+ var clock = (ts) => new Date(ts).toLocaleTimeString(void 0, {
53
+ hour: "2-digit",
54
+ minute: "2-digit",
55
+ second: "2-digit"
56
+ });
57
+ function jsonReporter() {
58
+ return (event) => process.stdout.write(`${JSON.stringify(event)}
59
+ `);
60
+ }
61
+ var tok = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
62
+ function plainReporter() {
63
+ let streaming = false;
64
+ const endStream = () => {
65
+ if (streaming) {
66
+ process.stdout.write("\n");
67
+ streaming = false;
68
+ }
69
+ };
70
+ const accums = /* @__PURE__ */ new Map();
71
+ const reportIteration = (key, indentPath) => {
72
+ const a = accums.get(key);
73
+ if (!a) return;
74
+ accums.delete(key);
75
+ const parts = [];
76
+ if (a.bodyStatus)
77
+ parts.push(`body=${statusColor(a.bodyStatus, a.bodyStatus)}`);
78
+ if (a.until)
79
+ parts.push(`until=${a.until.met ? pc.green("met") : pc.gray("not met")}`);
80
+ if (a.stopOn?.met) parts.push(`stopOn=${pc.red("met")}`);
81
+ if (a.review) {
82
+ const rv = `review=${statusColor(a.review.status, a.review.status)}`;
83
+ parts.push(
84
+ a.review.summary ? `${rv} ${pc.dim(`(${a.review.summary})`)}` : rv
85
+ );
86
+ }
87
+ parts.push(`${tok(a.tokensIn)}/${tok(a.tokensOut)} tok`);
88
+ console.log(
89
+ `${indent(indentPath)} ${pc.gray(`\u21B3 iter ${a.iteration}:`)} ${parts.join(pc.gray(" \xB7 "))}`
90
+ );
91
+ };
92
+ return (event) => {
93
+ const key = event.path.join(" / ");
94
+ switch (event.kind) {
95
+ case "engine:text":
96
+ process.stdout.write(event.delta);
97
+ streaming = true;
98
+ return;
99
+ case "loop:start":
100
+ endStream();
101
+ console.log(
102
+ `${indent(event.path)}${pc.cyan("\u25B8 loop")} ${pc.bold(last(event.path))}${event.max ? pc.gray(` (max ${event.max})`) : ""}`
103
+ );
104
+ return;
105
+ case "loop:iteration":
106
+ endStream();
107
+ reportIteration(key, event.path);
108
+ accums.set(key, {
109
+ iteration: event.iteration,
110
+ tokensIn: 0,
111
+ tokensOut: 0
112
+ });
113
+ console.log(
114
+ `${indent(event.path)}${pc.gray(` iteration ${event.iteration}`)}`
115
+ );
116
+ return;
117
+ case "loop:condition": {
118
+ endStream();
119
+ const a = accums.get(key);
120
+ if (a) {
121
+ if (event.which === "until")
122
+ a.until = { met: event.result.met, reason: event.result.reason };
123
+ else if (event.which === "stopOn")
124
+ a.stopOn = { met: event.result.met, reason: event.result.reason };
125
+ }
126
+ console.log(
127
+ `${indent(event.path)} ${pc.magenta(event.which)}: ${event.result.met ? pc.green("met") : pc.gray("not met")} \u2014 ${pc.dim(event.result.reason)}`
128
+ );
129
+ return;
130
+ }
131
+ case "loop:review": {
132
+ endStream();
133
+ const a = accums.get(key);
134
+ if (a)
135
+ a.review = {
136
+ status: event.outcome.status,
137
+ summary: event.outcome.summary
138
+ };
139
+ console.log(
140
+ `${indent(event.path)} ${pc.blue("review")}: ${statusColor(event.outcome.status, event.outcome.status)}${event.outcome.summary ? pc.dim(` \u2014 ${event.outcome.summary}`) : ""}`
141
+ );
142
+ return;
143
+ }
144
+ case "job:end": {
145
+ const a = accums.get(key);
146
+ if (a) a.bodyStatus = event.outcome.status;
147
+ return;
148
+ }
149
+ case "engine:usage": {
150
+ const a = accums.get(key);
151
+ if (a) {
152
+ a.tokensIn += event.usage.inputTokens;
153
+ a.tokensOut += event.usage.outputTokens;
154
+ }
155
+ return;
156
+ }
157
+ case "loop:end":
158
+ endStream();
159
+ reportIteration(key, event.path);
160
+ console.log(
161
+ `${indent(event.path)}${pc.cyan("\u25C2 loop")} ${pc.bold(last(event.path))} \u2192 ${statusColor(event.outcome.status, event.outcome.status)} ${pc.gray(`(${event.iterations} iter)`)}`
162
+ );
163
+ return;
164
+ case "dag:start":
165
+ endStream();
166
+ console.log(
167
+ `${indent(event.path)}${pc.cyan("\u25B8 dag")} ${pc.bold(last(event.path))} ${pc.gray(`[${event.nodes.join(", ")}]`)}`
168
+ );
169
+ return;
170
+ case "dag:node":
171
+ if (event.phase === "start") return;
172
+ endStream();
173
+ console.log(
174
+ `${indent(event.path)} ${pc.gray("node")} ${event.node}: ${event.outcome ? statusColor(event.outcome.status, event.phase === "skip" ? "skipped" : event.outcome.status) : event.phase}`
175
+ );
176
+ return;
177
+ case "dag:end":
178
+ endStream();
179
+ console.log(
180
+ `${indent(event.path)}${pc.cyan("\u25C2 dag")} ${pc.bold(last(event.path))} \u2192 ${statusColor(event.outcome.status, event.outcome.status)}`
181
+ );
182
+ return;
183
+ case "job:start":
184
+ endStream();
185
+ console.log(`${indent(event.path)} ${pc.gray("\u2022")} ${event.label}`);
186
+ return;
187
+ case "engine:tool":
188
+ endStream();
189
+ console.log(
190
+ `${indent(event.path)} ${pc.dim(`tool ${event.phase}: ${event.name}`)}`
191
+ );
192
+ return;
193
+ case "limit:wait":
194
+ endStream();
195
+ console.log(
196
+ `${indent(event.path)} ${pc.yellow(`\u23F3 ${event.code.toLowerCase()}`)}: waiting ${Math.round(event.waitMs / 1e3)}s, resuming at ${clock(event.resumeAt)}`
197
+ );
198
+ return;
199
+ case "limit:pause":
200
+ endStream();
201
+ console.log(
202
+ `${indent(event.path)} ${pc.cyan(`\u23F8 ${event.code.toLowerCase()}`)}: ${pc.dim(event.reason)}`
203
+ );
204
+ return;
205
+ case "log":
206
+ endStream();
207
+ console.log(
208
+ `${indent(event.path)} ${pc.dim(`[${event.level}] ${event.message}`)}`
209
+ );
210
+ return;
211
+ case "error":
212
+ endStream();
213
+ console.log(
214
+ `${indent(event.path)} ${pc.red(`\u2717 ${event.code}: ${event.message}`)}`
215
+ );
216
+ return;
217
+ default:
218
+ return;
219
+ }
220
+ };
221
+ }
222
+ function printSummary(result, resumeCommand) {
223
+ const { outcome, stats } = result;
224
+ const line = pc.dim("\u2500".repeat(56));
225
+ console.log(`
226
+ ${line}`);
227
+ console.log(
228
+ `${pc.bold("Result")} ${statusColor(outcome.status, outcome.status.toUpperCase())}${outcome.confidence != null ? pc.gray(` confidence ${outcome.confidence.toFixed(2)}`) : ""}`
229
+ );
230
+ if (outcome.summary) console.log(`${pc.dim("Summary")} ${outcome.summary}`);
231
+ if (outcome.status === "paused") {
232
+ console.log(
233
+ resumeCommand ? `${pc.dim("Resume")} ${pc.cyan(resumeCommand)}` : `${pc.dim("Resume")} ${pc.yellow("re-run with --checkpoint <path> to make a pause resumable")}`
234
+ );
235
+ }
236
+ console.log(line);
237
+ console.log(`${pc.bold("Loops")}`);
238
+ for (const loop2 of stats.loops) {
239
+ const reviews = loop2.reviewsPassed + loop2.reviewsFailed;
240
+ console.log(
241
+ ` ${loop2.path || "(root)"} \u2014 ${loop2.iterations} iter` + (reviews ? `, reviews ${pc.green(String(loop2.reviewsPassed))}/${pc.red(String(loop2.reviewsFailed))}` : "") + (loop2.lastStatus ? ` \u2192 ${statusColor(loop2.lastStatus, loop2.lastStatus)}` : "")
242
+ );
243
+ }
244
+ if (stats.loops.length === 0) console.log(pc.dim(" (none)"));
245
+ console.log(line);
246
+ console.log(
247
+ `${pc.bold("Usage")} ${stats.agentCalls} agent call(s), ${pc.cyan(String(stats.totalInputTokens))} in / ${pc.cyan(String(stats.totalOutputTokens))} out tokens, ${(stats.elapsedMs / 1e3).toFixed(1)}s`
248
+ );
249
+ for (const m of stats.models) {
250
+ console.log(
251
+ pc.dim(
252
+ ` ${m.model}: ${m.calls} call(s), ${m.inputTokens} in / ${m.outputTokens} out`
253
+ )
254
+ );
255
+ }
256
+ if (result.budget) {
257
+ const b = result.budget;
258
+ const spent = b.remaining === 0 ? pc.red(String(b.spent)) : pc.cyan(String(b.spent));
259
+ console.log(
260
+ `${pc.bold("Budget")} ${spent} / ${b.limit} tokens ${pc.gray(`(${b.remaining} remaining)`)}`
261
+ );
262
+ }
263
+ if (stats.errors.length) {
264
+ console.log(line);
265
+ console.log(`${pc.bold(pc.red("Errors"))} (${stats.errors.length})`);
266
+ for (const e of stats.errors.slice(0, 10)) {
267
+ console.log(pc.red(` \u2717 [${e.code}] ${e.path}: ${e.message}`));
268
+ }
269
+ }
270
+ console.log(line);
271
+ }
272
+ function last(path2) {
273
+ return path2[path2.length - 1] ?? "(root)";
274
+ }
275
+ var FlagSpec = z.object({
276
+ prompt: z.string().min(
277
+ 1,
278
+ "a --prompt or --prompt-file is required when no definition file is given"
279
+ ),
280
+ engine: z.string().optional(),
281
+ workerModel: z.string().optional(),
282
+ validatorModel: z.string().optional(),
283
+ reviewerModel: z.string().optional(),
284
+ max: z.number().int().positive().optional(),
285
+ untilAgent: z.string().optional(),
286
+ threshold: z.number().min(0).max(1).default(0.8),
287
+ startAgent: z.string().optional(),
288
+ review: z.string().optional(),
289
+ reviewThreshold: z.number().min(0).max(1).default(0.85),
290
+ interval: z.number().int().nonnegative().optional(),
291
+ maxTokens: z.number().int().positive().optional()
292
+ });
293
+ function parseDuration(value) {
294
+ if (/^\d+$/.test(value)) return Number(value);
295
+ const out = ms(value);
296
+ if (typeof out !== "number" || Number.isNaN(out)) {
297
+ throw new Error(`invalid duration: "${value}" (try 30s, 5m, 1h)`);
298
+ }
299
+ return out;
300
+ }
301
+ function buildJobFromFlags(input) {
302
+ const spec = FlagSpec.parse(input);
303
+ const engine = spec.engine;
304
+ const worker = agentJob({
305
+ label: "worker",
306
+ engine,
307
+ model: spec.workerModel,
308
+ maxTokens: spec.maxTokens,
309
+ // On a review-restart, fold the reviewer's objection into the next prompt
310
+ // so the retry is informed rather than a blind repeat.
311
+ prompt: (ctx) => ctx.lastReview ? `${spec.prompt}
312
+
313
+ Your previous attempt was REJECTED in review: ${ctx.lastReview.summary ?? ctx.lastReview.status}. Address that specifically this time.` : spec.prompt
314
+ });
315
+ const until = spec.untilAgent ? agentCheck({
316
+ question: spec.untilAgent,
317
+ threshold: spec.threshold,
318
+ model: spec.validatorModel,
319
+ engine
320
+ }) : bodyPassed();
321
+ const start = spec.startAgent ? agentCheck({
322
+ question: spec.startAgent,
323
+ threshold: 0.5,
324
+ model: spec.validatorModel,
325
+ engine
326
+ }) : void 0;
327
+ const review = spec.review ? gateJob(
328
+ "review",
329
+ agentCheck({
330
+ question: spec.review,
331
+ threshold: spec.reviewThreshold,
332
+ model: spec.reviewerModel,
333
+ engine
334
+ })
335
+ ) : void 0;
336
+ return loop({
337
+ name: "main",
338
+ body: worker,
339
+ start,
340
+ until,
341
+ review,
342
+ max: spec.max,
343
+ delayMs: spec.interval
344
+ });
345
+ }
346
+ var ON_LIMIT_VALUES = ["auto", "wait", "exit-resume", "fail"];
347
+ function resolvePrompt(flags) {
348
+ if (flags.promptFile != null && flags.prompt != null) {
349
+ throw new Error("pass either --prompt or --prompt-file, not both");
350
+ }
351
+ if (flags.promptFile != null) {
352
+ const resolved = path.resolve(flags.promptFile);
353
+ if (!fs.existsSync(resolved))
354
+ throw new Error(`prompt file not found: ${flags.promptFile}`);
355
+ return fs.readFileSync(resolved, "utf8");
356
+ }
357
+ return flags.prompt ?? "";
358
+ }
359
+ async function loadJob(file) {
360
+ const resolved = path.resolve(file);
361
+ if (!fs.existsSync(resolved)) {
362
+ throw new Error(
363
+ `loop file not found: ${file}
364
+ (omit the file argument to use flags mode, or run \`loops run --help\`)`
365
+ );
366
+ }
367
+ let mod;
368
+ try {
369
+ mod = await import(pathToFileURL(resolved).href);
370
+ } catch (e) {
371
+ const detail = e instanceof Error ? e.message : String(e);
372
+ const esmHint = /ES Module|import statement outside a module|ERR_REQUIRE_ESM/i.test(detail) ? `
373
+ hint: the recipe's folder is not an ES module scope. Add a package.json with {"type":"module"} next to it (repos that use loops as a submodule already have this).` : "";
374
+ throw new Error(
375
+ `failed to load loop file ${file}:
376
+ ${detail}${esmHint}
377
+ (the file is imported and run like \`node <file>\`; fix the error above, or run \`loops validate ${file}\` to check it without executing)`
378
+ );
379
+ }
380
+ const def = mod.default ?? mod.job ?? mod.loop;
381
+ const title = path.basename(file).replace(/\.(loop\.)?(t|j)sx?$/, "");
382
+ if (typeof def === "function") return { job: def, title };
383
+ if (def && typeof def === "object" && "body" in def)
384
+ return { job: loop(def), title };
385
+ throw new Error(
386
+ `${file}: default export must be a Job (from loop()/dag()/agentJob()) or a LoopConfig`
387
+ );
388
+ }
389
+ function buildFromFlags(flags) {
390
+ const num = (v) => v == null ? void 0 : Number(v);
391
+ const prompt = resolvePrompt(flags);
392
+ try {
393
+ return buildJobFromFlags({
394
+ prompt,
395
+ engine: flags.engine,
396
+ workerModel: flags.workerModel,
397
+ validatorModel: flags.validatorModel,
398
+ reviewerModel: flags.reviewerModel,
399
+ max: num(flags.max),
400
+ untilAgent: flags.until,
401
+ threshold: num(flags.threshold),
402
+ startAgent: flags.start,
403
+ review: flags.review,
404
+ reviewThreshold: num(flags.reviewThreshold),
405
+ interval: flags.interval != null ? parseDuration(flags.interval) : void 0,
406
+ maxTokens: num(flags.maxTokens)
407
+ });
408
+ } catch (e) {
409
+ if (e instanceof z.ZodError) {
410
+ throw new Error(
411
+ `invalid flags:
412
+ - ${e.issues.map((i) => i.message).join("\n - ")}`
413
+ );
414
+ }
415
+ throw e;
416
+ }
417
+ }
418
+ async function execute(file, flags) {
419
+ const { job, title } = file ? await loadJob(file) : { job: buildFromFlags(flags), title: "loop" };
420
+ const engineOptions = {};
421
+ if (flags.defaultModel) engineOptions.defaultModel = flags.defaultModel;
422
+ if (flags.apiKey) engineOptions.apiKey = flags.apiKey;
423
+ if (flags.cliBinary) engineOptions.cliBinary = flags.cliBinary;
424
+ if (flags.permissionMode)
425
+ engineOptions.permissionMode = flags.permissionMode;
426
+ if (flags.engineArg?.length) engineOptions.cliArgs = flags.engineArg;
427
+ let state;
428
+ if (flags.state) {
429
+ let parsed;
430
+ try {
431
+ parsed = JSON.parse(flags.state);
432
+ } catch (e) {
433
+ throw new Error(
434
+ `--state must be valid JSON: ${e instanceof Error ? e.message : String(e)}`
435
+ );
436
+ }
437
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
438
+ throw new Error(
439
+ `--state must be a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`
440
+ );
441
+ }
442
+ state = parsed;
443
+ }
444
+ let budget;
445
+ if (flags.budget != null) {
446
+ budget = Number(flags.budget);
447
+ if (!Number.isFinite(budget) || budget <= 0) {
448
+ throw new Error(
449
+ `--budget must be a positive number of tokens, got "${flags.budget}"`
450
+ );
451
+ }
452
+ }
453
+ let onLimit;
454
+ if (flags.onLimit != null) {
455
+ if (!ON_LIMIT_VALUES.includes(flags.onLimit)) {
456
+ throw new Error(
457
+ `--on-limit must be one of ${ON_LIMIT_VALUES.join(" | ")}, got "${flags.onLimit}"`
458
+ );
459
+ }
460
+ onLimit = flags.onLimit;
461
+ }
462
+ const maxWaitMs = flags.maxWait != null ? parseDuration(flags.maxWait) : void 0;
463
+ const mode = flags.json ? "json" : flags.tui === false || !process.stdout.isTTY ? "plain" : "tui";
464
+ const resumeCommand = buildResumeCommand(file, flags);
465
+ const hub = createHub();
466
+ const signals = installSignalHandlers();
467
+ const runOptions = {
468
+ engine: flags.engine,
469
+ engineOptions,
470
+ signal: signals.controller.signal,
471
+ onEvent: hub.emit,
472
+ state,
473
+ budget,
474
+ recordTo: flags.record,
475
+ checkpoint: flags.checkpoint,
476
+ resumeFrom: flags.resume,
477
+ onLimit,
478
+ maxWaitMs,
479
+ resumeCommand
480
+ };
481
+ let result;
482
+ if (mode === "tui") {
483
+ const { render } = await import('ink');
484
+ const { App } = await import('./App-3YQS6DXA.js');
485
+ const instance = render(
486
+ /* @__PURE__ */ jsx(
487
+ App,
488
+ {
489
+ hub,
490
+ title,
491
+ onAbort: () => signals.controller.abort()
492
+ }
493
+ )
494
+ );
495
+ result = await run(job, runOptions);
496
+ instance.unmount();
497
+ await instance.waitUntilExit().catch(() => {
498
+ });
499
+ printSummary(result, resumeCommand);
500
+ } else {
501
+ const unsubscribe = hub.subscribe(
502
+ mode === "json" ? jsonReporter() : plainReporter()
503
+ );
504
+ result = await run(job, runOptions);
505
+ unsubscribe();
506
+ if (mode !== "json") printSummary(result, resumeCommand);
507
+ }
508
+ if (result.outcome.status === "paused") printResumeGuidance(file, flags);
509
+ signals.dispose();
510
+ process.exitCode = exitCodeFor(result.outcome);
511
+ }
512
+ function buildResumeCommand(file, flags) {
513
+ if (!flags.checkpoint) return void 0;
514
+ const parts = ["loops", "run"];
515
+ if (file) parts.push(quoteArg(file));
516
+ parts.push("--resume", quoteArg(flags.checkpoint));
517
+ if (flags.engine) parts.push("--engine", flags.engine);
518
+ if (flags.budget) parts.push("--budget", flags.budget);
519
+ if (flags.onLimit) parts.push("--on-limit", flags.onLimit);
520
+ if (flags.maxWait) parts.push("--max-wait", flags.maxWait);
521
+ if (flags.record) parts.push("--record", quoteArg(flags.record));
522
+ if (flags.checkpoint) parts.push("--checkpoint", quoteArg(flags.checkpoint));
523
+ if (flags.tui === false) parts.push("--no-tui");
524
+ if (flags.json) parts.push("--json");
525
+ return parts.join(" ");
526
+ }
527
+ function quoteArg(value) {
528
+ return /[\s'"]/.test(value) ? `'${value.replace(/'/g, `'\\''`)}'` : value;
529
+ }
530
+ function printResumeGuidance(file, flags) {
531
+ const cmd = buildResumeCommand(file, flags);
532
+ if (cmd) {
533
+ process.stderr.write(`
534
+ Paused at a limit. Resume with:
535
+ ${cmd}
536
+ `);
537
+ } else {
538
+ process.stderr.write(
539
+ "\nPaused at a limit. No checkpoint was configured, so there is no warm state to resume.\nRe-run with --checkpoint <path> to make a pause resumable.\n"
540
+ );
541
+ }
542
+ }
543
+ async function main(argv = process.argv) {
544
+ const program = new Command();
545
+ program.name("loops").description(
546
+ "Run a prompt/agent in a loop with a fresh context every iteration. A nestable job primitive: loops, DAG stages, agent-validated conditions, review-restart."
547
+ ).version("0.1.0");
548
+ program.command("run", { isDefault: true }).argument(
549
+ "[file]",
550
+ "a loop-definition file (default-exports a Job); omit to use flags"
551
+ ).option("-p, --prompt <text>", "worker prompt (no-file mode)").option(
552
+ "-f, --prompt-file <path>",
553
+ "read the worker prompt from a file (no-file mode)"
554
+ ).option(
555
+ "-e, --engine <name>",
556
+ "default engine: agent-sdk | claude-cli | anthropic-api"
557
+ ).option("--default-model <id>", "fallback model id for engines").option("--worker-model <id>", "model for the worker job").option(
558
+ "--validator-model <id>",
559
+ "small model for agent-validated conditions"
560
+ ).option("--reviewer-model <id>", "model for the review job").option("-n, --max <n>", "max iterations").option("-u, --until <question>", "agent-validated stop condition").option("-t, --threshold <0..1>", "confidence threshold for --until", "0.8").option("--start <question>", "agent-validated start gate").option(
561
+ "--review <instructions>",
562
+ "review job; failing it restarts the loop"
563
+ ).option(
564
+ "--review-threshold <0..1>",
565
+ "confidence threshold for --review",
566
+ "0.85"
567
+ ).option("-i, --interval <dur>", "delay between iterations (e.g. 30s, 5m)").option("--max-tokens <n>", "max output tokens per agent turn").option("--api-key <key>", "Anthropic API key (anthropic-api engine)").option(
568
+ "--cli-binary <path>",
569
+ "path to the claude binary (claude-cli engine)"
570
+ ).option(
571
+ "--permission-mode <mode>",
572
+ "tool permission mode for claude-cli/agent-sdk (default | acceptEdits | bypassPermissions | plan | dontAsk | auto)"
573
+ ).option(
574
+ "--engine-arg <arg>",
575
+ "extra arg forwarded to the claude-cli engine (repeatable)",
576
+ (v, acc) => acc.concat(v),
577
+ []
578
+ ).option("--state <json>", "seed the shared run state (JSON)").option("--budget <tokens>", "cap total tokens (input+output) for the run").option("--record <path>", "append a JSONL run record to this path").option(
579
+ "--checkpoint <path>",
580
+ "snapshot run state to this path at each loop/dag/job boundary"
581
+ ).option(
582
+ "--resume <path>",
583
+ "restore run state from a prior --checkpoint file"
584
+ ).option(
585
+ "--on-limit <policy>",
586
+ "on a rate/quota/budget limit: auto | wait | exit-resume | fail (default auto)"
587
+ ).option(
588
+ "--max-wait <dur>",
589
+ "cap an auto/wait limit-wait (e.g. 5m, 30s); default 5m"
590
+ ).option("--json", "emit NDJSON events to stdout (no TUI)").option("--no-tui", "plain line output instead of the Ink TUI").action(
591
+ (file, flags) => execute(file, flags)
592
+ );
593
+ program.command("validate").argument("<file>", "a loop-definition file to check").description(
594
+ "load a .loop.ts and print its shape without running it: the cheap, no-model pre-flight an agent runs before `loops run`"
595
+ ).action(async (file) => {
596
+ const { job } = await loadJob(file);
597
+ const plan = renderPlan(jobMeta(job));
598
+ process.stdout.write(
599
+ `\u2713 ${file} loads (not executed)
600
+ ${plan.map((l) => ` ${l}`).join("\n")}
601
+ `
602
+ );
603
+ });
604
+ program.command("describe").argument("<file>", "a loop-definition file").description(
605
+ "print a loop's shape (its gate, body, and dag nodes) without running it"
606
+ ).action(async (file) => {
607
+ const { job } = await loadJob(file);
608
+ process.stdout.write(`${renderPlan(jobMeta(job)).join("\n")}
609
+ `);
610
+ });
611
+ await program.parseAsync(argv);
612
+ }
613
+
614
+ // src/index.ts
615
+ main().catch((err) => {
616
+ console.error(err instanceof Error ? err.message : String(err));
617
+ process.exit(1);
618
+ });
619
+ //# sourceMappingURL=index.js.map
620
+ //# sourceMappingURL=index.js.map