@lchase/yak 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -61
- package/dist/index.js +311 -124
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs-site/static/img/yak-mascot.png" alt="yak mascot" width="320" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# yak
|
|
2
6
|
|
|
3
7
|
An agentic workflow engine. *It shaves the yak so you don't.*
|
|
@@ -6,77 +10,56 @@ Reads a YAML DAG, runs steps (shell commands, pure transforms, coding-agent
|
|
|
6
10
|
invocations, human gates), and journals everything to disk so runs resume
|
|
7
11
|
exactly where they stopped.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @lchase/yak
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yak run path/to/workflow.yaml
|
|
23
|
+
```
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
See `docs-site/docs/quickstart.md` (free, no API key, uses a mock adapter)
|
|
26
|
+
and `docs-site/docs/tutorial.md` (a real run that finds and fixes an actual
|
|
27
|
+
bug) for a guided walkthrough — also live at
|
|
28
|
+
[lchase.github.io/yak/docs/tutorial](https://lchase.github.io/yak/docs/tutorial).
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
journal correctly before nondeterministic steps go anywhere near it.
|
|
30
|
+
## Capabilities
|
|
22
31
|
|
|
23
32
|
- **Step kinds:** `command` (streamed shell exec, capture stdout/stderr/
|
|
24
33
|
exitCode, optional `idleTimeoutMs` — SIGTERM then SIGKILL after a grace
|
|
25
|
-
period
|
|
26
|
-
|
|
27
|
-
(prompt from `{ file }`/`{ inline }` with `{{placeholder}}` interpolation,
|
|
28
|
-
`context: 'fresh' | { inherit } | { session }`, Zod schema validation via
|
|
34
|
+
period), `transform` (named pure function from `.yak/transforms.ts`),
|
|
35
|
+
`agent` (prompt from `{ file }`/`{ inline }`, Zod schema validation via
|
|
29
36
|
`.yak/schemas.ts`, schema repair loop with retries), `loop` (bounded by
|
|
30
|
-
`maxIterations`, plus a
|
|
31
|
-
|
|
32
|
-
`isolation: 'worktree'`
|
|
33
|
-
|
|
34
|
-
steps' output), and `gate` (human decision point, `schema`-validated
|
|
35
|
-
answer, optional `skipIf`).
|
|
37
|
+
`maxIterations`, plus a no-progress exhaustion check), `map`
|
|
38
|
+
(concurrency cap, per-item failure policy, optional
|
|
39
|
+
`isolation: 'worktree'`), and `gate` (human decision point,
|
|
40
|
+
schema-validated answer, optional `skipIf`).
|
|
36
41
|
- **Suspend/resume:** a `gate` or an exhausted budget suspends the run
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
gates inline instead of exiting to resume later.
|
|
42
|
+
rather than failing it. `yak pending` lists every run across the repo
|
|
43
|
+
awaiting a human answer; `yak resume <run-id>` replays and continues;
|
|
44
|
+
`--interactive` prompts through open gates inline instead of exiting to
|
|
45
|
+
resume later.
|
|
42
46
|
- **Worktree isolation:** `yak run --isolation worktree` runs the whole
|
|
43
|
-
workflow inside a fresh git worktree
|
|
44
|
-
|
|
45
|
-
worktree.
|
|
47
|
+
workflow inside a fresh git worktree; `map` steps can additionally
|
|
48
|
+
isolate each item into its own sibling worktree.
|
|
46
49
|
- **Introspection:** `yak graph <workflow>` emits a workflow's DAG as
|
|
47
50
|
Mermaid; `yak watch [<run-id>]` live-tails a run's step statuses in a
|
|
48
51
|
terminal UI; `yak artifacts [<run-id>]` lists a `map` step's per-item
|
|
49
|
-
artifact files
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **Caching:** two-tier content-addressed cache key (`semanticKey` over
|
|
57
|
-
step id + input artifact hashes, `definitionKey` over the step's own
|
|
58
|
-
definition). `cache: strict` (default) re-runs on any mismatch;
|
|
59
|
-
`cache: loose` reuses the artifact and marks the step `stale` when only
|
|
60
|
-
the definition moved.
|
|
61
|
-
- **Journal:** append-only JSONL event log of every step's lifecycle.
|
|
62
|
-
- **Resume:** `yak resume <run-id>` replays a run's journal to find what
|
|
63
|
-
already completed (a step left `step.started` with no matching
|
|
64
|
-
`step.completed`/`step.failed` is treated as interrupted), recomputes each
|
|
65
|
-
completed step's cache keys against the frozen workflow, reuses the
|
|
66
|
-
on-disk artifact on a match, and re-runs everything from the first
|
|
67
|
-
mismatch downward.
|
|
68
|
-
- **Status:** `yak status [<run-id>]` replays a run's journal into per-step
|
|
69
|
-
state (`completed`/`cached`/`stale`/`failed`/`pending`), with no db or
|
|
70
|
-
extra durable state beyond the journal. Defaults to the most recent run
|
|
71
|
-
when `<run-id>` is omitted.
|
|
52
|
+
artifact files; `yak status [<run-id>]` reports per-step state
|
|
53
|
+
(`completed`/`cached`/`stale`/`failed`/`pending`).
|
|
54
|
+
- **Caching:** content-addressed — re-runs only what a workflow or
|
|
55
|
+
artifact change actually invalidates, everything else is reused from a
|
|
56
|
+
prior run.
|
|
57
|
+
- **Journal:** append-only event log of every step's lifecycle is the
|
|
58
|
+
only durable state — no database.
|
|
72
59
|
- **Adapters:** `mock` (fixture-driven, deterministic, free) and
|
|
73
|
-
`claude-code` (real, wraps `@anthropic-ai/claude-agent-sdk
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
`--adapter` override. `claude-code.ts`'s own tests stub `query()` at the
|
|
77
|
-
module boundary — CI never calls a real model, per CLAUDE.md.
|
|
60
|
+
`claude-code` (real, wraps `@anthropic-ai/claude-agent-sdk`, also works
|
|
61
|
+
against any Anthropic-compatible local model server via
|
|
62
|
+
`ANTHROPIC_BASE_URL`). Selected with `yak run --adapter mock|claude-code`.
|
|
78
63
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
beyond spec.md, and npm publish (`docs-site/`'s quickstart still targets
|
|
82
|
-
a git-clone install for that reason).
|
|
64
|
+
Full behavior reference: `spec.md`. Release process and commit
|
|
65
|
+
conventions: `CONTRIBUTING.md`.
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,7 @@ async function writeRejectedOutput(runDir, stepId, attempt, content) {
|
|
|
70
70
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
71
71
|
import { mkdir as mkdir6, readdir as readdir2, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
72
72
|
import path15 from "path";
|
|
73
|
+
import { z as z9 } from "zod";
|
|
73
74
|
|
|
74
75
|
// src/ir/graph.ts
|
|
75
76
|
function buildProducerMap(steps) {
|
|
@@ -206,7 +207,7 @@ var rawStepSchema = z2.lazy(
|
|
|
206
207
|
var rawWorkflowSchema = z2.object({
|
|
207
208
|
name: z2.string(),
|
|
208
209
|
version: z2.string(),
|
|
209
|
-
inputSchema:
|
|
210
|
+
inputSchema: rawSchemaSpecSchema.optional(),
|
|
210
211
|
steps: z2.array(rawStepSchema)
|
|
211
212
|
});
|
|
212
213
|
function normalizeStep(raw) {
|
|
@@ -318,6 +319,9 @@ function normalizeWorkflow(raw) {
|
|
|
318
319
|
};
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
// src/ir/types.ts
|
|
323
|
+
var INPUT_ARTIFACT = "input";
|
|
324
|
+
|
|
321
325
|
// src/ir/validate.ts
|
|
322
326
|
import { execFile } from "child_process";
|
|
323
327
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -481,6 +485,7 @@ async function validateWorkflow(workflow, cwd = process.cwd(), runIsolation = "n
|
|
|
481
485
|
checkKnownKinds(flatSteps);
|
|
482
486
|
checkDuplicateIds(flatSteps);
|
|
483
487
|
const producerOf = buildProducerMap(workflow.steps);
|
|
488
|
+
checkReservedArtifactNames(flatSteps);
|
|
484
489
|
checkNeedsSatisfied(workflow.steps, producerOf);
|
|
485
490
|
checkNoCycles(workflow.steps, producerOf);
|
|
486
491
|
checkExitCodeReads(workflow.steps, producerOf);
|
|
@@ -569,9 +574,19 @@ function checkDuplicateIds(steps) {
|
|
|
569
574
|
seen.add(step.id);
|
|
570
575
|
}
|
|
571
576
|
}
|
|
577
|
+
function checkReservedArtifactNames(steps) {
|
|
578
|
+
for (const step of steps) {
|
|
579
|
+
if (step.produces === INPUT_ARTIFACT) {
|
|
580
|
+
throw new WorkflowValidationError(
|
|
581
|
+
`step "${step.id}": "${INPUT_ARTIFACT}" is a reserved artifact name (the run input) \u2014 a step cannot produce it`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
572
586
|
function checkNeedsSatisfied(steps, producerOf) {
|
|
573
587
|
for (const step of steps) {
|
|
574
588
|
for (const need of step.needs ?? []) {
|
|
589
|
+
if (need === INPUT_ARTIFACT) continue;
|
|
575
590
|
if (!producerOf.has(need)) {
|
|
576
591
|
throw new WorkflowValidationError(
|
|
577
592
|
`step "${step.id}": needs artifact "${need}" but no step produces it`
|
|
@@ -681,9 +696,9 @@ function checkFlatAnswerSchema(step, schema) {
|
|
|
681
696
|
async function checkGateRenderPlaceholders(steps, cwd) {
|
|
682
697
|
for (const step of steps) {
|
|
683
698
|
if (step.kind !== "gate") continue;
|
|
684
|
-
const
|
|
699
|
+
const text2 = "file" in step.render ? await readFile3(path3.resolve(cwd, step.render.file), "utf8") : step.render.inline;
|
|
685
700
|
const allowed = new Set(step.needs ?? []);
|
|
686
|
-
for (const root of extractTemplateRoots(
|
|
701
|
+
for (const root of extractTemplateRoots(text2)) {
|
|
687
702
|
if (!allowed.has(root)) {
|
|
688
703
|
throw new WorkflowValidationError(
|
|
689
704
|
`step "${step.id}": render references "{{${root}}}" but "${root}" is not in needs`
|
|
@@ -695,9 +710,9 @@ async function checkGateRenderPlaceholders(steps, cwd) {
|
|
|
695
710
|
async function checkAgentPromptPlaceholders(steps, cwd) {
|
|
696
711
|
for (const step of steps) {
|
|
697
712
|
if (step.kind !== "agent") continue;
|
|
698
|
-
const
|
|
713
|
+
const text2 = "file" in step.prompt ? await readFile3(path3.resolve(cwd, step.prompt.file), "utf8") : step.prompt.inline;
|
|
699
714
|
const allowed = new Set(agentInputNames(step));
|
|
700
|
-
for (const root of extractTemplateRoots(
|
|
715
|
+
for (const root of extractTemplateRoots(text2)) {
|
|
701
716
|
if (!allowed.has(root)) {
|
|
702
717
|
throw new WorkflowValidationError(
|
|
703
718
|
`step "${step.id}": prompt references "{{${root}}}" but "${root}" is not in needs or context.inherit`
|
|
@@ -883,8 +898,8 @@ async function evalExpr(expr, context, cwd) {
|
|
|
883
898
|
|
|
884
899
|
// src/steps/gate.ts
|
|
885
900
|
async function renderGate(step, inputs, cwd) {
|
|
886
|
-
const
|
|
887
|
-
return renderTemplate(
|
|
901
|
+
const text2 = "file" in step.render ? await readFile6(path7.resolve(cwd, step.render.file), "utf8") : step.render.inline;
|
|
902
|
+
return renderTemplate(text2, inputs);
|
|
888
903
|
}
|
|
889
904
|
async function completeGate(step, ctx, answer, opts) {
|
|
890
905
|
let artifactName;
|
|
@@ -997,8 +1012,8 @@ import {
|
|
|
997
1012
|
import { readFile as readFile7 } from "fs/promises";
|
|
998
1013
|
import path8 from "path";
|
|
999
1014
|
async function buildPrompt(step, inputs, cwd) {
|
|
1000
|
-
const
|
|
1001
|
-
return renderTemplate(
|
|
1015
|
+
const text2 = "file" in step.prompt ? await readFile7(path8.resolve(cwd, step.prompt.file), "utf8") : step.prompt.inline;
|
|
1016
|
+
return renderTemplate(text2, inputs);
|
|
1002
1017
|
}
|
|
1003
1018
|
async function callAgentOnce(step, inputs, ctx, promptSuffix) {
|
|
1004
1019
|
const prompt = await buildPrompt(step, inputs, ctx.cwd) + (promptSuffix ?? "");
|
|
@@ -1248,11 +1263,20 @@ var ClaudeCodeAdapter = class {
|
|
|
1248
1263
|
await mkdir4(path9.dirname(sessionLogPath), { recursive: true });
|
|
1249
1264
|
const filesChanged = /* @__PURE__ */ new Set();
|
|
1250
1265
|
let result;
|
|
1251
|
-
|
|
1252
|
-
|
|
1266
|
+
const stream = query({ prompt: req.prompt, options });
|
|
1267
|
+
try {
|
|
1268
|
+
for await (const message of stream) {
|
|
1269
|
+
await appendFile2(sessionLogPath, `${JSON.stringify(message)}
|
|
1253
1270
|
`, "utf8");
|
|
1254
|
-
|
|
1255
|
-
|
|
1271
|
+
collectFilesChanged(message, filesChanged);
|
|
1272
|
+
if (message.type === "result") result = message;
|
|
1273
|
+
}
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
throw new AgentStepFailedError({
|
|
1276
|
+
reason: "adapter-error",
|
|
1277
|
+
detail: `claude-code adapter for step "${this.stepId}": query() stream errored: ${err.message}`,
|
|
1278
|
+
recoverable: true
|
|
1279
|
+
});
|
|
1256
1280
|
}
|
|
1257
1281
|
if (!result) {
|
|
1258
1282
|
throw new AgentStepFailedError({
|
|
@@ -2063,7 +2087,7 @@ async function runEligibleSteps(workflow, ctx, resumeState) {
|
|
|
2063
2087
|
const completed = /* @__PURE__ */ new Set();
|
|
2064
2088
|
const failedSteps = /* @__PURE__ */ new Set();
|
|
2065
2089
|
const inFlight = /* @__PURE__ */ new Map();
|
|
2066
|
-
const artifactHashes =
|
|
2090
|
+
const artifactHashes = new Map(ctx.seedArtifactHashes ?? []);
|
|
2067
2091
|
let failed = false;
|
|
2068
2092
|
let suspended = false;
|
|
2069
2093
|
if (resumeState) trustResumedSteps(remaining, producerOf, resumeState, completed, artifactHashes);
|
|
@@ -2259,6 +2283,31 @@ async function runStep(step, ctx, artifactHashes, workflowName, agentSessionIds)
|
|
|
2259
2283
|
|
|
2260
2284
|
// src/engine/run.ts
|
|
2261
2285
|
var DEFAULT_ADAPTER = "claude-code";
|
|
2286
|
+
var InputValidationError = class extends Error {
|
|
2287
|
+
name = "InputValidationError";
|
|
2288
|
+
};
|
|
2289
|
+
async function resolveRunInput(workflow, rawInput, cwd) {
|
|
2290
|
+
const stepNeedsInput = flattenSteps(workflow.steps).some((s) => (s.needs ?? []).includes(INPUT_ARTIFACT));
|
|
2291
|
+
if (rawInput === void 0 && workflow.inputSchema === void 0 && !stepNeedsInput) {
|
|
2292
|
+
return void 0;
|
|
2293
|
+
}
|
|
2294
|
+
const provided = rawInput ?? {};
|
|
2295
|
+
if (workflow.inputSchema === void 0) return provided;
|
|
2296
|
+
const schema = await resolveSchemaSpec(workflow.inputSchema, cwd);
|
|
2297
|
+
const parsed = schema.safeParse(provided);
|
|
2298
|
+
if (!parsed.success) {
|
|
2299
|
+
throw new InputValidationError(
|
|
2300
|
+
`--input does not satisfy the workflow's inputSchema:
|
|
2301
|
+
${parsed.errorSummary}`
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
return parsed.data;
|
|
2305
|
+
}
|
|
2306
|
+
async function seedInputHash(runDir) {
|
|
2307
|
+
const value = await readArtifactRawOrUndefined(runDir, INPUT_ARTIFACT);
|
|
2308
|
+
if (value === void 0) return void 0;
|
|
2309
|
+
return /* @__PURE__ */ new Map([[INPUT_ARTIFACT, sha256(JSON.stringify(value, null, 2))]]);
|
|
2310
|
+
}
|
|
2262
2311
|
function defaultRunsDir() {
|
|
2263
2312
|
return path15.join(process.cwd(), ".runs");
|
|
2264
2313
|
}
|
|
@@ -2287,18 +2336,33 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2287
2336
|
const raw = await loadWorkflowYaml(workflowPath);
|
|
2288
2337
|
const workflow = normalizeWorkflow(raw);
|
|
2289
2338
|
await validateWorkflow(workflow, cwd, isolation);
|
|
2339
|
+
const inputValue = await resolveRunInput(workflow, opts.input, cwd);
|
|
2290
2340
|
const runId = generateRunId();
|
|
2291
2341
|
const runDir = path15.join(runsDir, runId);
|
|
2292
2342
|
await mkdir6(runDir, { recursive: true });
|
|
2293
2343
|
await writeFile4(path15.join(runDir, "workflow.json"), JSON.stringify(workflow, null, 2), "utf8");
|
|
2344
|
+
let seedArtifactHashes;
|
|
2345
|
+
let inputWritten;
|
|
2346
|
+
if (inputValue !== void 0) {
|
|
2347
|
+
inputWritten = await writeArtifact(runDir, INPUT_ARTIFACT, inputValue, z9.unknown());
|
|
2348
|
+
seedArtifactHashes = /* @__PURE__ */ new Map([[INPUT_ARTIFACT, inputWritten.hash]]);
|
|
2349
|
+
}
|
|
2294
2350
|
await appendJournalEvent(runDir, runId, {
|
|
2295
2351
|
t: "run.started",
|
|
2296
2352
|
runId,
|
|
2297
2353
|
workflow: workflow.name,
|
|
2298
|
-
inputHash: sha256(JSON.stringify(workflow)),
|
|
2354
|
+
inputHash: inputWritten?.hash ?? sha256(JSON.stringify(workflow)),
|
|
2299
2355
|
adapter,
|
|
2300
2356
|
isolation
|
|
2301
2357
|
});
|
|
2358
|
+
if (inputWritten) {
|
|
2359
|
+
await appendJournalEvent(runDir, runId, {
|
|
2360
|
+
t: "artifact.written",
|
|
2361
|
+
name: inputWritten.name,
|
|
2362
|
+
hash: inputWritten.hash,
|
|
2363
|
+
bytes: inputWritten.bytes
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2302
2366
|
let stepCwd = cwd;
|
|
2303
2367
|
if (isolation === "worktree") {
|
|
2304
2368
|
const worktreePath = worktreePathFor(runsDir, runId);
|
|
@@ -2310,7 +2374,14 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2310
2374
|
}
|
|
2311
2375
|
stepCwd = worktreePath;
|
|
2312
2376
|
}
|
|
2313
|
-
const status = await runEligibleSteps(workflow, {
|
|
2377
|
+
const status = await runEligibleSteps(workflow, {
|
|
2378
|
+
runId,
|
|
2379
|
+
runDir,
|
|
2380
|
+
cwd: stepCwd,
|
|
2381
|
+
cacheDir,
|
|
2382
|
+
adapter,
|
|
2383
|
+
seedArtifactHashes
|
|
2384
|
+
});
|
|
2314
2385
|
await appendJournalEvent(runDir, runId, { t: "run.finished", status });
|
|
2315
2386
|
return { runId, runDir, status };
|
|
2316
2387
|
}
|
|
@@ -2383,9 +2454,18 @@ async function resumeRun(runId, opts = {}) {
|
|
|
2383
2454
|
);
|
|
2384
2455
|
}
|
|
2385
2456
|
const stepCwd = persistedIsolation === "worktree" ? worktreePathFor(runsDir, runId) : cwd;
|
|
2457
|
+
const seedArtifactHashes = await seedInputHash(runDir);
|
|
2386
2458
|
const status = await runEligibleSteps(
|
|
2387
2459
|
workflow,
|
|
2388
|
-
{
|
|
2460
|
+
{
|
|
2461
|
+
runId,
|
|
2462
|
+
runDir,
|
|
2463
|
+
cwd: stepCwd,
|
|
2464
|
+
cacheDir,
|
|
2465
|
+
adapter: persistedAdapter,
|
|
2466
|
+
loopContinuations,
|
|
2467
|
+
seedArtifactHashes
|
|
2468
|
+
},
|
|
2389
2469
|
resumeState
|
|
2390
2470
|
);
|
|
2391
2471
|
await appendJournalEvent(runDir, runId, { t: "run.finished", status });
|
|
@@ -2463,7 +2543,55 @@ async function pendingCommand(opts = {}) {
|
|
|
2463
2543
|
return 0;
|
|
2464
2544
|
}
|
|
2465
2545
|
|
|
2546
|
+
// src/engine/status.ts
|
|
2547
|
+
function stepStatusesFromJournal(steps, events) {
|
|
2548
|
+
const stateByStep = /* @__PURE__ */ new Map();
|
|
2549
|
+
for (const event of events) {
|
|
2550
|
+
if (event.t === "step.started") {
|
|
2551
|
+
stateByStep.set(event.stepId, "pending");
|
|
2552
|
+
} else if (event.t === "step.completed") {
|
|
2553
|
+
stateByStep.set(event.stepId, event.stale ? "stale" : event.cached ? "cached" : "completed");
|
|
2554
|
+
} else if (event.t === "step.failed") {
|
|
2555
|
+
stateByStep.set(event.stepId, "failed");
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return steps.map((step) => ({ stepId: step.id, status: stateByStep.get(step.id) ?? "pending" }));
|
|
2559
|
+
}
|
|
2560
|
+
function failedStepsFromJournal(events) {
|
|
2561
|
+
return events.filter((e) => e.t === "step.failed").map((e) => ({ stepId: e.stepId, failure: e.failure }));
|
|
2562
|
+
}
|
|
2563
|
+
function formatStepFailure(failure) {
|
|
2564
|
+
return `${failure.detail} (${failure.reason})`;
|
|
2565
|
+
}
|
|
2566
|
+
function loopStatusFromJournal(step, events) {
|
|
2567
|
+
const bodyStepIds = new Set(step.body.map((s) => s.id));
|
|
2568
|
+
let state = "pending";
|
|
2569
|
+
for (const event of events) {
|
|
2570
|
+
if (event.t === "step.started" && event.stepId === step.id) state = "running";
|
|
2571
|
+
else if (event.t === "step.completed" && event.stepId === step.id) state = "completed";
|
|
2572
|
+
else if (event.t === "step.failed" && event.stepId === step.id) state = "failed";
|
|
2573
|
+
else if (event.t === "run.suspended" && event.loopStepId === step.id) state = "suspended";
|
|
2574
|
+
}
|
|
2575
|
+
const iteration = events.filter((e) => e.t === "step.started" && bodyStepIds.has(e.stepId)).reduce((max, e) => Math.max(max, e.iteration ?? 0), 0);
|
|
2576
|
+
const consumedTokens = events.filter((e) => e.t === "budget.consumed" && bodyStepIds.has(e.stepId)).reduce((sum, e) => sum + e.tokens, 0);
|
|
2577
|
+
const lastIterationEvent = [...events].reverse().find((e) => e.t === "loop.iteration" && e.stepId === step.id);
|
|
2578
|
+
const noProgressSignal = lastIterationEvent?.t === "loop.iteration" ? lastIterationEvent.signal : void 0;
|
|
2579
|
+
return {
|
|
2580
|
+
stepId: step.id,
|
|
2581
|
+
state,
|
|
2582
|
+
iteration,
|
|
2583
|
+
maxIterations: step.budget.maxIterations,
|
|
2584
|
+
consumedTokens,
|
|
2585
|
+
...step.budget.maxTokens !== void 0 ? { maxTokens: step.budget.maxTokens } : {},
|
|
2586
|
+
...noProgressSignal !== void 0 ? { noProgressSignal } : {}
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function formatLoopBudget(detail) {
|
|
2590
|
+
return detail.maxTokens !== void 0 ? `iteration ${detail.iteration}/${detail.maxIterations}, tokens ${detail.consumedTokens}/${detail.maxTokens}` : `iteration ${detail.iteration}/${detail.maxIterations}, tokens ${detail.consumedTokens}`;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2466
2593
|
// src/cli/commands/resume.ts
|
|
2594
|
+
var EX_SUSPEND = 78;
|
|
2467
2595
|
async function resumeCommand(runId, adapter) {
|
|
2468
2596
|
let result;
|
|
2469
2597
|
try {
|
|
@@ -2476,94 +2604,162 @@ async function resumeCommand(runId, adapter) {
|
|
|
2476
2604
|
console.log(`run ${result.runId} finished: ok`);
|
|
2477
2605
|
return 0;
|
|
2478
2606
|
}
|
|
2479
|
-
|
|
2607
|
+
if (result.status === "suspended") {
|
|
2608
|
+
console.log(`run ${result.runId} suspended \u2014 resume with:
|
|
2609
|
+
yak resume ${result.runId}`);
|
|
2610
|
+
return EX_SUSPEND;
|
|
2611
|
+
}
|
|
2612
|
+
console.error(`run ${result.runId} failed:`);
|
|
2613
|
+
const events = await readJournal(result.runDir);
|
|
2614
|
+
for (const { stepId, failure } of failedStepsFromJournal(events)) {
|
|
2615
|
+
console.error(` ${stepId}: ${formatStepFailure(failure)}`);
|
|
2616
|
+
}
|
|
2480
2617
|
return 1;
|
|
2481
2618
|
}
|
|
2482
2619
|
|
|
2483
2620
|
// src/cli/commands/run.ts
|
|
2484
2621
|
import path18 from "path";
|
|
2622
|
+
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
2485
2623
|
|
|
2486
2624
|
// src/cli/interactive.ts
|
|
2487
2625
|
import { createInterface } from "readline";
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
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 = () => {
|
|
2626
|
+
import { confirm, isCancel, select, text } from "@clack/prompts";
|
|
2627
|
+
var PromptCancelledError = class extends Error {
|
|
2628
|
+
constructor() {
|
|
2629
|
+
super("prompt cancelled");
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
async function promptForAnswer(request, promptField, announce = () => {
|
|
2505
2633
|
}) {
|
|
2506
2634
|
if (request.kind === "gate") {
|
|
2507
|
-
announce(
|
|
2508
|
-
${request.rendered}
|
|
2509
|
-
`);
|
|
2635
|
+
announce(request.rendered);
|
|
2510
2636
|
} else {
|
|
2511
|
-
announce(`
|
|
2512
|
-
loop "${request.stepId}" exhausted (${request.tripped}) at iteration ${request.iteration}
|
|
2513
|
-
`);
|
|
2637
|
+
announce(`loop "${request.stepId}" exhausted (${request.tripped}) at iteration ${request.iteration}`);
|
|
2514
2638
|
}
|
|
2515
2639
|
const schema = request.answerSchema;
|
|
2516
2640
|
const required = new Set(schema.required ?? []);
|
|
2517
2641
|
const answer = {};
|
|
2518
2642
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
2519
|
-
const value = await promptField(
|
|
2643
|
+
const value = await promptField({ name, prop, required: required.has(name) });
|
|
2520
2644
|
if (value !== void 0) answer[name] = value;
|
|
2521
2645
|
}
|
|
2522
2646
|
return answer;
|
|
2523
2647
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2648
|
+
function clackFieldPrompt() {
|
|
2649
|
+
return async ({ name, prop, required }) => {
|
|
2650
|
+
if (prop.enum) {
|
|
2651
|
+
const result = await select({
|
|
2652
|
+
message: name,
|
|
2653
|
+
options: prop.enum.map((value) => ({ value, label: value }))
|
|
2654
|
+
});
|
|
2655
|
+
if (isCancel(result)) throw new PromptCancelledError();
|
|
2656
|
+
return result;
|
|
2531
2657
|
}
|
|
2532
|
-
if (prop.
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
return n;
|
|
2658
|
+
if (prop.type === "boolean") {
|
|
2659
|
+
const result = await confirm({ message: name });
|
|
2660
|
+
if (isCancel(result)) throw new PromptCancelledError();
|
|
2661
|
+
return result;
|
|
2537
2662
|
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2663
|
+
const raw = await text({
|
|
2664
|
+
message: required ? name : `${name} (optional)`,
|
|
2665
|
+
placeholder: required ? void 0 : "blank to skip",
|
|
2666
|
+
validate(value) {
|
|
2667
|
+
if (!value) return required ? "required" : void 0;
|
|
2668
|
+
if (prop.type === "number" && Number.isNaN(Number(value))) return "must be a number";
|
|
2669
|
+
return void 0;
|
|
2670
|
+
}
|
|
2671
|
+
});
|
|
2672
|
+
if (isCancel(raw)) throw new PromptCancelledError();
|
|
2673
|
+
if (raw === "") return void 0;
|
|
2674
|
+
return prop.type === "number" ? Number(raw) : raw;
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function lineFieldPrompt(input, output) {
|
|
2678
|
+
const rl = createInterface({ input, output });
|
|
2679
|
+
const queued = [];
|
|
2680
|
+
const waiters = [];
|
|
2681
|
+
rl.on("line", (line) => {
|
|
2682
|
+
const waiter = waiters.shift();
|
|
2683
|
+
if (waiter) waiter(line);
|
|
2684
|
+
else queued.push(line);
|
|
2685
|
+
});
|
|
2686
|
+
const askLine = (question) => {
|
|
2687
|
+
output.write(question);
|
|
2688
|
+
const queuedLine = queued.shift();
|
|
2689
|
+
if (queuedLine !== void 0) return Promise.resolve(queuedLine);
|
|
2690
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
2691
|
+
};
|
|
2692
|
+
const promptField = async ({ name, prop, required }) => {
|
|
2693
|
+
const label = required ? name : `${name} (optional, blank to skip)`;
|
|
2694
|
+
for (; ; ) {
|
|
2695
|
+
const raw = (await askLine(prop.enum ? `${label} [${prop.enum.join("/")}]: ` : `${label}: `)).trim();
|
|
2696
|
+
if (!raw) {
|
|
2697
|
+
if (!required) return void 0;
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
if (prop.enum && !prop.enum.includes(raw)) continue;
|
|
2701
|
+
if (prop.type === "number") {
|
|
2702
|
+
const n = Number(raw);
|
|
2703
|
+
if (Number.isNaN(n)) continue;
|
|
2704
|
+
return n;
|
|
2705
|
+
}
|
|
2706
|
+
if (prop.type === "boolean") return /^y(es)?$/i.test(raw);
|
|
2707
|
+
return raw;
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
return { promptField, close: () => rl.close() };
|
|
2541
2711
|
}
|
|
2542
2712
|
|
|
2543
2713
|
// src/cli/commands/run.ts
|
|
2544
|
-
var
|
|
2714
|
+
var EX_SUSPEND2 = 78;
|
|
2545
2715
|
async function answerOpenRequestsInteractively(runId, runDir) {
|
|
2546
2716
|
const events = await readJournal(runDir);
|
|
2547
2717
|
const openIds = new Set(openRequestStepIds(events));
|
|
2548
2718
|
if (openIds.size === 0) return;
|
|
2549
2719
|
const workflow = await readRunWorkflow(runDir);
|
|
2550
2720
|
const orderedIds = workflow.steps.map((s) => s.id).filter((id) => openIds.has(id));
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2721
|
+
const isRealTerminal = process.stdin.isTTY && process.stdout.isTTY;
|
|
2722
|
+
const { promptField, close } = isRealTerminal ? { promptField: clackFieldPrompt(), close: () => {
|
|
2723
|
+
} } : lineFieldPrompt(process.stdin, process.stdout);
|
|
2724
|
+
const announce = isRealTerminal ? log.info : (text2) => console.log(`
|
|
2725
|
+
${text2}
|
|
2726
|
+
`);
|
|
2727
|
+
if (isRealTerminal) intro("yak: answer pending gates");
|
|
2728
|
+
try {
|
|
2729
|
+
for (const stepId of orderedIds) {
|
|
2730
|
+
const request = await readPendingRequest(runDir, stepId);
|
|
2731
|
+
if (!request) continue;
|
|
2732
|
+
try {
|
|
2733
|
+
const answer = await promptForAnswer(request, promptField, announce);
|
|
2734
|
+
await writeAnswer(runDir, stepId, answer);
|
|
2735
|
+
} catch (err) {
|
|
2736
|
+
if (err instanceof PromptCancelledError) {
|
|
2737
|
+
cancel(`"${stepId}" left unanswered \u2014 run still suspended`);
|
|
2738
|
+
throw new Error(`answering "${stepId}" was cancelled`);
|
|
2739
|
+
}
|
|
2740
|
+
throw err;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
} finally {
|
|
2744
|
+
close();
|
|
2557
2745
|
}
|
|
2746
|
+
if (isRealTerminal) outro("all gates answered");
|
|
2558
2747
|
}
|
|
2559
2748
|
async function runCommand(workflowPath, opts = {}) {
|
|
2560
|
-
let result
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2749
|
+
let result;
|
|
2750
|
+
try {
|
|
2751
|
+
result = await executeWorkflowFile(path18.resolve(workflowPath), {
|
|
2752
|
+
adapter: opts.adapter,
|
|
2753
|
+
isolation: opts.isolation,
|
|
2754
|
+
input: opts.input
|
|
2755
|
+
});
|
|
2756
|
+
while (opts.interactive && result.status === "suspended") {
|
|
2757
|
+
await answerOpenRequestsInteractively(result.runId, result.runDir);
|
|
2758
|
+
result = await resumeRun(result.runId, { adapter: opts.adapter });
|
|
2759
|
+
}
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
console.error(err.message);
|
|
2762
|
+
return 1;
|
|
2567
2763
|
}
|
|
2568
2764
|
if (result.status === "ok") {
|
|
2569
2765
|
console.log(`run ${result.runId} finished: ok`);
|
|
@@ -2572,57 +2768,18 @@ async function runCommand(workflowPath, opts = {}) {
|
|
|
2572
2768
|
if (result.status === "suspended") {
|
|
2573
2769
|
console.log(`run ${result.runId} suspended \u2014 resume with:
|
|
2574
2770
|
yak resume ${result.runId}`);
|
|
2575
|
-
return
|
|
2771
|
+
return EX_SUSPEND2;
|
|
2772
|
+
}
|
|
2773
|
+
console.error(`run ${result.runId} failed:`);
|
|
2774
|
+
const events = await readJournal(result.runDir);
|
|
2775
|
+
for (const { stepId, failure } of failedStepsFromJournal(events)) {
|
|
2776
|
+
console.error(` ${stepId}: ${formatStepFailure(failure)}`);
|
|
2576
2777
|
}
|
|
2577
|
-
console.error(`run ${result.runId} finished: failed`);
|
|
2578
2778
|
return 1;
|
|
2579
2779
|
}
|
|
2580
2780
|
|
|
2581
2781
|
// src/cli/commands/status.ts
|
|
2582
2782
|
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
2783
|
async function statusCommand(runId, opts = {}) {
|
|
2627
2784
|
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2628
2785
|
const resolvedRunId = runId ?? await findLatestRunId(runsDir);
|
|
@@ -2638,6 +2795,7 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2638
2795
|
}
|
|
2639
2796
|
const events = await readJournal(runDir);
|
|
2640
2797
|
const statuses = stepStatusesFromJournal(workflow.steps, events);
|
|
2798
|
+
const failuresByStep = new Map(failedStepsFromJournal(events).map((f) => [f.stepId, f]));
|
|
2641
2799
|
console.log(`run ${resolvedRunId}:`);
|
|
2642
2800
|
for (const { stepId, status } of statuses) {
|
|
2643
2801
|
console.log(` ${stepId}: ${status}`);
|
|
@@ -2647,6 +2805,8 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2647
2805
|
const signal = detail.noProgressSignal !== void 0 ? `, noProgress signal ${detail.noProgressSignal}` : "";
|
|
2648
2806
|
console.log(` ${formatLoopBudget(detail)}${signal}`);
|
|
2649
2807
|
}
|
|
2808
|
+
const failed = failuresByStep.get(stepId);
|
|
2809
|
+
if (failed) console.log(` ${formatStepFailure(failed.failure)}`);
|
|
2650
2810
|
}
|
|
2651
2811
|
return 0;
|
|
2652
2812
|
}
|
|
@@ -2728,6 +2888,22 @@ async function watchCommand(runId, opts = {}) {
|
|
|
2728
2888
|
return 0;
|
|
2729
2889
|
}
|
|
2730
2890
|
|
|
2891
|
+
// src/cli/parse-input.ts
|
|
2892
|
+
function parseInputPairs(pairs) {
|
|
2893
|
+
if (pairs === void 0 || pairs.length === 0) return void 0;
|
|
2894
|
+
const out = {};
|
|
2895
|
+
for (const pair of pairs) {
|
|
2896
|
+
const eq = pair.indexOf("=");
|
|
2897
|
+
if (eq <= 0) {
|
|
2898
|
+
throw new Error(`--input must be key=value, got "${pair}"`);
|
|
2899
|
+
}
|
|
2900
|
+
const key = pair.slice(0, eq);
|
|
2901
|
+
if (key in out) throw new Error(`--input key "${key}" given more than once`);
|
|
2902
|
+
out[key] = pair.slice(eq + 1);
|
|
2903
|
+
}
|
|
2904
|
+
return out;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2731
2907
|
// src/cli/index.ts
|
|
2732
2908
|
function requireAdapterId(value) {
|
|
2733
2909
|
if (value !== "mock" && value !== "claude-code") {
|
|
@@ -2743,13 +2919,19 @@ function requireIsolation(value) {
|
|
|
2743
2919
|
}
|
|
2744
2920
|
var program = new Command();
|
|
2745
2921
|
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").
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2922
|
+
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").option(
|
|
2923
|
+
"--input <pair...>",
|
|
2924
|
+
"workflow input as key=value, repeatable \u2014 validated against the workflow inputSchema"
|
|
2925
|
+
).action(
|
|
2926
|
+
async (workflow, options) => {
|
|
2927
|
+
process.exitCode = await runCommand(workflow, {
|
|
2928
|
+
adapter: requireAdapterId(options.adapter),
|
|
2929
|
+
interactive: options.interactive,
|
|
2930
|
+
isolation: requireIsolation(options.isolation),
|
|
2931
|
+
input: parseInputPairs(options.input)
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
);
|
|
2753
2935
|
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
2936
|
process.exitCode = await resumeCommand(runId, options.adapter ? requireAdapterId(options.adapter) : void 0);
|
|
2755
2937
|
});
|
|
@@ -2768,4 +2950,9 @@ program.command("watch").description("Live-tail a run's step statuses in a termi
|
|
|
2768
2950
|
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
2951
|
process.exitCode = await artifactsCommand(runId);
|
|
2770
2952
|
});
|
|
2771
|
-
|
|
2953
|
+
try {
|
|
2954
|
+
await program.parseAsync();
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
console.error(err.message);
|
|
2957
|
+
process.exitCode = 1;
|
|
2958
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lchase/yak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "An agentic workflow engine",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@anthropic-ai/claude-agent-sdk": "^0.3.228",
|
|
33
33
|
"@anthropic-ai/sdk": "^0.116.0",
|
|
34
|
+
"@clack/prompts": "^1.7.0",
|
|
34
35
|
"ajv": "^8.20.0",
|
|
35
36
|
"commander": "^12.1.0",
|
|
36
37
|
"ink": "^7.1.1",
|