@lchase/yak 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/index.js +2771 -0
  4. package/package.json +54 -0
package/dist/index.js ADDED
@@ -0,0 +1,2771 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/artifacts.ts
7
+ import path16 from "path";
8
+
9
+ // src/engine/artifacts.ts
10
+ import { randomUUID } from "crypto";
11
+ import { mkdir, readdir, readFile, rename, writeFile } from "fs/promises";
12
+ import path from "path";
13
+ import { z } from "zod";
14
+
15
+ // src/util/hash.ts
16
+ import { createHash } from "crypto";
17
+ function sha256(input) {
18
+ return createHash("sha256").update(input).digest("hex");
19
+ }
20
+
21
+ // src/engine/artifacts.ts
22
+ async function writeFileAtomic(finalPath, content) {
23
+ const tmpPath = path.join(path.dirname(finalPath), `.${path.basename(finalPath)}.${randomUUID()}.tmp`);
24
+ await writeFile(tmpPath, content, "utf8");
25
+ await rename(tmpPath, finalPath);
26
+ }
27
+ function artifactPath(runDir, name, iteration) {
28
+ const fileName = iteration === void 0 ? `${name}.json` : `${name}.${iteration}.json`;
29
+ return path.join(runDir, "artifacts", fileName);
30
+ }
31
+ async function writeArtifact(runDir, name, value, schema, iteration) {
32
+ const parsed = schema.parse(value);
33
+ const json = JSON.stringify(parsed, null, 2);
34
+ await mkdir(path.join(runDir, "artifacts"), { recursive: true });
35
+ const filePath = artifactPath(runDir, name, iteration);
36
+ await writeFileAtomic(filePath, json);
37
+ return { name, hash: sha256(json), bytes: Buffer.byteLength(json), path: filePath };
38
+ }
39
+ async function readArtifact(runDir, name, schema, iteration) {
40
+ const json = await readFile(artifactPath(runDir, name, iteration), "utf8");
41
+ return schema.parse(JSON.parse(json));
42
+ }
43
+ async function readArtifactRaw(runDir, name, iteration) {
44
+ return readArtifact(runDir, name, z.unknown(), iteration);
45
+ }
46
+ async function readArtifactRawOrUndefined(runDir, name) {
47
+ try {
48
+ return await readArtifactRaw(runDir, name);
49
+ } catch (err) {
50
+ if (err.code === "ENOENT") return void 0;
51
+ throw err;
52
+ }
53
+ }
54
+ async function listMapItemArtifacts(runDir, produces) {
55
+ const dir = path.join(runDir, "artifacts");
56
+ const entries = await readdir(dir).catch(() => []);
57
+ const escaped = produces.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
+ const re = new RegExp(`^${escaped}\\.(\\d+)\\.json$`);
59
+ return entries.map((name) => re.exec(name)).filter((m) => m !== null).map((m) => Number(m[1])).sort((a, b) => a - b);
60
+ }
61
+ async function writeRejectedOutput(runDir, stepId, attempt, content) {
62
+ const dir = path.join(runDir, "artifacts", ".rejected");
63
+ await mkdir(dir, { recursive: true });
64
+ const filePath = path.join(dir, `${stepId}.${attempt}.txt`);
65
+ await writeFileAtomic(filePath, content);
66
+ return filePath;
67
+ }
68
+
69
+ // src/engine/run.ts
70
+ import { randomBytes as randomBytes2 } from "crypto";
71
+ import { mkdir as mkdir6, readdir as readdir2, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
72
+ import path15 from "path";
73
+
74
+ // src/ir/graph.ts
75
+ function buildProducerMap(steps) {
76
+ const producerOf = /* @__PURE__ */ new Map();
77
+ for (const step of steps) {
78
+ if (step.produces) producerOf.set(step.produces, step.id);
79
+ }
80
+ return producerOf;
81
+ }
82
+ function agentInputNames(step) {
83
+ const names = new Set(step.needs ?? []);
84
+ if (typeof step.context === "object" && "inherit" in step.context) {
85
+ for (const name of step.context.inherit) names.add(name);
86
+ }
87
+ return [...names];
88
+ }
89
+ function mapInputNames(step) {
90
+ const names = new Set(step.needs ?? []);
91
+ names.add(step.over);
92
+ return [...names];
93
+ }
94
+ function inputNamesOf(step) {
95
+ if (step.kind === "agent") return agentInputNames(step);
96
+ if (step.kind === "map") return mapInputNames(step);
97
+ return step.needs ?? [];
98
+ }
99
+ function dependenciesOf(step, producerOf) {
100
+ return inputNamesOf(step).map((need) => producerOf.get(need)).filter((id) => id !== void 0);
101
+ }
102
+ function flattenSteps(steps) {
103
+ const result = [];
104
+ for (const step of steps) {
105
+ result.push(step);
106
+ if (step.kind === "loop") result.push(...flattenSteps(step.body));
107
+ if (step.kind === "map") result.push(...flattenSteps([step.step]));
108
+ }
109
+ return result;
110
+ }
111
+ function renderMermaid(workflow) {
112
+ const producerOf = buildProducerMap(flattenSteps(workflow.steps));
113
+ const lines = ["flowchart TD"];
114
+ renderStepList(workflow.steps, producerOf, lines);
115
+ return lines.join("\n");
116
+ }
117
+ function renderStepList(steps, producerOf, lines) {
118
+ for (const step of steps) {
119
+ const nested = step.kind === "loop" ? step.body : step.kind === "map" ? [step.step] : void 0;
120
+ if (nested) {
121
+ lines.push(` subgraph ${step.id}`);
122
+ renderStepList(nested, producerOf, lines);
123
+ lines.push(" end");
124
+ } else {
125
+ lines.push(` ${step.id}[${step.id}]`);
126
+ }
127
+ for (const name of inputNamesOf(step)) {
128
+ const producer = producerOf.get(name);
129
+ if (producer) lines.push(` ${producer} -->|${name}| ${step.id}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ // src/ir/load.ts
135
+ import { readFile as readFile2 } from "fs/promises";
136
+ import { parse } from "yaml";
137
+ async function loadWorkflowYaml(filePath) {
138
+ const contents = await readFile2(filePath, "utf8");
139
+ return parse(contents);
140
+ }
141
+
142
+ // src/ir/normalize.ts
143
+ import { z as z2 } from "zod";
144
+ var rawCommandSchema = z2.object({
145
+ run: z2.string(),
146
+ cwd: z2.string().optional(),
147
+ failOn: z2.enum(["exitCode", "never"]).optional(),
148
+ capture: z2.array(z2.enum(["stdout", "stderr", "exitCode"])).optional()
149
+ });
150
+ var rawTransformSchema = z2.object({
151
+ fn: z2.string()
152
+ });
153
+ var rawAgentContextSchema = z2.union([
154
+ z2.literal("fresh"),
155
+ z2.object({ inherit: z2.array(z2.string()) }),
156
+ z2.object({ session: z2.string() })
157
+ ]);
158
+ var rawSchemaSpecSchema = z2.union([z2.string(), z2.object({ inline: z2.record(z2.unknown()) })]);
159
+ var rawAgentSchema = z2.object({
160
+ prompt: z2.union([z2.object({ file: z2.string() }), z2.object({ inline: z2.string() })]),
161
+ schema: rawSchemaSpecSchema.optional(),
162
+ context: rawAgentContextSchema.optional(),
163
+ tools: z2.array(z2.string()).optional(),
164
+ model: z2.string().optional(),
165
+ repairAttempts: z2.number().optional()
166
+ });
167
+ var rawExprSchema = z2.union([z2.string(), z2.object({ fn: z2.string() })]);
168
+ var rawGateSchema = z2.object({
169
+ schema: rawSchemaSpecSchema,
170
+ render: z2.union([z2.object({ file: z2.string() }), z2.object({ inline: z2.string() })])
171
+ });
172
+ var rawBudgetSchema = z2.object({
173
+ maxIterations: z2.number(),
174
+ maxTokens: z2.number().optional(),
175
+ maxUsd: z2.number().optional(),
176
+ noProgress: z2.object({ signal: rawExprSchema, rounds: z2.number() }).optional()
177
+ });
178
+ var rawStepSchema = z2.lazy(
179
+ () => z2.object({
180
+ id: z2.string(),
181
+ needs: z2.array(z2.string()).optional(),
182
+ produces: z2.string().optional(),
183
+ cache: z2.enum(["strict", "loose"]).optional(),
184
+ skipIf: rawExprSchema.optional(),
185
+ finally: z2.boolean().optional(),
186
+ command: rawCommandSchema.optional(),
187
+ transform: rawTransformSchema.optional(),
188
+ agent: rawAgentSchema.optional(),
189
+ gate: rawGateSchema.optional(),
190
+ loop: z2.object({
191
+ until: rawExprSchema,
192
+ budget: rawBudgetSchema,
193
+ onExhausted: z2.enum(["suspend", "fail", "continue"]).optional(),
194
+ freshContext: z2.boolean().optional(),
195
+ body: z2.array(rawStepSchema)
196
+ }).optional(),
197
+ map: z2.object({
198
+ over: z2.string(),
199
+ step: rawStepSchema,
200
+ concurrency: z2.number().optional(),
201
+ isolation: z2.enum(["worktree", "none"]).optional(),
202
+ onItemFailure: z2.enum(["skip", "fail", "retry"]).optional()
203
+ }).optional()
204
+ })
205
+ );
206
+ var rawWorkflowSchema = z2.object({
207
+ name: z2.string(),
208
+ version: z2.string(),
209
+ inputSchema: z2.string().optional(),
210
+ steps: z2.array(rawStepSchema)
211
+ });
212
+ function normalizeStep(raw) {
213
+ if (raw.command) {
214
+ const step = {
215
+ id: raw.id,
216
+ needs: raw.needs ?? [],
217
+ cache: raw.cache ?? "strict",
218
+ kind: "command",
219
+ run: raw.command.run,
220
+ failOn: raw.command.failOn ?? "exitCode",
221
+ capture: raw.command.capture ?? ["stdout", "stderr", "exitCode"],
222
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {},
223
+ ...raw.command.cwd !== void 0 ? { cwd: raw.command.cwd } : {},
224
+ ...raw.skipIf !== void 0 ? { skipIf: raw.skipIf } : {},
225
+ ...raw.finally !== void 0 ? { finally: raw.finally } : {}
226
+ };
227
+ return step;
228
+ }
229
+ if (raw.transform) {
230
+ const step = {
231
+ id: raw.id,
232
+ needs: raw.needs ?? [],
233
+ cache: raw.cache ?? "strict",
234
+ kind: "transform",
235
+ fn: raw.transform.fn,
236
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {},
237
+ ...raw.skipIf !== void 0 ? { skipIf: raw.skipIf } : {},
238
+ ...raw.finally !== void 0 ? { finally: raw.finally } : {}
239
+ };
240
+ return step;
241
+ }
242
+ if (raw.agent) {
243
+ const step = {
244
+ id: raw.id,
245
+ needs: raw.needs ?? [],
246
+ cache: raw.cache ?? "strict",
247
+ kind: "agent",
248
+ prompt: raw.agent.prompt,
249
+ ...raw.agent.schema !== void 0 ? { schema: raw.agent.schema } : {},
250
+ ...raw.agent.context !== void 0 ? { context: raw.agent.context } : {},
251
+ ...raw.agent.tools !== void 0 ? { tools: raw.agent.tools } : {},
252
+ ...raw.agent.model !== void 0 ? { model: raw.agent.model } : {},
253
+ ...raw.agent.repairAttempts !== void 0 ? { repairAttempts: raw.agent.repairAttempts } : {},
254
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {},
255
+ ...raw.skipIf !== void 0 ? { skipIf: raw.skipIf } : {},
256
+ ...raw.finally !== void 0 ? { finally: raw.finally } : {}
257
+ };
258
+ return step;
259
+ }
260
+ if (raw.gate) {
261
+ const step = {
262
+ id: raw.id,
263
+ needs: raw.needs ?? [],
264
+ cache: raw.cache ?? "strict",
265
+ kind: "gate",
266
+ schema: raw.gate.schema,
267
+ render: raw.gate.render,
268
+ ...raw.skipIf !== void 0 ? { skipIf: raw.skipIf } : {},
269
+ ...raw.finally !== void 0 ? { finally: raw.finally } : {},
270
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {}
271
+ };
272
+ return step;
273
+ }
274
+ if (raw.loop) {
275
+ const step = {
276
+ id: raw.id,
277
+ needs: raw.needs ?? [],
278
+ cache: raw.cache ?? "strict",
279
+ kind: "loop",
280
+ body: raw.loop.body.map(normalizeStep),
281
+ until: raw.loop.until,
282
+ budget: raw.loop.budget,
283
+ onExhausted: raw.loop.onExhausted ?? "suspend",
284
+ freshContext: raw.loop.freshContext ?? true,
285
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {}
286
+ };
287
+ return step;
288
+ }
289
+ if (raw.map) {
290
+ const step = {
291
+ id: raw.id,
292
+ needs: raw.needs ?? [],
293
+ cache: raw.cache ?? "strict",
294
+ kind: "map",
295
+ over: raw.map.over,
296
+ step: normalizeStep(raw.map.step),
297
+ concurrency: raw.map.concurrency ?? 4,
298
+ // Ticket 07: the M3 loader default is 'none', not 'worktree' — real
299
+ // worktree isolation is M5's job, and requesting it explicitly is a
300
+ // load-time error (checked in ir/validate.ts), never a silent no-op.
301
+ isolation: raw.map.isolation ?? "none",
302
+ onItemFailure: raw.map.onItemFailure ?? "skip",
303
+ ...raw.produces !== void 0 ? { produces: raw.produces } : {}
304
+ };
305
+ return step;
306
+ }
307
+ throw new Error(
308
+ `step "${raw.id}": only "command", "transform", "agent", "gate", "loop", and "map" steps are supported`
309
+ );
310
+ }
311
+ function normalizeWorkflow(raw) {
312
+ const parsed = rawWorkflowSchema.parse(raw);
313
+ return {
314
+ name: parsed.name,
315
+ version: parsed.version,
316
+ ...parsed.inputSchema !== void 0 ? { inputSchema: parsed.inputSchema } : {},
317
+ steps: parsed.steps.map(normalizeStep)
318
+ };
319
+ }
320
+
321
+ // src/ir/validate.ts
322
+ import { execFile } from "child_process";
323
+ import { readFile as readFile3 } from "fs/promises";
324
+ import path3 from "path";
325
+ import { promisify } from "util";
326
+
327
+ // src/expr/template.ts
328
+ import jexl from "jexl";
329
+ var PLACEHOLDER_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
330
+ function extractTemplateRoots(template) {
331
+ const roots = /* @__PURE__ */ new Set();
332
+ for (const match of template.matchAll(PLACEHOLDER_RE)) {
333
+ const expr = match[1].trim();
334
+ const root = expr.split(/[.[]/, 1)[0];
335
+ if (root) roots.add(root);
336
+ }
337
+ return [...roots];
338
+ }
339
+ async function renderTemplate(template, inputs) {
340
+ let result = "";
341
+ let lastIndex = 0;
342
+ for (const match of template.matchAll(PLACEHOLDER_RE)) {
343
+ const [full, exprRaw] = match;
344
+ const expr = exprRaw.trim();
345
+ const start = match.index;
346
+ const value = await jexl.eval(expr, inputs);
347
+ if (value === void 0) {
348
+ throw new Error(`prompt template placeholder "{{${expr}}}" resolved to undefined`);
349
+ }
350
+ result += template.slice(lastIndex, start);
351
+ result += typeof value === "string" ? value : JSON.stringify(value);
352
+ lastIndex = start + full.length;
353
+ }
354
+ result += template.slice(lastIndex);
355
+ return result;
356
+ }
357
+
358
+ // src/ir/schema-resolve.ts
359
+ import path2 from "path";
360
+ import { pathToFileURL } from "url";
361
+ import { Ajv } from "ajv";
362
+ import { ZodBoolean, ZodDefault, ZodEnum, ZodNumber, ZodObject, ZodOptional, ZodString, ZodType } from "zod";
363
+ import { zodToJsonSchema } from "zod-to-json-schema";
364
+ async function resolveZodSchema(name, cwd) {
365
+ const modulePath = path2.resolve(cwd, ".yak/schemas.ts");
366
+ const mod = await import(pathToFileURL(modulePath).href);
367
+ const schema = mod[name];
368
+ if (!(schema instanceof ZodType)) {
369
+ throw new Error(`schema "${name}" not found in .yak/schemas.ts`);
370
+ }
371
+ return schema;
372
+ }
373
+ function formatZodError(error) {
374
+ return error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n");
375
+ }
376
+ var ZodResolvedSchema = class {
377
+ constructor(schema) {
378
+ this.schema = schema;
379
+ }
380
+ schema;
381
+ toJsonSchema(name) {
382
+ return name === void 0 ? zodToJsonSchema(this.schema) : zodToJsonSchema(this.schema, name);
383
+ }
384
+ safeParse(value) {
385
+ const parsed = this.schema.safeParse(value);
386
+ if (parsed.success) return { success: true, data: parsed.data };
387
+ return { success: false, errorSummary: formatZodError(parsed.error) };
388
+ }
389
+ parseDefaults() {
390
+ return this.safeParse({});
391
+ }
392
+ isFlatAnswerObject() {
393
+ if (!(this.schema instanceof ZodObject)) {
394
+ return { ok: false, reason: "must be a top-level object" };
395
+ }
396
+ for (const [field, fieldSchemaRaw] of Object.entries(this.schema.shape)) {
397
+ let fieldSchema = fieldSchemaRaw;
398
+ while (fieldSchema instanceof ZodOptional || fieldSchema instanceof ZodDefault) {
399
+ fieldSchema = fieldSchema instanceof ZodOptional ? fieldSchema.unwrap() : fieldSchema._def.innerType;
400
+ }
401
+ const isScalar = fieldSchema instanceof ZodString || fieldSchema instanceof ZodNumber || fieldSchema instanceof ZodBoolean;
402
+ if (!isScalar && !(fieldSchema instanceof ZodEnum)) {
403
+ return { ok: false, reason: `field "${field}" is not a flat scalar/enum type` };
404
+ }
405
+ }
406
+ return { ok: true };
407
+ }
408
+ };
409
+ var ajv = new Ajv({ useDefaults: true, allErrors: true, strict: false });
410
+ function formatAjvErrors(errors) {
411
+ return errors.map((err) => `${err.instancePath.replace(/^\//, "").replace(/\//g, ".")}: ${err.message}`).join("\n");
412
+ }
413
+ var JsonResolvedSchema = class {
414
+ constructor(schema) {
415
+ this.schema = schema;
416
+ this.validateFn = ajv.compile(schema);
417
+ }
418
+ schema;
419
+ validateFn;
420
+ toJsonSchema() {
421
+ return this.schema;
422
+ }
423
+ safeParse(value) {
424
+ const data = value !== null && typeof value === "object" ? structuredClone(value) : value;
425
+ const valid = this.validateFn(data);
426
+ if (valid) return { success: true, data };
427
+ return { success: false, errorSummary: formatAjvErrors(this.validateFn.errors ?? []) };
428
+ }
429
+ parseDefaults() {
430
+ return this.safeParse({});
431
+ }
432
+ isFlatAnswerObject() {
433
+ const schema = this.schema;
434
+ if (schema.type !== "object" || typeof schema.properties !== "object" || schema.properties === null) {
435
+ return { ok: false, reason: "must be a top-level object" };
436
+ }
437
+ for (const [field, fieldSchemaRaw] of Object.entries(schema.properties)) {
438
+ const type = fieldSchemaRaw.type;
439
+ const isScalar = type === "string" || type === "number" || type === "integer" || type === "boolean";
440
+ const isEnum = Array.isArray(fieldSchemaRaw.enum);
441
+ if (!isScalar && !isEnum) {
442
+ return { ok: false, reason: `field "${field}" is not a flat scalar/enum type` };
443
+ }
444
+ }
445
+ return { ok: true };
446
+ }
447
+ };
448
+ function wrapZodSchema(schema) {
449
+ return new ZodResolvedSchema(schema);
450
+ }
451
+ async function resolveSchemaSpec(spec, cwd) {
452
+ if (typeof spec === "string") {
453
+ return new ZodResolvedSchema(await resolveZodSchema(spec, cwd));
454
+ }
455
+ return new JsonResolvedSchema(spec.inline);
456
+ }
457
+ function describeSchemaSpec(spec) {
458
+ return typeof spec === "string" ? `"${spec}"` : "inline schema";
459
+ }
460
+
461
+ // src/ir/validate.ts
462
+ var WorkflowValidationError = class extends Error {
463
+ };
464
+ var KNOWN_KINDS = /* @__PURE__ */ new Set(["agent", "command", "transform", "gate", "map", "loop"]);
465
+ var KNOWN_AGENT_TOOLS = /* @__PURE__ */ new Set([
466
+ "Read",
467
+ "Edit",
468
+ "Write",
469
+ "Bash",
470
+ "Grep",
471
+ "Glob",
472
+ "WebFetch",
473
+ "WebSearch",
474
+ "NotebookEdit",
475
+ "TodoWrite",
476
+ "Task",
477
+ "Skill"
478
+ ]);
479
+ async function validateWorkflow(workflow, cwd = process.cwd(), runIsolation = "none") {
480
+ const flatSteps = flattenSteps(workflow.steps);
481
+ checkKnownKinds(flatSteps);
482
+ checkDuplicateIds(flatSteps);
483
+ const producerOf = buildProducerMap(workflow.steps);
484
+ checkNeedsSatisfied(workflow.steps, producerOf);
485
+ checkNoCycles(workflow.steps, producerOf);
486
+ checkExitCodeReads(workflow.steps, producerOf);
487
+ checkAgentToolNames(flatSteps);
488
+ await checkAgentSchemaKeys(flatSteps, cwd);
489
+ await checkAgentPromptPlaceholders(flatSteps, cwd);
490
+ checkSessionChainDepth(flatSteps);
491
+ checkMapIsolation(workflow.steps, runIsolation);
492
+ await checkGateSchemaKeys(flatSteps, cwd);
493
+ await checkGateRenderPlaceholders(flatSteps, cwd);
494
+ checkSandboxImage(flatSteps);
495
+ await checkDockerAvailable(flatSteps);
496
+ }
497
+ function checkSandboxImage(steps) {
498
+ for (const step of steps) {
499
+ if (step.kind !== "command" || step.sandbox !== "docker") continue;
500
+ if (!step.image) {
501
+ throw new WorkflowValidationError(
502
+ `step "${step.id}": sandbox: 'docker' requires an "image" field \u2014 yak has no default image`
503
+ );
504
+ }
505
+ }
506
+ }
507
+ var execFileAsync = promisify(execFile);
508
+ async function checkDockerAvailable(steps) {
509
+ const needsDocker = (step) => (step.kind === "command" || step.kind === "agent") && step.sandbox === "docker";
510
+ if (!steps.some(needsDocker)) return;
511
+ try {
512
+ await execFileAsync("docker", ["--version"]);
513
+ } catch {
514
+ throw new WorkflowValidationError(
515
+ `workflow uses sandbox: 'docker' on at least one step, but the "docker" binary was not found on PATH`
516
+ );
517
+ }
518
+ }
519
+ var WRITE_CAPABLE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "Bash"]);
520
+ function checkMapIsolation(steps, runIsolation) {
521
+ for (const step of steps) {
522
+ if (step.kind === "loop") checkMapIsolation(step.body, runIsolation);
523
+ if (step.kind !== "map") continue;
524
+ if (step.isolation === "worktree") {
525
+ if (runIsolation !== "worktree") {
526
+ throw new WorkflowValidationError(
527
+ `step "${step.id}": isolation: 'worktree' requires the run itself to use --isolation worktree (there's no run branch to fork an item worktree from otherwise) \u2014 use isolation: 'none' or omit isolation, or run with --isolation worktree`
528
+ );
529
+ }
530
+ } else {
531
+ const concurrency = step.concurrency ?? 4;
532
+ if (concurrency > 1 && step.step.kind === "agent") {
533
+ const writeTool = (step.step.tools ?? []).find((tool) => WRITE_CAPABLE_TOOLS.has(tool));
534
+ if (writeTool) {
535
+ throw new WorkflowValidationError(
536
+ `step "${step.id}": isolation: 'none' with concurrency ${concurrency} and item step "${step.step.id}" declaring write-capable tool "${writeTool}" \u2014 concurrent items would share one cwd and race; use concurrency: 1 or drop the write-capable tool`
537
+ );
538
+ }
539
+ }
540
+ }
541
+ checkMapIsolation([step.step], runIsolation);
542
+ }
543
+ }
544
+ function checkAgentToolNames(steps) {
545
+ for (const step of steps) {
546
+ if (step.kind !== "agent") continue;
547
+ for (const tool of step.tools ?? []) {
548
+ if (!KNOWN_AGENT_TOOLS.has(tool)) {
549
+ throw new WorkflowValidationError(
550
+ `step "${step.id}": unknown or unsupported tool "${tool}" \u2014 must be one of ${[...KNOWN_AGENT_TOOLS].join(", ")}`
551
+ );
552
+ }
553
+ }
554
+ }
555
+ }
556
+ function checkKnownKinds(steps) {
557
+ for (const step of steps) {
558
+ if (!KNOWN_KINDS.has(step.kind)) {
559
+ throw new WorkflowValidationError(`step "${step.id}": unknown step kind "${step.kind}"`);
560
+ }
561
+ }
562
+ }
563
+ function checkDuplicateIds(steps) {
564
+ const seen = /* @__PURE__ */ new Set();
565
+ for (const step of steps) {
566
+ if (seen.has(step.id)) {
567
+ throw new WorkflowValidationError(`duplicate step id "${step.id}"`);
568
+ }
569
+ seen.add(step.id);
570
+ }
571
+ }
572
+ function checkNeedsSatisfied(steps, producerOf) {
573
+ for (const step of steps) {
574
+ for (const need of step.needs ?? []) {
575
+ if (!producerOf.has(need)) {
576
+ throw new WorkflowValidationError(
577
+ `step "${step.id}": needs artifact "${need}" but no step produces it`
578
+ );
579
+ }
580
+ }
581
+ if (step.kind === "loop") {
582
+ const localProducerOf = new Map([...producerOf, ...buildProducerMap(step.body)]);
583
+ checkNeedsSatisfied(step.body, localProducerOf);
584
+ }
585
+ if (step.kind === "map") {
586
+ checkNeedsSatisfied([step.step], producerOf);
587
+ }
588
+ }
589
+ }
590
+ function checkNoCycles(steps, producerOf) {
591
+ const stepsById = new Map(steps.map((s) => [s.id, s]));
592
+ const state = /* @__PURE__ */ new Map();
593
+ function visit(step, path21) {
594
+ const mark = state.get(step.id);
595
+ if (mark === "done") return;
596
+ if (mark === "visiting") {
597
+ const cycle = [...path21, step.id].join(" -> ");
598
+ throw new WorkflowValidationError(`cycle detected: ${cycle}`);
599
+ }
600
+ state.set(step.id, "visiting");
601
+ for (const depId of dependenciesOf(step, producerOf)) {
602
+ const dep = stepsById.get(depId);
603
+ if (dep) visit(dep, [...path21, step.id]);
604
+ }
605
+ state.set(step.id, "done");
606
+ }
607
+ for (const step of steps) visit(step, []);
608
+ }
609
+ function checkExitCodeReads(steps, producerOf) {
610
+ const stepsById = new Map(steps.map((s) => [s.id, s]));
611
+ for (const step of steps) {
612
+ for (const expr of exprsIn(step)) {
613
+ for (const artifactName of exitCodeReferences(expr)) {
614
+ const producerId = producerOf.get(artifactName);
615
+ if (!producerId) continue;
616
+ const producer = stepsById.get(producerId);
617
+ if (producer?.kind === "command" && (producer.failOn ?? "exitCode") === "exitCode") {
618
+ throw new WorkflowValidationError(
619
+ `step "${step.id}" reads "${artifactName}.exitCode", but step "${producer.id}" has failOn: 'exitCode' \u2014 that step can never produce a non-zero exitCode`
620
+ );
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+ function exprsIn(step) {
627
+ const exprs = [];
628
+ if (step.skipIf) exprs.push(step.skipIf);
629
+ if (step.kind === "loop") {
630
+ exprs.push(step.until);
631
+ if (step.budget.noProgress) exprs.push(step.budget.noProgress.signal);
632
+ for (const inner of step.body) exprs.push(...exprsIn(inner));
633
+ }
634
+ if (step.kind === "map") exprs.push(...exprsIn(step.step));
635
+ return exprs;
636
+ }
637
+ function exitCodeReferences(expr) {
638
+ if (typeof expr !== "string") return [];
639
+ const matches = [...expr.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)\.exitCode\b/g)];
640
+ return matches.map((m) => m[1]);
641
+ }
642
+ async function checkAgentSchemaKeys(steps, cwd) {
643
+ for (const step of steps) {
644
+ if (step.kind !== "agent" || !step.schema) continue;
645
+ await checkSchemaSpecResolves(step.id, step.schema, cwd);
646
+ }
647
+ }
648
+ async function checkSchemaSpecResolves(stepId, spec, cwd) {
649
+ try {
650
+ await resolveSchemaSpec(spec, cwd);
651
+ } catch (err) {
652
+ throw new WorkflowValidationError(
653
+ `step "${stepId}": could not resolve schema ${describeSchemaSpec(spec)}: ${err.message}`
654
+ );
655
+ }
656
+ }
657
+ async function checkGateSchemaKeys(steps, cwd) {
658
+ for (const step of steps) {
659
+ if (step.kind !== "gate") continue;
660
+ await checkSchemaSpecResolves(step.id, step.schema, cwd);
661
+ const schema = await resolveSchemaSpec(step.schema, cwd);
662
+ if (step.skipIf !== void 0) {
663
+ const defaulted = schema.parseDefaults();
664
+ if (!defaulted.success) {
665
+ throw new WorkflowValidationError(
666
+ `step "${step.id}": has skipIf but schema ${describeSchemaSpec(step.schema)} doesn't default every field (parsing {} failed: ${defaulted.errorSummary}) \u2014 every field needs a default so a skip can synthesize the answer artifact`
667
+ );
668
+ }
669
+ }
670
+ checkFlatAnswerSchema(step, schema);
671
+ }
672
+ }
673
+ function checkFlatAnswerSchema(step, schema) {
674
+ const check = schema.isFlatAnswerObject();
675
+ if (!check.ok) {
676
+ throw new WorkflowValidationError(
677
+ `step "${step.id}": schema ${describeSchemaSpec(step.schema)} ${check.reason} \u2014 --interactive can only render string/number/boolean/enum properties`
678
+ );
679
+ }
680
+ }
681
+ async function checkGateRenderPlaceholders(steps, cwd) {
682
+ for (const step of steps) {
683
+ if (step.kind !== "gate") continue;
684
+ const text = "file" in step.render ? await readFile3(path3.resolve(cwd, step.render.file), "utf8") : step.render.inline;
685
+ const allowed = new Set(step.needs ?? []);
686
+ for (const root of extractTemplateRoots(text)) {
687
+ if (!allowed.has(root)) {
688
+ throw new WorkflowValidationError(
689
+ `step "${step.id}": render references "{{${root}}}" but "${root}" is not in needs`
690
+ );
691
+ }
692
+ }
693
+ }
694
+ }
695
+ async function checkAgentPromptPlaceholders(steps, cwd) {
696
+ for (const step of steps) {
697
+ if (step.kind !== "agent") continue;
698
+ const text = "file" in step.prompt ? await readFile3(path3.resolve(cwd, step.prompt.file), "utf8") : step.prompt.inline;
699
+ const allowed = new Set(agentInputNames(step));
700
+ for (const root of extractTemplateRoots(text)) {
701
+ if (!allowed.has(root)) {
702
+ throw new WorkflowValidationError(
703
+ `step "${step.id}": prompt references "{{${root}}}" but "${root}" is not in needs or context.inherit`
704
+ );
705
+ }
706
+ }
707
+ }
708
+ }
709
+ function checkSessionChainDepth(steps) {
710
+ const stepsById = new Map(steps.map((s) => [s.id, s]));
711
+ function sessionParentOf(step) {
712
+ if (step.kind !== "agent") return void 0;
713
+ if (typeof step.context !== "object" || !("session" in step.context)) return void 0;
714
+ return step.context.session;
715
+ }
716
+ for (const step of steps) {
717
+ if (step.kind !== "agent") continue;
718
+ let depth = 0;
719
+ let current = sessionParentOf(step);
720
+ const seen = /* @__PURE__ */ new Set([step.id]);
721
+ while (current !== void 0 && !seen.has(current)) {
722
+ seen.add(current);
723
+ depth += 1;
724
+ const parentStep = stepsById.get(current);
725
+ current = parentStep ? sessionParentOf(parentStep) : void 0;
726
+ }
727
+ if (depth > 3) {
728
+ console.warn(
729
+ `step "${step.id}": context.session chain exceeds depth 3 (depth ${depth}) \u2014 spec \xA73.4 discourages long resume chains`
730
+ );
731
+ }
732
+ }
733
+ }
734
+
735
+ // src/steps/gate.ts
736
+ import { readFile as readFile6 } from "fs/promises";
737
+ import path7 from "path";
738
+ import { z as z4 } from "zod";
739
+
740
+ // src/engine/journal.ts
741
+ import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
742
+ import path4 from "path";
743
+ function journalPath(runDir) {
744
+ return path4.join(runDir, "journal.jsonl");
745
+ }
746
+ async function appendJournalEvent(runDir, runId, event) {
747
+ const envelope = { ...event, at: (/* @__PURE__ */ new Date()).toISOString(), runId };
748
+ await mkdir2(runDir, { recursive: true });
749
+ await appendFile(journalPath(runDir), `${JSON.stringify(envelope)}
750
+ `, "utf8");
751
+ }
752
+ async function readJournal(runDir) {
753
+ const contents = await readFile4(journalPath(runDir), "utf8").catch(() => "");
754
+ return contents.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
755
+ }
756
+ function completedStepsFromJournal(events) {
757
+ const started = /* @__PURE__ */ new Map();
758
+ const completed = /* @__PURE__ */ new Map();
759
+ for (const event of events) {
760
+ if (event.t === "step.started") {
761
+ started.set(event.stepId, { semanticKey: event.semanticKey, definitionKey: event.definitionKey });
762
+ completed.delete(event.stepId);
763
+ } else if (event.t === "step.completed") {
764
+ const info = started.get(event.stepId);
765
+ if (info) {
766
+ completed.set(event.stepId, {
767
+ stepId: event.stepId,
768
+ semanticKey: info.semanticKey,
769
+ definitionKey: info.definitionKey,
770
+ ...event.artifact !== void 0 ? { artifact: event.artifact } : {},
771
+ ...event.artifactHash !== void 0 ? { artifactHash: event.artifactHash } : {}
772
+ });
773
+ }
774
+ } else if (event.t === "step.failed") {
775
+ completed.delete(event.stepId);
776
+ }
777
+ }
778
+ return completed;
779
+ }
780
+
781
+ // src/engine/suspend.ts
782
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
783
+ import path5 from "path";
784
+ import { z as z3 } from "zod";
785
+ var LoopExhaustionAnswerSchema = z3.object({
786
+ action: z3.enum(["continue", "abort"]),
787
+ addIterations: z3.number().optional()
788
+ });
789
+ function requestPath(runDir, stepId) {
790
+ return path5.join(runDir, "pending", `${stepId}.request.json`);
791
+ }
792
+ function answerPath(runDir, stepId) {
793
+ return path5.join(runDir, "pending", `${stepId}.answer.json`);
794
+ }
795
+ async function writeRequest(runDir, runId, request) {
796
+ await mkdir3(path5.join(runDir, "pending"), { recursive: true });
797
+ await writeFile2(requestPath(runDir, request.stepId), JSON.stringify(request, null, 2), "utf8");
798
+ await appendJournalEvent(runDir, runId, {
799
+ t: "gate.opened",
800
+ stepId: request.stepId,
801
+ requestPath: requestPath(runDir, request.stepId)
802
+ });
803
+ }
804
+ async function writeGateRequest(runDir, runId, step, rendered, answerSchema) {
805
+ await writeRequest(runDir, runId, {
806
+ kind: "gate",
807
+ stepId: step.id,
808
+ runId,
809
+ rendered,
810
+ answerSchema,
811
+ context: { artifacts: step.needs ?? [] },
812
+ openedAt: (/* @__PURE__ */ new Date()).toISOString()
813
+ });
814
+ }
815
+ async function writeLoopExhaustedRequest(runDir, runId, stepId, iteration, tripped) {
816
+ await writeRequest(runDir, runId, {
817
+ kind: "loop-exhausted",
818
+ stepId,
819
+ runId,
820
+ tripped,
821
+ iteration,
822
+ answerSchema: loopExhaustionJsonSchema(),
823
+ openedAt: (/* @__PURE__ */ new Date()).toISOString()
824
+ });
825
+ }
826
+ function loopExhaustionJsonSchema() {
827
+ return {
828
+ type: "object",
829
+ properties: {
830
+ action: { type: "string", enum: ["continue", "abort"] },
831
+ addIterations: { type: "number" }
832
+ },
833
+ required: ["action"]
834
+ };
835
+ }
836
+ async function readPendingRequest(runDir, stepId) {
837
+ const raw = await readFile5(requestPath(runDir, stepId), "utf8").catch(() => void 0);
838
+ return raw === void 0 ? void 0 : JSON.parse(raw);
839
+ }
840
+ async function readAnswerRaw(runDir, stepId) {
841
+ const raw = await readFile5(answerPath(runDir, stepId), "utf8").catch(() => void 0);
842
+ return raw === void 0 ? void 0 : JSON.parse(raw);
843
+ }
844
+ async function writeAnswer(runDir, stepId, answer) {
845
+ await mkdir3(path5.join(runDir, "pending"), { recursive: true });
846
+ await writeFile2(answerPath(runDir, stepId), JSON.stringify(answer, null, 2), "utf8");
847
+ }
848
+ function openRequestStepIds(events) {
849
+ const open = /* @__PURE__ */ new Map();
850
+ for (const event of events) {
851
+ if (event.t === "gate.opened") open.set(event.stepId, true);
852
+ else if (event.t === "gate.answered") open.set(event.stepId, false);
853
+ }
854
+ return [...open.entries()].filter(([, isOpen]) => isOpen).map(([stepId]) => stepId);
855
+ }
856
+ async function resolveAnswer(runDir, stepId, gateAnswerSchema) {
857
+ const request = await readPendingRequest(runDir, stepId);
858
+ if (!request) throw new Error(`no pending request found for step "${stepId}"`);
859
+ const answerRaw = await readAnswerRaw(runDir, stepId);
860
+ if (answerRaw === void 0) return { status: "missing", stepId };
861
+ const schema = request.kind === "loop-exhausted" ? wrapZodSchema(LoopExhaustionAnswerSchema) : await gateAnswerSchema(request);
862
+ const parsed = schema.safeParse(answerRaw);
863
+ if (!parsed.success) return { status: "invalid", stepId, errors: parsed.errorSummary ?? "" };
864
+ return { status: "ok", stepId, request, answer: parsed.data };
865
+ }
866
+
867
+ // src/expr/eval.ts
868
+ import path6 from "path";
869
+ import { pathToFileURL as pathToFileURL2 } from "url";
870
+ import jexl2 from "jexl";
871
+ async function evalExpr(expr, context, cwd) {
872
+ if (typeof expr === "string") {
873
+ return jexl2.eval(expr, context);
874
+ }
875
+ const modulePath = path6.resolve(cwd, ".yak/predicates.ts");
876
+ const mod = await import(pathToFileURL2(modulePath).href);
877
+ const impl = mod[expr.fn];
878
+ if (typeof impl !== "function") {
879
+ throw new Error(`predicate "${expr.fn}" not found in .yak/predicates.ts`);
880
+ }
881
+ return impl(context);
882
+ }
883
+
884
+ // src/steps/gate.ts
885
+ async function renderGate(step, inputs, cwd) {
886
+ const text = "file" in step.render ? await readFile6(path7.resolve(cwd, step.render.file), "utf8") : step.render.inline;
887
+ return renderTemplate(text, inputs);
888
+ }
889
+ async function completeGate(step, ctx, answer, opts) {
890
+ let artifactName;
891
+ let artifactHash;
892
+ if (step.produces) {
893
+ const schema = await resolveSchemaSpec(step.schema, ctx.cwd);
894
+ const parsed = schema.safeParse(answer);
895
+ if (!parsed.success) {
896
+ throw new Error(`gate step "${step.id}": answer failed schema validation: ${parsed.errorSummary}`);
897
+ }
898
+ const written = await writeArtifact(ctx.runDir, step.produces, parsed.data, z4.unknown());
899
+ artifactName = written.name;
900
+ artifactHash = written.hash;
901
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
902
+ t: "artifact.written",
903
+ name: written.name,
904
+ hash: written.hash,
905
+ bytes: written.bytes
906
+ });
907
+ }
908
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
909
+ t: "step.completed",
910
+ stepId: step.id,
911
+ ...artifactName ? { artifact: artifactName, artifactHash } : {},
912
+ cached: false
913
+ });
914
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
915
+ t: "gate.answered",
916
+ stepId: step.id,
917
+ ...opts.skipped ? { skipped: true } : {}
918
+ });
919
+ }
920
+ async function runGateStep(step, inputs, ctx) {
921
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
922
+ t: "step.started",
923
+ stepId: step.id,
924
+ semanticKey: "",
925
+ definitionKey: ""
926
+ });
927
+ const schema = await resolveSchemaSpec(step.schema, ctx.cwd);
928
+ const skip = step.skipIf ? Boolean(await evalExpr(step.skipIf, inputs, ctx.cwd)) : false;
929
+ if (skip) {
930
+ await appendJournalEvent(ctx.runDir, ctx.runId, { t: "gate.opened", stepId: step.id, requestPath: "" });
931
+ const defaulted = schema.parseDefaults();
932
+ if (!defaulted.success) {
933
+ throw new Error(
934
+ `gate step "${step.id}": skipIf true but schema doesn't default every field: ${defaulted.errorSummary}`
935
+ );
936
+ }
937
+ await completeGate(step, ctx, defaulted.data, { skipped: true });
938
+ return "ok";
939
+ }
940
+ const rendered = await renderGate(step, inputs, ctx.cwd);
941
+ const answerSchema = schema.toJsonSchema();
942
+ await writeGateRequest(ctx.runDir, ctx.runId, step, rendered, answerSchema);
943
+ return "suspended";
944
+ }
945
+
946
+ // src/util/git.ts
947
+ import { execFile as execFile2 } from "child_process";
948
+ import { promisify as promisify2 } from "util";
949
+ var execFileAsync2 = promisify2(execFile2);
950
+ var WorktreeCreationError = class extends Error {
951
+ };
952
+ var ENGINE_AUTHOR = ["-c", "user.name=yak", "-c", "user.email=engine@yak.local"];
953
+ async function commitAllIfDirty(repoRoot, message) {
954
+ const { stdout } = await execFileAsync2("git", ["status", "--porcelain"], { cwd: repoRoot });
955
+ if (stdout.trim() === "") return;
956
+ await execFileAsync2("git", ["add", "-A"], { cwd: repoRoot });
957
+ await execFileAsync2("git", [...ENGINE_AUTHOR, "commit", "-m", message], { cwd: repoRoot });
958
+ }
959
+ var WORKTREE_ADD_RETRIES = 5;
960
+ var WORKTREE_ADD_RETRY_DELAY_MS = 50;
961
+ function isLockContention(err) {
962
+ const message = err instanceof Error ? err.message : String(err);
963
+ return message.includes("index.lock") || message.includes(".git/worktrees") && message.includes("lock");
964
+ }
965
+ async function createWorktree(repoRoot, branchName, baseRef, worktreePath) {
966
+ for (let attempt = 1; attempt <= WORKTREE_ADD_RETRIES; attempt++) {
967
+ try {
968
+ await execFileAsync2("git", ["worktree", "add", "-b", branchName, worktreePath, baseRef], { cwd: repoRoot });
969
+ return;
970
+ } catch (err) {
971
+ if (attempt < WORKTREE_ADD_RETRIES && isLockContention(err)) {
972
+ await new Promise((resolve) => setTimeout(resolve, WORKTREE_ADD_RETRY_DELAY_MS));
973
+ continue;
974
+ }
975
+ const message = err instanceof Error ? err.message : String(err);
976
+ throw new WorktreeCreationError(
977
+ `failed to create worktree at "${worktreePath}" on branch "${branchName}" from "${baseRef}": ${message}`
978
+ );
979
+ }
980
+ }
981
+ }
982
+
983
+ // src/engine/scheduler.ts
984
+ import path14 from "path";
985
+ import pLimit2 from "p-limit";
986
+ import { z as z8 } from "zod";
987
+
988
+ // src/adapters/claude-code.ts
989
+ import { spawn } from "child_process";
990
+ import { mkdir as mkdir4, appendFile as appendFile2 } from "fs/promises";
991
+ import path9 from "path";
992
+ import {
993
+ query
994
+ } from "@anthropic-ai/claude-agent-sdk";
995
+
996
+ // src/steps/agent.ts
997
+ import { readFile as readFile7 } from "fs/promises";
998
+ import path8 from "path";
999
+ async function buildPrompt(step, inputs, cwd) {
1000
+ const text = "file" in step.prompt ? await readFile7(path8.resolve(cwd, step.prompt.file), "utf8") : step.prompt.inline;
1001
+ return renderTemplate(text, inputs);
1002
+ }
1003
+ async function callAgentOnce(step, inputs, ctx, promptSuffix) {
1004
+ const prompt = await buildPrompt(step, inputs, ctx.cwd) + (promptSuffix ?? "");
1005
+ const schema = step.schema ? await resolveSchemaSpec(step.schema, ctx.cwd) : void 0;
1006
+ const jsonSchema = schema ? schema.toJsonSchema() : void 0;
1007
+ const response = await ctx.adapter.run({
1008
+ prompt,
1009
+ tools: step.tools ?? [],
1010
+ cwd: ctx.cwd,
1011
+ model: step.model,
1012
+ schema: jsonSchema,
1013
+ sessionId: ctx.sessionId,
1014
+ signal: new AbortController().signal
1015
+ });
1016
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1017
+ t: "budget.consumed",
1018
+ stepId: step.id,
1019
+ tokens: response.tokens.input + response.tokens.output
1020
+ });
1021
+ return { output: response.output, sessionId: response.sessionId };
1022
+ }
1023
+ var AgentStepFailedError = class extends Error {
1024
+ failure;
1025
+ constructor(failure) {
1026
+ super(failure.detail);
1027
+ this.failure = failure;
1028
+ }
1029
+ };
1030
+ function repairSuffix(jsonSchema, errorSummary) {
1031
+ return `
1032
+
1033
+ ---
1034
+ Your previous output did not match the required schema.
1035
+ Schema:
1036
+ ${JSON.stringify(jsonSchema)}
1037
+
1038
+ Validation errors:
1039
+ ${errorSummary}
1040
+
1041
+ Return output that satisfies the schema.`;
1042
+ }
1043
+ async function runAgentStep(step, inputs, ctx, onSessionId) {
1044
+ const schema = step.schema ? await resolveSchemaSpec(step.schema, ctx.cwd) : void 0;
1045
+ const maxAttempts = (step.repairAttempts ?? 2) + 1;
1046
+ let sessionId = ctx.sessionId;
1047
+ let suffix;
1048
+ let lastOutput;
1049
+ let lastErrorSummary = "";
1050
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1051
+ const call = await callAgentOnce(step, inputs, { ...ctx, sessionId }, suffix);
1052
+ sessionId = call.sessionId;
1053
+ onSessionId?.(sessionId);
1054
+ lastOutput = call.output;
1055
+ if (!schema) return call.output;
1056
+ const parsed = schema.safeParse(call.output);
1057
+ if (parsed.success) return parsed.data;
1058
+ lastErrorSummary = parsed.errorSummary ?? "";
1059
+ suffix = repairSuffix(schema.toJsonSchema(), lastErrorSummary);
1060
+ }
1061
+ await writeRejectedOutput(ctx.runDir, step.id, maxAttempts, JSON.stringify(lastOutput, null, 2));
1062
+ throw new AgentStepFailedError({
1063
+ reason: "schema-invalid",
1064
+ detail: lastErrorSummary,
1065
+ recoverable: false
1066
+ });
1067
+ }
1068
+
1069
+ // src/adapters/sandbox-network.ts
1070
+ import { randomBytes } from "crypto";
1071
+ import { execFile as execFile3 } from "child_process";
1072
+ import { promisify as promisify3 } from "util";
1073
+ var execFileAsync3 = promisify3(execFile3);
1074
+ var DEFAULT_ALLOWED_DOMAINS = ["api.anthropic.com"];
1075
+ var PROXY_IMAGE = "yak-agent-sandbox-proxy:latest";
1076
+ var PROXY_PORT = 3128;
1077
+ function randomSuffix() {
1078
+ return randomBytes(4).toString("hex");
1079
+ }
1080
+ function sandboxNetworkName(stepId) {
1081
+ return `yak-agent-${stepId.replace(/[^a-zA-Z0-9_.-]/g, "-")}-${randomSuffix()}`;
1082
+ }
1083
+ function proxyContainerName(networkName) {
1084
+ return `${networkName}-proxy`;
1085
+ }
1086
+ function createNetworkArgs(networkName) {
1087
+ return ["network", "create", "--internal", networkName];
1088
+ }
1089
+ function removeNetworkArgs(networkName) {
1090
+ return ["network", "rm", networkName];
1091
+ }
1092
+ function startProxyArgs(networkName, containerName, allowedDomains) {
1093
+ return [
1094
+ "run",
1095
+ "-d",
1096
+ "--rm",
1097
+ "--name",
1098
+ containerName,
1099
+ "--network",
1100
+ networkName,
1101
+ "-e",
1102
+ `ALLOWED_DOMAINS=${allowedDomains.join(",")}`,
1103
+ PROXY_IMAGE
1104
+ ];
1105
+ }
1106
+ function connectProxyToBridgeArgs(containerName) {
1107
+ return ["network", "connect", "bridge", containerName];
1108
+ }
1109
+ function removeProxyArgs(containerName) {
1110
+ return ["rm", "-f", containerName];
1111
+ }
1112
+ async function docker(args) {
1113
+ await execFileAsync3("docker", args);
1114
+ }
1115
+ async function createAgentSandboxNetwork(stepId, allowedDomains = DEFAULT_ALLOWED_DOMAINS) {
1116
+ const networkName = sandboxNetworkName(stepId);
1117
+ const containerName = proxyContainerName(networkName);
1118
+ await docker(createNetworkArgs(networkName));
1119
+ try {
1120
+ await docker(startProxyArgs(networkName, containerName, allowedDomains));
1121
+ await docker(connectProxyToBridgeArgs(containerName));
1122
+ } catch (err) {
1123
+ await docker(removeNetworkArgs(networkName)).catch(() => {
1124
+ });
1125
+ throw err;
1126
+ }
1127
+ return {
1128
+ networkName,
1129
+ proxyUrl: `http://${containerName}:${PROXY_PORT}`,
1130
+ async teardown() {
1131
+ await docker(removeProxyArgs(containerName)).catch(() => {
1132
+ });
1133
+ await docker(removeNetworkArgs(networkName)).catch(() => {
1134
+ });
1135
+ }
1136
+ };
1137
+ }
1138
+
1139
+ // src/adapters/claude-code.ts
1140
+ var AGENT_SANDBOX_IMAGE_DEFAULT = "yak-agent-sandbox:latest";
1141
+ var CONTAINER_CLI_PATH = "/app/claude-cli";
1142
+ var CONTAINER_WORKDIR = "/workspace";
1143
+ function dockerAgentRunArgs(image, cwd, networkName, proxyUrl, args, hostEnv = process.env) {
1144
+ const credentialEnv = ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"].filter((key) => hostEnv[key] !== void 0).flatMap((key) => ["-e", `${key}=${hostEnv[key]}`]);
1145
+ return [
1146
+ "run",
1147
+ "--rm",
1148
+ "-i",
1149
+ "--network",
1150
+ networkName,
1151
+ "-v",
1152
+ `${cwd}:${CONTAINER_WORKDIR}`,
1153
+ "-w",
1154
+ CONTAINER_WORKDIR,
1155
+ "-e",
1156
+ `HTTPS_PROXY=${proxyUrl}`,
1157
+ "-e",
1158
+ `HTTP_PROXY=${proxyUrl}`,
1159
+ ...credentialEnv,
1160
+ image ?? AGENT_SANDBOX_IMAGE_DEFAULT,
1161
+ CONTAINER_CLI_PATH,
1162
+ ...args
1163
+ ];
1164
+ }
1165
+ var FIXED_OPTIONS = {
1166
+ permissionMode: "bypassPermissions",
1167
+ allowDangerouslySkipPermissions: true
1168
+ };
1169
+ var FILE_MUTATING_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
1170
+ function collectFilesChanged(message, into) {
1171
+ if (message.type !== "assistant") return;
1172
+ for (const block of message.message.content) {
1173
+ if (block.type !== "tool_use") continue;
1174
+ if (!FILE_MUTATING_TOOLS.has(block.name)) continue;
1175
+ const input = block.input;
1176
+ if (input && typeof input === "object" && "file_path" in input) {
1177
+ const filePath = input.file_path;
1178
+ if (typeof filePath === "string") into.add(filePath);
1179
+ }
1180
+ }
1181
+ }
1182
+ function mapResultError(result) {
1183
+ const detail = result.errors.length > 0 ? result.errors.join("; ") : `${result.subtype} (no detail)`;
1184
+ switch (result.subtype) {
1185
+ case "error_max_budget_usd":
1186
+ return { reason: "budget-exhausted", detail, recoverable: false };
1187
+ case "error_max_turns":
1188
+ return { reason: "adapter-error", detail, recoverable: false };
1189
+ case "error_max_structured_output_retries":
1190
+ return { reason: "adapter-error", detail, recoverable: false };
1191
+ case "error_during_execution":
1192
+ default:
1193
+ return { reason: "adapter-error", detail, recoverable: true };
1194
+ }
1195
+ }
1196
+ var ClaudeCodeAdapter = class {
1197
+ constructor(runDir, stepId, sandbox) {
1198
+ this.runDir = runDir;
1199
+ this.stepId = stepId;
1200
+ this.sandbox = sandbox;
1201
+ }
1202
+ runDir;
1203
+ stepId;
1204
+ sandbox;
1205
+ id = "claude-code";
1206
+ async run(req) {
1207
+ const abortController = new AbortController();
1208
+ if (req.signal.aborted) abortController.abort(req.signal.reason);
1209
+ else req.signal.addEventListener("abort", () => abortController.abort(req.signal.reason));
1210
+ let network;
1211
+ if (this.sandbox) {
1212
+ network = await createAgentSandboxNetwork(this.stepId);
1213
+ }
1214
+ const options = {
1215
+ ...FIXED_OPTIONS,
1216
+ abortController,
1217
+ cwd: req.cwd,
1218
+ tools: req.tools,
1219
+ ...req.model !== void 0 ? { model: req.model } : {},
1220
+ ...req.systemPrompt !== void 0 ? { systemPrompt: req.systemPrompt } : {},
1221
+ // §3.4 `context: { session }` resume vs `'fresh'` (ticket 06): a
1222
+ // prior sessionId genuinely replays that session's transcript via
1223
+ // `resume`; omitting it is `'fresh'` — no explicit teardown needed,
1224
+ // and this Options object is always freshly built per call, never
1225
+ // reused/mutated across steps.
1226
+ ...req.sessionId !== void 0 ? { resume: req.sessionId } : {},
1227
+ ...req.schema !== void 0 ? { outputFormat: { type: "json_schema", schema: req.schema } } : {},
1228
+ // Ticket 07/08: substitutes a `docker run` wrapping the CLI child
1229
+ // process for the SDK's default local spawn. `spawnOptions.command`
1230
+ // (a host path) is deliberately dropped — see dockerAgentRunArgs's
1231
+ // doc comment — only `.args` (portable CLI flags) crosses over.
1232
+ ...network ? {
1233
+ spawnClaudeCodeProcess: (spawnOptions) => spawn(
1234
+ "docker",
1235
+ dockerAgentRunArgs(
1236
+ this.sandbox.image,
1237
+ req.cwd,
1238
+ network.networkName,
1239
+ network.proxyUrl,
1240
+ spawnOptions.args
1241
+ ),
1242
+ { signal: spawnOptions.signal }
1243
+ )
1244
+ } : {}
1245
+ };
1246
+ try {
1247
+ const sessionLogPath = path9.join(this.runDir, "sessions", `${this.stepId}.jsonl`);
1248
+ await mkdir4(path9.dirname(sessionLogPath), { recursive: true });
1249
+ const filesChanged = /* @__PURE__ */ new Set();
1250
+ let result;
1251
+ for await (const message of query({ prompt: req.prompt, options })) {
1252
+ await appendFile2(sessionLogPath, `${JSON.stringify(message)}
1253
+ `, "utf8");
1254
+ collectFilesChanged(message, filesChanged);
1255
+ if (message.type === "result") result = message;
1256
+ }
1257
+ if (!result) {
1258
+ throw new AgentStepFailedError({
1259
+ reason: "adapter-error",
1260
+ detail: `claude-code adapter for step "${this.stepId}": query() stream ended without a result message`,
1261
+ recoverable: true
1262
+ });
1263
+ }
1264
+ if (result.permission_denials.length > 0) {
1265
+ throw new AgentStepFailedError({
1266
+ reason: "tool-denied",
1267
+ detail: `tool call(s) denied: ${result.permission_denials.map((d) => d.tool_name).join(", ")}`,
1268
+ recoverable: false
1269
+ });
1270
+ }
1271
+ if (result.subtype !== "success") {
1272
+ throw new AgentStepFailedError(mapResultError(result));
1273
+ }
1274
+ return {
1275
+ output: result.structured_output ?? result.result,
1276
+ sessionId: result.session_id,
1277
+ tokens: { input: result.usage.input_tokens, output: result.usage.output_tokens },
1278
+ filesChanged: [...filesChanged].sort(),
1279
+ stopReason: "complete"
1280
+ };
1281
+ } finally {
1282
+ await network?.teardown();
1283
+ }
1284
+ }
1285
+ };
1286
+
1287
+ // src/adapters/mock.ts
1288
+ import { readFileSync } from "fs";
1289
+ import path10 from "path";
1290
+ var MockAdapter = class {
1291
+ /** Ticket 08: `iteration` (1-based) picks the starting fixture entry for a
1292
+ * step inside a loop body — e.g. iteration 3's `run()` starts at
1293
+ * `responses[2]`, so a fixture can script "iteration 1 fails, iteration 3
1294
+ * passes." Schema-repair retries within that same call still advance the
1295
+ * index normally from there. Omitted for non-loop steps, unchanged from
1296
+ * always starting at 0. */
1297
+ constructor(fixturesDir, workflowName, stepId, iteration) {
1298
+ this.stepId = stepId;
1299
+ const fixturePath = path10.join(fixturesDir, workflowName, `${stepId}.json`);
1300
+ const raw = readFileSync(fixturePath, "utf8");
1301
+ this.responses = JSON.parse(raw);
1302
+ this.callIndex = iteration !== void 0 ? iteration - 1 : 0;
1303
+ }
1304
+ stepId;
1305
+ id = "mock";
1306
+ callIndex = 0;
1307
+ responses;
1308
+ async run(_req) {
1309
+ const lastIndex = this.responses.length - 1;
1310
+ const entry = this.responses[Math.min(this.callIndex, lastIndex)];
1311
+ if (!entry) {
1312
+ throw new Error(`mock fixture for step "${this.stepId}" has no response entries`);
1313
+ }
1314
+ const callIndex = this.callIndex;
1315
+ this.callIndex += 1;
1316
+ return {
1317
+ output: entry.output,
1318
+ sessionId: `mock:${this.stepId}:${callIndex}`,
1319
+ tokens: entry.tokens ?? { input: 0, output: 0 },
1320
+ filesChanged: entry.filesChanged ?? [],
1321
+ stopReason: entry.stopReason ?? "complete"
1322
+ };
1323
+ }
1324
+ };
1325
+
1326
+ // src/steps/command.ts
1327
+ import { spawn as spawn2 } from "child_process";
1328
+ import { z as z5 } from "zod";
1329
+ var CommandResultSchema = z5.object({
1330
+ stdout: z5.string().optional(),
1331
+ stderr: z5.string().optional(),
1332
+ exitCode: z5.number().optional()
1333
+ });
1334
+ var CommandStepFailedError = class extends Error {
1335
+ failure;
1336
+ constructor(failure) {
1337
+ super(failure.detail);
1338
+ this.failure = failure;
1339
+ }
1340
+ };
1341
+ var KILL_GRACE_MS = 5e3;
1342
+ var DOCKER_CLI_ERROR_EXIT_CODE = 125;
1343
+ function dockerRunArgs(step, cwd) {
1344
+ const workdir = step.cwd ? `/workspace/${step.cwd}` : "/workspace";
1345
+ return [
1346
+ "run",
1347
+ "--rm",
1348
+ "--network",
1349
+ "none",
1350
+ "-v",
1351
+ `${cwd}:/workspace`,
1352
+ "-w",
1353
+ workdir,
1354
+ step.image,
1355
+ "sh",
1356
+ "-c",
1357
+ step.run
1358
+ ];
1359
+ }
1360
+ function feedLines(carry, chunk, onLine) {
1361
+ const combined = carry + chunk;
1362
+ const lines = combined.split("\n");
1363
+ const remainder = lines.pop() ?? "";
1364
+ for (const _line of lines) onLine();
1365
+ return remainder;
1366
+ }
1367
+ function runCommandStep(step, cwd, extraEnv) {
1368
+ const capture = step.capture ?? ["stdout", "stderr", "exitCode"];
1369
+ const failOn = step.failOn ?? "exitCode";
1370
+ return new Promise((resolve, reject) => {
1371
+ const child = step.sandbox === "docker" ? spawn2("docker", dockerRunArgs(step, cwd), {
1372
+ ...extraEnv ? { env: { ...process.env, ...extraEnv } } : {}
1373
+ }) : spawn2(step.run, {
1374
+ cwd: step.cwd ?? cwd,
1375
+ shell: true,
1376
+ ...extraEnv ? { env: { ...process.env, ...extraEnv } } : {}
1377
+ });
1378
+ let stdout = "";
1379
+ let stderr = "";
1380
+ let stdoutCarry = "";
1381
+ let stderrCarry = "";
1382
+ let settled = false;
1383
+ let timedOut = false;
1384
+ let idleTimer;
1385
+ let killTimer;
1386
+ const clearTimers = () => {
1387
+ if (idleTimer) clearTimeout(idleTimer);
1388
+ if (killTimer) clearTimeout(killTimer);
1389
+ };
1390
+ const onLine = () => {
1391
+ if (step.idleTimeoutMs === void 0) return;
1392
+ if (idleTimer) clearTimeout(idleTimer);
1393
+ idleTimer = setTimeout(onIdleTimeout, step.idleTimeoutMs);
1394
+ };
1395
+ function onIdleTimeout() {
1396
+ timedOut = true;
1397
+ child.kill("SIGTERM");
1398
+ killTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
1399
+ }
1400
+ if (step.idleTimeoutMs !== void 0) idleTimer = setTimeout(onIdleTimeout, step.idleTimeoutMs);
1401
+ child.stdout?.setEncoding("utf8");
1402
+ child.stdout?.on("data", (chunk) => {
1403
+ stdout += chunk;
1404
+ stdoutCarry = feedLines(stdoutCarry, chunk, onLine);
1405
+ });
1406
+ child.stderr?.setEncoding("utf8");
1407
+ child.stderr?.on("data", (chunk) => {
1408
+ stderr += chunk;
1409
+ stderrCarry = feedLines(stderrCarry, chunk, onLine);
1410
+ });
1411
+ child.on("error", (err) => {
1412
+ if (settled) return;
1413
+ settled = true;
1414
+ clearTimers();
1415
+ reject(
1416
+ step.sandbox === "docker" ? new CommandStepFailedError({
1417
+ reason: "sandbox-error",
1418
+ detail: `docker failed to start for command "${step.run}": ${err.message}`,
1419
+ recoverable: false
1420
+ }) : new CommandStepFailedError({
1421
+ reason: "command-failed",
1422
+ detail: `command "${step.run}" failed to start: ${err.message}`,
1423
+ recoverable: false
1424
+ })
1425
+ );
1426
+ });
1427
+ child.on("close", (code, signal) => {
1428
+ if (settled) return;
1429
+ settled = true;
1430
+ clearTimers();
1431
+ if (timedOut) {
1432
+ reject(
1433
+ new CommandStepFailedError({
1434
+ reason: "timeout",
1435
+ detail: `command "${step.run}" idle for longer than ${step.idleTimeoutMs}ms, killed`,
1436
+ recoverable: false
1437
+ })
1438
+ );
1439
+ return;
1440
+ }
1441
+ const exitCode = code ?? (signal ? 1 : 0);
1442
+ const result = {};
1443
+ if (capture.includes("stdout")) result.stdout = stdout;
1444
+ if (capture.includes("stderr")) result.stderr = stderr;
1445
+ if (capture.includes("exitCode")) result.exitCode = exitCode;
1446
+ if (step.sandbox === "docker" && exitCode === DOCKER_CLI_ERROR_EXIT_CODE) {
1447
+ reject(
1448
+ new CommandStepFailedError({
1449
+ reason: "sandbox-error",
1450
+ detail: `docker run exited ${DOCKER_CLI_ERROR_EXIT_CODE} for command "${step.run}" (image "${step.image}") \u2014 the container never started (bad image, daemon unreachable, invalid docker invocation), not the command inside it failing`,
1451
+ recoverable: false
1452
+ })
1453
+ );
1454
+ return;
1455
+ }
1456
+ if (failOn === "exitCode" && exitCode !== 0) {
1457
+ reject(
1458
+ new CommandStepFailedError({
1459
+ reason: "command-failed",
1460
+ detail: `command "${step.run}" exited with code ${exitCode}`,
1461
+ recoverable: false
1462
+ })
1463
+ );
1464
+ return;
1465
+ }
1466
+ resolve(result);
1467
+ });
1468
+ });
1469
+ }
1470
+
1471
+ // src/steps/loop.ts
1472
+ import { z as z6 } from "zod";
1473
+
1474
+ // src/engine/cache.ts
1475
+ import { mkdir as mkdir5, readFile as readFile8, rename as rename2, writeFile as writeFile3 } from "fs/promises";
1476
+ import { randomUUID as randomUUID2 } from "crypto";
1477
+ import path11 from "path";
1478
+ var ENGINE_VERSION = "0.0.1";
1479
+ function definitionPayload(step) {
1480
+ switch (step.kind) {
1481
+ case "command":
1482
+ return {
1483
+ run: step.run,
1484
+ cwd: step.cwd,
1485
+ failOn: step.failOn,
1486
+ capture: step.capture,
1487
+ skipIf: step.skipIf,
1488
+ finally: step.finally
1489
+ };
1490
+ case "transform":
1491
+ return { fn: step.fn, skipIf: step.skipIf, finally: step.finally };
1492
+ case "agent":
1493
+ return {
1494
+ prompt: step.prompt,
1495
+ tools: step.tools,
1496
+ schema: step.schema,
1497
+ model: step.model,
1498
+ repairAttempts: step.repairAttempts,
1499
+ skipIf: step.skipIf,
1500
+ finally: step.finally
1501
+ };
1502
+ case "gate":
1503
+ return { schema: step.schema, render: step.render, skipIf: step.skipIf, finally: step.finally };
1504
+ case "map":
1505
+ return {
1506
+ over: step.over,
1507
+ concurrency: step.concurrency,
1508
+ isolation: step.isolation,
1509
+ onItemFailure: step.onItemFailure,
1510
+ step: step.step
1511
+ };
1512
+ case "loop":
1513
+ return {
1514
+ until: step.until,
1515
+ budget: step.budget,
1516
+ onExhausted: step.onExhausted,
1517
+ freshContext: step.freshContext,
1518
+ body: step.body
1519
+ };
1520
+ }
1521
+ }
1522
+ function computeDefinitionKey(step) {
1523
+ return sha256(JSON.stringify({ def: definitionPayload(step), engineVersion: ENGINE_VERSION }));
1524
+ }
1525
+ function computeSemanticKey(step, inputArtifactHashes, iteration = 0) {
1526
+ const inputs = Object.keys(inputArtifactHashes).sort().map((name) => `${name}=${inputArtifactHashes[name]}`).join(",");
1527
+ const adapterId = step.kind === "agent" ? "default" : "";
1528
+ const modelId = step.kind === "agent" ? step.model ?? "" : "";
1529
+ return sha256(`${step.id}|${inputs}|${adapterId}|${modelId}|${iteration}`);
1530
+ }
1531
+ function decideCache(cacheMode, definitionKey, entry) {
1532
+ if (!entry) return { kind: "miss" };
1533
+ if (entry.definitionKey === definitionKey) return { kind: "hit", entry, stale: false };
1534
+ if (cacheMode === "loose") return { kind: "hit", entry, stale: true };
1535
+ return { kind: "miss" };
1536
+ }
1537
+ function entryPath(cacheDir, semanticKey) {
1538
+ return path11.join(cacheDir, `${semanticKey}.json`);
1539
+ }
1540
+ async function readCacheEntry(cacheDir, semanticKey) {
1541
+ const raw = await readFile8(entryPath(cacheDir, semanticKey), "utf8").catch(() => void 0);
1542
+ return raw === void 0 ? void 0 : JSON.parse(raw);
1543
+ }
1544
+ async function writeCacheEntry(cacheDir, entry) {
1545
+ await mkdir5(cacheDir, { recursive: true });
1546
+ const finalPath = entryPath(cacheDir, entry.semanticKey);
1547
+ const tmpPath = path11.join(cacheDir, `.${entry.semanticKey}.${randomUUID2()}.tmp`);
1548
+ await writeFile3(tmpPath, JSON.stringify(entry, null, 2), "utf8");
1549
+ await rename2(tmpPath, finalPath);
1550
+ }
1551
+
1552
+ // src/steps/transform.ts
1553
+ import path12 from "path";
1554
+ import { pathToFileURL as pathToFileURL3 } from "url";
1555
+ async function resolveTransformFn(fn, cwd) {
1556
+ const modulePath = path12.resolve(cwd, ".yak/transforms.ts");
1557
+ const mod = await import(pathToFileURL3(modulePath).href);
1558
+ const impl = mod[fn];
1559
+ if (typeof impl !== "function") {
1560
+ throw new Error(`transform "${fn}" not found in .yak/transforms.ts`);
1561
+ }
1562
+ return impl;
1563
+ }
1564
+ async function runTransformStep(step, inputs, cwd) {
1565
+ const impl = await resolveTransformFn(step.fn, cwd);
1566
+ return impl(inputs);
1567
+ }
1568
+
1569
+ // src/steps/loop.ts
1570
+ async function runLoopStep(step, ctx) {
1571
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1572
+ t: "step.started",
1573
+ stepId: step.id,
1574
+ semanticKey: ctx.semanticKey,
1575
+ definitionKey: ctx.definitionKey
1576
+ });
1577
+ if (ctx.loopContinuation?.action === "abort") {
1578
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1579
+ t: "step.failed",
1580
+ stepId: step.id,
1581
+ failure: {
1582
+ reason: "budget-exhausted",
1583
+ detail: `loop "${step.id}" aborted by human after budget exhaustion`,
1584
+ recoverable: false
1585
+ }
1586
+ });
1587
+ return "failed";
1588
+ }
1589
+ const resumeFrom = ctx.loopContinuation?.action === "continue" ? await buildResumeFrom(step, ctx) : void 0;
1590
+ const status = await runLoopBody(step, ctx, resumeFrom);
1591
+ if (status === "ok") {
1592
+ await appendJournalEvent(ctx.runDir, ctx.runId, { t: "step.completed", stepId: step.id, cached: false });
1593
+ }
1594
+ return status;
1595
+ }
1596
+ async function buildResumeFrom(step, ctx) {
1597
+ const events = await readJournal(ctx.runDir);
1598
+ const bodyStepIds = new Set(step.body.map((s) => s.id));
1599
+ const trippedIteration = [...events].reverse().find((e) => e.t === "loop.iteration" && e.stepId === step.id);
1600
+ const iteration = trippedIteration && trippedIteration.t === "loop.iteration" ? trippedIteration.n : 0;
1601
+ const priorValues = /* @__PURE__ */ new Map();
1602
+ const priorHashes = /* @__PURE__ */ new Map();
1603
+ for (const event of events) {
1604
+ if (event.t !== "step.completed" || event.iteration !== iteration) continue;
1605
+ if (!bodyStepIds.has(event.stepId) || !event.artifact) continue;
1606
+ priorValues.set(event.artifact, await readArtifactRaw(ctx.runDir, event.artifact, iteration));
1607
+ if (event.artifactHash) priorHashes.set(event.artifact, event.artifactHash);
1608
+ }
1609
+ const addIterations = ctx.loopContinuation?.addIterations ?? 0;
1610
+ return {
1611
+ startIteration: iteration + 1,
1612
+ priorValues,
1613
+ priorHashes,
1614
+ maxIterations: step.budget.maxIterations + addIterations
1615
+ };
1616
+ }
1617
+ async function runLoopBody(step, ctx, resumeFrom) {
1618
+ const localProducerOf = buildProducerMap(step.body);
1619
+ const bodyOrderIndex = new Map(step.body.map((s, i) => [s.id, i]));
1620
+ const bodyStepIds = new Set(step.body.map((s) => s.id));
1621
+ const effectiveMaxIterations = resumeFrom?.maxIterations ?? step.budget.maxIterations;
1622
+ let priorValues = resumeFrom?.priorValues ?? /* @__PURE__ */ new Map();
1623
+ let priorHashes = resumeFrom?.priorHashes ?? /* @__PURE__ */ new Map();
1624
+ let lastSignal;
1625
+ const bodySessionIds = /* @__PURE__ */ new Map();
1626
+ for (let iteration = resumeFrom?.startIteration ?? 1; ; iteration++) {
1627
+ const thisValues = /* @__PURE__ */ new Map();
1628
+ const thisHashes = /* @__PURE__ */ new Map();
1629
+ try {
1630
+ for (const [posIndex, bodyStep] of step.body.entries()) {
1631
+ await runOneBodyStep({
1632
+ bodyStep,
1633
+ posIndex,
1634
+ iteration,
1635
+ ctx,
1636
+ localProducerOf,
1637
+ bodyOrderIndex,
1638
+ priorValues,
1639
+ priorHashes,
1640
+ thisValues,
1641
+ thisHashes,
1642
+ freshContext: step.freshContext ?? true,
1643
+ bodySessionIds
1644
+ });
1645
+ }
1646
+ } catch (err) {
1647
+ if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
1648
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1649
+ t: "step.failed",
1650
+ stepId: step.id,
1651
+ failure: {
1652
+ reason: err.failure.reason,
1653
+ detail: `loop "${step.id}" body step failed: ${err.failure.detail}`,
1654
+ recoverable: false
1655
+ }
1656
+ });
1657
+ return "failed";
1658
+ }
1659
+ throw err;
1660
+ }
1661
+ const untilContext = Object.fromEntries(thisValues);
1662
+ if (await evalExpr(step.until, untilContext, ctx.cwd)) return "ok";
1663
+ let tripped;
1664
+ if (step.budget.noProgress) {
1665
+ lastSignal = await evalExpr(step.budget.noProgress.signal, untilContext, ctx.cwd);
1666
+ }
1667
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1668
+ t: "loop.iteration",
1669
+ stepId: step.id,
1670
+ n: iteration,
1671
+ ...lastSignal !== void 0 ? { signal: lastSignal } : {}
1672
+ });
1673
+ if (step.budget.noProgress) {
1674
+ tripped = await noProgressTripped(step, ctx, iteration) ? "noProgress" : tripped;
1675
+ }
1676
+ if (!tripped && iteration >= effectiveMaxIterations) tripped = "maxIterations";
1677
+ if (!tripped && step.budget.maxTokens !== void 0) {
1678
+ const tokensTotal = await sumBodyTokens(ctx.runDir, bodyStepIds);
1679
+ if (tokensTotal >= step.budget.maxTokens) tripped = "maxTokens";
1680
+ }
1681
+ if (tripped) return await handleExhausted(step, ctx, iteration, tripped);
1682
+ priorValues = thisValues;
1683
+ priorHashes = thisHashes;
1684
+ }
1685
+ }
1686
+ async function noProgressTripped(step, ctx, iteration) {
1687
+ const rounds = step.budget.noProgress.rounds;
1688
+ const events = await readJournal(ctx.runDir);
1689
+ const signals = events.filter((e) => e.t === "loop.iteration" && e.stepId === step.id && e.n <= iteration).map((e) => e.signal).filter((s) => typeof s === "number");
1690
+ if (signals.length < rounds) return false;
1691
+ const recent = signals.slice(-rounds);
1692
+ for (let i = 1; i < recent.length; i++) {
1693
+ if (recent[i] < recent[i - 1]) return false;
1694
+ }
1695
+ return true;
1696
+ }
1697
+ async function sumBodyTokens(runDir, bodyStepIds) {
1698
+ const events = await readJournal(runDir);
1699
+ return events.filter((e) => e.t === "budget.consumed" && bodyStepIds.has(e.stepId)).reduce((sum, e) => sum + e.tokens, 0);
1700
+ }
1701
+ async function handleExhausted(step, ctx, iteration, tripped) {
1702
+ const onExhausted = step.onExhausted ?? "suspend";
1703
+ if (onExhausted === "continue") return "ok";
1704
+ if (onExhausted === "fail") {
1705
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1706
+ t: "step.failed",
1707
+ stepId: step.id,
1708
+ failure: {
1709
+ reason: "budget-exhausted",
1710
+ detail: `loop "${step.id}" exhausted (${tripped}) after ${iteration} iterations`,
1711
+ recoverable: false
1712
+ }
1713
+ });
1714
+ return "failed";
1715
+ }
1716
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1717
+ t: "run.suspended",
1718
+ reason: "exhausted",
1719
+ loopStepId: step.id,
1720
+ iteration,
1721
+ tripped
1722
+ });
1723
+ await writeLoopExhaustedRequest(ctx.runDir, ctx.runId, step.id, iteration, tripped);
1724
+ return "suspended";
1725
+ }
1726
+ async function runOneBodyStep(args) {
1727
+ const {
1728
+ bodyStep,
1729
+ posIndex,
1730
+ iteration,
1731
+ ctx,
1732
+ localProducerOf,
1733
+ bodyOrderIndex,
1734
+ priorValues,
1735
+ priorHashes,
1736
+ thisValues,
1737
+ thisHashes,
1738
+ freshContext,
1739
+ bodySessionIds
1740
+ } = args;
1741
+ const inputs = {};
1742
+ const inputHashesForKey = {};
1743
+ for (const name of inputNamesOf(bodyStep)) {
1744
+ const producerId = localProducerOf.get(name);
1745
+ if (producerId !== void 0) {
1746
+ const producerPos = bodyOrderIndex.get(producerId);
1747
+ const fromThisRound = producerPos < posIndex;
1748
+ inputs[name] = fromThisRound ? thisValues.get(name) : priorValues.get(name);
1749
+ const hash = fromThisRound ? thisHashes.get(name) : priorHashes.get(name);
1750
+ if (hash) inputHashesForKey[name] = hash;
1751
+ } else {
1752
+ inputs[name] = await readArtifactRaw(ctx.runDir, name);
1753
+ const hash = ctx.outerArtifactHashes.get(name);
1754
+ if (hash) inputHashesForKey[name] = hash;
1755
+ }
1756
+ }
1757
+ const semanticKey = computeSemanticKey(bodyStep, inputHashesForKey, iteration);
1758
+ const definitionKey = computeDefinitionKey(bodyStep);
1759
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1760
+ t: "step.started",
1761
+ stepId: bodyStep.id,
1762
+ iteration,
1763
+ semanticKey,
1764
+ definitionKey
1765
+ });
1766
+ const cacheMode = bodyStep.cache ?? "strict";
1767
+ const existingEntry = await readCacheEntry(ctx.cacheDir, semanticKey);
1768
+ const decision = decideCache(cacheMode, definitionKey, existingEntry);
1769
+ let result;
1770
+ let cached = false;
1771
+ let stale = false;
1772
+ if (decision.kind === "hit") {
1773
+ result = decision.entry.artifact;
1774
+ cached = true;
1775
+ stale = decision.stale;
1776
+ } else {
1777
+ result = await runBodyStepKind(bodyStep, inputs, ctx, iteration, freshContext, bodySessionIds);
1778
+ }
1779
+ let artifactHash;
1780
+ if (bodyStep.produces) {
1781
+ const schema = bodyStep.kind === "command" ? CommandResultSchema : z6.unknown();
1782
+ const written = await writeArtifact(ctx.runDir, bodyStep.produces, result, schema, iteration);
1783
+ artifactHash = written.hash;
1784
+ thisValues.set(bodyStep.produces, result);
1785
+ thisHashes.set(bodyStep.produces, artifactHash);
1786
+ if (!cached) {
1787
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1788
+ t: "artifact.written",
1789
+ name: bodyStep.produces,
1790
+ hash: artifactHash,
1791
+ bytes: written.bytes
1792
+ });
1793
+ }
1794
+ }
1795
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1796
+ t: "step.completed",
1797
+ stepId: bodyStep.id,
1798
+ iteration,
1799
+ ...bodyStep.produces ? { artifact: bodyStep.produces, artifactHash } : {},
1800
+ cached,
1801
+ ...stale ? { stale: true } : {}
1802
+ });
1803
+ if (!cached) {
1804
+ await writeCacheEntry(ctx.cacheDir, {
1805
+ semanticKey,
1806
+ definitionKey,
1807
+ ...bodyStep.produces ? { artifactName: bodyStep.produces, artifactHash, artifact: result } : {}
1808
+ });
1809
+ }
1810
+ }
1811
+ async function runBodyStepKind(bodyStep, inputs, ctx, iteration, freshContext, bodySessionIds) {
1812
+ try {
1813
+ if (bodyStep.kind === "command") return await runCommandStep(bodyStep, ctx.cwd);
1814
+ if (bodyStep.kind === "transform") return await runTransformStep(bodyStep, inputs, ctx.cwd);
1815
+ if (bodyStep.kind === "agent") {
1816
+ const adapter = ctx.buildAdapter(bodyStep, iteration);
1817
+ const sessionId = freshContext ? void 0 : bodySessionIds.get(bodyStep.id);
1818
+ return await runAgentStep(
1819
+ bodyStep,
1820
+ inputs,
1821
+ { runId: ctx.runId, runDir: ctx.runDir, cwd: ctx.cwd, adapter, sessionId },
1822
+ freshContext ? void 0 : (nextSessionId) => bodySessionIds.set(bodyStep.id, nextSessionId)
1823
+ );
1824
+ }
1825
+ } catch (err) {
1826
+ if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
1827
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1828
+ t: "step.failed",
1829
+ stepId: bodyStep.id,
1830
+ iteration,
1831
+ failure: err.failure
1832
+ });
1833
+ }
1834
+ throw err;
1835
+ }
1836
+ throw new Error(`step "${bodyStep.id}": kind "${bodyStep.kind}" not supported inside a loop body in M3`);
1837
+ }
1838
+
1839
+ // src/steps/map.ts
1840
+ import path13 from "path";
1841
+ import pLimit from "p-limit";
1842
+ import { z as z7 } from "zod";
1843
+ var RETRY_ATTEMPTS = 2;
1844
+ async function runMapStep(step, ctx) {
1845
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1846
+ t: "step.started",
1847
+ stepId: step.id,
1848
+ semanticKey: ctx.semanticKey,
1849
+ definitionKey: ctx.definitionKey
1850
+ });
1851
+ if (step.isolation === "worktree") {
1852
+ await commitAllIfDirty(ctx.cwd, `yak: checkpoint before map ${step.id}`);
1853
+ }
1854
+ const items = await readArtifactRaw(ctx.runDir, step.over);
1855
+ const limit = pLimit(Math.min(step.concurrency ?? 4, ctx.globalConcurrency));
1856
+ const results = new Array(items.length);
1857
+ let anyFailed = false;
1858
+ await Promise.all(
1859
+ items.map(
1860
+ (item, index) => limit(async () => {
1861
+ const outcome = await runMapItem(step, item, index, ctx);
1862
+ results[index] = outcome.result;
1863
+ if (outcome.failed) anyFailed = true;
1864
+ })
1865
+ )
1866
+ );
1867
+ if (anyFailed && (step.onItemFailure ?? "skip") === "fail") {
1868
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1869
+ t: "step.failed",
1870
+ stepId: step.id,
1871
+ failure: {
1872
+ reason: "command-failed",
1873
+ detail: `map "${step.id}": at least one item failed under onItemFailure: 'fail'`,
1874
+ recoverable: false
1875
+ }
1876
+ });
1877
+ return "failed";
1878
+ }
1879
+ if (step.produces) {
1880
+ const written = await writeArtifact(ctx.runDir, step.produces, results, z7.array(z7.unknown()));
1881
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1882
+ t: "artifact.written",
1883
+ name: step.produces,
1884
+ hash: written.hash,
1885
+ bytes: written.bytes
1886
+ });
1887
+ }
1888
+ await appendJournalEvent(ctx.runDir, ctx.runId, { t: "step.completed", stepId: step.id, cached: false });
1889
+ return "ok";
1890
+ }
1891
+ function itemWorktreePath(runDir, runId, mapStepId, index) {
1892
+ const runsDir = path13.dirname(runDir);
1893
+ const yakRoot = path13.dirname(runsDir);
1894
+ return path13.join(yakRoot, ".yak", "worktrees", runId, mapStepId, String(index));
1895
+ }
1896
+ async function ensureItemWorktree(step, index, ctx) {
1897
+ const worktreePath = itemWorktreePath(ctx.runDir, ctx.runId, step.id, index);
1898
+ await createWorktree(ctx.cwd, `yak-item/${ctx.runId}/${step.id}/${index}`, "HEAD", worktreePath);
1899
+ return worktreePath;
1900
+ }
1901
+ async function runMapItem(step, item, index, ctx) {
1902
+ const onItemFailure = step.onItemFailure ?? "skip";
1903
+ const maxAttempts = onItemFailure === "retry" ? RETRY_ATTEMPTS + 1 : 1;
1904
+ let itemCwd;
1905
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1906
+ try {
1907
+ if (step.isolation === "worktree" && itemCwd === void 0) {
1908
+ itemCwd = await ensureItemWorktree(step, index, ctx);
1909
+ }
1910
+ const result = await runOneItemAttempt(step, item, index, ctx, itemCwd ?? ctx.cwd);
1911
+ return { result, failed: false };
1912
+ } catch (rawErr) {
1913
+ const err = rawErr instanceof WorktreeCreationError ? new CommandStepFailedError({ reason: "command-failed", detail: rawErr.message, recoverable: false }) : rawErr;
1914
+ if (!(err instanceof CommandStepFailedError || err instanceof AgentStepFailedError)) throw err;
1915
+ if (attempt < maxAttempts) {
1916
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1917
+ t: "map.item.retried",
1918
+ mapStepId: step.id,
1919
+ itemIndex: index,
1920
+ attempt,
1921
+ error: err.failure.detail
1922
+ });
1923
+ }
1924
+ }
1925
+ }
1926
+ if (onItemFailure === "fail") return { result: null, failed: true };
1927
+ return { result: null, failed: false };
1928
+ }
1929
+ async function runOneItemAttempt(step, item, index, ctx, itemCwd) {
1930
+ const itemStep = step.step;
1931
+ const syntheticId = `${step.id}[${index}]`;
1932
+ const inputs = { [step.over]: item };
1933
+ const inputHashesForKey = {};
1934
+ for (const name of inputNamesOf(itemStep)) {
1935
+ if (name === step.over) continue;
1936
+ inputs[name] = await readArtifactRaw(ctx.runDir, name);
1937
+ const hash = ctx.outerArtifactHashes.get(name);
1938
+ if (hash) inputHashesForKey[name] = hash;
1939
+ }
1940
+ const semanticKey = computeSemanticKey(itemStep, inputHashesForKey, index);
1941
+ const definitionKey = computeDefinitionKey(itemStep);
1942
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1943
+ t: "step.started",
1944
+ stepId: syntheticId,
1945
+ iteration: index,
1946
+ semanticKey,
1947
+ definitionKey
1948
+ });
1949
+ const cacheMode = itemStep.cache ?? "strict";
1950
+ const existingEntry = await readCacheEntry(ctx.cacheDir, semanticKey);
1951
+ const decision = decideCache(cacheMode, definitionKey, existingEntry);
1952
+ let result;
1953
+ let cached = false;
1954
+ let stale = false;
1955
+ if (decision.kind === "hit") {
1956
+ result = decision.entry.artifact;
1957
+ cached = true;
1958
+ stale = decision.stale;
1959
+ } else {
1960
+ try {
1961
+ if (itemStep.kind === "command") {
1962
+ result = await runCommandStep(itemStep, itemCwd, {
1963
+ MAP_ITEM_INDEX: String(index),
1964
+ MAP_ITEM: JSON.stringify(item)
1965
+ });
1966
+ } else if (itemStep.kind === "transform") {
1967
+ result = await runTransformStep(itemStep, inputs, itemCwd);
1968
+ } else if (itemStep.kind === "agent") {
1969
+ const adapter = ctx.buildAdapter(itemStep, index);
1970
+ result = await runAgentStep(itemStep, inputs, { runId: ctx.runId, runDir: ctx.runDir, cwd: itemCwd, adapter });
1971
+ } else {
1972
+ throw new Error(`step "${itemStep.id}": kind "${itemStep.kind}" not supported as a map item step in M3`);
1973
+ }
1974
+ } catch (err) {
1975
+ if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
1976
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1977
+ t: "step.failed",
1978
+ stepId: syntheticId,
1979
+ iteration: index,
1980
+ failure: err.failure
1981
+ });
1982
+ }
1983
+ throw err;
1984
+ }
1985
+ }
1986
+ let artifactHash;
1987
+ if (step.produces) {
1988
+ const schema = itemStep.kind === "command" ? CommandResultSchema : z7.unknown();
1989
+ const written = await writeArtifact(ctx.runDir, step.produces, result, schema, index);
1990
+ artifactHash = written.hash;
1991
+ if (!cached) {
1992
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
1993
+ t: "artifact.written",
1994
+ name: step.produces,
1995
+ hash: artifactHash,
1996
+ bytes: written.bytes
1997
+ });
1998
+ }
1999
+ }
2000
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2001
+ t: "step.completed",
2002
+ stepId: syntheticId,
2003
+ iteration: index,
2004
+ ...step.produces ? { artifact: step.produces, artifactHash } : {},
2005
+ cached,
2006
+ ...stale ? { stale: true } : {}
2007
+ });
2008
+ if (!cached) {
2009
+ await writeCacheEntry(ctx.cacheDir, {
2010
+ semanticKey,
2011
+ definitionKey,
2012
+ ...step.produces ? { artifactName: step.produces, artifactHash, artifact: result } : {}
2013
+ });
2014
+ }
2015
+ return result;
2016
+ }
2017
+
2018
+ // src/engine/scheduler.ts
2019
+ var DEFAULT_CONCURRENCY = 4;
2020
+ function buildAdapter(adapterId, ctx, workflowName, step, iteration) {
2021
+ if (adapterId === "mock") {
2022
+ const fixturesDir = path14.join(ctx.cwd, "test", "fixtures");
2023
+ return new MockAdapter(fixturesDir, workflowName, step.id, iteration);
2024
+ }
2025
+ return new ClaudeCodeAdapter(
2026
+ ctx.runDir,
2027
+ step.id,
2028
+ step.sandbox === "docker" ? { image: step.image } : void 0
2029
+ );
2030
+ }
2031
+ function collectInputHashes(step, artifactHashes) {
2032
+ const inputArtifactHashes = {};
2033
+ for (const need of inputNamesOf(step)) {
2034
+ const hash = artifactHashes.get(need);
2035
+ if (hash) inputArtifactHashes[need] = hash;
2036
+ }
2037
+ return inputArtifactHashes;
2038
+ }
2039
+ function trustResumedSteps(remaining, producerOf, resumeState, completed, artifactHashes) {
2040
+ let progressed = true;
2041
+ while (progressed) {
2042
+ progressed = false;
2043
+ for (const step of remaining.values()) {
2044
+ const info = resumeState.get(step.id);
2045
+ if (!info) continue;
2046
+ if (!dependenciesOf(step, producerOf).every((dep) => completed.has(dep))) continue;
2047
+ if (step.kind !== "gate") {
2048
+ const semanticKey = computeSemanticKey(step, collectInputHashes(step, artifactHashes));
2049
+ const definitionKey = computeDefinitionKey(step);
2050
+ if (semanticKey !== info.semanticKey || definitionKey !== info.definitionKey) continue;
2051
+ }
2052
+ completed.add(step.id);
2053
+ remaining.delete(step.id);
2054
+ if (info.artifact && info.artifactHash) artifactHashes.set(info.artifact, info.artifactHash);
2055
+ progressed = true;
2056
+ }
2057
+ }
2058
+ }
2059
+ async function runEligibleSteps(workflow, ctx, resumeState) {
2060
+ const limit = pLimit2(ctx.concurrency ?? DEFAULT_CONCURRENCY);
2061
+ const producerOf = buildProducerMap(workflow.steps);
2062
+ const remaining = new Map(workflow.steps.map((s) => [s.id, s]));
2063
+ const completed = /* @__PURE__ */ new Set();
2064
+ const failedSteps = /* @__PURE__ */ new Set();
2065
+ const inFlight = /* @__PURE__ */ new Map();
2066
+ const artifactHashes = /* @__PURE__ */ new Map();
2067
+ let failed = false;
2068
+ let suspended = false;
2069
+ if (resumeState) trustResumedSteps(remaining, producerOf, resumeState, completed, artifactHashes);
2070
+ const agentSessionIds = /* @__PURE__ */ new Map();
2071
+ function launchEligible() {
2072
+ if (suspended) return;
2073
+ for (const step of remaining.values()) {
2074
+ if (inFlight.has(step.id)) continue;
2075
+ const deps = dependenciesOf(step, producerOf);
2076
+ if (step.finally) {
2077
+ if (deps.every((dep) => completed.has(dep) || failedSteps.has(dep))) {
2078
+ inFlight.set(
2079
+ step.id,
2080
+ limit(() => runStep(step, ctx, artifactHashes, workflow.name, agentSessionIds))
2081
+ );
2082
+ }
2083
+ continue;
2084
+ }
2085
+ if (failed) continue;
2086
+ if (deps.every((dep) => completed.has(dep))) {
2087
+ inFlight.set(
2088
+ step.id,
2089
+ limit(() => runStep(step, ctx, artifactHashes, workflow.name, agentSessionIds))
2090
+ );
2091
+ }
2092
+ }
2093
+ }
2094
+ launchEligible();
2095
+ while (inFlight.size > 0) {
2096
+ const [doneId, status] = await Promise.race(
2097
+ [...inFlight.entries()].map(async ([id, p]) => [id, await p])
2098
+ );
2099
+ inFlight.delete(doneId);
2100
+ remaining.delete(doneId);
2101
+ if (status === "failed") {
2102
+ failed = true;
2103
+ failedSteps.add(doneId);
2104
+ } else if (status === "suspended") suspended = true;
2105
+ else completed.add(doneId);
2106
+ launchEligible();
2107
+ }
2108
+ if (!failed && !suspended && remaining.size > 0) {
2109
+ const stuck = [...remaining.keys()].join(", ");
2110
+ throw new Error(`workflow stalled: step(s) [${stuck}] never became eligible`);
2111
+ }
2112
+ return failed ? "failed" : suspended ? "suspended" : "ok";
2113
+ }
2114
+ async function runStep(step, ctx, artifactHashes, workflowName, agentSessionIds) {
2115
+ if (step.kind === "loop") {
2116
+ const semanticKey2 = computeSemanticKey(step, collectInputHashes(step, artifactHashes));
2117
+ const definitionKey2 = computeDefinitionKey(step);
2118
+ return runLoopStep(step, {
2119
+ runId: ctx.runId,
2120
+ runDir: ctx.runDir,
2121
+ cwd: ctx.cwd,
2122
+ cacheDir: ctx.cacheDir,
2123
+ semanticKey: semanticKey2,
2124
+ definitionKey: definitionKey2,
2125
+ outerArtifactHashes: artifactHashes,
2126
+ buildAdapter: (step2, iteration) => buildAdapter(ctx.adapter, ctx, workflowName, step2, iteration),
2127
+ loopContinuation: ctx.loopContinuations?.get(step.id)
2128
+ });
2129
+ }
2130
+ if (step.kind === "gate") {
2131
+ const inputs = {};
2132
+ for (const need of step.needs ?? []) {
2133
+ inputs[need] = step.finally ? await readArtifactRawOrUndefined(ctx.runDir, need) : await readArtifactRaw(ctx.runDir, need);
2134
+ }
2135
+ return runGateStep(step, inputs, { runId: ctx.runId, runDir: ctx.runDir, cwd: ctx.cwd });
2136
+ }
2137
+ if (step.kind === "map") {
2138
+ const semanticKey2 = computeSemanticKey(step, collectInputHashes(step, artifactHashes));
2139
+ const definitionKey2 = computeDefinitionKey(step);
2140
+ return runMapStep(step, {
2141
+ runId: ctx.runId,
2142
+ runDir: ctx.runDir,
2143
+ cwd: ctx.cwd,
2144
+ cacheDir: ctx.cacheDir,
2145
+ semanticKey: semanticKey2,
2146
+ definitionKey: definitionKey2,
2147
+ globalConcurrency: ctx.concurrency ?? DEFAULT_CONCURRENCY,
2148
+ outerArtifactHashes: artifactHashes,
2149
+ buildAdapter: (step2, itemIndex) => buildAdapter(ctx.adapter, ctx, workflowName, step2, itemIndex)
2150
+ });
2151
+ }
2152
+ const semanticKey = computeSemanticKey(step, collectInputHashes(step, artifactHashes));
2153
+ const definitionKey = computeDefinitionKey(step);
2154
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2155
+ t: "step.started",
2156
+ stepId: step.id,
2157
+ semanticKey,
2158
+ definitionKey
2159
+ });
2160
+ if (step.skipIf) {
2161
+ const skipInputs = {};
2162
+ for (const name of inputNamesOf(step)) {
2163
+ skipInputs[name] = step.finally ? await readArtifactRawOrUndefined(ctx.runDir, name) : await readArtifactRaw(ctx.runDir, name);
2164
+ }
2165
+ const skip = Boolean(await evalExpr(step.skipIf, skipInputs, ctx.cwd));
2166
+ if (skip) {
2167
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2168
+ t: "step.completed",
2169
+ stepId: step.id,
2170
+ cached: false,
2171
+ skipped: true
2172
+ });
2173
+ return "ok";
2174
+ }
2175
+ }
2176
+ const cacheMode = step.cache ?? "strict";
2177
+ const existingEntry = await readCacheEntry(ctx.cacheDir, semanticKey);
2178
+ const decision = decideCache(cacheMode, definitionKey, existingEntry);
2179
+ if (decision.kind === "hit") {
2180
+ const { entry } = decision;
2181
+ let artifactHash2;
2182
+ if (entry.artifactName) {
2183
+ const artifactSchema = step.kind === "command" ? CommandResultSchema : z8.unknown();
2184
+ const written = await writeArtifact(ctx.runDir, entry.artifactName, entry.artifact, artifactSchema);
2185
+ artifactHash2 = written.hash;
2186
+ artifactHashes.set(entry.artifactName, artifactHash2);
2187
+ }
2188
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2189
+ t: "step.completed",
2190
+ stepId: step.id,
2191
+ ...entry.artifactName ? { artifact: entry.artifactName, artifactHash: artifactHash2 } : {},
2192
+ cached: true,
2193
+ ...decision.stale ? { stale: true } : {}
2194
+ });
2195
+ return "ok";
2196
+ }
2197
+ let result;
2198
+ try {
2199
+ if (step.kind === "command") {
2200
+ result = await runCommandStep(step, ctx.cwd);
2201
+ } else if (step.kind === "transform") {
2202
+ const inputs = {};
2203
+ for (const need of step.needs ?? []) {
2204
+ inputs[need] = step.finally ? await readArtifactRawOrUndefined(ctx.runDir, need) : await readArtifactRaw(ctx.runDir, need);
2205
+ }
2206
+ result = await runTransformStep(step, inputs, ctx.cwd);
2207
+ } else {
2208
+ const inputs = {};
2209
+ for (const name of agentInputNames(step)) {
2210
+ inputs[name] = step.finally ? await readArtifactRawOrUndefined(ctx.runDir, name) : await readArtifactRaw(ctx.runDir, name);
2211
+ }
2212
+ const sessionId = typeof step.context === "object" && "session" in step.context ? agentSessionIds.get(step.context.session) : void 0;
2213
+ const adapter = buildAdapter(ctx.adapter, ctx, workflowName, step);
2214
+ result = await runAgentStep(
2215
+ step,
2216
+ inputs,
2217
+ { runId: ctx.runId, runDir: ctx.runDir, cwd: ctx.cwd, adapter, sessionId },
2218
+ (nextSessionId) => agentSessionIds.set(step.id, nextSessionId)
2219
+ );
2220
+ }
2221
+ } catch (err) {
2222
+ if (err instanceof CommandStepFailedError || err instanceof AgentStepFailedError) {
2223
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2224
+ t: "step.failed",
2225
+ stepId: step.id,
2226
+ failure: err.failure
2227
+ });
2228
+ return "failed";
2229
+ }
2230
+ throw err;
2231
+ }
2232
+ let artifactName;
2233
+ let artifactHash;
2234
+ if (step.produces) {
2235
+ const artifact = step.kind === "command" ? await writeArtifact(ctx.runDir, step.produces, result, CommandResultSchema) : await writeArtifact(ctx.runDir, step.produces, result, z8.unknown());
2236
+ artifactName = artifact.name;
2237
+ artifactHash = artifact.hash;
2238
+ artifactHashes.set(artifact.name, artifact.hash);
2239
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2240
+ t: "artifact.written",
2241
+ name: artifact.name,
2242
+ hash: artifact.hash,
2243
+ bytes: artifact.bytes
2244
+ });
2245
+ }
2246
+ await appendJournalEvent(ctx.runDir, ctx.runId, {
2247
+ t: "step.completed",
2248
+ stepId: step.id,
2249
+ ...artifactName ? { artifact: artifactName, artifactHash } : {},
2250
+ cached: false
2251
+ });
2252
+ await writeCacheEntry(ctx.cacheDir, {
2253
+ semanticKey,
2254
+ definitionKey,
2255
+ ...artifactName ? { artifactName, artifactHash, artifact: result } : {}
2256
+ });
2257
+ return "ok";
2258
+ }
2259
+
2260
+ // src/engine/run.ts
2261
+ var DEFAULT_ADAPTER = "claude-code";
2262
+ function defaultRunsDir() {
2263
+ return path15.join(process.cwd(), ".runs");
2264
+ }
2265
+ function resolveDirs(runsDir, cwd, cacheDir) {
2266
+ const resolvedRunsDir = runsDir ?? defaultRunsDir();
2267
+ return {
2268
+ runsDir: resolvedRunsDir,
2269
+ cwd: cwd ?? process.cwd(),
2270
+ cacheDir: cacheDir ?? path15.join(path15.dirname(resolvedRunsDir), ".yak", "cache")
2271
+ };
2272
+ }
2273
+ function worktreePathFor(runsDir, runId) {
2274
+ return path15.join(path15.dirname(runsDir), ".yak", "worktrees", runId);
2275
+ }
2276
+ function generateRunId() {
2277
+ const iso = (/* @__PURE__ */ new Date()).toISOString();
2278
+ const [datePart, timePart] = iso.split("T");
2279
+ const time = (timePart ?? "").replace(/\.\d+Z$/, "").replace(/:/g, "-");
2280
+ const suffix = randomBytes2(2).toString("hex");
2281
+ return `${datePart}T${time}Z-${suffix}`;
2282
+ }
2283
+ async function executeWorkflowFile(workflowPath, opts = {}) {
2284
+ const { runsDir, cwd, cacheDir } = resolveDirs(opts.runsDir, opts.cwd, opts.cacheDir);
2285
+ const adapter = opts.adapter ?? DEFAULT_ADAPTER;
2286
+ const isolation = opts.isolation ?? "none";
2287
+ const raw = await loadWorkflowYaml(workflowPath);
2288
+ const workflow = normalizeWorkflow(raw);
2289
+ await validateWorkflow(workflow, cwd, isolation);
2290
+ const runId = generateRunId();
2291
+ const runDir = path15.join(runsDir, runId);
2292
+ await mkdir6(runDir, { recursive: true });
2293
+ await writeFile4(path15.join(runDir, "workflow.json"), JSON.stringify(workflow, null, 2), "utf8");
2294
+ await appendJournalEvent(runDir, runId, {
2295
+ t: "run.started",
2296
+ runId,
2297
+ workflow: workflow.name,
2298
+ inputHash: sha256(JSON.stringify(workflow)),
2299
+ adapter,
2300
+ isolation
2301
+ });
2302
+ let stepCwd = cwd;
2303
+ if (isolation === "worktree") {
2304
+ const worktreePath = worktreePathFor(runsDir, runId);
2305
+ try {
2306
+ await createWorktree(cwd, `yak/${runId}`, "HEAD", worktreePath);
2307
+ } catch {
2308
+ await appendJournalEvent(runDir, runId, { t: "run.finished", status: "failed" });
2309
+ return { runId, runDir, status: "failed" };
2310
+ }
2311
+ stepCwd = worktreePath;
2312
+ }
2313
+ const status = await runEligibleSteps(workflow, { runId, runDir, cwd: stepCwd, cacheDir, adapter });
2314
+ await appendJournalEvent(runDir, runId, { t: "run.finished", status });
2315
+ return { runId, runDir, status };
2316
+ }
2317
+ async function findLatestRunId(runsDir) {
2318
+ const entries = await readdir2(runsDir).catch(() => []);
2319
+ return entries.sort().at(-1);
2320
+ }
2321
+ async function readRunWorkflow(runDir) {
2322
+ return JSON.parse(await readFile9(path15.join(runDir, "workflow.json"), "utf8"));
2323
+ }
2324
+ async function resolveOpenRequests(runId, runDir, workflow, cwd, events) {
2325
+ const loopContinuations = /* @__PURE__ */ new Map();
2326
+ const openIds = openRequestStepIds(events);
2327
+ if (openIds.length === 0) return loopContinuations;
2328
+ const flatSteps = flattenSteps(workflow.steps);
2329
+ const gateAnswerSchema = async (request) => {
2330
+ const step = flatSteps.find((s) => s.id === request.stepId);
2331
+ if (!step || step.kind !== "gate") {
2332
+ throw new Error(`pending request for step "${request.stepId}" but no gate step with that id exists`);
2333
+ }
2334
+ return resolveSchemaSpec(step.schema, cwd);
2335
+ };
2336
+ const resolutions = await Promise.all(openIds.map((stepId) => resolveAnswer(runDir, stepId, gateAnswerSchema)));
2337
+ const missing = resolutions.filter((r) => r.status === "missing");
2338
+ const invalid = resolutions.filter((r) => r.status === "invalid");
2339
+ if (missing.length > 0 || invalid.length > 0) {
2340
+ const lines = [
2341
+ ...missing.map((r) => ` ${r.stepId}: no answer file written yet`),
2342
+ ...invalid.map((r) => ` ${r.stepId}: invalid answer
2343
+ ${r.errors.replace(/^/gm, " ")}`)
2344
+ ];
2345
+ throw new Error(`run ${runId} still has unresolved pending requests:
2346
+ ${lines.join("\n")}`);
2347
+ }
2348
+ for (const resolution of resolutions) {
2349
+ if (resolution.status !== "ok") continue;
2350
+ const { request, answer } = resolution;
2351
+ if (request.kind === "loop-exhausted") {
2352
+ loopContinuations.set(request.stepId, answer);
2353
+ await appendJournalEvent(runDir, runId, { t: "gate.answered", stepId: request.stepId });
2354
+ continue;
2355
+ }
2356
+ const step = flatSteps.find((s) => s.id === request.stepId);
2357
+ if (!step || step.kind !== "gate") {
2358
+ throw new Error(`pending request for step "${request.stepId}" but no gate step with that id exists`);
2359
+ }
2360
+ await completeGate(step, { runId, runDir, cwd }, answer, { skipped: false });
2361
+ }
2362
+ return loopContinuations;
2363
+ }
2364
+ async function resumeRun(runId, opts = {}) {
2365
+ const { runsDir, cwd, cacheDir } = resolveDirs(opts.runsDir, opts.cwd, opts.cacheDir);
2366
+ const runDir = path15.join(runsDir, runId);
2367
+ const workflow = await readRunWorkflow(runDir);
2368
+ let events = await readJournal(runDir);
2369
+ const loopContinuations = await resolveOpenRequests(runId, runDir, workflow, cwd, events);
2370
+ events = await readJournal(runDir);
2371
+ const resumeState = completedStepsFromJournal(events);
2372
+ const startedEvent = events.find((e) => e.t === "run.started");
2373
+ const persistedAdapter = startedEvent?.adapter ?? DEFAULT_ADAPTER;
2374
+ if (opts.adapter !== void 0 && opts.adapter !== persistedAdapter) {
2375
+ throw new Error(
2376
+ `run ${runId} started with adapter "${persistedAdapter}" \u2014 cannot resume with adapter "${opts.adapter}"`
2377
+ );
2378
+ }
2379
+ const persistedIsolation = startedEvent?.isolation ?? "none";
2380
+ if (opts.isolation !== void 0 && opts.isolation !== persistedIsolation) {
2381
+ throw new Error(
2382
+ `run ${runId} started with isolation "${persistedIsolation}" \u2014 cannot resume with isolation "${opts.isolation}"`
2383
+ );
2384
+ }
2385
+ const stepCwd = persistedIsolation === "worktree" ? worktreePathFor(runsDir, runId) : cwd;
2386
+ const status = await runEligibleSteps(
2387
+ workflow,
2388
+ { runId, runDir, cwd: stepCwd, cacheDir, adapter: persistedAdapter, loopContinuations },
2389
+ resumeState
2390
+ );
2391
+ await appendJournalEvent(runDir, runId, { t: "run.finished", status });
2392
+ return { runId, runDir, status };
2393
+ }
2394
+
2395
+ // src/cli/commands/artifacts.ts
2396
+ async function artifactsCommand(runId, opts = {}) {
2397
+ const runsDir = opts.runsDir ?? defaultRunsDir();
2398
+ const resolvedRunId = runId ?? await findLatestRunId(runsDir);
2399
+ if (!resolvedRunId) {
2400
+ console.error("no runs found");
2401
+ return 1;
2402
+ }
2403
+ const runDir = path16.join(runsDir, resolvedRunId);
2404
+ const workflow = await readRunWorkflow(runDir).catch(() => void 0);
2405
+ if (!workflow) {
2406
+ console.error(`run ${resolvedRunId} not found`);
2407
+ return 1;
2408
+ }
2409
+ console.log(`run ${resolvedRunId}:`);
2410
+ const mapSteps = workflow.steps.filter((s) => s.kind === "map" && s.produces);
2411
+ if (mapSteps.length === 0) {
2412
+ console.log(" (no map steps with a produced artifact)");
2413
+ return 0;
2414
+ }
2415
+ for (const step of mapSteps) {
2416
+ const indices = await listMapItemArtifacts(runDir, step.produces);
2417
+ console.log(` ${step.id} (${step.produces}): items [${indices.join(", ")}]`);
2418
+ }
2419
+ return 0;
2420
+ }
2421
+
2422
+ // src/cli/commands/graph.ts
2423
+ async function graphCommand(workflowPath) {
2424
+ const raw = await loadWorkflowYaml(workflowPath);
2425
+ const workflow = normalizeWorkflow(raw);
2426
+ console.log(renderMermaid(workflow));
2427
+ return 0;
2428
+ }
2429
+
2430
+ // src/cli/commands/pending.ts
2431
+ import { readdir as readdir3 } from "fs/promises";
2432
+ import path17 from "path";
2433
+ async function pendingCommand(opts = {}) {
2434
+ const runsDir = opts.runsDir ?? defaultRunsDir();
2435
+ const runIds = (await readdir3(runsDir).catch(() => [])).sort();
2436
+ let anyPending = false;
2437
+ for (const runId of runIds) {
2438
+ const runDir = path17.join(runsDir, runId);
2439
+ const events = await readJournal(runDir);
2440
+ const last = events.at(-1);
2441
+ if (!last || last.t !== "run.finished" || last.status !== "suspended") continue;
2442
+ anyPending = true;
2443
+ console.log(`run ${runId} suspended:`);
2444
+ const openIds = openRequestStepIds(events);
2445
+ if (openIds.length === 0) {
2446
+ console.log(" (no open request found \u2014 loop budget suspend from before M4, unresumable)");
2447
+ continue;
2448
+ }
2449
+ for (const stepId of openIds) {
2450
+ const request = await readPendingRequest(runDir, stepId);
2451
+ if (!request) {
2452
+ console.log(` ${stepId}: (request file missing)`);
2453
+ continue;
2454
+ }
2455
+ if (request.kind === "gate") {
2456
+ console.log(` ${stepId} (gate): ${request.rendered.split("\n")[0]}`);
2457
+ } else {
2458
+ console.log(` ${stepId} (loop exhausted): tripped ${request.tripped} at iteration ${request.iteration}`);
2459
+ }
2460
+ }
2461
+ }
2462
+ if (!anyPending) console.log("nothing pending");
2463
+ return 0;
2464
+ }
2465
+
2466
+ // src/cli/commands/resume.ts
2467
+ async function resumeCommand(runId, adapter) {
2468
+ let result;
2469
+ try {
2470
+ result = await resumeRun(runId, { adapter });
2471
+ } catch (err) {
2472
+ console.error(err.message);
2473
+ return 1;
2474
+ }
2475
+ if (result.status === "ok") {
2476
+ console.log(`run ${result.runId} finished: ok`);
2477
+ return 0;
2478
+ }
2479
+ console.error(`run ${result.runId} finished: ${result.status}`);
2480
+ return 1;
2481
+ }
2482
+
2483
+ // src/cli/commands/run.ts
2484
+ import path18 from "path";
2485
+
2486
+ // src/cli/interactive.ts
2487
+ import { createInterface } from "readline";
2488
+ function terminalAsk(input, output) {
2489
+ const rl = createInterface({ input, output });
2490
+ const queued = [];
2491
+ const waiters = [];
2492
+ rl.on("line", (line) => {
2493
+ const waiter = waiters.shift();
2494
+ if (waiter) waiter(line);
2495
+ else queued.push(line);
2496
+ });
2497
+ return (question) => {
2498
+ output.write(question);
2499
+ const queuedLine = queued.shift();
2500
+ if (queuedLine !== void 0) return Promise.resolve(queuedLine);
2501
+ return new Promise((resolve) => waiters.push(resolve));
2502
+ };
2503
+ }
2504
+ async function promptForAnswer(request, ask, announce = () => {
2505
+ }) {
2506
+ if (request.kind === "gate") {
2507
+ announce(`
2508
+ ${request.rendered}
2509
+ `);
2510
+ } else {
2511
+ announce(`
2512
+ loop "${request.stepId}" exhausted (${request.tripped}) at iteration ${request.iteration}
2513
+ `);
2514
+ }
2515
+ const schema = request.answerSchema;
2516
+ const required = new Set(schema.required ?? []);
2517
+ const answer = {};
2518
+ for (const [name, prop] of Object.entries(schema.properties ?? {})) {
2519
+ const value = await promptField(ask, name, prop, required.has(name));
2520
+ if (value !== void 0) answer[name] = value;
2521
+ }
2522
+ return answer;
2523
+ }
2524
+ async function promptField(ask, name, prop, required) {
2525
+ const label = required ? name : `${name} (optional, blank to skip)`;
2526
+ for (; ; ) {
2527
+ const raw = (await ask(prop.enum ? `${label} [${prop.enum.join("/")}]: ` : `${label}: `)).trim();
2528
+ if (!raw) {
2529
+ if (!required) return void 0;
2530
+ continue;
2531
+ }
2532
+ if (prop.enum && !prop.enum.includes(raw)) continue;
2533
+ if (prop.type === "number") {
2534
+ const n = Number(raw);
2535
+ if (Number.isNaN(n)) continue;
2536
+ return n;
2537
+ }
2538
+ if (prop.type === "boolean") return /^y(es)?$/i.test(raw);
2539
+ return raw;
2540
+ }
2541
+ }
2542
+
2543
+ // src/cli/commands/run.ts
2544
+ var EX_SUSPEND = 78;
2545
+ async function answerOpenRequestsInteractively(runId, runDir) {
2546
+ const events = await readJournal(runDir);
2547
+ const openIds = new Set(openRequestStepIds(events));
2548
+ if (openIds.size === 0) return;
2549
+ const workflow = await readRunWorkflow(runDir);
2550
+ const orderedIds = workflow.steps.map((s) => s.id).filter((id) => openIds.has(id));
2551
+ const ask = terminalAsk(process.stdin, process.stdout);
2552
+ for (const stepId of orderedIds) {
2553
+ const request = await readPendingRequest(runDir, stepId);
2554
+ if (!request) continue;
2555
+ const answer = await promptForAnswer(request, ask, (text) => console.log(text));
2556
+ await writeAnswer(runDir, stepId, answer);
2557
+ }
2558
+ }
2559
+ async function runCommand(workflowPath, opts = {}) {
2560
+ let result = await executeWorkflowFile(path18.resolve(workflowPath), {
2561
+ adapter: opts.adapter,
2562
+ isolation: opts.isolation
2563
+ });
2564
+ while (opts.interactive && result.status === "suspended") {
2565
+ await answerOpenRequestsInteractively(result.runId, result.runDir);
2566
+ result = await resumeRun(result.runId, { adapter: opts.adapter });
2567
+ }
2568
+ if (result.status === "ok") {
2569
+ console.log(`run ${result.runId} finished: ok`);
2570
+ return 0;
2571
+ }
2572
+ if (result.status === "suspended") {
2573
+ console.log(`run ${result.runId} suspended \u2014 resume with:
2574
+ yak resume ${result.runId}`);
2575
+ return EX_SUSPEND;
2576
+ }
2577
+ console.error(`run ${result.runId} finished: failed`);
2578
+ return 1;
2579
+ }
2580
+
2581
+ // src/cli/commands/status.ts
2582
+ import path19 from "path";
2583
+
2584
+ // src/engine/status.ts
2585
+ function stepStatusesFromJournal(steps, events) {
2586
+ const stateByStep = /* @__PURE__ */ new Map();
2587
+ for (const event of events) {
2588
+ if (event.t === "step.started") {
2589
+ stateByStep.set(event.stepId, "pending");
2590
+ } else if (event.t === "step.completed") {
2591
+ stateByStep.set(event.stepId, event.stale ? "stale" : event.cached ? "cached" : "completed");
2592
+ } else if (event.t === "step.failed") {
2593
+ stateByStep.set(event.stepId, "failed");
2594
+ }
2595
+ }
2596
+ return steps.map((step) => ({ stepId: step.id, status: stateByStep.get(step.id) ?? "pending" }));
2597
+ }
2598
+ function loopStatusFromJournal(step, events) {
2599
+ const bodyStepIds = new Set(step.body.map((s) => s.id));
2600
+ let state = "pending";
2601
+ for (const event of events) {
2602
+ if (event.t === "step.started" && event.stepId === step.id) state = "running";
2603
+ else if (event.t === "step.completed" && event.stepId === step.id) state = "completed";
2604
+ else if (event.t === "step.failed" && event.stepId === step.id) state = "failed";
2605
+ else if (event.t === "run.suspended" && event.loopStepId === step.id) state = "suspended";
2606
+ }
2607
+ const iteration = events.filter((e) => e.t === "step.started" && bodyStepIds.has(e.stepId)).reduce((max, e) => Math.max(max, e.iteration ?? 0), 0);
2608
+ const consumedTokens = events.filter((e) => e.t === "budget.consumed" && bodyStepIds.has(e.stepId)).reduce((sum, e) => sum + e.tokens, 0);
2609
+ const lastIterationEvent = [...events].reverse().find((e) => e.t === "loop.iteration" && e.stepId === step.id);
2610
+ const noProgressSignal = lastIterationEvent?.t === "loop.iteration" ? lastIterationEvent.signal : void 0;
2611
+ return {
2612
+ stepId: step.id,
2613
+ state,
2614
+ iteration,
2615
+ maxIterations: step.budget.maxIterations,
2616
+ consumedTokens,
2617
+ ...step.budget.maxTokens !== void 0 ? { maxTokens: step.budget.maxTokens } : {},
2618
+ ...noProgressSignal !== void 0 ? { noProgressSignal } : {}
2619
+ };
2620
+ }
2621
+ function formatLoopBudget(detail) {
2622
+ return detail.maxTokens !== void 0 ? `iteration ${detail.iteration}/${detail.maxIterations}, tokens ${detail.consumedTokens}/${detail.maxTokens}` : `iteration ${detail.iteration}/${detail.maxIterations}, tokens ${detail.consumedTokens}`;
2623
+ }
2624
+
2625
+ // src/cli/commands/status.ts
2626
+ async function statusCommand(runId, opts = {}) {
2627
+ const runsDir = opts.runsDir ?? defaultRunsDir();
2628
+ const resolvedRunId = runId ?? await findLatestRunId(runsDir);
2629
+ if (!resolvedRunId) {
2630
+ console.error("no runs found");
2631
+ return 1;
2632
+ }
2633
+ const runDir = path19.join(runsDir, resolvedRunId);
2634
+ const workflow = await readRunWorkflow(runDir).catch(() => void 0);
2635
+ if (!workflow) {
2636
+ console.error(`run ${resolvedRunId} not found`);
2637
+ return 1;
2638
+ }
2639
+ const events = await readJournal(runDir);
2640
+ const statuses = stepStatusesFromJournal(workflow.steps, events);
2641
+ console.log(`run ${resolvedRunId}:`);
2642
+ for (const { stepId, status } of statuses) {
2643
+ console.log(` ${stepId}: ${status}`);
2644
+ const step = workflow.steps.find((s) => s.id === stepId);
2645
+ if (step?.kind === "loop") {
2646
+ const detail = loopStatusFromJournal(step, events);
2647
+ const signal = detail.noProgressSignal !== void 0 ? `, noProgress signal ${detail.noProgressSignal}` : "";
2648
+ console.log(` ${formatLoopBudget(detail)}${signal}`);
2649
+ }
2650
+ }
2651
+ return 0;
2652
+ }
2653
+
2654
+ // src/cli/commands/watch.ts
2655
+ import path20 from "path";
2656
+ import { render } from "ink";
2657
+ import React2 from "react";
2658
+
2659
+ // src/engine/watch.ts
2660
+ function isLoopStep(step) {
2661
+ return step.kind === "loop";
2662
+ }
2663
+ function snapshotFromJournal(workflow, events) {
2664
+ const stepStatuses = stepStatusesFromJournal(workflow.steps, events);
2665
+ const loopStatuses = flattenSteps(workflow.steps).filter(isLoopStep).map((step) => loopStatusFromJournal(step, events));
2666
+ const openGateIds = openRequestStepIds(events);
2667
+ const finishedEvent = [...events].reverse().find((e) => e.t === "run.finished");
2668
+ const runStatus = finishedEvent?.t === "run.finished" ? finishedEvent.status : void 0;
2669
+ return { stepStatuses, loopStatuses, openGateIds, ...runStatus !== void 0 ? { runStatus } : {} };
2670
+ }
2671
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2672
+ async function watchRun(runDir, workflow, onSnapshot, intervalMs = 300, deps = { readJournal, sleep: defaultSleep }) {
2673
+ let lastSerialized;
2674
+ for (; ; ) {
2675
+ const events = await deps.readJournal(runDir);
2676
+ const snapshot = snapshotFromJournal(workflow, events);
2677
+ const serialized = JSON.stringify(snapshot);
2678
+ if (serialized !== lastSerialized) {
2679
+ onSnapshot(snapshot);
2680
+ lastSerialized = serialized;
2681
+ }
2682
+ if (snapshot.runStatus !== void 0) return snapshot;
2683
+ await deps.sleep(intervalMs);
2684
+ }
2685
+ }
2686
+
2687
+ // src/cli/watch-ui.ts
2688
+ import { Box, Text } from "ink";
2689
+ import React from "react";
2690
+ var h = React.createElement;
2691
+ function WatchApp({ snapshot }) {
2692
+ const openGates = new Set(snapshot.openGateIds);
2693
+ const stepRows = snapshot.stepStatuses.map(
2694
+ (s) => h(
2695
+ Text,
2696
+ { key: s.stepId, color: openGates.has(s.stepId) ? "yellow" : void 0 },
2697
+ `${s.stepId}: ${s.status}${openGates.has(s.stepId) ? " \u2014 awaiting yak resume" : ""}`
2698
+ )
2699
+ );
2700
+ const loopRows = snapshot.loopStatuses.map(
2701
+ (l) => h(Text, { key: `loop-${l.stepId}`, dimColor: true }, ` ${l.stepId}: ${formatLoopBudget(l)}`)
2702
+ );
2703
+ const footer = snapshot.runStatus !== void 0 ? [h(Text, { key: "footer", bold: true }, `run ${snapshot.runStatus}`)] : [];
2704
+ return h(Box, { flexDirection: "column" }, ...stepRows, ...loopRows, ...footer);
2705
+ }
2706
+
2707
+ // src/cli/commands/watch.ts
2708
+ async function watchCommand(runId, opts = {}) {
2709
+ const runsDir = opts.runsDir ?? defaultRunsDir();
2710
+ const resolvedRunId = runId ?? await findLatestRunId(runsDir);
2711
+ if (!resolvedRunId) {
2712
+ console.error("no runs found");
2713
+ return 1;
2714
+ }
2715
+ const runDir = path20.join(runsDir, resolvedRunId);
2716
+ const workflow = await readRunWorkflow(runDir).catch(() => void 0);
2717
+ if (!workflow) {
2718
+ console.error(`run ${resolvedRunId} not found`);
2719
+ return 1;
2720
+ }
2721
+ const initialEvents = await readJournal(runDir);
2722
+ const instance = render(React2.createElement(WatchApp, { snapshot: snapshotFromJournal(workflow, initialEvents) }));
2723
+ await watchRun(runDir, workflow, (snapshot) => {
2724
+ instance.rerender(React2.createElement(WatchApp, { snapshot }));
2725
+ });
2726
+ instance.unmount();
2727
+ await instance.waitUntilExit();
2728
+ return 0;
2729
+ }
2730
+
2731
+ // src/cli/index.ts
2732
+ function requireAdapterId(value) {
2733
+ if (value !== "mock" && value !== "claude-code") {
2734
+ throw new Error(`--adapter must be "mock" or "claude-code", got "${value}"`);
2735
+ }
2736
+ return value;
2737
+ }
2738
+ function requireIsolation(value) {
2739
+ if (value !== "worktree" && value !== "none") {
2740
+ throw new Error(`--isolation must be "worktree" or "none", got "${value}"`);
2741
+ }
2742
+ return value;
2743
+ }
2744
+ var program = new Command();
2745
+ program.name("yak").description("An agentic workflow engine").version("0.0.1");
2746
+ program.command("run").description("Run a workflow YAML file").argument("<workflow>", "path to the workflow YAML file").option("--adapter <adapter>", "agent adapter to use", "claude-code").option("--interactive", "prompt inline for gate answers instead of exiting to resume later", false).option("--isolation <mode>", 'run inside a fresh git worktree ("worktree") or the plain cwd ("none")', "none").action(async (workflow, options) => {
2747
+ process.exitCode = await runCommand(workflow, {
2748
+ adapter: requireAdapterId(options.adapter),
2749
+ interactive: options.interactive,
2750
+ isolation: requireIsolation(options.isolation)
2751
+ });
2752
+ });
2753
+ program.command("resume").description("Resume an interrupted run").argument("<run-id>", "id of the run to resume").option("--adapter <adapter>", "must match the adapter the run started with, if given").action(async (runId, options) => {
2754
+ process.exitCode = await resumeCommand(runId, options.adapter ? requireAdapterId(options.adapter) : void 0);
2755
+ });
2756
+ program.command("status").description("Report a run's per-step status").argument("[run-id]", "id of the run to report on (default: most recent)").action(async (runId) => {
2757
+ process.exitCode = await statusCommand(runId);
2758
+ });
2759
+ program.command("pending").description("List every run across the repo awaiting a human answer").action(async () => {
2760
+ process.exitCode = await pendingCommand();
2761
+ });
2762
+ program.command("graph").description("Emit a workflow's DAG as Mermaid to stdout").argument("<workflow>", "path to the workflow YAML file").action(async (workflow) => {
2763
+ process.exitCode = await graphCommand(workflow);
2764
+ });
2765
+ program.command("watch").description("Live-tail a run's step statuses in a terminal UI").argument("[run-id]", "id of the run to watch (default: most recent)").action(async (runId) => {
2766
+ process.exitCode = await watchCommand(runId);
2767
+ });
2768
+ program.command("artifacts").description("List a map step's per-item artifact files for a run").argument("[run-id]", "id of the run to report on (default: most recent)").action(async (runId) => {
2769
+ process.exitCode = await artifactsCommand(runId);
2770
+ });
2771
+ program.parse();