@lchase/yak 0.4.1 → 0.5.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 +3 -1
- package/dist/index.js +136 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ bug) for a guided walkthrough — also live at
|
|
|
42
42
|
rather than failing it. `yak pending` lists every run across the repo
|
|
43
43
|
awaiting a human answer; `yak resume <run-id>` replays and continues;
|
|
44
44
|
`--interactive` prompts through open gates inline instead of exiting to
|
|
45
|
-
resume later.
|
|
45
|
+
resume later. `yak cancel <run-id>` terminates a live run's process tree
|
|
46
|
+
and journals a clean `failed`/`cancelled` terminal state, so any
|
|
47
|
+
out-of-process supervisor has a supported kill.
|
|
46
48
|
- **Worktree isolation:** `yak run --isolation worktree` runs the whole
|
|
47
49
|
workflow inside a fresh git worktree; `map` steps can additionally
|
|
48
50
|
isolate each item into its own sibling worktree.
|
package/dist/index.js
CHANGED
|
@@ -606,17 +606,17 @@ function checkNeedsSatisfied(steps, producerOf) {
|
|
|
606
606
|
function checkNoCycles(steps, producerOf) {
|
|
607
607
|
const stepsById = new Map(steps.map((s) => [s.id, s]));
|
|
608
608
|
const state = /* @__PURE__ */ new Map();
|
|
609
|
-
function visit(step,
|
|
609
|
+
function visit(step, path22) {
|
|
610
610
|
const mark = state.get(step.id);
|
|
611
611
|
if (mark === "done") return;
|
|
612
612
|
if (mark === "visiting") {
|
|
613
|
-
const cycle = [...
|
|
613
|
+
const cycle = [...path22, step.id].join(" -> ");
|
|
614
614
|
throw new WorkflowValidationError(`cycle detected: ${cycle}`);
|
|
615
615
|
}
|
|
616
616
|
state.set(step.id, "visiting");
|
|
617
617
|
for (const depId of dependenciesOf(step, producerOf)) {
|
|
618
618
|
const dep = stepsById.get(depId);
|
|
619
|
-
if (dep) visit(dep, [...
|
|
619
|
+
if (dep) visit(dep, [...path22, step.id]);
|
|
620
620
|
}
|
|
621
621
|
state.set(step.id, "done");
|
|
622
622
|
}
|
|
@@ -1652,11 +1652,28 @@ async function runLoopStep(step, ctx) {
|
|
|
1652
1652
|
return "failed";
|
|
1653
1653
|
}
|
|
1654
1654
|
const resumeFrom = ctx.loopContinuation?.action === "continue" ? await buildResumeFrom(step, ctx) : void 0;
|
|
1655
|
-
const
|
|
1656
|
-
if (status === "ok") {
|
|
1657
|
-
|
|
1655
|
+
const result = await runLoopBody(step, ctx, resumeFrom);
|
|
1656
|
+
if (result.status === "ok") {
|
|
1657
|
+
let artifact;
|
|
1658
|
+
if (step.produces) {
|
|
1659
|
+
const written = await writeArtifact(ctx.runDir, step.produces, result.untilContext, z6.unknown());
|
|
1660
|
+
artifact = { name: step.produces, hash: written.hash };
|
|
1661
|
+
ctx.outerArtifactHashes.set(step.produces, written.hash);
|
|
1662
|
+
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
1663
|
+
t: "artifact.written",
|
|
1664
|
+
name: step.produces,
|
|
1665
|
+
hash: written.hash,
|
|
1666
|
+
bytes: written.bytes
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
await appendJournalEvent(ctx.runDir, ctx.runId, {
|
|
1670
|
+
t: "step.completed",
|
|
1671
|
+
stepId: step.id,
|
|
1672
|
+
...artifact ? { artifact: artifact.name, artifactHash: artifact.hash } : {},
|
|
1673
|
+
cached: false
|
|
1674
|
+
});
|
|
1658
1675
|
}
|
|
1659
|
-
return status;
|
|
1676
|
+
return result.status;
|
|
1660
1677
|
}
|
|
1661
1678
|
async function buildResumeFrom(step, ctx) {
|
|
1662
1679
|
const events = await readJournal(ctx.runDir);
|
|
@@ -1719,12 +1736,12 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1719
1736
|
recoverable: false
|
|
1720
1737
|
}
|
|
1721
1738
|
});
|
|
1722
|
-
return "failed";
|
|
1739
|
+
return { status: "failed" };
|
|
1723
1740
|
}
|
|
1724
1741
|
throw err;
|
|
1725
1742
|
}
|
|
1726
1743
|
const untilContext = Object.fromEntries(thisValues);
|
|
1727
|
-
if (await evalExpr(step.until, untilContext, ctx.cwd)) return "ok";
|
|
1744
|
+
if (await evalExpr(step.until, untilContext, ctx.cwd)) return { status: "ok", untilContext };
|
|
1728
1745
|
let tripped;
|
|
1729
1746
|
if (step.budget.noProgress) {
|
|
1730
1747
|
lastSignal = await evalExpr(step.budget.noProgress.signal, untilContext, ctx.cwd);
|
|
@@ -1743,7 +1760,10 @@ async function runLoopBody(step, ctx, resumeFrom) {
|
|
|
1743
1760
|
const tokensTotal = await sumBodyTokens(ctx.runDir, bodyStepIds);
|
|
1744
1761
|
if (tokensTotal >= step.budget.maxTokens) tripped = "maxTokens";
|
|
1745
1762
|
}
|
|
1746
|
-
if (tripped)
|
|
1763
|
+
if (tripped) {
|
|
1764
|
+
const outcome = await handleExhausted(step, ctx, iteration, tripped);
|
|
1765
|
+
return outcome === "ok" ? { status: "ok", untilContext } : { status: outcome };
|
|
1766
|
+
}
|
|
1747
1767
|
priorValues = thisValues;
|
|
1748
1768
|
priorHashes = thisHashes;
|
|
1749
1769
|
}
|
|
@@ -2398,6 +2418,7 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2398
2418
|
inputHash: inputWritten?.hash ?? sha256(JSON.stringify(workflow)),
|
|
2399
2419
|
adapter,
|
|
2400
2420
|
isolation,
|
|
2421
|
+
pid: process.pid,
|
|
2401
2422
|
...opts.tag !== void 0 ? { tag: opts.tag } : {}
|
|
2402
2423
|
});
|
|
2403
2424
|
if (inputWritten) {
|
|
@@ -2486,6 +2507,7 @@ async function resumeRun(runId, opts = {}) {
|
|
|
2486
2507
|
const { runsDir, cwd, cacheDir } = resolveDirs(opts.runsDir, opts.cwd, opts.cacheDir);
|
|
2487
2508
|
const runDir = path15.join(runsDir, runId);
|
|
2488
2509
|
const workflow = await readRunWorkflow(runDir);
|
|
2510
|
+
await appendJournalEvent(runDir, runId, { t: "run.resumed", pid: process.pid });
|
|
2489
2511
|
let events = await readJournal(runDir);
|
|
2490
2512
|
const loopContinuations = await resolveOpenRequests(runId, runDir, workflow, cwd, events);
|
|
2491
2513
|
events = await readJournal(runDir);
|
|
@@ -2549,6 +2571,99 @@ async function artifactsCommand(runId, opts = {}) {
|
|
|
2549
2571
|
return 0;
|
|
2550
2572
|
}
|
|
2551
2573
|
|
|
2574
|
+
// src/engine/cancel.ts
|
|
2575
|
+
import path17 from "path";
|
|
2576
|
+
var SIGKILL_GRACE_MS = 5e3;
|
|
2577
|
+
var LIVENESS_POLL_MS = 100;
|
|
2578
|
+
var LIVENESS_POLLS = Math.ceil(SIGKILL_GRACE_MS / LIVENESS_POLL_MS);
|
|
2579
|
+
var realSignal = (pid, signal) => {
|
|
2580
|
+
process.kill(pid, signal);
|
|
2581
|
+
};
|
|
2582
|
+
function livePidFromJournal(events) {
|
|
2583
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2584
|
+
const event = events[i];
|
|
2585
|
+
if (event?.t === "run.resumed") return event.pid;
|
|
2586
|
+
}
|
|
2587
|
+
const started = events.find((e) => e.t === "run.started");
|
|
2588
|
+
return started?.t === "run.started" ? started.pid : void 0;
|
|
2589
|
+
}
|
|
2590
|
+
function isAlive(signal, pid) {
|
|
2591
|
+
try {
|
|
2592
|
+
signal(pid, 0);
|
|
2593
|
+
return true;
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
return err.code === "EPERM";
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
async function terminateProcessTree(signal, sleep, pid) {
|
|
2599
|
+
if (!isAlive(signal, pid)) return "not-running";
|
|
2600
|
+
try {
|
|
2601
|
+
signal(-pid, "SIGTERM");
|
|
2602
|
+
} catch {
|
|
2603
|
+
try {
|
|
2604
|
+
signal(pid, "SIGTERM");
|
|
2605
|
+
} catch {
|
|
2606
|
+
return "sigterm";
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
for (let poll = 0; poll < LIVENESS_POLLS; poll++) {
|
|
2610
|
+
if (!isAlive(signal, pid)) return "sigterm";
|
|
2611
|
+
await sleep(LIVENESS_POLL_MS);
|
|
2612
|
+
}
|
|
2613
|
+
if (!isAlive(signal, pid)) return "sigterm";
|
|
2614
|
+
try {
|
|
2615
|
+
signal(-pid, "SIGKILL");
|
|
2616
|
+
} catch {
|
|
2617
|
+
try {
|
|
2618
|
+
signal(pid, "SIGKILL");
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return "sigkill";
|
|
2623
|
+
}
|
|
2624
|
+
async function cancelRun(runId, opts = {}) {
|
|
2625
|
+
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2626
|
+
const signal = opts.signal ?? realSignal;
|
|
2627
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2628
|
+
const runDir = path17.join(runsDir, runId);
|
|
2629
|
+
const events = await readJournal(runDir);
|
|
2630
|
+
if (!events.some((e) => e.t === "run.started")) return { status: "not-found", runId };
|
|
2631
|
+
const last = events.at(-1);
|
|
2632
|
+
if (last?.t === "run.finished") {
|
|
2633
|
+
return { status: "already-terminal", runId, finishedStatus: last.status };
|
|
2634
|
+
}
|
|
2635
|
+
const pid = livePidFromJournal(events);
|
|
2636
|
+
const signalled = pid === void 0 ? "no-pid" : await terminateProcessTree(signal, sleep, pid);
|
|
2637
|
+
const settled = (await readJournal(runDir)).at(-1);
|
|
2638
|
+
if (settled?.t === "run.finished") {
|
|
2639
|
+
return { status: "already-terminal", runId, finishedStatus: settled.status };
|
|
2640
|
+
}
|
|
2641
|
+
await appendJournalEvent(runDir, runId, {
|
|
2642
|
+
t: "run.finished",
|
|
2643
|
+
status: "failed",
|
|
2644
|
+
reason: "cancelled"
|
|
2645
|
+
});
|
|
2646
|
+
return { status: "cancelled", runId, signalled };
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/cli/commands/cancel.ts
|
|
2650
|
+
async function cancelCommand(runId, opts = {}) {
|
|
2651
|
+
const outcome = await cancelRun(runId, { runsDir: opts.runsDir });
|
|
2652
|
+
switch (outcome.status) {
|
|
2653
|
+
case "not-found":
|
|
2654
|
+
console.error(`run ${runId} not found`);
|
|
2655
|
+
return 1;
|
|
2656
|
+
case "already-terminal":
|
|
2657
|
+
console.log(`run ${runId} already finished: ${outcome.finishedStatus} \u2014 nothing to cancel`);
|
|
2658
|
+
return 0;
|
|
2659
|
+
case "cancelled": {
|
|
2660
|
+
const note = outcome.signalled === "no-pid" ? " (no pid recorded \u2014 marked cancelled without signalling)" : outcome.signalled === "not-running" ? " (process already gone \u2014 marked cancelled)" : outcome.signalled === "sigkill" ? " (SIGKILL after grace period)" : "";
|
|
2661
|
+
console.log(`run ${runId} cancelled${note}`);
|
|
2662
|
+
return 0;
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2552
2667
|
// src/cli/commands/graph.ts
|
|
2553
2668
|
async function graphCommand(workflowPath) {
|
|
2554
2669
|
const raw = await loadWorkflowYaml(workflowPath);
|
|
@@ -2559,13 +2674,13 @@ async function graphCommand(workflowPath) {
|
|
|
2559
2674
|
|
|
2560
2675
|
// src/cli/commands/pending.ts
|
|
2561
2676
|
import { readdir as readdir3 } from "fs/promises";
|
|
2562
|
-
import
|
|
2677
|
+
import path18 from "path";
|
|
2563
2678
|
async function pendingCommand(opts = {}) {
|
|
2564
2679
|
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2565
2680
|
const runIds = (await readdir3(runsDir).catch(() => [])).sort();
|
|
2566
2681
|
let anyPending = false;
|
|
2567
2682
|
for (const runId of runIds) {
|
|
2568
|
-
const runDir =
|
|
2683
|
+
const runDir = path18.join(runsDir, runId);
|
|
2569
2684
|
const events = await readJournal(runDir);
|
|
2570
2685
|
const last = events.at(-1);
|
|
2571
2686
|
if (!last || last.t !== "run.finished" || last.status !== "suspended") continue;
|
|
@@ -2669,7 +2784,7 @@ async function resumeCommand(runId, adapter) {
|
|
|
2669
2784
|
}
|
|
2670
2785
|
|
|
2671
2786
|
// src/cli/commands/run.ts
|
|
2672
|
-
import
|
|
2787
|
+
import path19 from "path";
|
|
2673
2788
|
import { cancel, intro, log, outro } from "@clack/prompts";
|
|
2674
2789
|
|
|
2675
2790
|
// src/cli/interactive.ts
|
|
@@ -2799,7 +2914,7 @@ ${text2}
|
|
|
2799
2914
|
async function runCommand(workflowPath, opts = {}) {
|
|
2800
2915
|
let result;
|
|
2801
2916
|
try {
|
|
2802
|
-
result = await executeWorkflowFile(
|
|
2917
|
+
result = await executeWorkflowFile(path19.resolve(workflowPath), {
|
|
2803
2918
|
adapter: opts.adapter,
|
|
2804
2919
|
isolation: opts.isolation,
|
|
2805
2920
|
input: opts.input,
|
|
@@ -2839,7 +2954,7 @@ async function runCommand(workflowPath, opts = {}) {
|
|
|
2839
2954
|
}
|
|
2840
2955
|
|
|
2841
2956
|
// src/cli/commands/status.ts
|
|
2842
|
-
import
|
|
2957
|
+
import path20 from "path";
|
|
2843
2958
|
async function statusCommand(runId, opts = {}) {
|
|
2844
2959
|
const runsDir = opts.runsDir ?? defaultRunsDir();
|
|
2845
2960
|
const resolvedRunId = runId ?? await findLatestRunId(runsDir);
|
|
@@ -2847,7 +2962,7 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2847
2962
|
console.error("no runs found");
|
|
2848
2963
|
return 1;
|
|
2849
2964
|
}
|
|
2850
|
-
const runDir =
|
|
2965
|
+
const runDir = path20.join(runsDir, resolvedRunId);
|
|
2851
2966
|
const workflow = await readRunWorkflow(runDir).catch(() => void 0);
|
|
2852
2967
|
if (!workflow) {
|
|
2853
2968
|
console.error(`run ${resolvedRunId} not found`);
|
|
@@ -2873,7 +2988,7 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2873
2988
|
}
|
|
2874
2989
|
|
|
2875
2990
|
// src/cli/commands/watch.ts
|
|
2876
|
-
import
|
|
2991
|
+
import path21 from "path";
|
|
2877
2992
|
import { render } from "ink";
|
|
2878
2993
|
import React2 from "react";
|
|
2879
2994
|
|
|
@@ -2933,7 +3048,7 @@ async function watchCommand(runId, opts = {}) {
|
|
|
2933
3048
|
console.error("no runs found");
|
|
2934
3049
|
return 1;
|
|
2935
3050
|
}
|
|
2936
|
-
const runDir =
|
|
3051
|
+
const runDir = path21.join(runsDir, resolvedRunId);
|
|
2937
3052
|
const workflow = await readRunWorkflow(runDir).catch(() => void 0);
|
|
2938
3053
|
if (!workflow) {
|
|
2939
3054
|
console.error(`run ${resolvedRunId} not found`);
|
|
@@ -3010,6 +3125,9 @@ program.command("run").description("Run a workflow YAML file").argument("<workfl
|
|
|
3010
3125
|
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) => {
|
|
3011
3126
|
process.exitCode = await resumeCommand(runId, options.adapter ? requireAdapterId(options.adapter) : void 0);
|
|
3012
3127
|
});
|
|
3128
|
+
program.command("cancel").description("Terminate a live run and journal a clean 'cancelled' terminal state").argument("<run-id>", "id of the run to cancel").action(async (runId) => {
|
|
3129
|
+
process.exitCode = await cancelCommand(runId);
|
|
3130
|
+
});
|
|
3013
3131
|
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) => {
|
|
3014
3132
|
process.exitCode = await statusCommand(runId);
|
|
3015
3133
|
});
|