@lchase/yak 0.3.1 → 0.4.1
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/dist/index.js +69 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -488,6 +488,7 @@ async function validateWorkflow(workflow, cwd = process.cwd(), runIsolation = "n
|
|
|
488
488
|
checkReservedArtifactNames(flatSteps);
|
|
489
489
|
checkNeedsSatisfied(workflow.steps, producerOf);
|
|
490
490
|
checkNoCycles(workflow.steps, producerOf);
|
|
491
|
+
checkExprArtifactIdentifiers(workflow.steps, producerOf);
|
|
491
492
|
checkExitCodeReads(workflow.steps, producerOf);
|
|
492
493
|
checkAgentToolNames(flatSteps);
|
|
493
494
|
await checkAgentSchemaKeys(flatSteps, cwd);
|
|
@@ -621,6 +622,42 @@ function checkNoCycles(steps, producerOf) {
|
|
|
621
622
|
}
|
|
622
623
|
for (const step of steps) visit(step, []);
|
|
623
624
|
}
|
|
625
|
+
var JEXL_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
626
|
+
function checkExprArtifactIdentifiers(steps, producerOf) {
|
|
627
|
+
for (const step of steps) {
|
|
628
|
+
if (step.skipIf !== void 0) checkExprRefs(step.id, "skipIf", step.skipIf, producerOf);
|
|
629
|
+
if (step.kind === "loop") {
|
|
630
|
+
const localProducerOf = new Map([...producerOf, ...buildProducerMap(step.body)]);
|
|
631
|
+
checkExprRefs(step.id, "until", step.until, localProducerOf);
|
|
632
|
+
if (step.budget.noProgress) {
|
|
633
|
+
checkExprRefs(step.id, "noProgress.signal", step.budget.noProgress.signal, localProducerOf);
|
|
634
|
+
}
|
|
635
|
+
checkExprArtifactIdentifiers(step.body, localProducerOf);
|
|
636
|
+
}
|
|
637
|
+
if (step.kind === "map") checkExprArtifactIdentifiers([step.step], producerOf);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function checkExprRefs(stepId, field, expr, producerOf) {
|
|
641
|
+
if (typeof expr !== "string") return;
|
|
642
|
+
for (const name of producerOf.keys()) {
|
|
643
|
+
if (JEXL_IDENTIFIER.test(name)) continue;
|
|
644
|
+
const bounded = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`);
|
|
645
|
+
if (!bounded.test(expr)) continue;
|
|
646
|
+
throw new WorkflowValidationError(
|
|
647
|
+
`step "${stepId}": ${field} references artifact "${name}", but "${name}" is not a valid jexl identifier \u2014 jexl reads it as an expression ("a-b" parses as "a minus b"), not a name, so the artifact is invisible to ${field}. Rename it to an identifier (e.g. "${toIdentifier(name)}").`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function escapeRegExp(s) {
|
|
652
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
653
|
+
}
|
|
654
|
+
function toIdentifier(name) {
|
|
655
|
+
const camel = name.replace(
|
|
656
|
+
/[^A-Za-z0-9]+([A-Za-z0-9])?/g,
|
|
657
|
+
(_, c) => c ? c.toUpperCase() : ""
|
|
658
|
+
);
|
|
659
|
+
return JEXL_IDENTIFIER.test(camel) ? camel : `a${camel}`;
|
|
660
|
+
}
|
|
624
661
|
function checkExitCodeReads(steps, producerOf) {
|
|
625
662
|
const stepsById = new Map(steps.map((s) => [s.id, s]));
|
|
626
663
|
for (const step of steps) {
|
|
@@ -768,6 +805,10 @@ async function readJournal(runDir) {
|
|
|
768
805
|
const contents = await readFile4(journalPath(runDir), "utf8").catch(() => "");
|
|
769
806
|
return contents.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
770
807
|
}
|
|
808
|
+
function runTagFromJournal(events) {
|
|
809
|
+
const started = events.find((e) => e.t === "run.started");
|
|
810
|
+
return started?.t === "run.started" ? started.tag : void 0;
|
|
811
|
+
}
|
|
771
812
|
function completedStepsFromJournal(events) {
|
|
772
813
|
const started = /* @__PURE__ */ new Map();
|
|
773
814
|
const completed = /* @__PURE__ */ new Map();
|
|
@@ -2322,6 +2363,9 @@ function resolveDirs(runsDir, cwd, cacheDir) {
|
|
|
2322
2363
|
function worktreePathFor(runsDir, runId) {
|
|
2323
2364
|
return path15.join(path15.dirname(runsDir), ".yak", "worktrees", runId);
|
|
2324
2365
|
}
|
|
2366
|
+
function worktreeBranchFor(runId) {
|
|
2367
|
+
return `yak/${runId}`;
|
|
2368
|
+
}
|
|
2325
2369
|
function generateRunId() {
|
|
2326
2370
|
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2327
2371
|
const [datePart, timePart] = iso.split("T");
|
|
@@ -2353,7 +2397,8 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2353
2397
|
workflow: workflow.name,
|
|
2354
2398
|
inputHash: inputWritten?.hash ?? sha256(JSON.stringify(workflow)),
|
|
2355
2399
|
adapter,
|
|
2356
|
-
isolation
|
|
2400
|
+
isolation,
|
|
2401
|
+
...opts.tag !== void 0 ? { tag: opts.tag } : {}
|
|
2357
2402
|
});
|
|
2358
2403
|
if (inputWritten) {
|
|
2359
2404
|
await appendJournalEvent(runDir, runId, {
|
|
@@ -2363,11 +2408,16 @@ async function executeWorkflowFile(workflowPath, opts = {}) {
|
|
|
2363
2408
|
bytes: inputWritten.bytes
|
|
2364
2409
|
});
|
|
2365
2410
|
}
|
|
2411
|
+
await opts.onStart?.({
|
|
2412
|
+
runId,
|
|
2413
|
+
runDir,
|
|
2414
|
+
...isolation === "worktree" ? { worktreeBranch: worktreeBranchFor(runId) } : {}
|
|
2415
|
+
});
|
|
2366
2416
|
let stepCwd = cwd;
|
|
2367
2417
|
if (isolation === "worktree") {
|
|
2368
2418
|
const worktreePath = worktreePathFor(runsDir, runId);
|
|
2369
2419
|
try {
|
|
2370
|
-
await createWorktree(cwd,
|
|
2420
|
+
await createWorktree(cwd, worktreeBranchFor(runId), "HEAD", worktreePath);
|
|
2371
2421
|
} catch {
|
|
2372
2422
|
await appendJournalEvent(runDir, runId, { t: "run.finished", status: "failed" });
|
|
2373
2423
|
return { runId, runDir, status: "failed" };
|
|
@@ -2520,7 +2570,8 @@ async function pendingCommand(opts = {}) {
|
|
|
2520
2570
|
const last = events.at(-1);
|
|
2521
2571
|
if (!last || last.t !== "run.finished" || last.status !== "suspended") continue;
|
|
2522
2572
|
anyPending = true;
|
|
2523
|
-
|
|
2573
|
+
const tag = runTagFromJournal(events);
|
|
2574
|
+
console.log(`run ${runId} suspended${tag !== void 0 ? ` (tag: ${tag})` : ""}:`);
|
|
2524
2575
|
const openIds = openRequestStepIds(events);
|
|
2525
2576
|
if (openIds.length === 0) {
|
|
2526
2577
|
console.log(" (no open request found \u2014 loop budget suspend from before M4, unresumable)");
|
|
@@ -2751,7 +2802,16 @@ async function runCommand(workflowPath, opts = {}) {
|
|
|
2751
2802
|
result = await executeWorkflowFile(path18.resolve(workflowPath), {
|
|
2752
2803
|
adapter: opts.adapter,
|
|
2753
2804
|
isolation: opts.isolation,
|
|
2754
|
-
input: opts.input
|
|
2805
|
+
input: opts.input,
|
|
2806
|
+
tag: opts.tag,
|
|
2807
|
+
// yak#23: emit the run id (and worktree branch) to stderr the moment
|
|
2808
|
+
// `run.started` is journalled, so an out-of-process launcher can
|
|
2809
|
+
// capture it without waiting for a terminal state. stdout is
|
|
2810
|
+
// untouched — the terminal-state line there is unchanged.
|
|
2811
|
+
onStart: ({ runId, worktreeBranch }) => {
|
|
2812
|
+
console.error(`run ${runId} started`);
|
|
2813
|
+
if (worktreeBranch) console.error(` worktree branch ${worktreeBranch}`);
|
|
2814
|
+
}
|
|
2755
2815
|
});
|
|
2756
2816
|
while (opts.interactive && result.status === "suspended") {
|
|
2757
2817
|
await answerOpenRequestsInteractively(result.runId, result.runDir);
|
|
@@ -2796,7 +2856,8 @@ async function statusCommand(runId, opts = {}) {
|
|
|
2796
2856
|
const events = await readJournal(runDir);
|
|
2797
2857
|
const statuses = stepStatusesFromJournal(workflow.steps, events);
|
|
2798
2858
|
const failuresByStep = new Map(failedStepsFromJournal(events).map((f) => [f.stepId, f]));
|
|
2799
|
-
|
|
2859
|
+
const tag = runTagFromJournal(events);
|
|
2860
|
+
console.log(`run ${resolvedRunId}${tag !== void 0 ? ` (tag: ${tag})` : ""}:`);
|
|
2800
2861
|
for (const { stepId, status } of statuses) {
|
|
2801
2862
|
console.log(` ${stepId}: ${status}`);
|
|
2802
2863
|
const step = workflow.steps.find((s) => s.id === stepId);
|
|
@@ -2935,13 +2996,14 @@ program.name("yak").description("An agentic workflow engine").version(readVersio
|
|
|
2935
2996
|
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(
|
|
2936
2997
|
"--input <pair...>",
|
|
2937
2998
|
"workflow input as key=value, repeatable \u2014 validated against the workflow inputSchema"
|
|
2938
|
-
).action(
|
|
2999
|
+
).option("--tag <string>", "opaque correlation tag, stored on the run and echoed by yak pending/status").action(
|
|
2939
3000
|
async (workflow, options) => {
|
|
2940
3001
|
process.exitCode = await runCommand(workflow, {
|
|
2941
3002
|
adapter: requireAdapterId(options.adapter),
|
|
2942
3003
|
interactive: options.interactive,
|
|
2943
3004
|
isolation: requireIsolation(options.isolation),
|
|
2944
|
-
input: parseInputPairs(options.input)
|
|
3005
|
+
input: parseInputPairs(options.input),
|
|
3006
|
+
tag: options.tag
|
|
2945
3007
|
});
|
|
2946
3008
|
}
|
|
2947
3009
|
);
|