@lchase/yak 0.6.1 → 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 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; `map` steps can additionally
50
- isolate each item into its own sibling worktree.
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
 
@@ -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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/yak",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "An agentic workflow engine",
5
5
  "license": "MIT",
6
6
  "type": "module",