@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,2163 @@
1
+ import { redactSecrets } from './chunk-JFTXJ7I2.js';
2
+ import { isEngine } from './chunk-XC46B4FD.js';
3
+ import { isLimitError, waitMsFor } from './chunk-Y2SD7GBL.js';
4
+ import { LoopError } from './chunk-I3STY7U6.js';
5
+ import { readFileSync, mkdtempSync, existsSync, writeFileSync, appendFileSync, mkdirSync, rmSync } from 'fs';
6
+ import { execa } from 'execa';
7
+ import { tmpdir } from 'os';
8
+ import { join, dirname } from 'path';
9
+
10
+ // src/core/describe.ts
11
+ var META = /* @__PURE__ */ new WeakMap();
12
+ var LABEL = /* @__PURE__ */ new WeakMap();
13
+ function setMeta(target, meta) {
14
+ META.set(target, meta);
15
+ return target;
16
+ }
17
+ function jobMeta(job) {
18
+ return typeof job === "function" ? META.get(job) : void 0;
19
+ }
20
+ function setLabel(cond, label) {
21
+ LABEL.set(cond, label);
22
+ return cond;
23
+ }
24
+ function condLabel(input) {
25
+ if (typeof input === "function") {
26
+ const l = LABEL.get(input);
27
+ if (l) return l;
28
+ }
29
+ return "check";
30
+ }
31
+ function describeConditions(input) {
32
+ if (input == null) return [];
33
+ if (Array.isArray(input)) return input.flatMap(describeConditions);
34
+ return [condLabel(input)];
35
+ }
36
+ var count = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
37
+ function renderPlan(meta, indent = "") {
38
+ if (!meta) return [`${indent}(a runnable job, shape not introspectable)`];
39
+ const nm = meta.name ? ` "${meta.name}"` : "";
40
+ const out = [];
41
+ switch (meta.kind) {
42
+ case "loop": {
43
+ const max = typeof meta.max === "number" ? ` (max ${meta.max})` : "";
44
+ out.push(`${indent}loop${nm}${max}`);
45
+ const start = meta.start;
46
+ const gate = meta.gate;
47
+ const stopOn = meta.stopOn;
48
+ if (start?.length) out.push(`${indent} start: ${start.join(", ")}`);
49
+ if (gate?.length) out.push(`${indent} gate: ${gate.join(", ")}`);
50
+ if (stopOn?.length) out.push(`${indent} stopOn: ${stopOn.join(", ")}`);
51
+ const tail = [meta.review ? "review" : null, meta.commit ? "commit" : null].filter(Boolean);
52
+ if (tail.length) out.push(`${indent} on convergence: ${tail.join(" + ")}`);
53
+ out.push(`${indent} body:`);
54
+ out.push(...renderPlan(meta.body, `${indent} `));
55
+ break;
56
+ }
57
+ case "dag": {
58
+ const nodes = meta.nodes ?? [];
59
+ out.push(`${indent}dag${nm} (${count(nodes.length, "node")})`);
60
+ for (const node of nodes) {
61
+ const bits = [];
62
+ if (node.needs?.length) bits.push(`needs ${node.needs.join(", ")}`);
63
+ if (node.isolate) bits.push("isolated");
64
+ out.push(`${indent} - ${node.name}${bits.length ? ` (${bits.join("; ")})` : ""}`);
65
+ out.push(...renderPlan(node.job, `${indent} `));
66
+ }
67
+ break;
68
+ }
69
+ case "agent":
70
+ out.push(`${indent}agent${nm}${meta.ground ? " (grounded)" : ""}`);
71
+ break;
72
+ case "fn":
73
+ out.push(`${indent}fn${nm}`);
74
+ break;
75
+ default:
76
+ out.push(`${indent}${meta.kind}${nm}`);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ // src/core/budget.ts
82
+ var Budget = class {
83
+ limit;
84
+ headroom;
85
+ soft;
86
+ tokens = 0;
87
+ constructor(config) {
88
+ this.limit = config.limit;
89
+ this.headroom = config.headroom ?? 0;
90
+ this.soft = config.soft ?? false;
91
+ }
92
+ /** Record consumed tokens. Non-finite or non-positive values are ignored. */
93
+ add(tokens) {
94
+ if (Number.isFinite(tokens) && tokens > 0) this.tokens += tokens;
95
+ }
96
+ spent() {
97
+ return this.tokens;
98
+ }
99
+ remaining() {
100
+ return Math.max(0, this.limit - this.tokens);
101
+ }
102
+ /** True once the next call would breach the cap (accounting for headroom). */
103
+ exceeded() {
104
+ return this.tokens + this.headroom >= this.limit;
105
+ }
106
+ };
107
+ function assertBudget(ctx) {
108
+ const budget = ctx.budget;
109
+ if (!budget || !budget.exceeded()) return;
110
+ if (budget.soft) {
111
+ ctx.log(
112
+ `token budget reached (${budget.spent()}/${budget.limit}) \u2014 continuing (soft)`,
113
+ "warn"
114
+ );
115
+ return;
116
+ }
117
+ throw new LoopError({
118
+ code: "BUDGET",
119
+ phase: "engine",
120
+ message: `token budget exhausted: ${budget.spent()}/${budget.limit} tokens spent`
121
+ });
122
+ }
123
+ function fromFile(path) {
124
+ return readFileSync(path, "utf8").trim();
125
+ }
126
+ function defineSkill(skill) {
127
+ if (!skill.name) throw new Error("defineSkill: `name` is required");
128
+ if (!skill.instructions?.trim()) throw new Error(`defineSkill "${skill.name}": empty instructions`);
129
+ return skill;
130
+ }
131
+ function defineAgent(def) {
132
+ if (!def.name) throw new Error("defineAgent: `name` is required");
133
+ if (!def.system?.trim()) throw new Error(`defineAgent "${def.name}": empty system prompt`);
134
+ def.skills?.forEach((s) => defineSkill(s));
135
+ return def;
136
+ }
137
+ function resolveSystem(agent) {
138
+ if (!agent.skills?.length) return agent.system;
139
+ const methods = agent.skills.map((s) => `### ${s.name}
140
+
141
+ ${s.instructions.trim()}`).join("\n\n");
142
+ return `${agent.system.trim()}
143
+
144
+ ## Methodologies you apply
145
+
146
+ ${methods}`;
147
+ }
148
+ function isForge(value) {
149
+ return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.createPr === "function";
150
+ }
151
+ function buildViewArgs(branch) {
152
+ return ["pr", "view", branch, "--json", "number,url,headRefName"];
153
+ }
154
+ function buildCreateArgs(input) {
155
+ const args = [
156
+ "pr",
157
+ "create",
158
+ "--base",
159
+ input.base,
160
+ "--head",
161
+ input.branch,
162
+ "--title",
163
+ input.title,
164
+ "--body-file",
165
+ "-"
166
+ ];
167
+ if (input.draft) args.push("--draft");
168
+ return args;
169
+ }
170
+ function buildEditArgs(pr, patch) {
171
+ const args = ["pr", "edit", String(pr.number)];
172
+ if (patch.title) args.push("--title", patch.title);
173
+ if (patch.body !== void 0) args.push("--body-file", "-");
174
+ return args;
175
+ }
176
+ function buildMergeArgs(pr, opts) {
177
+ const args = ["pr", "merge", String(pr.number)];
178
+ args.push(opts.squash === false ? "--merge" : "--squash");
179
+ if (opts.auto) args.push("--auto");
180
+ if (opts.subject) args.push("--subject", opts.subject);
181
+ if (opts.body !== void 0) args.push("--body-file", "-");
182
+ if (opts.deleteBranch) args.push("--delete-branch");
183
+ return args;
184
+ }
185
+ function buildChecksArgs(pr) {
186
+ return ["pr", "checks", String(pr.number), "--required"];
187
+ }
188
+ async function gh(bin, args, opts, input) {
189
+ let r;
190
+ try {
191
+ r = await execa(bin, args, {
192
+ cwd: opts.cwd,
193
+ cancelSignal: opts.signal,
194
+ reject: false,
195
+ all: true,
196
+ stdin: input === void 0 ? "ignore" : void 0,
197
+ input
198
+ });
199
+ } catch (e) {
200
+ throw new LoopError({
201
+ code: "CONFIG",
202
+ message: `the GitHub CLI (gh) is required for PR operations but could not be run (install it and run \`gh auth login\`): ${e.message}`
203
+ });
204
+ }
205
+ return {
206
+ stdout: r.stdout ?? "",
207
+ all: r.all ?? r.stdout ?? "",
208
+ exitCode: r.exitCode ?? 1
209
+ };
210
+ }
211
+ function ghOrThrow(r, action) {
212
+ if (r.exitCode !== 0)
213
+ throw new LoopError({
214
+ code: "CONFIG",
215
+ message: `gh ${action} failed (exit ${r.exitCode}): ${redactSecrets(String(r.all).slice(0, 400))}`
216
+ });
217
+ }
218
+ var GhForge = class {
219
+ constructor(bin = "gh") {
220
+ this.bin = bin;
221
+ }
222
+ bin;
223
+ name = "gh";
224
+ async viewPr(branch, opts) {
225
+ const r = await gh(this.bin, buildViewArgs(branch), opts);
226
+ if (r.exitCode !== 0) return void 0;
227
+ try {
228
+ const j = JSON.parse(r.stdout);
229
+ return { number: j.number, url: j.url, branch: j.headRefName };
230
+ } catch {
231
+ return void 0;
232
+ }
233
+ }
234
+ async createPr(input, opts) {
235
+ const r = await gh(this.bin, buildCreateArgs(input), opts, input.body);
236
+ ghOrThrow(r, "pr create");
237
+ const url = r.stdout.trim().split("\n").pop() ?? "";
238
+ const m = url.match(/\/pull\/(\d+)/);
239
+ return { number: m ? Number(m[1]) : 0, url, branch: input.branch };
240
+ }
241
+ async editPr(pr, patch, opts) {
242
+ const r = await gh(this.bin, buildEditArgs(pr, patch), opts, patch.body);
243
+ ghOrThrow(r, "pr edit");
244
+ }
245
+ async mergePr(pr, opts) {
246
+ const r = await gh(this.bin, buildMergeArgs(pr, opts), opts, opts.body);
247
+ ghOrThrow(r, "pr merge");
248
+ }
249
+ async checksPass(pr, opts) {
250
+ const r = await gh(this.bin, buildChecksArgs(pr), opts);
251
+ return r.exitCode === 0;
252
+ }
253
+ };
254
+ var MockForge = class {
255
+ constructor(opts = {}) {
256
+ this.opts = opts;
257
+ this.prs = new Map(Object.entries(opts.existing ?? {}));
258
+ }
259
+ opts;
260
+ name = "mock-forge";
261
+ calls = [];
262
+ prs;
263
+ seq = 100;
264
+ async viewPr(branch) {
265
+ this.calls.push({ method: "viewPr", args: { branch } });
266
+ return this.prs.get(branch);
267
+ }
268
+ async createPr(input) {
269
+ this.calls.push({ method: "createPr", args: { ...input } });
270
+ const number = this.seq += 1;
271
+ const pr = {
272
+ number,
273
+ url: `https://example.test/pull/${number}`,
274
+ branch: input.branch
275
+ };
276
+ this.prs.set(input.branch, pr);
277
+ return pr;
278
+ }
279
+ async editPr(pr, patch) {
280
+ this.calls.push({ method: "editPr", args: { pr, patch } });
281
+ }
282
+ async mergePr(pr, opts) {
283
+ this.calls.push({
284
+ method: "mergePr",
285
+ args: {
286
+ pr,
287
+ squash: opts.squash,
288
+ auto: opts.auto,
289
+ subject: opts.subject,
290
+ body: opts.body,
291
+ deleteBranch: opts.deleteBranch
292
+ }
293
+ });
294
+ }
295
+ async checksPass() {
296
+ this.calls.push({ method: "checksPass", args: {} });
297
+ return this.opts.checks ?? true;
298
+ }
299
+ };
300
+ function toCondition(input, combine = "all") {
301
+ if (Array.isArray(input)) {
302
+ const conds = input.map((i) => toCondition(i, combine));
303
+ return combine === "any" ? any(...conds) : all(...conds);
304
+ }
305
+ return coerceOne(input);
306
+ }
307
+ function coerceOne(fn) {
308
+ return async (ctx, last) => {
309
+ const r = await fn(
310
+ ctx,
311
+ last
312
+ );
313
+ if (typeof r === "boolean") {
314
+ return { met: r, reason: `predicate: ${r}` };
315
+ }
316
+ if (r && typeof r === "object" && "met" in r) {
317
+ const res = r;
318
+ if (typeof res.met !== "boolean") {
319
+ throw new LoopError({
320
+ code: "VALIDATION",
321
+ message: `condition returned a non-boolean "met": ${String(res.met)}`
322
+ });
323
+ }
324
+ return r;
325
+ }
326
+ return { met: Boolean(r), reason: `coerced: ${String(r)}` };
327
+ };
328
+ }
329
+ function predicate(fn, reason = "predicate") {
330
+ return async (ctx, last) => {
331
+ const met = await fn(ctx, last);
332
+ return { met, reason: met ? `${reason}: true` : `${reason}: false` };
333
+ };
334
+ }
335
+ function bodyPassed() {
336
+ return async (_ctx, last) => ({
337
+ met: last?.status === "pass",
338
+ confidence: last?.confidence,
339
+ reason: `last body status = ${last?.status ?? "none"}`
340
+ });
341
+ }
342
+ function minConfidence(threshold) {
343
+ return async (_ctx, last) => {
344
+ const c = last?.confidence ?? 0;
345
+ return {
346
+ met: c >= threshold,
347
+ confidence: c,
348
+ reason: `confidence ${c.toFixed(2)} ${c >= threshold ? ">=" : "<"} ${threshold}`
349
+ };
350
+ };
351
+ }
352
+ function commandSucceeds(command, args = [], opts = {}) {
353
+ return setLabel(async (ctx) => {
354
+ try {
355
+ const r = await execa(command, args, {
356
+ cwd: opts.cwd ?? ctx.workspace.dir,
357
+ timeout: opts.timeoutMs,
358
+ cancelSignal: ctx.signal,
359
+ reject: false,
360
+ stdin: "ignore",
361
+ // Inherit the running environment's vars (BASE_URL, …) so the gate can
362
+ // test the live preview, not just static files on disk.
363
+ env: ctx.environment?.env
364
+ });
365
+ return {
366
+ met: r.exitCode === 0,
367
+ reason: `\`${command}\` exited ${r.exitCode ?? "?"}`
368
+ };
369
+ } catch (e) {
370
+ return {
371
+ met: false,
372
+ reason: `\`${command}\` failed to run: ${e instanceof Error ? e.message : String(e)}`
373
+ };
374
+ }
375
+ }, `${command}${args.length ? ` ${args.join(" ")}` : ""}`);
376
+ }
377
+ function forgeChecks() {
378
+ return async (ctx) => {
379
+ const branch = ctx.workspace.branch;
380
+ if (!branch) return { met: false, reason: "no branch checked out" };
381
+ const forge = ctx.forge ?? new GhForge();
382
+ const fopts = { cwd: ctx.workspace.dir, signal: ctx.signal };
383
+ const pr = await forge.viewPr(branch, fopts);
384
+ if (!pr) return { met: false, reason: `no open PR for "${branch}"` };
385
+ const ok = await forge.checksPass(pr, fopts);
386
+ return {
387
+ met: ok,
388
+ reason: ok ? "required checks pass" : "required checks not green"
389
+ };
390
+ };
391
+ }
392
+ var always = async () => ({ met: true, reason: "always" });
393
+ var never = async () => ({ met: false, reason: "never" });
394
+ function not(c) {
395
+ const cond = toCondition(c);
396
+ return async (ctx, last) => {
397
+ const r = await cond(ctx, last);
398
+ return {
399
+ met: !r.met,
400
+ confidence: r.confidence,
401
+ reason: `not(${r.reason})`
402
+ };
403
+ };
404
+ }
405
+ function all(...inputs) {
406
+ const conds = inputs.map((i) => toCondition(i));
407
+ return async (ctx, last) => {
408
+ const results = [];
409
+ for (const c of conds) {
410
+ const r = await c(ctx, last);
411
+ results.push(r);
412
+ if (!r.met) return { met: false, reason: `all -> failed: ${r.reason}` };
413
+ }
414
+ return {
415
+ met: true,
416
+ reason: `all(${results.map((r) => r.reason).join(" & ")})`
417
+ };
418
+ };
419
+ }
420
+ function any(...inputs) {
421
+ const conds = inputs.map((i) => toCondition(i));
422
+ return async (ctx, last) => {
423
+ const reasons = [];
424
+ for (const c of conds) {
425
+ const r = await c(ctx, last);
426
+ reasons.push(r.reason);
427
+ if (r.met)
428
+ return {
429
+ met: true,
430
+ confidence: r.confidence,
431
+ reason: `any -> ${r.reason}`
432
+ };
433
+ }
434
+ return { met: false, reason: `any(${reasons.join(" | ")})` };
435
+ };
436
+ }
437
+ function quorum(k, ...inputs) {
438
+ if (k < 1 || k > inputs.length)
439
+ throw new LoopError({
440
+ code: "CONFIG",
441
+ message: `quorum requires 1 <= k <= inputs (got k=${k}, n=${inputs.length})`
442
+ });
443
+ const conds = inputs.map((i) => toCondition(i));
444
+ return setLabel(async (ctx, last) => {
445
+ const settled = await Promise.allSettled(conds.map((c) => c(ctx, last)));
446
+ const results = settled.map(
447
+ (s) => s.status === "fulfilled" ? s.value : {
448
+ met: false,
449
+ reason: `judge errored: ${s.reason instanceof Error ? s.reason.message : String(s.reason)}`
450
+ }
451
+ );
452
+ const held = results.filter((r) => r.met);
453
+ const confs = held.map((r) => r.confidence).filter((c) => typeof c === "number");
454
+ const confidence = confs.length ? confs.reduce((a, b) => a + b, 0) / confs.length : void 0;
455
+ return {
456
+ met: held.length >= k,
457
+ confidence,
458
+ reason: `quorum ${held.length}/${inputs.length} held (need ${k})`
459
+ };
460
+ }, `quorum ${k}/${inputs.length}`);
461
+ }
462
+ function defaultContext(ctx, last) {
463
+ const parts = [];
464
+ if (last?.summary) parts.push(`Last outcome summary: ${last.summary}`);
465
+ if (last?.status) parts.push(`Last outcome status: ${last.status}`);
466
+ if (last?.data !== void 0)
467
+ parts.push(`Last outcome data: ${safeJson(last.data)}`);
468
+ const stateKeys = Object.keys(ctx.state);
469
+ if (stateKeys.length) parts.push(`Shared state: ${safeJson(ctx.state)}`);
470
+ return parts.join("\n") || "(no prior context)";
471
+ }
472
+ function safeJson(value, limit = 4e3) {
473
+ try {
474
+ const s = JSON.stringify(value, null, 2) ?? String(value);
475
+ return s.length > limit ? `${s.slice(0, limit)}\u2026 (truncated)` : s;
476
+ } catch {
477
+ return String(value);
478
+ }
479
+ }
480
+ function* balancedObjects(text) {
481
+ let cursor = 0;
482
+ while (cursor < text.length) {
483
+ const start = text.indexOf("{", cursor);
484
+ if (start === -1) return;
485
+ let depth = 0;
486
+ let inString = false;
487
+ let escaped = false;
488
+ let end = -1;
489
+ for (let i = start; i < text.length; i += 1) {
490
+ const ch = text[i];
491
+ if (inString) {
492
+ if (escaped) escaped = false;
493
+ else if (ch === "\\") escaped = true;
494
+ else if (ch === '"') inString = false;
495
+ continue;
496
+ }
497
+ if (ch === '"') inString = true;
498
+ else if (ch === "{") depth += 1;
499
+ else if (ch === "}" && --depth === 0) {
500
+ end = i;
501
+ break;
502
+ }
503
+ }
504
+ if (end === -1) return;
505
+ yield text.slice(start, end + 1);
506
+ cursor = end + 1;
507
+ }
508
+ }
509
+ function toVerdict(obj) {
510
+ const verdict = obj.verdict === "yes" ? "yes" : "no";
511
+ const confidence = typeof obj.confidence === "number" ? clamp01(obj.confidence) : 0;
512
+ const reason = typeof obj.reason === "string" ? obj.reason : "(no reason given)";
513
+ return { verdict, confidence, reason };
514
+ }
515
+ function parseVerdict(text) {
516
+ let fallback;
517
+ for (const candidate of balancedObjects(text)) {
518
+ let parsed;
519
+ try {
520
+ parsed = JSON.parse(candidate);
521
+ } catch {
522
+ continue;
523
+ }
524
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
525
+ continue;
526
+ const obj = parsed;
527
+ if ("verdict" in obj) return toVerdict(obj);
528
+ fallback ??= obj;
529
+ }
530
+ if (fallback) return toVerdict(fallback);
531
+ throw new LoopError({
532
+ code: "VALIDATION",
533
+ message: `validator returned no JSON verdict: ${text.slice(0, 200)}`
534
+ });
535
+ }
536
+ function clamp01(n) {
537
+ return Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;
538
+ }
539
+ var VALIDATOR_SYSTEM = 'You are a strict, sceptical evaluator. You judge whether a stated condition is truly met given the evidence. Do not be generous. The `confidence` field is MANDATORY: always include a number in 0..1 for how sure you are, and when in doubt give a LOW number \u2014 never omit it (an omitted confidence is treated as zero). Respond with ONLY a single JSON object and no other text:\n{"verdict":"yes"|"no","confidence":<number 0..1>,"reason":"<one sentence>"}';
540
+ var CONFIDENCE_TAG_SYSTEM = "You are a rigorous, report-only reviewer. Do not edit anything and do not imply you will. Assess the evidence against the stated condition, listing each concern tied to a concrete location and a concrete failure scenario (not a vibe). Judge against the stated contract, not an ideal: do not penalise the absence of hardening the contract does not require, and when the evidence meets the contract and you cannot name a concrete fault, say so plainly. Close with a single final line and nothing after it: `<confidence>N%</confidence>` \u2014 N is an integer 0-100, where 100 means you found no genuine contract violation or real bug, and below 100 means at least one concrete, addressable concern is open.";
541
+ function parseConfidenceTag(text) {
542
+ const re = /<confidence>\s*([0-9]+(?:\.[0-9]+)?)\s*%?\s*<\/confidence>/gi;
543
+ let m;
544
+ let last = null;
545
+ while ((m = re.exec(text)) !== null) last = m;
546
+ if (!last) return null;
547
+ let n = parseFloat(last[1]);
548
+ if (n > 1) n = n / 100;
549
+ return { confidence: clamp01(n), findings: text.slice(0, last.index).trim() };
550
+ }
551
+ function validatorScoreSystem(dimensions) {
552
+ return `You are a strict, sceptical evaluator. Score how well the condition is met on EACH named dimension, from 0 (not at all) to 1 (fully). Do not be generous; when in doubt score low. Respond with ONLY a single JSON object and no other text:
553
+ {"scores":{${dimensions.map((d) => `"${d}":<0..1>`).join(",")}},"reason":"<one sentence>"}`;
554
+ }
555
+ function geometricMean(values) {
556
+ if (!values.length) return 0;
557
+ if (values.some((v) => v <= 0)) return 0;
558
+ return Math.exp(values.reduce((a, b) => a + Math.log(b), 0) / values.length);
559
+ }
560
+ function parseScores(text, dimensions) {
561
+ for (const candidate of balancedObjects(text)) {
562
+ let parsed;
563
+ try {
564
+ parsed = JSON.parse(candidate);
565
+ } catch {
566
+ continue;
567
+ }
568
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
569
+ continue;
570
+ const raw = parsed.scores;
571
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
572
+ const scoreObj = raw;
573
+ const scores = {};
574
+ for (const d of dimensions) {
575
+ const val = scoreObj[d];
576
+ scores[d] = typeof val === "number" ? clamp01(val) : 0;
577
+ }
578
+ const reasonField = parsed.reason;
579
+ return {
580
+ score: geometricMean(dimensions.map((d) => scores[d])),
581
+ scores,
582
+ reason: typeof reasonField === "string" ? reasonField : "(no reason given)"
583
+ };
584
+ }
585
+ throw new LoopError({
586
+ code: "VALIDATION",
587
+ message: `validator returned no JSON scores: ${text.slice(0, 200)}`
588
+ });
589
+ }
590
+ function agentCheck(config) {
591
+ const threshold = config.threshold ?? 0.8;
592
+ const confidenceTag = config.confidenceTag === true;
593
+ const dimensions = !confidenceTag && config.dimensions?.length ? config.dimensions : void 0;
594
+ return setLabel(async (ctx, last) => {
595
+ const engine = config.engine ? ctx.resolveEngine(config.engine) : ctx.engine;
596
+ const contextText = await (config.context ?? defaultContext)(ctx, last);
597
+ const closing = confidenceTag ? "Write your review now, then close with `<confidence>N%</confidence>`." : `Return the JSON ${dimensions ? "scores" : "verdict"} now.`;
598
+ const prompt = `CONDITION TO EVALUATE:
599
+ ${config.question}
600
+
601
+ EVIDENCE:
602
+ ${contextText}
603
+
604
+ ` + closing;
605
+ const baseSystem = confidenceTag ? CONFIDENCE_TAG_SYSTEM : dimensions ? validatorScoreSystem(dimensions) : VALIDATOR_SYSTEM;
606
+ const system = config.agent ? `${resolveSystem(config.agent)}
607
+
608
+ ${baseSystem}` : baseSystem;
609
+ let result;
610
+ try {
611
+ assertBudget(ctx);
612
+ result = await engine.run(
613
+ {
614
+ prompt,
615
+ system,
616
+ model: config.model ?? config.agent?.model,
617
+ // A report-then-rate reviewer needs room for findings before the tag.
618
+ maxTokens: config.maxTokens ?? (confidenceTag ? 2048 : 512)
619
+ },
620
+ (e) => {
621
+ if (e.type === "usage") {
622
+ ctx.emit({
623
+ kind: "engine:usage",
624
+ ts: Date.now(),
625
+ path: [...ctx.path],
626
+ model: e.model,
627
+ usage: e.usage
628
+ });
629
+ }
630
+ },
631
+ ctx.signal
632
+ );
633
+ } catch (e) {
634
+ throw LoopError.from(e, { code: "ENGINE", path: ctx.path });
635
+ }
636
+ if (confidenceTag) {
637
+ const parsed = parseConfidenceTag(result.text);
638
+ if (!parsed)
639
+ return {
640
+ met: false,
641
+ confidence: 0,
642
+ reason: `no <confidence> tag: ${result.text.slice(0, 140)}`
643
+ };
644
+ const pct = Math.round(parsed.confidence * 100);
645
+ const need = Math.round(threshold * 100);
646
+ return {
647
+ met: parsed.confidence >= threshold,
648
+ confidence: parsed.confidence,
649
+ reason: `confidence ${pct}% (need ${need}%)${parsed.findings ? ` \u2014 ${parsed.findings.slice(0, 280)}` : ""}`
650
+ };
651
+ }
652
+ if (dimensions) {
653
+ let sv;
654
+ try {
655
+ sv = parseScores(result.text, dimensions);
656
+ } catch {
657
+ return {
658
+ met: false,
659
+ confidence: 0,
660
+ reason: `unparseable scores: ${result.text.slice(0, 120)}`
661
+ };
662
+ }
663
+ const detail = dimensions.map((d) => `${d}=${sv.scores[d].toFixed(2)}`).join(", ");
664
+ return {
665
+ met: sv.score >= threshold,
666
+ confidence: sv.score,
667
+ reason: `geo ${sv.score.toFixed(2)} (need ${threshold}) [${detail}] \u2014 ${sv.reason}`
668
+ };
669
+ }
670
+ let v;
671
+ try {
672
+ v = parseVerdict(result.text);
673
+ } catch {
674
+ return {
675
+ met: false,
676
+ confidence: 0,
677
+ reason: `unparseable verdict: ${result.text.slice(0, 120)}`
678
+ };
679
+ }
680
+ const met = v.verdict === "yes" && v.confidence >= threshold;
681
+ return {
682
+ met,
683
+ confidence: v.confidence,
684
+ reason: `${v.verdict} @ ${v.confidence.toFixed(2)} (need ${threshold}) \u2014 ${v.reason}`
685
+ };
686
+ }, `judge "${config.question}" >=${threshold}`);
687
+ }
688
+ function gateJob(label, condition) {
689
+ const cond = toCondition(condition);
690
+ return setMeta(async (ctx) => {
691
+ ctx.emit({ kind: "job:start", ts: Date.now(), path: [...ctx.path], label });
692
+ const r = await cond(ctx, ctx.lastOutcome);
693
+ const outcome = {
694
+ status: r.met ? "pass" : "fail",
695
+ confidence: r.confidence,
696
+ summary: r.reason
697
+ };
698
+ ctx.emit({
699
+ kind: "job:end",
700
+ ts: Date.now(),
701
+ path: [...ctx.path],
702
+ label,
703
+ outcome
704
+ });
705
+ return outcome;
706
+ }, { kind: "gate", name: label });
707
+ }
708
+ var FS = "";
709
+ var RS = "";
710
+ var LOG_FORMAT = `%H${FS}%aI${FS}%s${FS}%b${RS}`;
711
+ async function git(args, { cwd, signal }, input) {
712
+ const r = await execa("git", args, {
713
+ cwd,
714
+ cancelSignal: signal,
715
+ reject: false,
716
+ stdin: input === void 0 ? "ignore" : void 0,
717
+ input
718
+ });
719
+ return { stdout: r.stdout ?? "", exitCode: r.exitCode ?? 1 };
720
+ }
721
+ async function isRepo(opts) {
722
+ try {
723
+ const r = await git(["rev-parse", "--is-inside-work-tree"], opts);
724
+ return r.exitCode === 0 && r.stdout.trim() === "true";
725
+ } catch {
726
+ return false;
727
+ }
728
+ }
729
+ async function currentBranch(opts) {
730
+ const r = await git(["rev-parse", "--abbrev-ref", "HEAD"], opts);
731
+ if (r.exitCode !== 0) return void 0;
732
+ const name = r.stdout.trim();
733
+ return name && name !== "HEAD" ? name : void 0;
734
+ }
735
+ async function headSha(opts) {
736
+ const r = await git(["rev-parse", "HEAD"], opts);
737
+ return r.exitCode === 0 ? r.stdout.trim() || void 0 : void 0;
738
+ }
739
+ async function stageAll(opts) {
740
+ await git(["add", "-A"], opts);
741
+ }
742
+ async function hasStagedChanges(opts) {
743
+ const r = await git(["diff", "--cached", "--quiet"], opts);
744
+ return r.exitCode === 1;
745
+ }
746
+ async function isDirty(opts) {
747
+ const r = await git(["status", "--porcelain"], opts);
748
+ return r.stdout.trim().length > 0;
749
+ }
750
+ async function commit(input, opts) {
751
+ if (!input.allowEmpty && !await hasStagedChanges(opts)) return void 0;
752
+ const message = input.body ? `${input.subject}
753
+
754
+ ${input.body}
755
+ ` : `${input.subject}
756
+ `;
757
+ const args = ["commit", "-F", "-"];
758
+ if (input.allowEmpty) args.push("--allow-empty");
759
+ const r = await git(args, opts, message);
760
+ if (r.exitCode !== 0) {
761
+ throw new Error(
762
+ `git commit failed (exit ${r.exitCode}): ${r.stdout}`.trim()
763
+ );
764
+ }
765
+ return headSha(opts);
766
+ }
767
+ async function log(query) {
768
+ const { cwd, signal, since, max } = query;
769
+ const ref = query.ref ?? "HEAD";
770
+ const args = ["log", `--format=${LOG_FORMAT}`];
771
+ if (max != null) args.push(`-n${max}`);
772
+ args.push(since ? `${since}..${ref}` : ref);
773
+ const r = await git(args, { cwd, signal });
774
+ if (r.exitCode !== 0) return [];
775
+ return parseLog(r.stdout);
776
+ }
777
+ function parseLog(stdout) {
778
+ const records = [];
779
+ for (const chunk of stdout.split(RS)) {
780
+ const fields = chunk.replace(/^\n+/, "").split(FS);
781
+ if (fields.length < 4 || !fields[0].trim()) continue;
782
+ records.push({
783
+ sha: fields[0].trim(),
784
+ date: fields[1].trim(),
785
+ subject: fields[2],
786
+ body: fields[3].trim()
787
+ });
788
+ }
789
+ return records;
790
+ }
791
+ async function addWorktree(repoDir, opts) {
792
+ const dir = mkdtempSync(join(tmpdir(), "loops-wt-"));
793
+ const r = await git(
794
+ ["worktree", "add", "-b", opts.branch, dir, opts.base ?? "HEAD"],
795
+ { cwd: repoDir, signal: opts.signal }
796
+ );
797
+ if (r.exitCode !== 0)
798
+ throw new Error(
799
+ `git worktree add failed (exit ${r.exitCode}): ${r.stdout}`.trim()
800
+ );
801
+ return { dir, branch: opts.branch };
802
+ }
803
+ async function removeWorktree(repoDir, dir, opts = {}) {
804
+ await git(["worktree", "remove", "--force", dir], {
805
+ cwd: repoDir,
806
+ signal: opts.signal
807
+ });
808
+ }
809
+ async function deleteBranch(repoDir, branch, opts = {}) {
810
+ await git(["branch", "-D", branch], { cwd: repoDir, signal: opts.signal });
811
+ }
812
+ async function mergeBranch(repoDir, branch, opts = {}) {
813
+ const r = await git(
814
+ ["merge", "--no-ff", "-m", opts.message ?? `merge ${branch}`, branch],
815
+ { cwd: repoDir, signal: opts.signal }
816
+ );
817
+ if (r.exitCode === 0) return { ok: true, conflict: false };
818
+ await git(["merge", "--abort"], { cwd: repoDir, signal: opts.signal });
819
+ return { ok: false, conflict: true };
820
+ }
821
+ async function mergeNoCommit(repoDir, branch, opts = {}) {
822
+ const r = await git(["merge", "--no-ff", "--no-commit", branch], {
823
+ cwd: repoDir,
824
+ signal: opts.signal
825
+ });
826
+ if (r.exitCode === 0) return { clean: true, conflicted: [] };
827
+ return { clean: false, conflicted: await conflictedFiles(repoDir, opts) };
828
+ }
829
+ async function conflictedFiles(repoDir, opts = {}) {
830
+ const r = await git(["diff", "--name-only", "--diff-filter=U"], {
831
+ cwd: repoDir,
832
+ signal: opts.signal
833
+ });
834
+ return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
835
+ }
836
+ async function mergeAbort(repoDir, opts = {}) {
837
+ await git(["merge", "--abort"], { cwd: repoDir, signal: opts.signal });
838
+ }
839
+ async function push(opts) {
840
+ const branch = opts.branch ?? await currentBranch(opts);
841
+ const args = ["push"];
842
+ if (opts.setUpstream ?? true) args.push("-u");
843
+ if (opts.force) args.push("--force-with-lease");
844
+ args.push(opts.remote ?? "origin");
845
+ if (branch) args.push(branch);
846
+ const r = await execa("git", args, {
847
+ cwd: opts.cwd,
848
+ cancelSignal: opts.signal,
849
+ reject: false,
850
+ stdin: "ignore",
851
+ all: true
852
+ });
853
+ return { ok: (r.exitCode ?? 1) === 0, output: (r.all ?? r.stdout ?? "").trim() };
854
+ }
855
+ var SCRATCH_DIR = ".loops";
856
+ var LEDGER_FILE = "ledger.md";
857
+ var PROMPT_FILE = "prompt.md";
858
+ function ledgerPath(workspace) {
859
+ return join(workspace.dir, SCRATCH_DIR, LEDGER_FILE);
860
+ }
861
+ function promptPath(workspace) {
862
+ return join(workspace.dir, SCRATCH_DIR, PROMPT_FILE);
863
+ }
864
+ function ensureDir(workspace) {
865
+ mkdirSync(join(workspace.dir, SCRATCH_DIR), { recursive: true });
866
+ ensureIgnored(workspace);
867
+ }
868
+ function ensureIgnored(workspace) {
869
+ const dir = join(workspace.dir, SCRATCH_DIR);
870
+ if (!existsSync(dir)) return;
871
+ const ignore = join(dir, ".gitignore");
872
+ if (!existsSync(ignore)) writeFileSync(ignore, "*\n");
873
+ }
874
+ function read(path) {
875
+ try {
876
+ return readFileSync(path, "utf8").trim();
877
+ } catch {
878
+ return "";
879
+ }
880
+ }
881
+ function reset(path) {
882
+ try {
883
+ rmSync(path, { force: true });
884
+ } catch {
885
+ }
886
+ }
887
+ function appendPrompt(workspace, note) {
888
+ ensureDir(workspace);
889
+ const n = typeof note === "string" ? { body: note } : note;
890
+ const header = n.heading ? `## ${n.heading}${n.author ? ` \u2014 ${n.author}` : ""}
891
+
892
+ ` : n.author ? `_${n.author}:_ ` : "";
893
+ appendFileSync(promptPath(workspace), `${header}${n.body.trim()}
894
+
895
+ `);
896
+ }
897
+ function readPrompt(workspace) {
898
+ return read(promptPath(workspace));
899
+ }
900
+ function resetPrompt(workspace) {
901
+ reset(promptPath(workspace));
902
+ }
903
+ function appendLedger(workspace, entry) {
904
+ ensureDir(workspace);
905
+ const path = ledgerPath(workspace);
906
+ if (typeof entry === "string") {
907
+ const body = entry.trim();
908
+ if (body) appendFileSync(path, `${body}
909
+
910
+ `);
911
+ return;
912
+ }
913
+ const head = entry.label ? `### ${entry.label}${entry.iteration ? ` \xB7 iteration ${entry.iteration}` : ""}` : entry.iteration ? `### iteration ${entry.iteration}` : "";
914
+ const lines = [];
915
+ if (head) lines.push(head);
916
+ if (entry.text?.trim()) lines.push(entry.text.trim());
917
+ if (entry.tools?.length) lines.push(`_actions: ${entry.tools.join(", ")}_`);
918
+ if (!lines.length) return;
919
+ appendFileSync(path, `${lines.join("\n\n")}
920
+
921
+ `);
922
+ }
923
+ function readLedger(workspace) {
924
+ return read(ledgerPath(workspace));
925
+ }
926
+ function resetLedger(workspace) {
927
+ reset(ledgerPath(workspace));
928
+ }
929
+
930
+ // src/core/consolidate.ts
931
+ var CONSOLIDATE_SYSTEM = "You maintain a project's CONSOLIDATED LEDGER from its commit history \u2014 the bounded coarse memory a fresh context reads to continue safely. Capture the current state and the open threads, and PRESERVE every binding decision, convention and constraint with its exact values verbatim (downstream work must honour them, so dropping or generalising even one is a failure). Tight markdown; MERGE new commits into the prior ledger, deduplicate, omit only narrative \u2014 never omit a decision.";
932
+ function digest(body, n = 280) {
933
+ const text = body.split("\n").filter((l) => l.trim() && !l.trimStart().startsWith("#")).join(" ").replace(/\s+/g, " ").trim();
934
+ return text.length > n ? `${text.slice(0, n).trimEnd()}\u2026` : text;
935
+ }
936
+ async function consolidate(ctx, opts = {}) {
937
+ const records = await log({
938
+ cwd: ctx.workspace.dir,
939
+ max: opts.max ?? 30,
940
+ since: opts.since,
941
+ signal: ctx.signal
942
+ });
943
+ const entries = records.map(
944
+ (r) => `- ${r.sha.slice(0, 7)} ${r.subject}${r.body ? `
945
+ ${digest(r.body)}` : ""}`
946
+ ).join("\n");
947
+ const engine = opts.engine ? ctx.resolveEngine(opts.engine) : ctx.engine;
948
+ const result = await engine.run(
949
+ {
950
+ prompt: (opts.prior ? `CURRENT LEDGER:
951
+ ${opts.prior}
952
+
953
+ ` : "") + `COMMITS (newest first):
954
+ ${entries || "(none)"}
955
+
956
+ Output the updated consolidated ledger.`,
957
+ system: CONSOLIDATE_SYSTEM,
958
+ model: opts.model,
959
+ maxTokens: 1e3
960
+ },
961
+ () => {
962
+ },
963
+ ctx.signal
964
+ );
965
+ return result.text.trim();
966
+ }
967
+ var COMPACT_SYSTEM = "You write the HANDOFF a future agent reads if it lost ALL memory of this work. Include EVERYTHING it needs to continue safely, as structured markdown: ## Why (the problem and the root cause), ## What (exactly what changed, and where \u2014 names, paths, signatures), ## Alternatives (what was ruled out and why), ## Constraints (the invariants and limits that shaped it), ## Next (what is left or to watch). Preserve every decision and specific value verbatim. Completeness matters more than brevity \u2014 drop only literal repetition and play-by-play narration, never a decision or a detail. Omit a section only if it truly has nothing. No preamble.";
968
+ function truncate(s, n) {
969
+ const t = s.trim();
970
+ return t.length > n ? `${t.slice(0, n).trimEnd()}
971
+ \u2026` : t;
972
+ }
973
+ async function compactLedger(ctx, text, opts = {}) {
974
+ const trimmed = text.trim();
975
+ if (!trimmed) return "";
976
+ const max = opts.maxChars ?? 2e3;
977
+ if (trimmed.length <= max) return trimmed;
978
+ try {
979
+ const engine = opts.engine ? ctx.resolveEngine(opts.engine) : ctx.engine;
980
+ const result = await engine.run(
981
+ {
982
+ prompt: `WORKING LOG:
983
+ ${trimmed}
984
+
985
+ Write the complete handoff.`,
986
+ system: COMPACT_SYSTEM,
987
+ model: opts.model,
988
+ maxTokens: 1200
989
+ },
990
+ () => {
991
+ },
992
+ ctx.signal
993
+ );
994
+ return result.text.trim() || truncate(trimmed, max);
995
+ } catch {
996
+ return truncate(trimmed, max);
997
+ }
998
+ }
999
+ async function composeCommitBody(ctx, workspace, opts = {}) {
1000
+ const material = [readPrompt(workspace), readLedger(workspace)].map((s) => s.trim()).filter(Boolean).join("\n\n");
1001
+ return material ? await compactLedger(ctx, material, opts) : "";
1002
+ }
1003
+ function consolidateJob(config = {}) {
1004
+ return async (ctx) => {
1005
+ const label = config.label ?? "consolidate";
1006
+ const subject = config.subject ?? "consolidate: ledger";
1007
+ const path = [...ctx.path];
1008
+ ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
1009
+ try {
1010
+ const recent = await log({ cwd: ctx.workspace.dir, max: 50, signal: ctx.signal });
1011
+ const prior = recent.find((r) => r.subject === subject)?.body || void 0;
1012
+ const ledger = await consolidate(ctx, { ...config, prior });
1013
+ const sha = await commit(
1014
+ { subject, body: ledger, allowEmpty: true },
1015
+ { cwd: ctx.workspace.dir, signal: ctx.signal }
1016
+ );
1017
+ const outcome = {
1018
+ status: "pass",
1019
+ summary: sha ? `ledger ${sha.slice(0, 7)}` : "ledger unchanged",
1020
+ data: { sha: sha ?? null }
1021
+ };
1022
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
1023
+ return outcome;
1024
+ } catch (e) {
1025
+ const error = LoopError.from(e, { code: "BODY", path: ctx.path });
1026
+ ctx.emit({
1027
+ kind: "error",
1028
+ ts: Date.now(),
1029
+ path,
1030
+ message: error.message,
1031
+ code: error.code
1032
+ });
1033
+ const outcome = { status: "fail", summary: error.message, error };
1034
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
1035
+ return outcome;
1036
+ }
1037
+ };
1038
+ }
1039
+
1040
+ // src/core/ground.ts
1041
+ function truncate2(s, n) {
1042
+ return s.length > n ? `${s.slice(0, n).trimEnd()}
1043
+ \u2026` : s;
1044
+ }
1045
+ async function groundingText(workspace, opts = {}) {
1046
+ const records = await log({
1047
+ cwd: workspace.dir,
1048
+ since: opts.since,
1049
+ max: opts.max ?? 10,
1050
+ signal: opts.signal
1051
+ });
1052
+ if (!records.length) return "";
1053
+ const where = workspace.branch ? `\`${workspace.branch}\`` : "this branch";
1054
+ const header = `## Recent work on ${where} (the commit log)
1055
+ What prior iterations already did and why \u2014 read it before working so you do not repeat a dead end. Most recent first.`;
1056
+ const bodyChars = opts.bodyChars ?? 1200;
1057
+ const entries = records.map((r) => {
1058
+ const head = `### ${r.sha.slice(0, 7)} ${r.subject}`;
1059
+ return r.body ? `${head}
1060
+
1061
+ ${truncate2(r.body, bodyChars)}` : head;
1062
+ });
1063
+ return `${header}
1064
+
1065
+ ${entries.join("\n\n")}`;
1066
+ }
1067
+ var SELECT_SYSTEM = "You select which past commits are relevant CONTEXT for a task. Be selective: return only genuinely relevant commits, fewer is better. Output ONLY shas, comma-separated, most relevant first \u2014 or the single word NONE.";
1068
+ function pickShas(text, records) {
1069
+ const ids = text.toLowerCase().match(/[0-9a-f]{7,40}/g) ?? [];
1070
+ const out = [];
1071
+ const seen = /* @__PURE__ */ new Set();
1072
+ for (const id of ids) {
1073
+ const rec = records.find((r) => r.sha.startsWith(id));
1074
+ if (rec && !seen.has(rec.sha)) {
1075
+ seen.add(rec.sha);
1076
+ out.push(rec);
1077
+ }
1078
+ }
1079
+ return out;
1080
+ }
1081
+ async function retrieveLedger(ctx, opts) {
1082
+ const records = await log({
1083
+ cwd: ctx.workspace.dir,
1084
+ max: opts.candidates ?? 100,
1085
+ signal: ctx.signal
1086
+ });
1087
+ if (!records.length) return "";
1088
+ const list = records.map((r) => `${r.sha.slice(0, 9)}: ${r.subject}`).join("\n");
1089
+ const engine = opts.engine ? ctx.resolveEngine(opts.engine) : ctx.engine;
1090
+ const result = await engine.run(
1091
+ {
1092
+ prompt: `TASK:
1093
+ ${opts.intent}
1094
+
1095
+ CANDIDATE COMMITS (sha: subject):
1096
+ ${list}
1097
+
1098
+ Return the shas relevant to the TASK (up to ${opts.max ?? 8}), or NONE.`,
1099
+ system: SELECT_SYSTEM,
1100
+ model: opts.model,
1101
+ maxTokens: 200
1102
+ },
1103
+ () => {
1104
+ },
1105
+ ctx.signal
1106
+ );
1107
+ const picked = pickShas(result.text, records).slice(0, opts.max ?? 8);
1108
+ if (!picked.length) return "";
1109
+ const where = ctx.workspace.branch ? `\`${ctx.workspace.branch}\`` : "this branch";
1110
+ const header = `## Relevant prior work on ${where} (retrieved for this task)
1111
+ Commits a search judged relevant \u2014 read them before working.`;
1112
+ const bodyChars = opts.bodyChars ?? 1200;
1113
+ const entries = picked.map((r) => {
1114
+ const head = `### ${r.sha.slice(0, 7)} ${r.subject}`;
1115
+ return r.body ? `${head}
1116
+
1117
+ ${truncate2(r.body, bodyChars)}` : head;
1118
+ });
1119
+ return `${header}
1120
+
1121
+ ${entries.join("\n\n")}`;
1122
+ }
1123
+
1124
+ // src/core/job.ts
1125
+ var HANDOFF_MARK = "===HANDOFF===";
1126
+ function recordBlock() {
1127
+ return `## Before you finish: the handoff
1128
+ Answer one question for whoever continues this: **what is everything future-you needs to know about this if you lost all memory of it?** The harness keeps your answer as the memory the next agent reads and as the commit body, so carry the WHY, not just the what \u2014 write it so they cannot repeat your dead ends or break your decisions.
1129
+ End your reply with this block (keep the \`${HANDOFF_MARK}\` marker exactly; drop a section only if it truly has nothing):
1130
+
1131
+ ${HANDOFF_MARK}
1132
+ ## Why
1133
+ <the problem and the root cause you found>
1134
+ ## What
1135
+ <the change you made>
1136
+ ## Alternatives
1137
+ <what you ruled out, and why>
1138
+ ## Constraints
1139
+ <the invariants and limits that shaped it>
1140
+ ## Next
1141
+ <what is left, or what to watch>`;
1142
+ }
1143
+ function splitTurn(text) {
1144
+ const lines = text.split("\n");
1145
+ for (let i = lines.length - 1; i >= 0; i--) {
1146
+ if (lines[i].trim().replace(/\s+/g, "").toUpperCase() === HANDOFF_MARK) {
1147
+ return {
1148
+ work: lines.slice(0, i).join("\n").trim(),
1149
+ handoff: lines.slice(i + 1).join("\n").trim() || void 0
1150
+ };
1151
+ }
1152
+ }
1153
+ return { work: text.trim() };
1154
+ }
1155
+ async function withGrounding(ctx, userPrompt, ground) {
1156
+ const opts = typeof ground === "object" ? ground : {};
1157
+ const parts = [];
1158
+ const committed = opts.retrieve ? await retrieveLedger(ctx, {
1159
+ intent: userPrompt,
1160
+ max: opts.max,
1161
+ bodyChars: opts.bodyChars,
1162
+ candidates: typeof opts.retrieve === "object" ? opts.retrieve.candidates : void 0,
1163
+ model: typeof opts.retrieve === "object" ? opts.retrieve.model : void 0
1164
+ }) : await groundingText(ctx.workspace, {
1165
+ max: opts.max,
1166
+ bodyChars: opts.bodyChars,
1167
+ signal: ctx.signal
1168
+ });
1169
+ if (committed) parts.push(committed);
1170
+ if (opts.includeScratch !== false) {
1171
+ const working = readLedger(ctx.workspace);
1172
+ if (working)
1173
+ parts.push(
1174
+ `## Working memory (this run so far)
1175
+
1176
+ What earlier turns in this run tried and found \u2014 build on it.
1177
+
1178
+ ${working}`
1179
+ );
1180
+ const handoff = readPrompt(ctx.workspace);
1181
+ if (handoff)
1182
+ parts.push(
1183
+ `## Handoff so far (what earlier work distilled for the next agent)
1184
+
1185
+ ${handoff}`
1186
+ );
1187
+ }
1188
+ if (opts.recordInstruction !== false) parts.push(recordBlock());
1189
+ parts.push(userPrompt);
1190
+ return parts.join("\n\n---\n\n");
1191
+ }
1192
+ function summariseTools(uses) {
1193
+ return [...uses].map(([name, n]) => n > 1 ? `${name}\xD7${n}` : name);
1194
+ }
1195
+ var TERMINAL = (text) => ({
1196
+ status: "pass",
1197
+ summary: text.trim().slice(0, 280),
1198
+ data: text
1199
+ });
1200
+ function agentJob(config) {
1201
+ const job = async (ctx) => {
1202
+ const path = [...ctx.path];
1203
+ const label = config.label ?? config.agent?.name ?? "agent";
1204
+ ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
1205
+ const engine = ctx.resolveEngine(config.engine);
1206
+ const userPrompt = typeof config.prompt === "function" ? await config.prompt(ctx) : config.prompt;
1207
+ const prompt = config.ground ? await withGrounding(ctx, userPrompt, config.ground) : userPrompt;
1208
+ const system = config.system !== void 0 ? typeof config.system === "function" ? config.system(ctx) : config.system : config.agent ? resolveSystem(config.agent) : void 0;
1209
+ let result;
1210
+ const toolUses = /* @__PURE__ */ new Map();
1211
+ try {
1212
+ assertBudget(ctx);
1213
+ result = await engine.run(
1214
+ {
1215
+ prompt,
1216
+ system,
1217
+ model: config.model ?? config.agent?.model,
1218
+ maxTokens: config.maxTokens,
1219
+ allowedTools: config.allowedTools ?? config.agent?.tools,
1220
+ leaf: config.leaf ?? config.agent?.leaf,
1221
+ cwd: config.cwd ?? ctx.workspace.dir,
1222
+ timeoutMs: config.timeoutMs
1223
+ },
1224
+ (e) => {
1225
+ const ts = Date.now();
1226
+ switch (e.type) {
1227
+ case "text":
1228
+ ctx.emit({ kind: "engine:text", ts, path, delta: e.delta });
1229
+ break;
1230
+ case "thinking":
1231
+ ctx.emit({ kind: "engine:thinking", ts, path, delta: e.delta });
1232
+ break;
1233
+ case "tool":
1234
+ if (e.phase === "use")
1235
+ toolUses.set(e.name, (toolUses.get(e.name) ?? 0) + 1);
1236
+ ctx.emit({
1237
+ kind: "engine:tool",
1238
+ ts,
1239
+ path,
1240
+ name: e.name,
1241
+ phase: e.phase
1242
+ });
1243
+ break;
1244
+ case "usage":
1245
+ ctx.emit({
1246
+ kind: "engine:usage",
1247
+ ts,
1248
+ path,
1249
+ model: e.model,
1250
+ usage: e.usage
1251
+ });
1252
+ break;
1253
+ }
1254
+ },
1255
+ ctx.signal
1256
+ );
1257
+ } catch (e) {
1258
+ const error = LoopError.from(e, {
1259
+ code: ctx.signal.aborted ? "ABORTED" : "ENGINE",
1260
+ phase: "body",
1261
+ path: ctx.path,
1262
+ iteration: ctx.iteration
1263
+ });
1264
+ ctx.emit({
1265
+ kind: "error",
1266
+ ts: Date.now(),
1267
+ path,
1268
+ message: error.message,
1269
+ code: error.code
1270
+ });
1271
+ const outcome2 = {
1272
+ status: ctx.signal.aborted ? "aborted" : "fail",
1273
+ summary: error.message,
1274
+ error
1275
+ };
1276
+ ctx.emit({
1277
+ kind: "job:end",
1278
+ ts: Date.now(),
1279
+ path,
1280
+ label,
1281
+ outcome: outcome2
1282
+ });
1283
+ return outcome2;
1284
+ }
1285
+ if (config.ground) {
1286
+ const { work, handoff } = splitTurn(result.text);
1287
+ appendLedger(ctx.workspace, {
1288
+ label,
1289
+ iteration: ctx.iteration,
1290
+ text: work,
1291
+ tools: summariseTools(toolUses)
1292
+ });
1293
+ if (handoff) appendPrompt(ctx.workspace, handoff);
1294
+ }
1295
+ const outcome = config.outcome ? await config.outcome(result.text, ctx) : TERMINAL(result.text);
1296
+ ctx.emit({
1297
+ kind: "job:end",
1298
+ ts: Date.now(),
1299
+ path,
1300
+ label,
1301
+ outcome
1302
+ });
1303
+ return outcome;
1304
+ };
1305
+ return setMeta(job, {
1306
+ kind: "agent",
1307
+ name: config.label ?? config.agent?.name ?? "agent",
1308
+ ground: !!config.ground
1309
+ });
1310
+ }
1311
+ function composeWay(ctx, last) {
1312
+ const sections = [];
1313
+ const head = [];
1314
+ if (ctx.iteration) head.push(`iteration: ${ctx.iteration}`);
1315
+ if (last?.status) head.push(`status: ${last.status}`);
1316
+ if (typeof last?.confidence === "number")
1317
+ head.push(`confidence: ${last.confidence.toFixed(2)}`);
1318
+ if (head.length) sections.push(`## Outcome
1319
+ ${head.join("\n")}`);
1320
+ if (last?.summary) sections.push(`## Summary
1321
+ ${last.summary.trim()}`);
1322
+ if (ctx.lastReview?.summary)
1323
+ sections.push(`## Next
1324
+ ${ctx.lastReview.summary.trim()}`);
1325
+ return sections.join("\n\n");
1326
+ }
1327
+ function commitJob(config) {
1328
+ return async (ctx) => {
1329
+ const label = config.label ?? "commit";
1330
+ const path = [...ctx.path];
1331
+ const cwd = ctx.workspace.dir;
1332
+ ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
1333
+ try {
1334
+ if (!await isRepo({ cwd, signal: ctx.signal })) {
1335
+ throw new LoopError({
1336
+ code: "CONFIG",
1337
+ message: `commitJob "${label}" requires a git repository (cwd: ${cwd})`
1338
+ });
1339
+ }
1340
+ const last = ctx.lastOutcome;
1341
+ const subject = typeof config.subject === "function" ? await config.subject(ctx, last) : config.subject;
1342
+ const body = config.body !== void 0 ? typeof config.body === "function" ? await config.body(ctx, last) : config.body : await composeCommitBody(ctx, ctx.workspace, {
1343
+ model: config.compactModel
1344
+ }) || composeWay(ctx, last);
1345
+ if (config.stageAll ?? true) {
1346
+ ensureIgnored(ctx.workspace);
1347
+ await stageAll({ cwd, signal: ctx.signal });
1348
+ }
1349
+ const sha = await commit(
1350
+ { subject, body, allowEmpty: config.allowEmpty },
1351
+ { cwd, signal: ctx.signal }
1352
+ );
1353
+ if (sha) {
1354
+ resetPrompt(ctx.workspace);
1355
+ resetLedger(ctx.workspace);
1356
+ }
1357
+ const outcome = sha ? {
1358
+ status: "pass",
1359
+ summary: `committed ${sha.slice(0, 7)}: ${subject}`,
1360
+ data: { sha }
1361
+ } : { status: "pass", summary: "nothing to commit", data: { sha: null } };
1362
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
1363
+ return outcome;
1364
+ } catch (e) {
1365
+ const error = LoopError.from(e, {
1366
+ code: "BODY",
1367
+ phase: "body",
1368
+ path: ctx.path,
1369
+ iteration: ctx.iteration
1370
+ });
1371
+ ctx.emit({
1372
+ kind: "error",
1373
+ ts: Date.now(),
1374
+ path,
1375
+ message: error.message,
1376
+ code: error.code
1377
+ });
1378
+ const outcome = { status: "fail", summary: error.message, error };
1379
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
1380
+ return outcome;
1381
+ }
1382
+ };
1383
+ }
1384
+ function kickback(to, reason, over) {
1385
+ return {
1386
+ status: "fail",
1387
+ summary: reason,
1388
+ ...over,
1389
+ kickback: { to, reason }
1390
+ };
1391
+ }
1392
+ function fnJob(label, fn) {
1393
+ const job = async (ctx) => {
1394
+ const path = [...ctx.path];
1395
+ ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
1396
+ let outcome;
1397
+ try {
1398
+ outcome = await fn(ctx);
1399
+ } catch (e) {
1400
+ const error = LoopError.from(e, {
1401
+ code: "BODY",
1402
+ phase: "body",
1403
+ path: ctx.path,
1404
+ iteration: ctx.iteration
1405
+ });
1406
+ outcome = { status: "fail", summary: error.message, error };
1407
+ ctx.emit({
1408
+ kind: "error",
1409
+ ts: Date.now(),
1410
+ path,
1411
+ message: error.message,
1412
+ code: error.code
1413
+ });
1414
+ }
1415
+ ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
1416
+ return outcome;
1417
+ };
1418
+ return setMeta(job, { kind: "fn", name: label });
1419
+ }
1420
+
1421
+ // src/core/context.ts
1422
+ function childContext(parent, over) {
1423
+ return {
1424
+ engine: parent.engine,
1425
+ resolveEngine: parent.resolveEngine,
1426
+ signal: parent.signal,
1427
+ emit: parent.emit,
1428
+ state: parent.state,
1429
+ // A child inherits the parent's workspace by default; a concurrency
1430
+ // boundary forks it into an isolated worktree by passing `workspace`.
1431
+ workspace: over.workspace ?? parent.workspace,
1432
+ environment: over.environment ?? parent.environment,
1433
+ forge: parent.forge,
1434
+ budget: parent.budget,
1435
+ onLimit: parent.onLimit,
1436
+ maxWaitMs: parent.maxWaitMs,
1437
+ resumeCommand: parent.resumeCommand,
1438
+ log: parent.log,
1439
+ depth: over.depth,
1440
+ path: over.path,
1441
+ // Inherit the enclosing iteration by default. A `loop` always passes one
1442
+ // explicitly; a `dag`/`sequence` does not, so without this a node nested in a
1443
+ // loop would reset to 0 — the "Attempt 0" confound where a retry body could not
1444
+ // see which attempt it was on. A top-level dag still gets 0 (the root's value).
1445
+ iteration: over.iteration ?? parent.iteration,
1446
+ lastOutcome: over.lastOutcome,
1447
+ lastReview: over.lastReview
1448
+ };
1449
+ }
1450
+
1451
+ // src/core/loop.ts
1452
+ var VALID_STATUS = /* @__PURE__ */ new Set([
1453
+ "pass",
1454
+ "fail",
1455
+ "aborted",
1456
+ "exhausted",
1457
+ "paused"
1458
+ ]);
1459
+ function decideLimit(error, ctx) {
1460
+ const reason = error.message;
1461
+ if (ctx.onLimit === "exit-resume") return { kind: "pause", reason };
1462
+ const waitMs = waitMsFor(error);
1463
+ if (waitMs == null) return { kind: "pause", reason };
1464
+ if (ctx.onLimit === "wait") return { kind: "wait", waitMs };
1465
+ if (waitMs <= ctx.maxWaitMs) return { kind: "wait", waitMs };
1466
+ return {
1467
+ kind: "pause",
1468
+ reason: `${reason} (reset in ${Math.round(waitMs / 1e3)}s exceeds maxWait ${Math.round(ctx.maxWaitMs / 1e3)}s)`
1469
+ };
1470
+ }
1471
+ var yieldToLoop = () => new Promise((r) => setImmediate(r));
1472
+ function loop(config) {
1473
+ if (!config.name)
1474
+ throw new LoopError({
1475
+ code: "CONFIG",
1476
+ message: "loop() requires a non-empty name"
1477
+ });
1478
+ const start = config.start ? toCondition(config.start) : void 0;
1479
+ const until = config.until ? toCondition(config.until) : void 0;
1480
+ const stopOn = config.stopOn ? toCondition(config.stopOn) : void 0;
1481
+ const onError = config.retry?.onError ?? "continue";
1482
+ const job = async (parent) => {
1483
+ const path = [...parent.path, config.name];
1484
+ const depth = parent.depth + 1;
1485
+ const ts = () => Date.now();
1486
+ let lastReview;
1487
+ let iteration = 0;
1488
+ const ctxAt = (iter, lastOutcome) => childContext(parent, {
1489
+ depth,
1490
+ path,
1491
+ iteration: iter,
1492
+ lastOutcome,
1493
+ lastReview
1494
+ });
1495
+ parent.emit({ kind: "loop:start", ts: ts(), path, depth, max: config.max });
1496
+ const commitCfg = config.commit ? config.commit === true ? {
1497
+ label: `${config.name}:checkpoint`,
1498
+ subject: (_c, l) => l?.summary?.split("\n")[0]?.trim().slice(0, 72) || `chore(${config.name}): checkpoint`
1499
+ } : { label: `${config.name}:checkpoint`, ...config.commit } : void 0;
1500
+ const checkpoint = commitCfg ? commitJob(commitCfg) : void 0;
1501
+ const recordMilestone = async (ctx) => {
1502
+ if (!checkpoint) return;
1503
+ const outcome = await checkpoint(ctx);
1504
+ if (outcome.status !== "pass") {
1505
+ parent.emit({
1506
+ kind: "error",
1507
+ ts: ts(),
1508
+ path,
1509
+ message: `checkpoint commit did not pass: ${outcome.summary ?? outcome.status}`,
1510
+ code: outcome.error?.code ?? "UNKNOWN"
1511
+ });
1512
+ }
1513
+ };
1514
+ const finish = async (outcome, iterations) => {
1515
+ parent.emit({ kind: "loop:end", ts: ts(), path, outcome, iterations });
1516
+ if (config.onComplete) {
1517
+ try {
1518
+ await config.onComplete(outcome, ctxAt(iterations));
1519
+ } catch (e) {
1520
+ const error = LoopError.from(e, {
1521
+ code: "BODY",
1522
+ phase: "review",
1523
+ path,
1524
+ iteration: iterations
1525
+ });
1526
+ parent.emit({
1527
+ kind: "error",
1528
+ ts: ts(),
1529
+ path,
1530
+ message: `onComplete threw: ${error.message}`,
1531
+ code: error.code
1532
+ });
1533
+ }
1534
+ }
1535
+ return outcome;
1536
+ };
1537
+ const gate = async (cond, which, ctx, last) => {
1538
+ try {
1539
+ return await cond(ctx, last);
1540
+ } catch (e) {
1541
+ throw LoopError.from(e, {
1542
+ code: "VALIDATION",
1543
+ phase: which,
1544
+ path,
1545
+ iteration
1546
+ });
1547
+ }
1548
+ };
1549
+ try {
1550
+ if (start) {
1551
+ const r = await gate(start, "start", ctxAt(0), void 0);
1552
+ parent.emit({
1553
+ kind: "loop:condition",
1554
+ ts: ts(),
1555
+ path,
1556
+ which: "start",
1557
+ result: r
1558
+ });
1559
+ if (!r.met)
1560
+ return finish(
1561
+ { status: "aborted", summary: `start gate not met: ${r.reason}` },
1562
+ 0
1563
+ );
1564
+ }
1565
+ let last;
1566
+ let consecutiveErrors = 0;
1567
+ let consecutiveReviewFails = 0;
1568
+ while (true) {
1569
+ await yieldToLoop();
1570
+ if (parent.signal.aborted)
1571
+ return finish(
1572
+ { status: "aborted", summary: "aborted by signal" },
1573
+ iteration
1574
+ );
1575
+ if (config.max != null && iteration >= config.max) {
1576
+ return finish(
1577
+ {
1578
+ status: "exhausted",
1579
+ summary: last?.summary ?? `reached max iterations (${config.max})`,
1580
+ confidence: last?.confidence,
1581
+ data: last?.data
1582
+ },
1583
+ iteration
1584
+ );
1585
+ }
1586
+ iteration += 1;
1587
+ const ctx = ctxAt(iteration, last);
1588
+ parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
1589
+ let bodyThrew = false;
1590
+ try {
1591
+ last = await config.body(ctx);
1592
+ consecutiveErrors = 0;
1593
+ } catch (e) {
1594
+ bodyThrew = true;
1595
+ const error = LoopError.from(e, {
1596
+ code: "BODY",
1597
+ phase: "body",
1598
+ path,
1599
+ iteration
1600
+ });
1601
+ parent.emit({
1602
+ kind: "error",
1603
+ ts: ts(),
1604
+ path,
1605
+ message: error.message,
1606
+ code: error.code
1607
+ });
1608
+ consecutiveErrors += 1;
1609
+ const tooMany = config.retry?.maxConsecutive != null && consecutiveErrors >= config.retry.maxConsecutive;
1610
+ if (onError === "fail" || tooMany)
1611
+ return finish(
1612
+ { status: "fail", summary: error.message, error },
1613
+ iteration
1614
+ );
1615
+ last = { status: "fail", summary: error.message, error };
1616
+ if (config.retry?.backoffMs)
1617
+ await delay(config.retry.backoffMs, parent.signal);
1618
+ }
1619
+ if (!last || !VALID_STATUS.has(last.status)) {
1620
+ const error = new LoopError({
1621
+ code: "VALIDATION",
1622
+ phase: "body",
1623
+ path,
1624
+ iteration,
1625
+ message: `body returned an Outcome with no valid "status" (got ${JSON.stringify(last?.status)})`
1626
+ });
1627
+ parent.emit({
1628
+ kind: "error",
1629
+ ts: ts(),
1630
+ path,
1631
+ message: error.message,
1632
+ code: error.code
1633
+ });
1634
+ return finish(
1635
+ { status: "fail", summary: error.message, error },
1636
+ iteration
1637
+ );
1638
+ }
1639
+ if (last.status === "fail" && isLimitError(last.error) && ctx.onLimit !== "fail") {
1640
+ const action = decideLimit(last.error, ctx);
1641
+ if (action.kind === "wait") {
1642
+ const now = ts();
1643
+ parent.emit({
1644
+ kind: "limit:wait",
1645
+ ts: now,
1646
+ path,
1647
+ code: last.error.code,
1648
+ waitMs: action.waitMs,
1649
+ resumeAt: now + action.waitMs
1650
+ });
1651
+ await delay(action.waitMs, parent.signal);
1652
+ iteration -= 1;
1653
+ last = void 0;
1654
+ continue;
1655
+ }
1656
+ parent.emit({
1657
+ kind: "limit:pause",
1658
+ ts: ts(),
1659
+ path,
1660
+ code: last.error.code,
1661
+ reason: action.reason,
1662
+ resumeCommand: ctx.resumeCommand
1663
+ });
1664
+ return finish(
1665
+ {
1666
+ status: "paused",
1667
+ summary: action.reason,
1668
+ error: last.error,
1669
+ data: last.data
1670
+ },
1671
+ iteration
1672
+ );
1673
+ }
1674
+ if (!bodyThrew && last.status === "fail" && last.error && !last.error.retryable) {
1675
+ return finish(
1676
+ { status: "fail", summary: last.summary, error: last.error },
1677
+ iteration
1678
+ );
1679
+ }
1680
+ if (config.onIteration) {
1681
+ try {
1682
+ await config.onIteration(last, ctx);
1683
+ } catch (e) {
1684
+ throw LoopError.from(e, {
1685
+ code: "VALIDATION",
1686
+ phase: "body",
1687
+ path,
1688
+ iteration
1689
+ });
1690
+ }
1691
+ }
1692
+ if (stopOn) {
1693
+ const r = await gate(stopOn, "stopOn", ctx, last);
1694
+ parent.emit({
1695
+ kind: "loop:condition",
1696
+ ts: ts(),
1697
+ path,
1698
+ which: "stopOn",
1699
+ result: r
1700
+ });
1701
+ if (r.met)
1702
+ return finish(
1703
+ {
1704
+ status: "aborted",
1705
+ summary: `stopOn met: ${r.reason}`,
1706
+ data: last.data
1707
+ },
1708
+ iteration
1709
+ );
1710
+ }
1711
+ const conv = until ? await gate(until, "until", ctx, last) : {
1712
+ met: last.status === "pass",
1713
+ confidence: last.confidence,
1714
+ reason: `body status = ${last.status}`
1715
+ };
1716
+ if (until)
1717
+ parent.emit({
1718
+ kind: "loop:condition",
1719
+ ts: ts(),
1720
+ path,
1721
+ which: "until",
1722
+ result: conv
1723
+ });
1724
+ if (conv.met) {
1725
+ if (!config.review) {
1726
+ await recordMilestone(ctxAt(iteration, last));
1727
+ return finish(
1728
+ {
1729
+ status: "pass",
1730
+ confidence: conv.confidence ?? last.confidence,
1731
+ summary: last.summary,
1732
+ data: last.data
1733
+ },
1734
+ iteration
1735
+ );
1736
+ }
1737
+ let reviewOutcome;
1738
+ try {
1739
+ reviewOutcome = await config.review(ctxAt(iteration, last));
1740
+ } catch (e) {
1741
+ throw LoopError.from(e, {
1742
+ code: "VALIDATION",
1743
+ phase: "review",
1744
+ path,
1745
+ iteration
1746
+ });
1747
+ }
1748
+ parent.emit({
1749
+ kind: "loop:review",
1750
+ ts: ts(),
1751
+ path,
1752
+ outcome: reviewOutcome
1753
+ });
1754
+ if (reviewOutcome.status === "pass") {
1755
+ await recordMilestone(ctxAt(iteration, last));
1756
+ return finish(
1757
+ {
1758
+ status: "pass",
1759
+ confidence: reviewOutcome.confidence ?? conv.confidence,
1760
+ summary: reviewOutcome.summary ?? last.summary,
1761
+ data: last.data
1762
+ },
1763
+ iteration
1764
+ );
1765
+ }
1766
+ consecutiveReviewFails += 1;
1767
+ lastReview = reviewOutcome;
1768
+ parent.log(
1769
+ `review did not pass (${reviewOutcome.summary ?? reviewOutcome.status}); re-entering ${config.name}`,
1770
+ "warn"
1771
+ );
1772
+ if (config.maxReviewRestarts != null && consecutiveReviewFails >= config.maxReviewRestarts) {
1773
+ return finish(
1774
+ {
1775
+ status: "exhausted",
1776
+ summary: `review rejected ${consecutiveReviewFails}\xD7 (maxReviewRestarts)`,
1777
+ data: last.data
1778
+ },
1779
+ iteration
1780
+ );
1781
+ }
1782
+ }
1783
+ if (config.delayMs) await delay(config.delayMs, parent.signal);
1784
+ }
1785
+ } catch (e) {
1786
+ const error = LoopError.from(e, { code: "UNKNOWN", path, iteration });
1787
+ parent.emit({
1788
+ kind: "error",
1789
+ ts: ts(),
1790
+ path,
1791
+ message: error.message,
1792
+ code: error.code
1793
+ });
1794
+ return finish(
1795
+ { status: "fail", summary: error.message, error },
1796
+ iteration
1797
+ );
1798
+ }
1799
+ };
1800
+ return setMeta(job, {
1801
+ kind: "loop",
1802
+ name: config.name,
1803
+ max: config.max,
1804
+ start: describeConditions(config.start),
1805
+ gate: describeConditions(config.until),
1806
+ stopOn: describeConditions(config.stopOn),
1807
+ review: !!config.review,
1808
+ commit: !!config.commit,
1809
+ body: jobMeta(config.body)
1810
+ });
1811
+ }
1812
+ function delay(ms, signal) {
1813
+ if (signal.aborted) return Promise.resolve();
1814
+ return new Promise((resolve) => {
1815
+ const t = setTimeout(done, ms);
1816
+ const onAbort = () => done();
1817
+ function done() {
1818
+ clearTimeout(t);
1819
+ signal.removeEventListener("abort", onAbort);
1820
+ resolve();
1821
+ }
1822
+ signal.addEventListener("abort", onAbort, { once: true });
1823
+ });
1824
+ }
1825
+
1826
+ // src/engines/registry.ts
1827
+ var EngineRegistry = class {
1828
+ constructor(opts = {}) {
1829
+ this.opts = opts;
1830
+ this.registerBuiltins();
1831
+ }
1832
+ opts;
1833
+ factories = /* @__PURE__ */ new Map();
1834
+ cache = /* @__PURE__ */ new Map();
1835
+ /** Add or override an engine. The key is what you pass as an `EngineRef`. */
1836
+ register(name, factory) {
1837
+ this.factories.set(name, factory);
1838
+ this.cache.delete(name);
1839
+ return this;
1840
+ }
1841
+ has(name) {
1842
+ return this.factories.has(name);
1843
+ }
1844
+ names() {
1845
+ return [...this.factories.keys()];
1846
+ }
1847
+ /** Resolve a ref to an `Engine`: instance → as-is; name → built/cached. */
1848
+ create(ref, fallback) {
1849
+ if (isEngine(ref)) return ref;
1850
+ const name = ref ?? fallback;
1851
+ const cached = this.cache.get(name);
1852
+ if (cached) return cached;
1853
+ const factory = this.factories.get(name);
1854
+ if (!factory) {
1855
+ throw new LoopError({
1856
+ code: "CONFIG",
1857
+ message: `unknown engine "${name}" (have: ${this.names().join(", ")})`
1858
+ });
1859
+ }
1860
+ const engine = factory(this.opts);
1861
+ this.cache.set(name, engine);
1862
+ return engine;
1863
+ }
1864
+ registerBuiltins() {
1865
+ this.register(
1866
+ "agent-sdk",
1867
+ (o) => lazy(
1868
+ () => import('./agent-sdk-RF5VJZAT.js').then((m) => new m.AgentSdkEngine(o)),
1869
+ "agent-sdk"
1870
+ )
1871
+ );
1872
+ this.register(
1873
+ "claude-cli",
1874
+ (o) => lazy(
1875
+ () => import('./claude-cli-U7WEVAOL.js').then((m) => new m.ClaudeCliEngine(o)),
1876
+ "claude-cli"
1877
+ )
1878
+ );
1879
+ this.register(
1880
+ "anthropic-api",
1881
+ (o) => lazy(
1882
+ () => import('./anthropic-api-XJY6Y4T2.js').then((m) => new m.AnthropicApiEngine(o)),
1883
+ "anthropic-api"
1884
+ )
1885
+ );
1886
+ this.register(
1887
+ "codex",
1888
+ (o) => lazy(() => import('./codex-6I5UZ2HM.js').then((m) => new m.CodexEngine(o)), "codex")
1889
+ );
1890
+ }
1891
+ };
1892
+ function lazy(load, name) {
1893
+ let inner;
1894
+ return {
1895
+ name,
1896
+ run(req, onEvent, signal) {
1897
+ inner ??= load();
1898
+ return inner.then((engine) => engine.run(req, onEvent, signal));
1899
+ }
1900
+ };
1901
+ }
1902
+
1903
+ // src/core/stats.ts
1904
+ var Stats = class {
1905
+ startedAt = Date.now();
1906
+ loops = /* @__PURE__ */ new Map();
1907
+ models = /* @__PURE__ */ new Map();
1908
+ errors = [];
1909
+ record(event) {
1910
+ const key = event.path.join(" / ") || "(root)";
1911
+ switch (event.kind) {
1912
+ case "loop:start":
1913
+ this.loopFor(key);
1914
+ break;
1915
+ case "loop:iteration":
1916
+ this.loopFor(key).iterations = event.iteration;
1917
+ break;
1918
+ case "loop:review": {
1919
+ const s = this.loopFor(key);
1920
+ if (event.outcome.status === "pass") s.reviewsPassed += 1;
1921
+ else s.reviewsFailed += 1;
1922
+ break;
1923
+ }
1924
+ case "loop:end":
1925
+ this.loopFor(key).lastStatus = event.outcome.status;
1926
+ break;
1927
+ case "engine:usage": {
1928
+ const m = this.modelFor(event.model);
1929
+ m.calls += 1;
1930
+ m.inputTokens += event.usage.inputTokens;
1931
+ m.outputTokens += event.usage.outputTokens;
1932
+ break;
1933
+ }
1934
+ case "error":
1935
+ this.errors.push({
1936
+ path: key,
1937
+ code: event.code,
1938
+ message: event.message,
1939
+ ts: event.ts
1940
+ });
1941
+ break;
1942
+ }
1943
+ }
1944
+ snapshot() {
1945
+ const models = [...this.models.values()];
1946
+ return {
1947
+ startedAt: this.startedAt,
1948
+ elapsedMs: Date.now() - this.startedAt,
1949
+ loops: [...this.loops.values()],
1950
+ models,
1951
+ totalInputTokens: models.reduce((a, m) => a + m.inputTokens, 0),
1952
+ totalOutputTokens: models.reduce((a, m) => a + m.outputTokens, 0),
1953
+ agentCalls: models.reduce((a, m) => a + m.calls, 0),
1954
+ errors: this.errors
1955
+ };
1956
+ }
1957
+ loopFor(path) {
1958
+ let s = this.loops.get(path);
1959
+ if (!s) {
1960
+ s = { path, iterations: 0, reviewsPassed: 0, reviewsFailed: 0 };
1961
+ this.loops.set(path, s);
1962
+ }
1963
+ return s;
1964
+ }
1965
+ modelFor(model) {
1966
+ let m = this.models.get(model);
1967
+ if (!m) {
1968
+ m = { model, calls: 0, inputTokens: 0, outputTokens: 0 };
1969
+ this.models.set(model, m);
1970
+ }
1971
+ return m;
1972
+ }
1973
+ };
1974
+ var NOISE = /* @__PURE__ */ new Set([
1975
+ "engine:text",
1976
+ "engine:thinking"
1977
+ ]);
1978
+ var CHECKPOINT_AT = /* @__PURE__ */ new Set([
1979
+ "loop:iteration",
1980
+ "loop:end",
1981
+ "dag:end",
1982
+ "job:end"
1983
+ ]);
1984
+ function ensureDir2(path) {
1985
+ const dir = dirname(path);
1986
+ if (dir && dir !== ".") mkdirSync(dir, { recursive: true });
1987
+ }
1988
+ function makeRecorder(path) {
1989
+ ensureDir2(path);
1990
+ writeFileSync(path, "");
1991
+ return (event) => {
1992
+ if (NOISE.has(event.kind)) return;
1993
+ try {
1994
+ appendFileSync(path, `${JSON.stringify(event)}
1995
+ `);
1996
+ } catch {
1997
+ }
1998
+ };
1999
+ }
2000
+ function makeCheckpointer(path, state) {
2001
+ ensureDir2(path);
2002
+ return (event) => {
2003
+ if (!CHECKPOINT_AT.has(event.kind)) return;
2004
+ flushCheckpoint(path, state);
2005
+ };
2006
+ }
2007
+ function flushCheckpoint(path, state) {
2008
+ ensureDir2(path);
2009
+ try {
2010
+ writeFileSync(path, JSON.stringify({ ts: Date.now(), state }, null, 2));
2011
+ } catch {
2012
+ }
2013
+ }
2014
+ function loadCheckpoint(path) {
2015
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
2016
+ if (parsed && typeof parsed === "object" && "state" in parsed && parsed.state && typeof parsed.state === "object") {
2017
+ return parsed.state;
2018
+ }
2019
+ return {};
2020
+ }
2021
+
2022
+ // src/runtime/runner.ts
2023
+ var DEFAULT_MAX_WAIT_MS = 3e5;
2024
+ var EXIT_PAUSED = 75;
2025
+ async function run(job, options = {}) {
2026
+ const registry = new EngineRegistry(options.engineOptions ?? {});
2027
+ for (const [name, value] of Object.entries(options.engines ?? {})) {
2028
+ registry.register(name, typeof value === "function" ? value : () => value);
2029
+ }
2030
+ const defaultEngine = options.engine ?? "agent-sdk";
2031
+ const stats = new Stats();
2032
+ const controller = new AbortController();
2033
+ if (options.signal) {
2034
+ if (options.signal.aborted) controller.abort();
2035
+ else
2036
+ options.signal.addEventListener("abort", () => controller.abort(), {
2037
+ once: true
2038
+ });
2039
+ }
2040
+ const budget = options.budget != null ? new Budget(
2041
+ typeof options.budget === "number" ? { limit: options.budget } : options.budget
2042
+ ) : void 0;
2043
+ let initialState = options.state ?? {};
2044
+ if (options.resumeFrom) {
2045
+ try {
2046
+ initialState = { ...loadCheckpoint(options.resumeFrom), ...initialState };
2047
+ } catch (e) {
2048
+ throw new LoopError({
2049
+ code: "CONFIG",
2050
+ message: `cannot resume from "${options.resumeFrom}": ${e instanceof Error ? e.message : String(e)}`
2051
+ });
2052
+ }
2053
+ }
2054
+ const sinks = [];
2055
+ if (options.recordTo) sinks.push(makeRecorder(options.recordTo));
2056
+ if (options.checkpoint)
2057
+ sinks.push(makeCheckpointer(options.checkpoint, initialState));
2058
+ const emit = (event) => {
2059
+ stats.record(event);
2060
+ if (budget && event.kind === "engine:usage")
2061
+ budget.add(event.usage.inputTokens + event.usage.outputTokens);
2062
+ options.onEvent?.(event);
2063
+ for (const sink of sinks) sink(event);
2064
+ };
2065
+ const resolveEngine = (ref) => registry.create(ref, defaultEngine);
2066
+ const dir = options.cwd ?? process.cwd();
2067
+ const workspace = {
2068
+ dir,
2069
+ branch: await currentBranch({ cwd: dir, signal: controller.signal })
2070
+ };
2071
+ let environment;
2072
+ if (options.environment) {
2073
+ try {
2074
+ environment = await options.environment.up(workspace, controller.signal);
2075
+ } catch (e) {
2076
+ const error = LoopError.from(e, { code: "CONFIG" });
2077
+ emit({
2078
+ kind: "error",
2079
+ ts: Date.now(),
2080
+ path: [],
2081
+ message: `environment "${options.environment.name}" failed to start: ${error.message}`,
2082
+ code: error.code
2083
+ });
2084
+ return {
2085
+ outcome: {
2086
+ status: "fail",
2087
+ summary: `environment failed to start: ${error.message}`,
2088
+ error
2089
+ },
2090
+ stats: stats.snapshot(),
2091
+ budget: budget ? {
2092
+ limit: budget.limit,
2093
+ spent: budget.spent(),
2094
+ remaining: budget.remaining()
2095
+ } : void 0
2096
+ };
2097
+ }
2098
+ }
2099
+ const rootCtx = {
2100
+ engine: resolveEngine(defaultEngine),
2101
+ resolveEngine,
2102
+ signal: controller.signal,
2103
+ emit,
2104
+ state: initialState,
2105
+ workspace,
2106
+ environment,
2107
+ forge: options.forge,
2108
+ budget,
2109
+ onLimit: options.onLimit ?? "auto",
2110
+ maxWaitMs: options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS,
2111
+ resumeCommand: options.resumeCommand,
2112
+ iteration: 0,
2113
+ depth: 0,
2114
+ path: [],
2115
+ log: (message, level = "info") => emit({ kind: "log", ts: Date.now(), path: [], level, message })
2116
+ };
2117
+ let outcome;
2118
+ try {
2119
+ outcome = await job(rootCtx);
2120
+ } catch (e) {
2121
+ const error = LoopError.from(e, { code: "UNKNOWN" });
2122
+ emit({
2123
+ kind: "error",
2124
+ ts: Date.now(),
2125
+ path: [],
2126
+ message: error.message,
2127
+ code: error.code
2128
+ });
2129
+ outcome = { status: "fail", summary: error.message, error };
2130
+ } finally {
2131
+ if (environment) await environment.down(controller.signal).catch(() => {
2132
+ });
2133
+ }
2134
+ if (outcome.status === "paused" && options.checkpoint)
2135
+ flushCheckpoint(options.checkpoint, initialState);
2136
+ return {
2137
+ outcome,
2138
+ stats: stats.snapshot(),
2139
+ budget: budget ? {
2140
+ limit: budget.limit,
2141
+ spent: budget.spent(),
2142
+ remaining: budget.remaining()
2143
+ } : void 0
2144
+ };
2145
+ }
2146
+ function exitCodeFor(outcome) {
2147
+ switch (outcome.status) {
2148
+ case "pass":
2149
+ return 0;
2150
+ case "fail":
2151
+ return 1;
2152
+ case "exhausted":
2153
+ return 2;
2154
+ case "aborted":
2155
+ return 130;
2156
+ case "paused":
2157
+ return EXIT_PAUSED;
2158
+ }
2159
+ }
2160
+
2161
+ export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, childContext, commandSucceeds, commit, commitJob, compactLedger, composeCommitBody, conflictedFiles, consolidate, consolidateJob, currentBranch, defineAgent, defineSkill, deleteBranch, describeConditions, ensureIgnored, exitCodeFor, fnJob, forgeChecks, fromFile, gateJob, groundingText, hasStagedChanges, headSha, isDirty, isForge, isRepo, jobMeta, kickback, ledgerPath, log, loop, mergeAbort, mergeBranch, mergeNoCommit, minConfidence, never, not, predicate, promptPath, push, quorum, readLedger, readPrompt, removeWorktree, renderPlan, resetLedger, resetPrompt, resolveSystem, retrieveLedger, run, setMeta, stageAll, toCondition };
2162
+ //# sourceMappingURL=chunk-3BPU34DE.js.map
2163
+ //# sourceMappingURL=chunk-3BPU34DE.js.map