@lchase/yak 0.6.0 → 0.7.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 +5 -2
- package/dist/index.js +70 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,11 @@ bug) for a guided walkthrough — also live at
|
|
|
46
46
|
and journals a clean `failed`/`cancelled` terminal state, so any
|
|
47
47
|
out-of-process supervisor has a supported kill.
|
|
48
48
|
- **Worktree isolation:** `yak run --isolation worktree` runs the whole
|
|
49
|
-
workflow inside a fresh git worktree
|
|
50
|
-
|
|
49
|
+
workflow inside a fresh git worktree forked from `HEAD`. `--from <ref>`
|
|
50
|
+
forks that worktree from another commit (`origin/main`, a branch, a
|
|
51
|
+
SHA) without moving the checkout; the ref is resolved once and resume
|
|
52
|
+
reuses that commit. `map` steps can additionally isolate each item
|
|
53
|
+
into its own sibling worktree forked from the run worktree.
|
|
51
54
|
- **Introspection:** `yak graph <workflow>` emits a workflow's DAG as
|
|
52
55
|
Mermaid; `yak watch [<run-id>]` live-tails a run's step statuses in a
|
|
53
56
|
terminal UI; `yak artifacts [<run-id>]` lists a `map` step's per-item
|
package/dist/index.js
CHANGED
|
@@ -68,7 +68,7 @@ async function writeRejectedOutput(runDir, stepId, attempt, content) {
|
|
|
68
68
|
|
|
69
69
|
// src/engine/run.ts
|
|
70
70
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
71
|
-
import { mkdir as mkdir6, readdir as readdir2, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
71
|
+
import { access, mkdir as mkdir6, readdir as readdir2, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
72
72
|
import path15 from "path";
|
|
73
73
|
import { z as z9 } from "zod";
|
|
74
74
|
|
|
@@ -344,7 +344,7 @@ import { promisify } from "util";
|
|
|
344
344
|
import jexl from "jexl";
|
|
345
345
|
var PLACEHOLDER_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
346
346
|
var IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/y;
|
|
347
|
-
var NOT_A_ROOT = /* @__PURE__ */ new Set(["true", "false", "in"]);
|
|
347
|
+
var NOT_A_ROOT = /* @__PURE__ */ new Set(["true", "false", "in", "null", "undefined"]);
|
|
348
348
|
function extractTemplateRoots(template) {
|
|
349
349
|
const roots = [];
|
|
350
350
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1158,6 +1158,9 @@ import { promisify as promisify2 } from "util";
|
|
|
1158
1158
|
var execFileAsync2 = promisify2(execFile2);
|
|
1159
1159
|
var WorktreeCreationError = class extends Error {
|
|
1160
1160
|
};
|
|
1161
|
+
var UnknownRefError = class extends Error {
|
|
1162
|
+
name = "UnknownRefError";
|
|
1163
|
+
};
|
|
1161
1164
|
var ENGINE_AUTHOR = ["-c", "user.name=yak", "-c", "user.email=engine@yak.local"];
|
|
1162
1165
|
async function commitAllIfDirty(repoRoot, message) {
|
|
1163
1166
|
const { stdout } = await execFileAsync2("git", ["status", "--porcelain"], { cwd: repoRoot });
|
|
@@ -1188,6 +1191,37 @@ async function createWorktree(repoRoot, branchName, baseRef, worktreePath) {
|
|
|
1188
1191
|
}
|
|
1189
1192
|
}
|
|
1190
1193
|
}
|
|
1194
|
+
async function resolveCommit(repoRoot, ref) {
|
|
1195
|
+
try {
|
|
1196
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "--verify", `${ref}^{commit}`], { cwd: repoRoot });
|
|
1197
|
+
const sha = stdout.trim();
|
|
1198
|
+
if (sha === "") throw new UnknownRefError(`unknown ref "${ref}"`);
|
|
1199
|
+
return sha;
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
if (err instanceof UnknownRefError) throw err;
|
|
1202
|
+
throw new UnknownRefError(`unknown ref "${ref}"`);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function localBranchExists(repoRoot, branchName) {
|
|
1206
|
+
try {
|
|
1207
|
+
await execFileAsync2("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branchName}`], {
|
|
1208
|
+
cwd: repoRoot
|
|
1209
|
+
});
|
|
1210
|
+
return true;
|
|
1211
|
+
} catch {
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function attachWorktree(repoRoot, branchName, worktreePath) {
|
|
1216
|
+
try {
|
|
1217
|
+
await execFileAsync2("git", ["worktree", "add", worktreePath, branchName], { cwd: repoRoot });
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1220
|
+
throw new WorktreeCreationError(
|
|
1221
|
+
`failed to attach worktree at "${worktreePath}" on existing branch "${branchName}": ${message}`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1191
1225
|
|
|
1192
1226
|
// src/engine/scheduler.ts
|
|
1193
1227
|
import path14 from "path";
|
|
@@ -2612,6 +2646,25 @@ function worktreePathFor(runsDir, runId) {
|
|
|
2612
2646
|
function worktreeBranchFor(runId) {
|
|
2613
2647
|
return `yak/${runId}`;
|
|
2614
2648
|
}
|
|
2649
|
+
async function pathExists(p) {
|
|
2650
|
+
try {
|
|
2651
|
+
await access(p);
|
|
2652
|
+
return true;
|
|
2653
|
+
} catch {
|
|
2654
|
+
return false;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
async function materializeRunWorktree(repoRoot, runsDir, runId, worktreeBase) {
|
|
2658
|
+
const worktreePath = worktreePathFor(runsDir, runId);
|
|
2659
|
+
if (await pathExists(worktreePath)) return;
|
|
2660
|
+
if (worktreeBase === void 0) return;
|
|
2661
|
+
const branchName = worktreeBranchFor(runId);
|
|
2662
|
+
if (await localBranchExists(repoRoot, branchName)) {
|
|
2663
|
+
await attachWorktree(repoRoot, branchName, worktreePath);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
await createWorktree(repoRoot, branchName, worktreeBase, worktreePath);
|
|
2667
|
+
}
|
|
2615
2668
|
function generateRunId() {
|
|
2616
2669
|
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2617
2670
|
const [datePart, timePart] = iso.split("T");
|
|
@@ -2623,10 +2676,14 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2623
2676
|
const { runsDir, cwd, cacheDir } = resolveDirs(opts.runsDir, opts.cwd, opts.cacheDir);
|
|
2624
2677
|
const adapter = opts.adapter ?? DEFAULT_ADAPTER;
|
|
2625
2678
|
const isolation = opts.isolation ?? "none";
|
|
2679
|
+
if (opts.from !== void 0 && isolation !== "worktree") {
|
|
2680
|
+
throw new Error("--from requires --isolation worktree");
|
|
2681
|
+
}
|
|
2626
2682
|
const raw = await loadWorkflowYaml(workflowPath);
|
|
2627
2683
|
const workflow = normalizeWorkflow(raw);
|
|
2628
2684
|
await validateWorkflow(workflow, cwd, isolation);
|
|
2629
2685
|
const inputValue = await resolveRunInput(workflow, opts.input, cwd);
|
|
2686
|
+
const worktreeBase = opts.from !== void 0 ? await resolveCommit(cwd, opts.from) : void 0;
|
|
2630
2687
|
const runId = generateRunId();
|
|
2631
2688
|
const runDir = path15.join(runsDir, runId);
|
|
2632
2689
|
await mkdir6(runDir, { recursive: true });
|
|
@@ -2645,7 +2702,8 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2645
2702
|
adapter,
|
|
2646
2703
|
isolation,
|
|
2647
2704
|
pid: process.pid,
|
|
2648
|
-
...opts.tag !== void 0 ? { tag: opts.tag } : {}
|
|
2705
|
+
...opts.tag !== void 0 ? { tag: opts.tag } : {},
|
|
2706
|
+
...worktreeBase !== void 0 ? { worktreeBase } : {}
|
|
2649
2707
|
});
|
|
2650
2708
|
if (inputWritten) {
|
|
2651
2709
|
await appendJournalEvent(runDir, runId, {
|
|
@@ -2664,7 +2722,7 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2664
2722
|
if (isolation === "worktree") {
|
|
2665
2723
|
const worktreePath = worktreePathFor(runsDir, runId);
|
|
2666
2724
|
try {
|
|
2667
|
-
await createWorktree(cwd, worktreeBranchFor(runId), "HEAD", worktreePath);
|
|
2725
|
+
await createWorktree(cwd, worktreeBranchFor(runId), worktreeBase ?? "HEAD", worktreePath);
|
|
2668
2726
|
} catch {
|
|
2669
2727
|
await appendJournalEvent(runDir, runId, { t: "run.finished", status: "failed" });
|
|
2670
2728
|
return { runId, runDir, status: "failed" };
|
|
@@ -2751,6 +2809,9 @@ async function resumeRun(runId, opts = {}) {
|
|
|
2751
2809
|
`run ${runId} started with isolation "${persistedIsolation}" \u2014 cannot resume with isolation "${opts.isolation}"`
|
|
2752
2810
|
);
|
|
2753
2811
|
}
|
|
2812
|
+
if (persistedIsolation === "worktree") {
|
|
2813
|
+
await materializeRunWorktree(cwd, runsDir, runId, startedEvent?.worktreeBase);
|
|
2814
|
+
}
|
|
2754
2815
|
const stepCwd = persistedIsolation === "worktree" ? worktreePathFor(runsDir, runId) : cwd;
|
|
2755
2816
|
const seedArtifactHashes = await seedInputHash(runDir);
|
|
2756
2817
|
const status = await runEligibleSteps(
|
|
@@ -3145,6 +3206,7 @@ async function runCommand(workflowPath, opts = {}) {
|
|
|
3145
3206
|
isolation: opts.isolation,
|
|
3146
3207
|
input: opts.input,
|
|
3147
3208
|
tag: opts.tag,
|
|
3209
|
+
from: opts.from,
|
|
3148
3210
|
// yak#23: emit the run id (and worktree branch) to stderr the moment
|
|
3149
3211
|
// `run.started` is journalled, so an out-of-process launcher can
|
|
3150
3212
|
// capture it without waiting for a terminal state. stdout is
|
|
@@ -3335,6 +3397,9 @@ function requireIsolation(value) {
|
|
|
3335
3397
|
var program = new Command();
|
|
3336
3398
|
program.name("yak").description("An agentic workflow engine").version(readVersion());
|
|
3337
3399
|
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(
|
|
3400
|
+
"--from <ref>",
|
|
3401
|
+
"fork the isolation worktree from this ref instead of HEAD (requires --isolation worktree)"
|
|
3402
|
+
).option(
|
|
3338
3403
|
"--input <pair...>",
|
|
3339
3404
|
"workflow input as key=value, repeatable \u2014 validated against the workflow inputSchema"
|
|
3340
3405
|
).option("--tag <string>", "opaque correlation tag, stored on the run and echoed by yak pending/status").action(
|
|
@@ -3343,6 +3408,7 @@ program.command("run").description("Run a workflow YAML file").argument("<workfl
|
|
|
3343
3408
|
adapter: requireAdapterId(options.adapter),
|
|
3344
3409
|
interactive: options.interactive,
|
|
3345
3410
|
isolation: requireIsolation(options.isolation),
|
|
3411
|
+
from: options.from,
|
|
3346
3412
|
input: parseInputPairs(options.input),
|
|
3347
3413
|
tag: options.tag
|
|
3348
3414
|
});
|