@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.
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/bin/loops.mjs +16 -0
- package/dist/App-3YQS6DXA.js +461 -0
- package/dist/App-3YQS6DXA.js.map +1 -0
- package/dist/agent-sdk-RF5VJZAT.js +95 -0
- package/dist/agent-sdk-RF5VJZAT.js.map +1 -0
- package/dist/anthropic-api-XJY6Y4T2.js +131 -0
- package/dist/anthropic-api-XJY6Y4T2.js.map +1 -0
- package/dist/api.d.ts +949 -0
- package/dist/api.js +898 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-33YIGWNU.js +63 -0
- package/dist/chunk-33YIGWNU.js.map +1 -0
- package/dist/chunk-3BPU34DE.js +2163 -0
- package/dist/chunk-3BPU34DE.js.map +1 -0
- package/dist/chunk-CXEPZHSR.js +86 -0
- package/dist/chunk-CXEPZHSR.js.map +1 -0
- package/dist/chunk-I3STY7U6.js +61 -0
- package/dist/chunk-I3STY7U6.js.map +1 -0
- package/dist/chunk-JFTXJ7I2.js +18 -0
- package/dist/chunk-JFTXJ7I2.js.map +1 -0
- package/dist/chunk-XC46B4FD.js +9 -0
- package/dist/chunk-XC46B4FD.js.map +1 -0
- package/dist/chunk-Y2SD7GBL.js +30 -0
- package/dist/chunk-Y2SD7GBL.js.map +1 -0
- package/dist/claude-cli-U7WEVAOL.js +124 -0
- package/dist/claude-cli-U7WEVAOL.js.map +1 -0
- package/dist/codex-6I5UZ2HM.js +60 -0
- package/dist/codex-6I5UZ2HM.js.map +1 -0
- package/dist/env/command.d.ts +53 -0
- package/dist/env/command.js +3 -0
- package/dist/env/command.js.map +1 -0
- package/dist/env/docker.d.ts +38 -0
- package/dist/env/docker.js +33 -0
- package/dist/env/docker.js.map +1 -0
- package/dist/env/sst.d.ts +39 -0
- package/dist/env/sst.js +20 -0
- package/dist/env/sst.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +620 -0
- package/dist/index.js.map +1 -0
- package/dist/types-B4wGVpqo.d.ts +898 -0
- package/package.json +100 -0
- package/skills/author-loop/SKILL.md +121 -0
package/dist/api.js
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import { mergeNoCommit, stageAll, commit, mergeAbort, log, setMeta, jobMeta, isRepo, addWorktree, childContext, composeCommitBody, mergeBranch, removeWorktree, deleteBranch, push, consolidate, toCondition, GhForge } from './chunk-3BPU34DE.js';
|
|
2
|
+
export { Budget, EXIT_PAUSED, EngineRegistry, GhForge, MockForge, Stats, addWorktree, agentCheck, agentJob, all, always, any, appendLedger, appendPrompt, bodyPassed, buildChecksArgs, buildCreateArgs, buildEditArgs, buildMergeArgs, buildViewArgs, 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, stageAll, toCondition } from './chunk-3BPU34DE.js';
|
|
3
|
+
import './chunk-JFTXJ7I2.js';
|
|
4
|
+
export { SUBAGENT_TOOLS, isEngine } from './chunk-XC46B4FD.js';
|
|
5
|
+
import './chunk-Y2SD7GBL.js';
|
|
6
|
+
import { LoopError } from './chunk-I3STY7U6.js';
|
|
7
|
+
export { LoopError } from './chunk-I3STY7U6.js';
|
|
8
|
+
import pLimit from 'p-limit';
|
|
9
|
+
import toposort from 'toposort';
|
|
10
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
function stripFence(s) {
|
|
14
|
+
const m = /^```[^\n]*\n([\s\S]*?)\n```$/.exec(s.trim());
|
|
15
|
+
return `${(m ? m[1] : s).replace(/\s+$/, "")}
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
function firstLine(s) {
|
|
19
|
+
return s.split("\n").find((l) => l.trim()) ?? "";
|
|
20
|
+
}
|
|
21
|
+
async function mergeSynthesis(ctx, config) {
|
|
22
|
+
const cwd = ctx.workspace.dir;
|
|
23
|
+
const engine = config.engine ? ctx.resolveEngine(config.engine) : ctx.engine;
|
|
24
|
+
const merge = await mergeNoCommit(cwd, config.branch, { signal: ctx.signal });
|
|
25
|
+
try {
|
|
26
|
+
if (!merge.clean) {
|
|
27
|
+
for (const file of merge.conflicted) {
|
|
28
|
+
const conflicted = readFileSync(join(cwd, file), "utf8");
|
|
29
|
+
const out = await engine.run(
|
|
30
|
+
{
|
|
31
|
+
prompt: `Resolve this git merge conflict in \`${file}\`. Combine both sides coherently, preserving the intent of each. Output ONLY the fully resolved file content \u2014 no conflict markers, no commentary, no code fence.
|
|
32
|
+
|
|
33
|
+
${conflicted}`,
|
|
34
|
+
model: config.model,
|
|
35
|
+
maxTokens: 4e3
|
|
36
|
+
},
|
|
37
|
+
() => {
|
|
38
|
+
},
|
|
39
|
+
ctx.signal
|
|
40
|
+
);
|
|
41
|
+
writeFileSync(join(cwd, file), stripFence(out.text));
|
|
42
|
+
}
|
|
43
|
+
await stageAll({ cwd, signal: ctx.signal });
|
|
44
|
+
}
|
|
45
|
+
const body = await synthesiseBody(ctx, engine, config);
|
|
46
|
+
const sha = await commit(
|
|
47
|
+
{
|
|
48
|
+
subject: config.message ?? `merge: ${config.branch} (synthesis)`,
|
|
49
|
+
body,
|
|
50
|
+
allowEmpty: true
|
|
51
|
+
// a merge commit may have an empty diff after resolution
|
|
52
|
+
},
|
|
53
|
+
{ cwd, signal: ctx.signal }
|
|
54
|
+
);
|
|
55
|
+
return { ok: true, conflict: !merge.clean, sha };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
await mergeAbort(cwd, { signal: ctx.signal }).catch(() => {
|
|
58
|
+
});
|
|
59
|
+
throw LoopError.from(e, { code: "BODY", path: ctx.path });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function synthesiseBody(ctx, engine, config) {
|
|
63
|
+
const ways = await log({
|
|
64
|
+
cwd: ctx.workspace.dir,
|
|
65
|
+
ref: config.branch,
|
|
66
|
+
max: 8,
|
|
67
|
+
signal: ctx.signal
|
|
68
|
+
});
|
|
69
|
+
const summary = ways.map((w) => `- ${w.subject}${w.body ? `: ${firstLine(w.body)}` : ""}`).join("\n");
|
|
70
|
+
const out = await engine.run(
|
|
71
|
+
{
|
|
72
|
+
prompt: `A branch is being merged. Its commits:
|
|
73
|
+
${summary || "(none)"}
|
|
74
|
+
|
|
75
|
+
Write a concise MERGE SYNTHESIS for the commit body: what this line of work accomplished, how it integrates, and any tradeoff reconciled. A few sentences, no preamble.`,
|
|
76
|
+
system: "You write merge synthesis commit bodies that capture intent.",
|
|
77
|
+
model: config.model,
|
|
78
|
+
maxTokens: 600
|
|
79
|
+
},
|
|
80
|
+
() => {
|
|
81
|
+
},
|
|
82
|
+
ctx.signal
|
|
83
|
+
);
|
|
84
|
+
return out.text.trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/dag.ts
|
|
88
|
+
function slug(s) {
|
|
89
|
+
return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "node";
|
|
90
|
+
}
|
|
91
|
+
function normalize(node) {
|
|
92
|
+
return typeof node === "function" ? { job: node } : node;
|
|
93
|
+
}
|
|
94
|
+
function dag(config) {
|
|
95
|
+
if (!config.name)
|
|
96
|
+
throw new LoopError({
|
|
97
|
+
code: "CONFIG",
|
|
98
|
+
message: "dag() requires a non-empty name"
|
|
99
|
+
});
|
|
100
|
+
const names = Object.keys(config.nodes);
|
|
101
|
+
const nodes = new Map(
|
|
102
|
+
names.map((n) => [n, normalize(config.nodes[n])])
|
|
103
|
+
);
|
|
104
|
+
const edges = [];
|
|
105
|
+
for (const [name, node] of nodes) {
|
|
106
|
+
for (const dep of node.needs ?? []) {
|
|
107
|
+
if (!nodes.has(dep)) {
|
|
108
|
+
throw new LoopError({
|
|
109
|
+
code: "CONFIG",
|
|
110
|
+
message: `dag "${config.name}": node "${name}" needs unknown node "${dep}"`
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
edges.push([dep, name]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let order;
|
|
117
|
+
try {
|
|
118
|
+
order = toposort.array(names, edges);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
throw new LoopError({
|
|
121
|
+
code: "CONFIG",
|
|
122
|
+
message: `dag "${config.name}": dependency cycle detected`,
|
|
123
|
+
cause: e
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const stopOnError = config.stopOnError ?? true;
|
|
127
|
+
const maxKickbacks = config.maxKickbacks ?? 0;
|
|
128
|
+
const dependents = new Map(names.map((n) => [n, []]));
|
|
129
|
+
for (const [dep, name] of edges) dependents.get(dep).push(name);
|
|
130
|
+
const ancestorsOf = (name) => {
|
|
131
|
+
const seen = /* @__PURE__ */ new Set();
|
|
132
|
+
const stack = [...nodes.get(name).needs ?? []];
|
|
133
|
+
while (stack.length) {
|
|
134
|
+
const n = stack.pop();
|
|
135
|
+
if (seen.has(n)) continue;
|
|
136
|
+
seen.add(n);
|
|
137
|
+
stack.push(...nodes.get(n).needs ?? []);
|
|
138
|
+
}
|
|
139
|
+
return seen;
|
|
140
|
+
};
|
|
141
|
+
const dirtyFrom = (target) => {
|
|
142
|
+
const seen = /* @__PURE__ */ new Set([target]);
|
|
143
|
+
const stack = [target];
|
|
144
|
+
while (stack.length) {
|
|
145
|
+
const n = stack.pop();
|
|
146
|
+
for (const d of dependents.get(n))
|
|
147
|
+
if (!seen.has(d)) {
|
|
148
|
+
seen.add(d);
|
|
149
|
+
stack.push(d);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return seen;
|
|
153
|
+
};
|
|
154
|
+
const limitN = config.concurrency && config.concurrency > 0 ? config.concurrency : names.length || 1;
|
|
155
|
+
const job = async (parent) => {
|
|
156
|
+
const path = [...parent.path, config.name];
|
|
157
|
+
const depth = parent.depth + 1;
|
|
158
|
+
const ts = () => Date.now();
|
|
159
|
+
parent.emit({ kind: "dag:start", ts: ts(), path, depth, nodes: names });
|
|
160
|
+
const limit = pLimit(limitN);
|
|
161
|
+
const results = /* @__PURE__ */ new Map();
|
|
162
|
+
const memo = /* @__PURE__ */ new Map();
|
|
163
|
+
let stopped = false;
|
|
164
|
+
const pendingKickback = /* @__PURE__ */ new Map();
|
|
165
|
+
const nodeCtx = (name, workspace, environment) => childContext(parent, {
|
|
166
|
+
depth,
|
|
167
|
+
path: [...path, name],
|
|
168
|
+
workspace,
|
|
169
|
+
environment,
|
|
170
|
+
lastReview: pendingKickback.get(name)
|
|
171
|
+
});
|
|
172
|
+
const mergeLimit = pLimit(1);
|
|
173
|
+
let forkSeq2 = 0;
|
|
174
|
+
const runNodeJob = async (name, node) => {
|
|
175
|
+
const isolated2 = node.isolate ?? config.isolation === "worktree";
|
|
176
|
+
if (!isolated2) return node.job(nodeCtx(name));
|
|
177
|
+
const base = parent.workspace;
|
|
178
|
+
if (!await isRepo({ cwd: base.dir, signal: parent.signal })) {
|
|
179
|
+
parent.log(
|
|
180
|
+
`node "${name}" requested worktree isolation but ${base.dir} is not a git repo; running in the shared workspace`,
|
|
181
|
+
"warn"
|
|
182
|
+
);
|
|
183
|
+
return node.job(nodeCtx(name));
|
|
184
|
+
}
|
|
185
|
+
const branch = `loops/${slug(config.name)}-${slug(name)}-${forkSeq2 += 1}`;
|
|
186
|
+
const wt = await addWorktree(base.dir, {
|
|
187
|
+
branch,
|
|
188
|
+
base: "HEAD",
|
|
189
|
+
signal: parent.signal
|
|
190
|
+
});
|
|
191
|
+
const wtWs = { dir: wt.dir, branch };
|
|
192
|
+
let envHandle;
|
|
193
|
+
try {
|
|
194
|
+
if (config.environment)
|
|
195
|
+
envHandle = await config.environment.up(wtWs, parent.signal);
|
|
196
|
+
const outcome2 = await node.job(nodeCtx(name, wtWs, envHandle));
|
|
197
|
+
if (outcome2.status === "pass") {
|
|
198
|
+
await stageAll({ cwd: wt.dir, signal: parent.signal });
|
|
199
|
+
await commit(
|
|
200
|
+
{
|
|
201
|
+
subject: `chore(${slug(name)}): worktree changes`,
|
|
202
|
+
body: await composeCommitBody(parent, wtWs)
|
|
203
|
+
},
|
|
204
|
+
{ cwd: wt.dir, signal: parent.signal }
|
|
205
|
+
);
|
|
206
|
+
const merged = await mergeLimit(
|
|
207
|
+
() => mergeBranch(base.dir, branch, {
|
|
208
|
+
signal: parent.signal,
|
|
209
|
+
message: `merge ${branch} (node ${name})`
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
if (!merged.ok) {
|
|
213
|
+
if (config.onConflict !== "synthesize") {
|
|
214
|
+
return {
|
|
215
|
+
status: "fail",
|
|
216
|
+
summary: `node "${name}" landed with a merge conflict; needs resolution`,
|
|
217
|
+
error: new LoopError({
|
|
218
|
+
code: "BODY",
|
|
219
|
+
message: `merge conflict landing node "${name}"`,
|
|
220
|
+
path: [...path, name]
|
|
221
|
+
})
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await mergeLimit(
|
|
226
|
+
() => mergeSynthesis(parent, {
|
|
227
|
+
branch,
|
|
228
|
+
message: `merge: ${branch} (node ${name}, synthesis)`
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
const error = LoopError.from(e, {
|
|
233
|
+
code: "BODY",
|
|
234
|
+
path: [...path, name]
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
status: "fail",
|
|
238
|
+
summary: `node "${name}" merge synthesis failed: ${error.message}`,
|
|
239
|
+
error
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
await deleteBranch(base.dir, branch, { signal: parent.signal }).catch(
|
|
244
|
+
() => {
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return outcome2;
|
|
249
|
+
} finally {
|
|
250
|
+
if (envHandle)
|
|
251
|
+
await envHandle.down(parent.signal).catch(() => {
|
|
252
|
+
});
|
|
253
|
+
await removeWorktree(base.dir, wt.dir, {
|
|
254
|
+
signal: parent.signal
|
|
255
|
+
}).catch(() => {
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const record = (name, outcome2, phase) => {
|
|
260
|
+
results.set(name, outcome2);
|
|
261
|
+
parent.emit({
|
|
262
|
+
kind: "dag:node",
|
|
263
|
+
ts: ts(),
|
|
264
|
+
path,
|
|
265
|
+
node: name,
|
|
266
|
+
phase,
|
|
267
|
+
outcome: outcome2
|
|
268
|
+
});
|
|
269
|
+
if (phase === "done" && outcome2.status !== "pass" && nodes.get(name).optional !== true && stopOnError && // A node requesting a kickback is going to be re-run — don't let its
|
|
270
|
+
// (provisional) non-pass abort siblings before the feedback is resolved.
|
|
271
|
+
!(maxKickbacks > 0 && outcome2.kickback)) {
|
|
272
|
+
stopped = true;
|
|
273
|
+
}
|
|
274
|
+
return outcome2;
|
|
275
|
+
};
|
|
276
|
+
const run2 = (name) => {
|
|
277
|
+
const existing = memo.get(name);
|
|
278
|
+
if (existing) return existing;
|
|
279
|
+
const node = nodes.get(name);
|
|
280
|
+
const promise = (async () => {
|
|
281
|
+
try {
|
|
282
|
+
const needs = node.needs ?? [];
|
|
283
|
+
const deps = await Promise.all(needs.map(run2));
|
|
284
|
+
const blocked = needs.some((_, i) => deps[i].status !== "pass");
|
|
285
|
+
if (blocked)
|
|
286
|
+
return record(
|
|
287
|
+
name,
|
|
288
|
+
{ status: "aborted", summary: "blocked by a failed dependency" },
|
|
289
|
+
"done"
|
|
290
|
+
);
|
|
291
|
+
if (parent.signal.aborted || stopped)
|
|
292
|
+
return record(
|
|
293
|
+
name,
|
|
294
|
+
{ status: "aborted", summary: "aborted before start" },
|
|
295
|
+
"done"
|
|
296
|
+
);
|
|
297
|
+
const result = await limit(
|
|
298
|
+
async () => {
|
|
299
|
+
if (parent.signal.aborted || stopped)
|
|
300
|
+
return {
|
|
301
|
+
outcome: {
|
|
302
|
+
status: "aborted",
|
|
303
|
+
summary: "aborted before start"
|
|
304
|
+
},
|
|
305
|
+
phase: "done"
|
|
306
|
+
};
|
|
307
|
+
if (node.when) {
|
|
308
|
+
const r = await toCondition(node.when)(
|
|
309
|
+
nodeCtx(name),
|
|
310
|
+
void 0
|
|
311
|
+
);
|
|
312
|
+
if (!r.met)
|
|
313
|
+
return {
|
|
314
|
+
outcome: {
|
|
315
|
+
status: "pass",
|
|
316
|
+
summary: `skipped: ${r.reason}`,
|
|
317
|
+
data: { skipped: true }
|
|
318
|
+
},
|
|
319
|
+
phase: "skip"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
parent.emit({
|
|
323
|
+
kind: "dag:node",
|
|
324
|
+
ts: ts(),
|
|
325
|
+
path,
|
|
326
|
+
node: name,
|
|
327
|
+
phase: "start"
|
|
328
|
+
});
|
|
329
|
+
return { outcome: await runNodeJob(name, node), phase: "done" };
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
return record(name, result.outcome, result.phase);
|
|
333
|
+
} catch (e) {
|
|
334
|
+
const error = LoopError.from(e, {
|
|
335
|
+
code: "BODY",
|
|
336
|
+
phase: "body",
|
|
337
|
+
path: [...path, name]
|
|
338
|
+
});
|
|
339
|
+
parent.emit({
|
|
340
|
+
kind: "error",
|
|
341
|
+
ts: ts(),
|
|
342
|
+
path: [...path, name],
|
|
343
|
+
message: error.message,
|
|
344
|
+
code: error.code
|
|
345
|
+
});
|
|
346
|
+
return record(
|
|
347
|
+
name,
|
|
348
|
+
{ status: "fail", summary: error.message, error },
|
|
349
|
+
"done"
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
})();
|
|
353
|
+
memo.set(name, promise);
|
|
354
|
+
return promise;
|
|
355
|
+
};
|
|
356
|
+
await Promise.all(names.map(run2));
|
|
357
|
+
if (maxKickbacks > 0) {
|
|
358
|
+
let used = 0;
|
|
359
|
+
const rejected = /* @__PURE__ */ new Set();
|
|
360
|
+
const emitKickback = (from, to, reason, accepted, note) => parent.emit({
|
|
361
|
+
kind: "dag:kickback",
|
|
362
|
+
ts: ts(),
|
|
363
|
+
path,
|
|
364
|
+
from,
|
|
365
|
+
to,
|
|
366
|
+
reason,
|
|
367
|
+
accepted,
|
|
368
|
+
note
|
|
369
|
+
});
|
|
370
|
+
for (; ; ) {
|
|
371
|
+
const from = order.find(
|
|
372
|
+
(n) => results.get(n)?.kickback && !rejected.has(n)
|
|
373
|
+
);
|
|
374
|
+
if (!from) break;
|
|
375
|
+
const { to, reason } = results.get(from).kickback;
|
|
376
|
+
const allow = nodes.get(from).acceptsKickbackTo;
|
|
377
|
+
const note = !nodes.has(to) ? `unknown node "${to}"` : !ancestorsOf(from).has(to) ? `"${to}" is not an ancestor of "${from}"` : allow && !allow.includes(to) ? `"${from}" does not accept kickback to "${to}"` : void 0;
|
|
378
|
+
if (note) {
|
|
379
|
+
rejected.add(from);
|
|
380
|
+
emitKickback(from, to, reason, false, note);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (used >= maxKickbacks) {
|
|
384
|
+
emitKickback(
|
|
385
|
+
from,
|
|
386
|
+
to,
|
|
387
|
+
reason,
|
|
388
|
+
false,
|
|
389
|
+
`kickback budget (${maxKickbacks}) exhausted`
|
|
390
|
+
);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
used += 1;
|
|
394
|
+
emitKickback(from, to, reason, true);
|
|
395
|
+
const dirty = dirtyFrom(to);
|
|
396
|
+
for (const d of dirty) {
|
|
397
|
+
memo.delete(d);
|
|
398
|
+
results.delete(d);
|
|
399
|
+
rejected.delete(d);
|
|
400
|
+
}
|
|
401
|
+
pendingKickback.set(to, {
|
|
402
|
+
status: "fail",
|
|
403
|
+
summary: `Kicked back from "${from}": ${reason}`,
|
|
404
|
+
data: { kickback: true, from }
|
|
405
|
+
});
|
|
406
|
+
stopped = false;
|
|
407
|
+
await Promise.all(names.map(run2));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const requiredFailed = names.filter(
|
|
411
|
+
(n) => results.get(n)?.status === "fail" && nodes.get(n).optional !== true
|
|
412
|
+
);
|
|
413
|
+
const requiredAborted = names.filter(
|
|
414
|
+
(n) => results.get(n)?.status === "aborted" && nodes.get(n).optional !== true
|
|
415
|
+
);
|
|
416
|
+
const data = Object.fromEntries(results);
|
|
417
|
+
let outcome;
|
|
418
|
+
if (parent.signal.aborted) {
|
|
419
|
+
outcome = {
|
|
420
|
+
status: "aborted",
|
|
421
|
+
summary: `dag "${config.name}" aborted`,
|
|
422
|
+
data
|
|
423
|
+
};
|
|
424
|
+
} else if (requiredFailed.length > 0 || requiredAborted.length > 0) {
|
|
425
|
+
outcome = {
|
|
426
|
+
status: "fail",
|
|
427
|
+
summary: `dag "${config.name}": ${requiredFailed.length + requiredAborted.length} required node(s) did not complete`,
|
|
428
|
+
data
|
|
429
|
+
};
|
|
430
|
+
} else {
|
|
431
|
+
outcome = {
|
|
432
|
+
status: "pass",
|
|
433
|
+
summary: `dag "${config.name}": all ${names.length} node(s) green`,
|
|
434
|
+
data
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
parent.emit({ kind: "dag:end", ts: ts(), path, outcome });
|
|
438
|
+
return outcome;
|
|
439
|
+
};
|
|
440
|
+
return setMeta(job, {
|
|
441
|
+
kind: "dag",
|
|
442
|
+
name: config.name,
|
|
443
|
+
nodes: Object.entries(config.nodes).map(([name, v]) => {
|
|
444
|
+
const node = typeof v === "function" ? void 0 : v;
|
|
445
|
+
const nodeJob = node ? node.job : v;
|
|
446
|
+
return {
|
|
447
|
+
name,
|
|
448
|
+
needs: node?.needs ?? [],
|
|
449
|
+
isolate: node?.isolate ?? false,
|
|
450
|
+
job: jobMeta(nodeJob)
|
|
451
|
+
};
|
|
452
|
+
})
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function sequence(name, ...jobs) {
|
|
456
|
+
const nodes = {};
|
|
457
|
+
jobs.forEach((job, i) => {
|
|
458
|
+
nodes[`step-${i}`] = { job, needs: i > 0 ? [`step-${i - 1}`] : [] };
|
|
459
|
+
});
|
|
460
|
+
return dag({ name, nodes, concurrency: 1, stopOnError: true });
|
|
461
|
+
}
|
|
462
|
+
function parallel(name, jobs, concurrency) {
|
|
463
|
+
const record = Array.isArray(jobs) ? Object.fromEntries(jobs.map((j, i) => [`task-${i}`, j])) : jobs;
|
|
464
|
+
return dag({ name, nodes: record, concurrency, stopOnError: false });
|
|
465
|
+
}
|
|
466
|
+
function slug2(s) {
|
|
467
|
+
return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "x";
|
|
468
|
+
}
|
|
469
|
+
function tournament(config) {
|
|
470
|
+
if (!config.name)
|
|
471
|
+
throw new LoopError({
|
|
472
|
+
code: "CONFIG",
|
|
473
|
+
message: "tournament() requires a non-empty name"
|
|
474
|
+
});
|
|
475
|
+
if (config.n < 1)
|
|
476
|
+
throw new LoopError({ code: "CONFIG", message: "tournament() needs n >= 1" });
|
|
477
|
+
return async (parent) => {
|
|
478
|
+
const path = [...parent.path, config.name];
|
|
479
|
+
const base = parent.workspace;
|
|
480
|
+
parent.emit({
|
|
481
|
+
kind: "job:start",
|
|
482
|
+
ts: Date.now(),
|
|
483
|
+
path,
|
|
484
|
+
label: config.name
|
|
485
|
+
});
|
|
486
|
+
if (!await isRepo({ cwd: base.dir, signal: parent.signal })) {
|
|
487
|
+
const error = new LoopError({
|
|
488
|
+
code: "CONFIG",
|
|
489
|
+
message: `tournament "${config.name}" requires a git repository (cwd: ${base.dir})`
|
|
490
|
+
});
|
|
491
|
+
return { status: "fail", summary: error.message, error };
|
|
492
|
+
}
|
|
493
|
+
const limit = pLimit(config.concurrency ?? config.n);
|
|
494
|
+
const attempts = await Promise.all(
|
|
495
|
+
Array.from(
|
|
496
|
+
{ length: config.n },
|
|
497
|
+
(_, i) => limit(async () => {
|
|
498
|
+
const branch = `loops/${slug2(config.name)}-cand-${i}`;
|
|
499
|
+
const wt = await addWorktree(base.dir, {
|
|
500
|
+
branch,
|
|
501
|
+
base: "HEAD",
|
|
502
|
+
signal: parent.signal
|
|
503
|
+
});
|
|
504
|
+
const ws = { dir: wt.dir, branch };
|
|
505
|
+
try {
|
|
506
|
+
const ctx = childContext(parent, {
|
|
507
|
+
depth: parent.depth + 1,
|
|
508
|
+
path: [...path, `#${i}`],
|
|
509
|
+
workspace: ws
|
|
510
|
+
});
|
|
511
|
+
const outcome2 = await config.candidate(i)(ctx);
|
|
512
|
+
await stageAll({ cwd: wt.dir, signal: parent.signal });
|
|
513
|
+
await commit(
|
|
514
|
+
{
|
|
515
|
+
subject: `${config.name}: candidate ${i}`,
|
|
516
|
+
body: await composeCommitBody(ctx, ws)
|
|
517
|
+
},
|
|
518
|
+
{ cwd: wt.dir, signal: parent.signal }
|
|
519
|
+
);
|
|
520
|
+
const score = outcome2.status === "pass" ? await config.judge(outcome2, ctx) : -1;
|
|
521
|
+
parent.log(`${config.name} candidate ${i}: score ${score}`);
|
|
522
|
+
return { i, branch, dir: wt.dir, outcome: outcome2, score };
|
|
523
|
+
} catch (e) {
|
|
524
|
+
const error = LoopError.from(e, { code: "BODY", path });
|
|
525
|
+
return {
|
|
526
|
+
i,
|
|
527
|
+
branch,
|
|
528
|
+
dir: wt.dir,
|
|
529
|
+
outcome: { status: "fail", summary: error.message, error },
|
|
530
|
+
score: -1
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
)
|
|
535
|
+
);
|
|
536
|
+
const winner = [...attempts].filter((a) => a.outcome.status === "pass" && a.score >= 0).sort((a, b) => b.score - a.score || a.i - b.i)[0];
|
|
537
|
+
let landed = false;
|
|
538
|
+
if (winner) {
|
|
539
|
+
const merged = await mergeBranch(base.dir, winner.branch, {
|
|
540
|
+
signal: parent.signal,
|
|
541
|
+
message: `${config.name}: land candidate ${winner.i} (score ${winner.score})`
|
|
542
|
+
});
|
|
543
|
+
landed = merged.ok;
|
|
544
|
+
}
|
|
545
|
+
for (const a of attempts) {
|
|
546
|
+
await removeWorktree(base.dir, a.dir, { signal: parent.signal }).catch(
|
|
547
|
+
() => {
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
if (a !== winner || landed)
|
|
551
|
+
await deleteBranch(base.dir, a.branch, {
|
|
552
|
+
signal: parent.signal
|
|
553
|
+
}).catch(() => {
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const outcome = winner ? {
|
|
557
|
+
status: landed ? "pass" : "fail",
|
|
558
|
+
confidence: winner.outcome.confidence,
|
|
559
|
+
summary: landed ? `tournament "${config.name}": landed candidate ${winner.i} (score ${winner.score}) of ${config.n}` : `tournament "${config.name}": winner ${winner.i} failed to land`,
|
|
560
|
+
data: {
|
|
561
|
+
winner: winner.i,
|
|
562
|
+
score: winner.score,
|
|
563
|
+
scores: attempts.map((a) => ({ i: a.i, score: a.score }))
|
|
564
|
+
}
|
|
565
|
+
} : {
|
|
566
|
+
status: "fail",
|
|
567
|
+
summary: `tournament "${config.name}": no candidate passed`,
|
|
568
|
+
data: { scores: attempts.map((a) => ({ i: a.i, score: a.score })) }
|
|
569
|
+
};
|
|
570
|
+
parent.emit({ kind: "job:end", ts: Date.now(), path, label: config.name, outcome });
|
|
571
|
+
return outcome;
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/core/pr.ts
|
|
576
|
+
function resolveForge(ctx) {
|
|
577
|
+
return ctx.forge ?? new GhForge();
|
|
578
|
+
}
|
|
579
|
+
async function derive(value, ctx, last) {
|
|
580
|
+
if (value === void 0) return void 0;
|
|
581
|
+
return typeof value === "function" ? await value(
|
|
582
|
+
ctx,
|
|
583
|
+
last
|
|
584
|
+
) : value;
|
|
585
|
+
}
|
|
586
|
+
function pushJob(config = {}) {
|
|
587
|
+
return async (ctx) => {
|
|
588
|
+
const label = config.label ?? "push";
|
|
589
|
+
const path = [...ctx.path];
|
|
590
|
+
ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
|
|
591
|
+
const branch = config.branch ?? ctx.workspace.branch;
|
|
592
|
+
const res = await push({
|
|
593
|
+
cwd: ctx.workspace.dir,
|
|
594
|
+
signal: ctx.signal,
|
|
595
|
+
remote: config.remote,
|
|
596
|
+
branch,
|
|
597
|
+
setUpstream: config.setUpstream,
|
|
598
|
+
force: config.force
|
|
599
|
+
});
|
|
600
|
+
const outcome = res.ok ? { status: "pass", summary: `pushed ${branch ?? "HEAD"}` } : {
|
|
601
|
+
status: "fail",
|
|
602
|
+
summary: `push failed: ${res.output}`,
|
|
603
|
+
error: new LoopError({ code: "BODY", message: res.output })
|
|
604
|
+
};
|
|
605
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
|
|
606
|
+
return outcome;
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function pullRequestJob(config = {}) {
|
|
610
|
+
return async (ctx) => {
|
|
611
|
+
const label = config.label ?? "pull-request";
|
|
612
|
+
const path = [...ctx.path];
|
|
613
|
+
ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
|
|
614
|
+
try {
|
|
615
|
+
const branch = ctx.workspace.branch;
|
|
616
|
+
if (!branch)
|
|
617
|
+
throw new LoopError({
|
|
618
|
+
code: "CONFIG",
|
|
619
|
+
message: `pullRequestJob "${label}" needs a branch checked out (detached HEAD or non-repo)`
|
|
620
|
+
});
|
|
621
|
+
const base = config.base ?? "main";
|
|
622
|
+
const last = ctx.lastOutcome;
|
|
623
|
+
if (config.push !== false) {
|
|
624
|
+
const res = await push({
|
|
625
|
+
cwd: ctx.workspace.dir,
|
|
626
|
+
signal: ctx.signal,
|
|
627
|
+
branch,
|
|
628
|
+
...typeof config.push === "object" ? config.push : {}
|
|
629
|
+
});
|
|
630
|
+
if (!res.ok)
|
|
631
|
+
throw new LoopError({
|
|
632
|
+
code: "BODY",
|
|
633
|
+
message: `push failed: ${res.output}`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const body = await derive(config.body, ctx, last) ?? await consolidate(ctx, {
|
|
637
|
+
since: base,
|
|
638
|
+
max: config.max ?? 50,
|
|
639
|
+
model: config.model
|
|
640
|
+
});
|
|
641
|
+
const title = await derive(config.title, ctx, last) ?? last?.summary ?? branch;
|
|
642
|
+
const forge = resolveForge(ctx);
|
|
643
|
+
const fopts = { cwd: ctx.workspace.dir, signal: ctx.signal };
|
|
644
|
+
const existing = await forge.viewPr(branch, fopts);
|
|
645
|
+
let pr;
|
|
646
|
+
if (existing) {
|
|
647
|
+
await forge.editPr(existing, { body }, fopts);
|
|
648
|
+
pr = existing;
|
|
649
|
+
} else {
|
|
650
|
+
pr = await forge.createPr(
|
|
651
|
+
{ title, body, base, branch, draft: config.draft },
|
|
652
|
+
fopts
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const outcome = {
|
|
656
|
+
status: "pass",
|
|
657
|
+
summary: `${existing ? "updated" : "opened"} PR #${pr.number}`,
|
|
658
|
+
data: { pr }
|
|
659
|
+
};
|
|
660
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
|
|
661
|
+
return outcome;
|
|
662
|
+
} catch (e) {
|
|
663
|
+
const error = LoopError.from(e, {
|
|
664
|
+
code: "BODY",
|
|
665
|
+
phase: "body",
|
|
666
|
+
path: ctx.path
|
|
667
|
+
});
|
|
668
|
+
ctx.emit({
|
|
669
|
+
kind: "error",
|
|
670
|
+
ts: Date.now(),
|
|
671
|
+
path,
|
|
672
|
+
message: error.message,
|
|
673
|
+
code: error.code
|
|
674
|
+
});
|
|
675
|
+
const outcome = { status: "fail", summary: error.message, error };
|
|
676
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
|
|
677
|
+
return outcome;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function mergeJob(config = {}) {
|
|
682
|
+
return async (ctx) => {
|
|
683
|
+
const label = config.label ?? "merge";
|
|
684
|
+
const path = [...ctx.path];
|
|
685
|
+
ctx.emit({ kind: "job:start", ts: Date.now(), path, label });
|
|
686
|
+
try {
|
|
687
|
+
const branch = ctx.workspace.branch;
|
|
688
|
+
if (!branch)
|
|
689
|
+
throw new LoopError({
|
|
690
|
+
code: "CONFIG",
|
|
691
|
+
message: `mergeJob "${label}" needs a branch checked out`
|
|
692
|
+
});
|
|
693
|
+
const forge = resolveForge(ctx);
|
|
694
|
+
const fopts = { cwd: ctx.workspace.dir, signal: ctx.signal };
|
|
695
|
+
const pr = await forge.viewPr(branch, fopts);
|
|
696
|
+
if (!pr)
|
|
697
|
+
throw new LoopError({
|
|
698
|
+
code: "CONFIG",
|
|
699
|
+
message: `mergeJob "${label}": no open PR for branch "${branch}" \u2014 run pullRequestJob first`
|
|
700
|
+
});
|
|
701
|
+
if (config.when) {
|
|
702
|
+
const r = await toCondition(config.when)(ctx, ctx.lastOutcome);
|
|
703
|
+
if (!r.met) {
|
|
704
|
+
const outcome2 = {
|
|
705
|
+
status: "fail",
|
|
706
|
+
summary: `merge gate not met: ${r.reason}`,
|
|
707
|
+
data: { pr }
|
|
708
|
+
};
|
|
709
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome: outcome2 });
|
|
710
|
+
return outcome2;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const last = ctx.lastOutcome;
|
|
714
|
+
const base = config.base ?? "main";
|
|
715
|
+
const body = await derive(config.body, ctx, last) ?? await consolidate(ctx, {
|
|
716
|
+
since: base,
|
|
717
|
+
max: config.max ?? 50,
|
|
718
|
+
model: config.model
|
|
719
|
+
});
|
|
720
|
+
const subject = await derive(config.subject, ctx, last);
|
|
721
|
+
await forge.mergePr(pr, {
|
|
722
|
+
...fopts,
|
|
723
|
+
squash: config.squash,
|
|
724
|
+
auto: config.auto,
|
|
725
|
+
subject,
|
|
726
|
+
body,
|
|
727
|
+
deleteBranch: config.deleteBranch
|
|
728
|
+
});
|
|
729
|
+
const outcome = {
|
|
730
|
+
status: "pass",
|
|
731
|
+
summary: `${config.auto ? "enqueued" : "merged"} PR #${pr.number}`,
|
|
732
|
+
data: { pr }
|
|
733
|
+
};
|
|
734
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
|
|
735
|
+
return outcome;
|
|
736
|
+
} catch (e) {
|
|
737
|
+
const error = LoopError.from(e, {
|
|
738
|
+
code: "BODY",
|
|
739
|
+
phase: "body",
|
|
740
|
+
path: ctx.path
|
|
741
|
+
});
|
|
742
|
+
ctx.emit({
|
|
743
|
+
kind: "error",
|
|
744
|
+
ts: Date.now(),
|
|
745
|
+
path,
|
|
746
|
+
message: error.message,
|
|
747
|
+
code: error.code
|
|
748
|
+
});
|
|
749
|
+
const outcome = { status: "fail", summary: error.message, error };
|
|
750
|
+
ctx.emit({ kind: "job:end", ts: Date.now(), path, label, outcome });
|
|
751
|
+
return outcome;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
|
|
756
|
+
var mergeLock = pLimit(1);
|
|
757
|
+
var forkSeq = 0;
|
|
758
|
+
function isolated(job, opts = {}) {
|
|
759
|
+
const label = opts.label ?? "isolated";
|
|
760
|
+
return async (parent) => {
|
|
761
|
+
const base = parent.workspace;
|
|
762
|
+
if (!await isRepo({ cwd: base.dir, signal: parent.signal })) {
|
|
763
|
+
parent.log(
|
|
764
|
+
`isolated("${label}") requested a worktree but ${base.dir} is not a git repo; running in the shared workspace`,
|
|
765
|
+
"warn"
|
|
766
|
+
);
|
|
767
|
+
return job(parent);
|
|
768
|
+
}
|
|
769
|
+
const branch = `loops/${slug3(label)}-${forkSeq += 1}`;
|
|
770
|
+
const wt = await addWorktree(base.dir, {
|
|
771
|
+
branch,
|
|
772
|
+
base: "HEAD",
|
|
773
|
+
signal: parent.signal
|
|
774
|
+
});
|
|
775
|
+
const wtWs = { dir: wt.dir, branch };
|
|
776
|
+
try {
|
|
777
|
+
const ctx = childContext(parent, {
|
|
778
|
+
workspace: wtWs,
|
|
779
|
+
depth: parent.depth + 1,
|
|
780
|
+
path: [...parent.path, label]
|
|
781
|
+
});
|
|
782
|
+
const outcome = await job(ctx);
|
|
783
|
+
if (outcome.status === "pass") {
|
|
784
|
+
await stageAll({ cwd: wt.dir, signal: parent.signal });
|
|
785
|
+
await commit(
|
|
786
|
+
{
|
|
787
|
+
subject: `chore(${slug3(label)}): worktree changes`,
|
|
788
|
+
body: await composeCommitBody(ctx, wtWs)
|
|
789
|
+
},
|
|
790
|
+
{ cwd: wt.dir, signal: parent.signal }
|
|
791
|
+
);
|
|
792
|
+
const merged = await mergeLock(
|
|
793
|
+
() => mergeBranch(base.dir, branch, {
|
|
794
|
+
signal: parent.signal,
|
|
795
|
+
message: `merge ${branch}`
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
if (!merged.ok) {
|
|
799
|
+
if (opts.onConflict !== "synthesize") {
|
|
800
|
+
return {
|
|
801
|
+
status: "fail",
|
|
802
|
+
summary: `isolated("${label}") landed with a merge conflict; needs resolution`,
|
|
803
|
+
error: new LoopError({
|
|
804
|
+
code: "BODY",
|
|
805
|
+
message: `merge conflict landing isolated("${label}")`,
|
|
806
|
+
path: [...parent.path, label]
|
|
807
|
+
})
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
await mergeLock(
|
|
812
|
+
() => mergeSynthesis(parent, {
|
|
813
|
+
branch,
|
|
814
|
+
message: `merge: ${branch} (synthesis)`
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
} catch (e) {
|
|
818
|
+
const error = LoopError.from(e, { code: "BODY", path: [...parent.path, label] });
|
|
819
|
+
return {
|
|
820
|
+
status: "fail",
|
|
821
|
+
summary: `isolated("${label}") merge synthesis failed: ${error.message}`,
|
|
822
|
+
error
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
await deleteBranch(base.dir, branch, { signal: parent.signal }).catch(() => {
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
return outcome;
|
|
830
|
+
} finally {
|
|
831
|
+
await removeWorktree(base.dir, wt.dir, { signal: parent.signal }).catch(() => {
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/engines/mock.ts
|
|
838
|
+
var MockEngine = class {
|
|
839
|
+
constructor(responder) {
|
|
840
|
+
this.responder = responder;
|
|
841
|
+
}
|
|
842
|
+
responder;
|
|
843
|
+
name = "mock";
|
|
844
|
+
async run(req, onEvent, signal) {
|
|
845
|
+
if (signal.aborted) {
|
|
846
|
+
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
847
|
+
}
|
|
848
|
+
const raw = this.responder(req);
|
|
849
|
+
const out = typeof raw === "string" ? { text: raw } : raw;
|
|
850
|
+
const model = out.model ?? "mock";
|
|
851
|
+
const usage = out.usage ?? { inputTokens: 10, outputTokens: 5 };
|
|
852
|
+
if (out.text) onEvent({ type: "text", delta: out.text });
|
|
853
|
+
onEvent({ type: "usage", usage, model });
|
|
854
|
+
return { text: out.text, usage, model, stopReason: "end_turn" };
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
function mockVerdict(verdict, confidence, reason = "mock") {
|
|
858
|
+
return new MockEngine(() => JSON.stringify({ verdict, confidence, reason }));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/env/environment.ts
|
|
862
|
+
function isEnvironment(value) {
|
|
863
|
+
return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.up === "function";
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/env/mock.ts
|
|
867
|
+
var MockEnvironment = class {
|
|
868
|
+
constructor(opts = {}) {
|
|
869
|
+
this.opts = opts;
|
|
870
|
+
}
|
|
871
|
+
opts;
|
|
872
|
+
name = "mock-env";
|
|
873
|
+
upCount = 0;
|
|
874
|
+
downCount = 0;
|
|
875
|
+
async up(workspace) {
|
|
876
|
+
this.upCount += 1;
|
|
877
|
+
const url = typeof this.opts.url === "function" ? this.opts.url(workspace) : this.opts.url ?? `http://localhost/${workspace.branch ?? "main"}`;
|
|
878
|
+
this.opts.onUp?.(workspace);
|
|
879
|
+
const onDown = this.opts.onDown;
|
|
880
|
+
return {
|
|
881
|
+
url,
|
|
882
|
+
env: { BASE_URL: url, ...this.opts.env },
|
|
883
|
+
down: async () => {
|
|
884
|
+
this.downCount += 1;
|
|
885
|
+
onDown?.();
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// src/api.ts
|
|
892
|
+
function defineJob(job) {
|
|
893
|
+
return job;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export { MockEngine, MockEnvironment, dag, defineJob, isEnvironment, isolated, mergeJob, mergeSynthesis, mockVerdict, parallel, pullRequestJob, pushJob, sequence, tournament };
|
|
897
|
+
//# sourceMappingURL=api.js.map
|
|
898
|
+
//# sourceMappingURL=api.js.map
|