@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.
Files changed (2) hide show
  1. package/dist/index.js +69 -7
  2. 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, `yak/${runId}`, "HEAD", worktreePath);
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
- console.log(`run ${runId} suspended:`);
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
- console.log(`run ${resolvedRunId}:`);
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
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/yak",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "An agentic workflow engine",
5
5
  "license": "MIT",
6
6
  "type": "module",