@hasna/loops 0.3.12 → 0.3.14

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/cli/index.js CHANGED
@@ -370,6 +370,35 @@ function validateTarget(value, label) {
370
370
  if (value.provider !== "codewith")
371
371
  throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
372
372
  }
373
+ if (value.variant !== undefined)
374
+ assertString(value.variant, `${label}.variant`);
375
+ if (value.permissionMode !== undefined) {
376
+ assertString(value.permissionMode, `${label}.permissionMode`);
377
+ const permissionModes = ["default", "plan", "auto", "bypass"];
378
+ if (!permissionModes.includes(value.permissionMode)) {
379
+ throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
380
+ }
381
+ if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
382
+ throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
383
+ }
384
+ if (value.permissionMode === "auto" && value.provider !== "claude") {
385
+ throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
386
+ }
387
+ }
388
+ if (value.sandbox !== undefined) {
389
+ assertString(value.sandbox, `${label}.sandbox`);
390
+ const codexLike = ["read-only", "workspace-write", "danger-full-access"];
391
+ const cursorLike = ["enabled", "disabled"];
392
+ if (["codewith", "codex"].includes(value.provider)) {
393
+ if (!codexLike.includes(value.sandbox))
394
+ throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
395
+ } else if (value.provider === "cursor") {
396
+ if (!cursorLike.includes(value.sandbox))
397
+ throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
398
+ } else {
399
+ throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
400
+ }
401
+ }
373
402
  return value;
374
403
  }
375
404
  throw new Error(`${label}.type must be command or agent`);
@@ -2045,6 +2074,7 @@ class Store {
2045
2074
  }
2046
2075
 
2047
2076
  // src/cli/index.ts
2077
+ import { createHash } from "crypto";
2048
2078
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2049
2079
  import { Command } from "commander";
2050
2080
 
@@ -2468,7 +2498,7 @@ function providerCommand(provider) {
2468
2498
  case "claude":
2469
2499
  return "claude";
2470
2500
  case "cursor":
2471
- return "cursor-agent";
2501
+ return "sh";
2472
2502
  case "codewith":
2473
2503
  return "codewith";
2474
2504
  case "aicopilot":
@@ -2479,22 +2509,107 @@ function providerCommand(provider) {
2479
2509
  return "codex";
2480
2510
  }
2481
2511
  }
2512
+ function codewithLikeSandbox(target) {
2513
+ const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
2514
+ if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
2515
+ throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
2516
+ }
2517
+ return sandbox;
2518
+ }
2519
+ function configStringValue(value) {
2520
+ return JSON.stringify(value);
2521
+ }
2522
+ function assertStringOption(value, label) {
2523
+ if (value !== undefined && typeof value !== "string")
2524
+ throw new Error(`${label} must be a string`);
2525
+ }
2526
+ function assertSupportedAgentOptions(target) {
2527
+ assertStringOption(target.variant, `${target.provider}.variant`);
2528
+ assertStringOption(target.model, `${target.provider}.model`);
2529
+ assertStringOption(target.agent, `${target.provider}.agent`);
2530
+ assertStringOption(target.authProfile, `${target.provider}.authProfile`);
2531
+ if (target.authProfile !== undefined && target.provider !== "codewith") {
2532
+ throw new Error(`${target.provider}.authProfile is supported only for codewith`);
2533
+ }
2534
+ if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
2535
+ throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
2536
+ }
2537
+ if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
2538
+ throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
2539
+ }
2540
+ if (["codewith", "codex"].includes(target.provider)) {
2541
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2542
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2543
+ }
2544
+ if (target.sandbox)
2545
+ codewithLikeSandbox(target);
2546
+ return;
2547
+ }
2548
+ if (target.provider === "claude") {
2549
+ if (target.sandbox !== undefined)
2550
+ throw new Error("claude.sandbox is not supported");
2551
+ return;
2552
+ }
2553
+ if (target.provider === "cursor") {
2554
+ if (target.permissionMode === "auto")
2555
+ throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
2556
+ if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
2557
+ throw new Error("cursor.sandbox must be enabled or disabled");
2558
+ }
2559
+ return;
2560
+ }
2561
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2562
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2563
+ }
2564
+ if (target.sandbox !== undefined)
2565
+ throw new Error(`${target.provider}.sandbox is not supported`);
2566
+ }
2482
2567
  function agentArgs(target) {
2568
+ assertSupportedAgentOptions(target);
2483
2569
  const isolation = target.configIsolation ?? "safe";
2570
+ const permissionMode = target.permissionMode ?? "default";
2484
2571
  const args = [];
2485
2572
  switch (target.provider) {
2486
2573
  case "claude":
2487
2574
  if (isolation === "safe")
2488
2575
  args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
2576
+ if (permissionMode !== "default") {
2577
+ const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
2578
+ if (mode)
2579
+ args.push("--permission-mode", mode);
2580
+ }
2489
2581
  args.push("-p", "--output-format", "json");
2490
2582
  if (target.model)
2491
2583
  args.push("--model", target.model);
2584
+ if (target.variant)
2585
+ args.push("--effort", target.variant);
2492
2586
  if (target.agent)
2493
2587
  args.push("--agent", target.agent);
2494
2588
  args.push(...target.extraArgs ?? []);
2495
2589
  return args;
2496
2590
  case "cursor":
2497
- args.push("agent", "-p");
2591
+ args.push("-c", [
2592
+ "set -eu",
2593
+ "if command -v cursor >/dev/null 2>&1; then",
2594
+ ' exec cursor agent "$@"',
2595
+ "elif command -v agent >/dev/null 2>&1; then",
2596
+ ' exec agent "$@"',
2597
+ "else",
2598
+ " echo 'Executable not found in PATH: cursor agent or agent' >&2",
2599
+ " exit 127",
2600
+ "fi"
2601
+ ].join(`
2602
+ `), "openloops-cursor", "-p");
2603
+ if (permissionMode === "plan")
2604
+ args.push("--mode", "plan");
2605
+ if (permissionMode === "bypass")
2606
+ args.push("--force");
2607
+ const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
2608
+ if (cursorSandbox) {
2609
+ if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
2610
+ throw new Error("cursor sandbox must be enabled or disabled");
2611
+ args.push("--sandbox", cursorSandbox);
2612
+ }
2498
2613
  if (target.model)
2499
2614
  args.push("--model", target.model);
2500
2615
  if (target.agent)
@@ -2502,7 +2617,10 @@ function agentArgs(target) {
2502
2617
  args.push(...target.extraArgs ?? []);
2503
2618
  return args;
2504
2619
  case "codewith":
2505
- args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
2620
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
2621
+ if (target.variant)
2622
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2623
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
2506
2624
  if (isolation === "safe")
2507
2625
  args.push("--ignore-rules");
2508
2626
  if (target.cwd)
@@ -2514,7 +2632,9 @@ function agentArgs(target) {
2514
2632
  args.push(...target.extraArgs ?? []);
2515
2633
  return args;
2516
2634
  case "codex":
2517
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
2635
+ if (target.variant)
2636
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2637
+ args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
2518
2638
  if (isolation === "safe")
2519
2639
  args.push("--ignore-rules");
2520
2640
  if (target.cwd)
@@ -2527,10 +2647,14 @@ function agentArgs(target) {
2527
2647
  args.push("run", "--format", "json");
2528
2648
  if (isolation === "safe")
2529
2649
  args.push("--pure");
2650
+ if (permissionMode === "bypass")
2651
+ args.push("--dangerously-skip-permissions");
2530
2652
  if (target.cwd)
2531
2653
  args.push("--dir", target.cwd);
2532
2654
  if (target.model)
2533
2655
  args.push("--model", target.model);
2656
+ if (target.variant)
2657
+ args.push("--variant", target.variant);
2534
2658
  if (target.agent)
2535
2659
  args.push("--agent", target.agent);
2536
2660
  args.push(...target.extraArgs ?? []);
@@ -2539,10 +2663,14 @@ function agentArgs(target) {
2539
2663
  args.push("run", "--format", "json");
2540
2664
  if (isolation === "safe")
2541
2665
  args.push("--pure");
2666
+ if (permissionMode === "bypass")
2667
+ args.push("--dangerously-skip-permissions");
2542
2668
  if (target.cwd)
2543
2669
  args.push("--dir", target.cwd);
2544
2670
  if (target.model)
2545
2671
  args.push("--model", target.model);
2672
+ if (target.variant)
2673
+ args.push("--variant", target.variant);
2546
2674
  if (target.agent)
2547
2675
  args.push("--agent", target.agent);
2548
2676
  args.push(...target.extraArgs ?? []);
@@ -2571,6 +2699,7 @@ function commandSpec(target) {
2571
2699
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2572
2700
  account: agentTarget.account,
2573
2701
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2702
+ preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2574
2703
  stdin: agentTarget.prompt
2575
2704
  };
2576
2705
  }
@@ -2638,11 +2767,15 @@ function remoteScript(spec, metadata) {
2638
2767
  `;
2639
2768
  }
2640
2769
  function remotePreflightScript(spec, metadata) {
2641
- return [
2770
+ const lines = [
2642
2771
  ...remoteBootstrapLines(spec, metadata),
2643
2772
  "command -v bash >/dev/null 2>&1",
2644
2773
  `command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
2645
- ].join(`
2774
+ ];
2775
+ if (spec.preflightAnyOf?.length) {
2776
+ lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
2777
+ }
2778
+ return lines.join(`
2646
2779
  `);
2647
2780
  }
2648
2781
  function transportEnv(opts) {
@@ -2795,6 +2928,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2795
2928
  if (!spec.shell && !executableExists(spec.command, env)) {
2796
2929
  throw new Error(commandNotFoundMessage(spec.command, env));
2797
2930
  }
2931
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2932
+ throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2933
+ }
2798
2934
  return {
2799
2935
  command: spec.command,
2800
2936
  accountProfile: spec.account?.profile,
@@ -2825,6 +2961,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
2825
2961
  durationMs: 0
2826
2962
  };
2827
2963
  }
2964
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2965
+ return {
2966
+ status: "failed",
2967
+ stdout,
2968
+ stderr,
2969
+ error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
2970
+ startedAt,
2971
+ finishedAt: nowIso(),
2972
+ durationMs: 0
2973
+ };
2974
+ }
2828
2975
  const child = spawn(spec.command, spec.args, {
2829
2976
  cwd: spec.cwd,
2830
2977
  env,
@@ -4241,17 +4388,35 @@ import { spawnSync as spawnSync4 } from "child_process";
4241
4388
  import { accessSync as accessSync2, constants as constants2 } from "fs";
4242
4389
  var PROVIDER_COMMANDS = [
4243
4390
  "claude",
4244
- "cursor-agent",
4391
+ "cursor agent",
4245
4392
  "codewith",
4246
4393
  "aicopilot",
4247
4394
  "opencode",
4248
4395
  "codex"
4249
4396
  ];
4250
4397
  function hasCommand(command) {
4398
+ if (command === "cursor agent") {
4399
+ return hasCommand("cursor") || hasCommand("agent");
4400
+ }
4251
4401
  const result = spawnSync4("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
4252
4402
  return (result.status ?? 1) === 0;
4253
4403
  }
4254
4404
  function commandVersion(command) {
4405
+ if (command === "cursor agent") {
4406
+ const cursorResult = spawnSync4("cursor", ["agent", "--version"], {
4407
+ encoding: "utf8",
4408
+ stdio: ["ignore", "pipe", "pipe"]
4409
+ });
4410
+ if ((cursorResult.status ?? 1) === 0)
4411
+ return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
4412
+ const agentResult = spawnSync4("agent", ["--version"], {
4413
+ encoding: "utf8",
4414
+ stdio: ["ignore", "pipe", "pipe"]
4415
+ });
4416
+ if ((agentResult.status ?? 1) === 0)
4417
+ return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
4418
+ return;
4419
+ }
4255
4420
  const result = spawnSync4(command, ["--version"], {
4256
4421
  encoding: "utf8",
4257
4422
  stdio: ["ignore", "pipe", "pipe"]
@@ -4330,7 +4495,7 @@ function runDoctor(store) {
4330
4495
  // package.json
4331
4496
  var package_default = {
4332
4497
  name: "@hasna/loops",
4333
- version: "0.3.12",
4498
+ version: "0.3.14",
4334
4499
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4335
4500
  type: "module",
4336
4501
  main: "dist/index.js",
@@ -4394,6 +4559,7 @@ var package_default = {
4394
4559
  bun: ">=1.0.0"
4395
4560
  },
4396
4561
  dependencies: {
4562
+ "@hasna/events": "^0.1.9",
4397
4563
  "@hasna/machines": "0.0.49",
4398
4564
  "@openrouter/ai-sdk-provider": "2.9.1",
4399
4565
  ai: "6.0.204",
@@ -4417,6 +4583,242 @@ function packageVersion() {
4417
4583
  return package_default.version;
4418
4584
  }
4419
4585
 
4586
+ // src/lib/templates.ts
4587
+ var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
4588
+ var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4589
+ var TEMPLATE_SUMMARIES = [
4590
+ {
4591
+ id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
4592
+ name: "Todos Task Worker + Verifier",
4593
+ description: "Create a one-shot workflow for a todos task: one agent performs the task, then a fresh verifier agent audits the result and records follow-up tasks or completion evidence.",
4594
+ kind: "workflow",
4595
+ variables: [
4596
+ { name: "taskId", required: true, description: "Todos task id to execute." },
4597
+ { name: "taskTitle", description: "Human-readable task title." },
4598
+ { name: "projectPath", required: true, description: "Repository or project working directory." },
4599
+ { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4600
+ { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4601
+ { name: "model", description: "Provider model." },
4602
+ { name: "variant", description: "Provider reasoning/model effort variant." },
4603
+ { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4604
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4605
+ ]
4606
+ },
4607
+ {
4608
+ id: EVENT_WORKER_VERIFIER_TEMPLATE_ID,
4609
+ name: "Hasna Event Worker + Verifier",
4610
+ description: "Create a one-shot workflow for a generic Hasna event: one agent handles the event, then a fresh verifier agent audits the result and records evidence or follow-up tasks.",
4611
+ kind: "workflow",
4612
+ variables: [
4613
+ { name: "eventId", required: true, description: "Hasna event id." },
4614
+ { name: "eventType", required: true, description: "Hasna event type." },
4615
+ { name: "eventSource", required: true, description: "Hasna event source." },
4616
+ { name: "eventJson", required: true, description: "Full event envelope JSON." },
4617
+ { name: "projectPath", required: true, description: "Repository or project working directory." },
4618
+ { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4619
+ { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4620
+ { name: "model", description: "Provider model." },
4621
+ { name: "variant", description: "Provider reasoning/model effort variant." },
4622
+ { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4623
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4624
+ ]
4625
+ }
4626
+ ];
4627
+ function compactJson(value) {
4628
+ return JSON.stringify(value);
4629
+ }
4630
+ function taskLabel(input) {
4631
+ const head = input.taskTitle?.trim() || input.taskId;
4632
+ return head.length > 160 ? `${head.slice(0, 157)}...` : head;
4633
+ }
4634
+ function agentTarget(input, prompt) {
4635
+ const provider = input.provider ?? "codewith";
4636
+ const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4637
+ return {
4638
+ type: "agent",
4639
+ provider,
4640
+ prompt,
4641
+ cwd: input.projectPath,
4642
+ model: input.model,
4643
+ variant: input.variant,
4644
+ agent: input.agent,
4645
+ authProfile: provider === "codewith" ? input.authProfile : undefined,
4646
+ configIsolation: "safe",
4647
+ permissionMode: input.permissionMode ?? "bypass",
4648
+ sandbox,
4649
+ account: input.account,
4650
+ timeoutMs: 45 * 60000
4651
+ };
4652
+ }
4653
+ function listLoopTemplates() {
4654
+ return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
4655
+ }
4656
+ function getLoopTemplate(id) {
4657
+ return listLoopTemplates().find((template) => template.id === id || template.name === id);
4658
+ }
4659
+ function renderTodosTaskWorkerVerifierWorkflow(input) {
4660
+ if (!input.taskId?.trim())
4661
+ throw new Error("taskId is required");
4662
+ if (!input.projectPath?.trim())
4663
+ throw new Error("projectPath is required");
4664
+ const taskContext = {
4665
+ taskId: input.taskId,
4666
+ taskTitle: input.taskTitle,
4667
+ taskDescription: input.taskDescription,
4668
+ eventId: input.eventId,
4669
+ eventType: input.eventType,
4670
+ projectPath: input.projectPath
4671
+ };
4672
+ const workerPrompt = [
4673
+ `/goal Complete todos task ${input.taskId} in ${input.projectPath}.`,
4674
+ "",
4675
+ "You are the worker agent for a task-triggered OpenLoops workflow.",
4676
+ "Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
4677
+ "Claim/start the task if appropriate, inspect the repository/project state, implement only the task scope, run focused validation, preserve unrelated user changes, and update the task with comments, evidence, changed files, commits, and blockers.",
4678
+ "Do not mark the task complete unless the work is genuinely done and validated.",
4679
+ "",
4680
+ `Task context JSON: ${compactJson(taskContext)}`
4681
+ ].join(`
4682
+ `);
4683
+ const verifierPrompt = [
4684
+ `/goal Verify todos task ${input.taskId} after the worker step.`,
4685
+ "",
4686
+ "You are the verifier agent for a task-triggered OpenLoops workflow.",
4687
+ "Use fresh context. Inspect the task, repository state, commits, tests, and worker evidence. Act as an adversarial reviewer focused on correctness, regressions, missing tests, security, and incomplete requirements.",
4688
+ "If the work is valid, record verification evidence in todos and mark/leave the task in the correct completed state according to the todos CLI. If it is not valid, add precise follow-up tasks or comments and leave the original task open or blocked with clear evidence.",
4689
+ "Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
4690
+ "",
4691
+ `Task context JSON: ${compactJson(taskContext)}`
4692
+ ].join(`
4693
+ `);
4694
+ return {
4695
+ name: `todos-task-${input.taskId.slice(0, 8)}-worker-verifier`,
4696
+ description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
4697
+ version: 1,
4698
+ steps: [
4699
+ {
4700
+ id: "worker",
4701
+ name: "Worker",
4702
+ description: "Implement the todos task and record evidence.",
4703
+ target: agentTarget(input, workerPrompt),
4704
+ timeoutMs: 45 * 60000
4705
+ },
4706
+ {
4707
+ id: "verifier",
4708
+ name: "Verifier",
4709
+ description: "Adversarially verify worker output and update todos.",
4710
+ dependsOn: ["worker"],
4711
+ target: agentTarget(input, verifierPrompt),
4712
+ timeoutMs: 30 * 60000
4713
+ }
4714
+ ]
4715
+ };
4716
+ }
4717
+ function renderEventWorkerVerifierWorkflow(input) {
4718
+ if (!input.eventId?.trim())
4719
+ throw new Error("eventId is required");
4720
+ if (!input.eventType?.trim())
4721
+ throw new Error("eventType is required");
4722
+ if (!input.eventSource?.trim())
4723
+ throw new Error("eventSource is required");
4724
+ if (!input.eventJson?.trim())
4725
+ throw new Error("eventJson is required");
4726
+ if (!input.projectPath?.trim())
4727
+ throw new Error("projectPath is required");
4728
+ const eventContext = {
4729
+ eventId: input.eventId,
4730
+ eventType: input.eventType,
4731
+ eventSource: input.eventSource,
4732
+ eventSubject: input.eventSubject,
4733
+ eventMessage: input.eventMessage,
4734
+ projectPath: input.projectPath
4735
+ };
4736
+ const workerPrompt = [
4737
+ `/goal Handle Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}) in ${input.projectPath}.`,
4738
+ "",
4739
+ "You are the worker agent for an event-triggered OpenLoops workflow.",
4740
+ "Investigate first before changing files. Read the full event envelope and decide the narrow action required by that event. Preserve unrelated user changes and update the relevant local CLI/task/knowledge system with evidence, changed files, commits, and blockers.",
4741
+ "If the event is informational or does not require action, record that finding and stop without making changes.",
4742
+ "",
4743
+ `Event context JSON: ${compactJson(eventContext)}`,
4744
+ `Full event envelope JSON: ${input.eventJson}`
4745
+ ].join(`
4746
+ `);
4747
+ const verifierPrompt = [
4748
+ `/goal Verify handling of Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}).`,
4749
+ "",
4750
+ "You are the verifier agent for an event-triggered OpenLoops workflow.",
4751
+ "Use fresh context. Inspect the event, repository/project state, worker evidence, tests, and any created tasks or notes. Act as an adversarial reviewer focused on correctness, regressions, security, missing evidence, and incomplete requirements.",
4752
+ "If the work is valid, record verification evidence in the relevant local system. If it is not valid, add precise follow-up tasks/comments and leave the event handling state open or blocked with clear evidence.",
4753
+ "",
4754
+ `Event context JSON: ${compactJson(eventContext)}`,
4755
+ `Full event envelope JSON: ${input.eventJson}`
4756
+ ].join(`
4757
+ `);
4758
+ return {
4759
+ name: `event-${input.eventSource}-${input.eventType}-${input.eventId.slice(0, 8)}-worker-verifier`.replace(/[^a-zA-Z0-9._:-]+/g, "-"),
4760
+ description: `Event-triggered worker/verifier workflow for ${input.eventSource}/${input.eventType}`,
4761
+ version: 1,
4762
+ steps: [
4763
+ {
4764
+ id: "worker",
4765
+ name: "Worker",
4766
+ description: "Handle the Hasna event and record evidence.",
4767
+ target: agentTarget(input, workerPrompt),
4768
+ timeoutMs: 45 * 60000
4769
+ },
4770
+ {
4771
+ id: "verifier",
4772
+ name: "Verifier",
4773
+ description: "Adversarially verify event handling.",
4774
+ dependsOn: ["worker"],
4775
+ target: agentTarget(input, verifierPrompt),
4776
+ timeoutMs: 30 * 60000
4777
+ }
4778
+ ]
4779
+ };
4780
+ }
4781
+ function renderLoopTemplate(id, values) {
4782
+ if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
4783
+ return renderTodosTaskWorkerVerifierWorkflow({
4784
+ taskId: values.taskId ?? "",
4785
+ taskTitle: values.taskTitle,
4786
+ taskDescription: values.taskDescription,
4787
+ projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4788
+ provider: values.provider,
4789
+ authProfile: values.authProfile,
4790
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4791
+ model: values.model,
4792
+ variant: values.variant,
4793
+ agent: values.agent,
4794
+ permissionMode: values.permissionMode,
4795
+ sandbox: values.sandbox,
4796
+ eventId: values.eventId,
4797
+ eventType: values.eventType
4798
+ });
4799
+ }
4800
+ if (id === EVENT_WORKER_VERIFIER_TEMPLATE_ID) {
4801
+ return renderEventWorkerVerifierWorkflow({
4802
+ eventId: values.eventId ?? "",
4803
+ eventType: values.eventType ?? "",
4804
+ eventSource: values.eventSource ?? "",
4805
+ eventSubject: values.eventSubject,
4806
+ eventMessage: values.eventMessage,
4807
+ eventJson: values.eventJson ?? "",
4808
+ projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4809
+ provider: values.provider,
4810
+ authProfile: values.authProfile,
4811
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4812
+ model: values.model,
4813
+ variant: values.variant,
4814
+ agent: values.agent,
4815
+ permissionMode: values.permissionMode,
4816
+ sandbox: values.sandbox
4817
+ });
4818
+ }
4819
+ throw new Error(`unknown template: ${id}`);
4820
+ }
4821
+
4420
4822
  // src/cli/index.ts
4421
4823
  var program = new Command;
4422
4824
  program.name("loops").description("Persistent local loops for commands and headless coding agents").version(packageVersion());
@@ -4535,6 +4937,78 @@ function accountFromOpts(opts) {
4535
4937
  throw new Error("--account-tool requires --account");
4536
4938
  return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
4537
4939
  }
4940
+ function parseVars(values) {
4941
+ const vars = {};
4942
+ for (const value of values ?? []) {
4943
+ const index = value.indexOf("=");
4944
+ if (index <= 0)
4945
+ throw new Error(`invalid --var value, expected key=value: ${value}`);
4946
+ vars[value.slice(0, index)] = value.slice(index + 1);
4947
+ }
4948
+ return vars;
4949
+ }
4950
+ function collectValues(value, previous = []) {
4951
+ previous.push(value);
4952
+ return previous;
4953
+ }
4954
+ function eventData(event) {
4955
+ const data = event.data;
4956
+ if (data && typeof data === "object" && !Array.isArray(data))
4957
+ return data;
4958
+ return {};
4959
+ }
4960
+ function eventMetadata(event) {
4961
+ const metadata = event.metadata;
4962
+ if (metadata && typeof metadata === "object" && !Array.isArray(metadata))
4963
+ return metadata;
4964
+ return {};
4965
+ }
4966
+ function stringField(value) {
4967
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
4968
+ }
4969
+ function slugSegment(value, fallback = "event") {
4970
+ return value.toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || fallback;
4971
+ }
4972
+ function stableSuffix(value) {
4973
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
4974
+ }
4975
+ function taskEventField(data, keys) {
4976
+ for (const key of keys) {
4977
+ const direct = stringField(data[key]);
4978
+ if (direct)
4979
+ return direct;
4980
+ }
4981
+ const task = data.task;
4982
+ if (task && typeof task === "object" && !Array.isArray(task)) {
4983
+ for (const key of keys) {
4984
+ const direct = stringField(task[key]);
4985
+ if (direct)
4986
+ return direct;
4987
+ }
4988
+ }
4989
+ const payload = data.payload;
4990
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
4991
+ for (const key of keys) {
4992
+ const direct = stringField(payload[key]);
4993
+ if (direct)
4994
+ return direct;
4995
+ }
4996
+ }
4997
+ return;
4998
+ }
4999
+ async function readEventEnvelopeFromStdin() {
5000
+ const raw = process.env.HASNA_EVENT_JSON || await Bun.stdin.text();
5001
+ const event = JSON.parse(raw);
5002
+ if (!event || typeof event !== "object" || Array.isArray(event))
5003
+ throw new Error("event JSON must be an object");
5004
+ if (!stringField(event.id))
5005
+ throw new Error("event.id is required");
5006
+ if (!stringField(event.type))
5007
+ throw new Error("event.type is required");
5008
+ if (!stringField(event.source))
5009
+ throw new Error("event.source is required");
5010
+ return event;
5011
+ }
4538
5012
  function providerAuthProfileFromOpts(opts, provider) {
4539
5013
  if (!opts.authProfile)
4540
5014
  return;
@@ -4542,6 +5016,40 @@ function providerAuthProfileFromOpts(opts, provider) {
4542
5016
  throw new Error("--auth-profile is currently supported only for --provider codewith");
4543
5017
  return opts.authProfile;
4544
5018
  }
5019
+ function sandboxFromOpts(opts, provider) {
5020
+ if (!opts.sandbox)
5021
+ return;
5022
+ const codexLike = ["read-only", "workspace-write", "danger-full-access"];
5023
+ const cursorLike = ["enabled", "disabled"];
5024
+ if (["codewith", "codex"].includes(provider)) {
5025
+ if (!codexLike.includes(opts.sandbox)) {
5026
+ throw new Error("--sandbox must be read-only, workspace-write, or danger-full-access for codewith/codex");
5027
+ }
5028
+ return opts.sandbox;
5029
+ }
5030
+ if (provider === "cursor") {
5031
+ if (!cursorLike.includes(opts.sandbox)) {
5032
+ throw new Error("--sandbox must be enabled or disabled for cursor");
5033
+ }
5034
+ return opts.sandbox;
5035
+ }
5036
+ throw new Error("--sandbox is currently supported only for --provider codewith, codex, or cursor");
5037
+ }
5038
+ function permissionModeFromOpts(opts, provider) {
5039
+ if (!opts.permissionMode)
5040
+ return;
5041
+ const mode = opts.permissionMode;
5042
+ if (!["default", "plan", "auto", "bypass"].includes(mode)) {
5043
+ throw new Error("--permission-mode must be default, plan, auto, or bypass");
5044
+ }
5045
+ if (mode === "plan" && !["claude", "cursor"].includes(provider)) {
5046
+ throw new Error("--permission-mode plan is currently supported only for claude or cursor");
5047
+ }
5048
+ if (mode === "auto" && provider !== "claude") {
5049
+ throw new Error("--permission-mode auto is currently supported only for claude");
5050
+ }
5051
+ return mode;
5052
+ }
4545
5053
  var create = program.command("create").description("create loops");
4546
5054
  addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell"))))).action((name, opts) => {
4547
5055
  const store = new Store;
@@ -4560,7 +5068,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
4560
5068
  store.close();
4561
5069
  }
4562
5070
  });
4563
- addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
5071
+ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
4564
5072
  const provider = opts.provider;
4565
5073
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
4566
5074
  throw new Error("unsupported provider");
@@ -4576,10 +5084,13 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
4576
5084
  prompt: opts.prompt,
4577
5085
  cwd: opts.cwd,
4578
5086
  model: opts.model,
5087
+ variant: opts.variant,
4579
5088
  agent: opts.agent,
4580
5089
  authProfile: providerAuthProfileFromOpts(opts, provider),
4581
5090
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
4582
5091
  configIsolation: opts.configIsolation,
5092
+ permissionMode: permissionModeFromOpts(opts, provider),
5093
+ sandbox: sandboxFromOpts(opts, provider),
4583
5094
  account: accountFromOpts(opts)
4584
5095
  };
4585
5096
  const loop = store.createLoop(baseCreateInput(name, opts, target));
@@ -4603,8 +5114,194 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
4603
5114
  }
4604
5115
  });
4605
5116
  var workflows = program.command("workflows").alias("workflow").description("manage workflow specs and runs");
5117
+ var templates = program.command("templates").alias("template").description("render and store reusable loop/workflow templates");
5118
+ var events = program.command("events").description("handle Hasna event envelopes from stdin or command transport");
4606
5119
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
4607
5120
  var goal = program.command("goal").description("inspect goal runs");
5121
+ templates.command("list").alias("ls").description("list built-in OpenLoops templates").action(() => {
5122
+ const values = listLoopTemplates();
5123
+ if (isJson())
5124
+ print(values);
5125
+ else {
5126
+ for (const template of values) {
5127
+ console.log(`${template.id} ${template.kind} ${template.description}`);
5128
+ }
5129
+ }
5130
+ });
5131
+ templates.command("show <id>").description("show a built-in template").action((id) => {
5132
+ const template = getLoopTemplate(id);
5133
+ if (!template)
5134
+ throw new Error(`template not found: ${id}`);
5135
+ print(template, `${template.id} ${template.kind}`);
5136
+ });
5137
+ templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, []).action((id, opts) => {
5138
+ const workflow = renderLoopTemplate(id, parseVars(opts.var));
5139
+ print(workflow, JSON.stringify(workflow, null, 2));
5140
+ });
5141
+ templates.command("create-workflow <id>").description("render and store a template as a workflow").option("--var <key=value>", "template variable; may be repeated", collectValues, []).action((id, opts) => {
5142
+ const store = new Store;
5143
+ try {
5144
+ const body = renderLoopTemplate(id, parseVars(opts.var));
5145
+ const workflow = store.createWorkflow(body);
5146
+ print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
5147
+ } finally {
5148
+ store.close();
5149
+ }
5150
+ });
5151
+ var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
5152
+ eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--account <profile>", "OpenAccounts profile name").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
5153
+ const event = await readEventEnvelopeFromStdin();
5154
+ const data = eventData(event);
5155
+ const metadata = eventMetadata(event);
5156
+ const taskId = taskEventField(data, ["id", "task_id", "taskId"]);
5157
+ if (!taskId)
5158
+ throw new Error("todos task event is missing task id in data.id, data.task_id, data.task.id, or data.payload.id");
5159
+ const taskTitle = taskEventField(data, ["title", "task_title", "taskTitle"]);
5160
+ const taskDescription = taskEventField(data, ["description", "body"]);
5161
+ const dataProjectPath = taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd"]);
5162
+ const metadataProjectPath = taskEventField(metadata, [
5163
+ "working_dir",
5164
+ "workingDir",
5165
+ "project_path",
5166
+ "projectPath",
5167
+ "project_canonical_path",
5168
+ "cwd"
5169
+ ]);
5170
+ const projectPath = opts.projectPath ?? dataProjectPath ?? metadataProjectPath ?? process.cwd();
5171
+ const idempotencyKey = `todos-task:${taskId}:${event.type}`;
5172
+ const provider = opts.provider;
5173
+ if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
5174
+ throw new Error("unsupported provider");
5175
+ const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode }, provider);
5176
+ const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
5177
+ const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
5178
+ const workflowBody = renderTodosTaskWorkerVerifierWorkflow({
5179
+ taskId,
5180
+ taskTitle,
5181
+ taskDescription,
5182
+ projectPath,
5183
+ provider,
5184
+ authProfile,
5185
+ account: accountFromOpts(opts),
5186
+ model: opts.model,
5187
+ variant: opts.variant,
5188
+ agent: opts.agent,
5189
+ permissionMode,
5190
+ sandbox,
5191
+ eventId: event.id,
5192
+ eventType: event.type
5193
+ });
5194
+ const idempotencySuffix = stableSuffix(idempotencyKey);
5195
+ workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:workflow`;
5196
+ workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}`;
5197
+ const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:run`;
5198
+ const legacyLoopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${event.id.slice(0, 8)}:run`;
5199
+ const loopInput = {
5200
+ name: loopName,
5201
+ description: `Run ${workflowBody.name} once for task ${taskId}; idempotency=${idempotencyKey}; event=${event.id}`,
5202
+ schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
5203
+ target: { type: "workflow", workflowId: "<created-workflow-id>" },
5204
+ overlap: "skip",
5205
+ maxAttempts: 1,
5206
+ retryDelayMs: 60000,
5207
+ leaseMs: 90 * 60000
5208
+ };
5209
+ if (opts.dryRun) {
5210
+ print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
5211
+ return;
5212
+ }
5213
+ const store = new Store;
5214
+ try {
5215
+ const existingLoop = store.findLoopByName(loopName) ?? store.findLoopByName(legacyLoopName);
5216
+ if (existingLoop) {
5217
+ const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
5218
+ print({
5219
+ deduped: true,
5220
+ idempotencyKey,
5221
+ dedupedBy: existingLoop.name === loopName ? "idempotency" : "legacy-event-name",
5222
+ event,
5223
+ workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined,
5224
+ loop: publicLoop(existingLoop)
5225
+ }, `deduped existing loop ${existingLoop.id} (${existingLoop.name}) for event=${event.id} idempotency=${idempotencyKey}`);
5226
+ return;
5227
+ }
5228
+ const existingWorkflow = store.findWorkflowByName(workflowBody.name);
5229
+ const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
5230
+ const loop = store.createLoop({
5231
+ ...loopInput,
5232
+ target: { type: "workflow", workflowId: workflow.id }
5233
+ });
5234
+ print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
5235
+ } finally {
5236
+ store.close();
5237
+ }
5238
+ });
5239
+ eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--account <profile>", "OpenAccounts profile name").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
5240
+ const event = await readEventEnvelopeFromStdin();
5241
+ const data = eventData(event);
5242
+ const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
5243
+ const provider = opts.provider;
5244
+ if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
5245
+ throw new Error("unsupported provider");
5246
+ const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode }, provider);
5247
+ const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
5248
+ const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
5249
+ const workflowBody = renderEventWorkerVerifierWorkflow({
5250
+ eventId: event.id,
5251
+ eventType: event.type,
5252
+ eventSource: event.source,
5253
+ eventSubject: stringField(event.subject),
5254
+ eventMessage: stringField(event.message),
5255
+ eventJson: JSON.stringify(event),
5256
+ projectPath,
5257
+ provider,
5258
+ authProfile,
5259
+ account: accountFromOpts(opts),
5260
+ model: opts.model,
5261
+ variant: opts.variant,
5262
+ agent: opts.agent,
5263
+ permissionMode,
5264
+ sandbox
5265
+ });
5266
+ const eventSuffix = event.id.slice(0, 8);
5267
+ const source = slugSegment(event.source, "source");
5268
+ const type = slugSegment(event.type, "type");
5269
+ workflowBody.name = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:workflow`;
5270
+ workflowBody.description = `Event-triggered worker/verifier workflow for ${event.source}/${event.type}`;
5271
+ const loopName = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:run`;
5272
+ const loopInput = {
5273
+ name: loopName,
5274
+ description: `Run ${workflowBody.name} once for event ${event.id}`,
5275
+ schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
5276
+ target: { type: "workflow", workflowId: "<created-workflow-id>" },
5277
+ overlap: "skip",
5278
+ maxAttempts: 1,
5279
+ retryDelayMs: 60000,
5280
+ leaseMs: 90 * 60000
5281
+ };
5282
+ if (opts.dryRun) {
5283
+ print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
5284
+ return;
5285
+ }
5286
+ const store = new Store;
5287
+ try {
5288
+ const existingLoop = store.findLoopByName(loopName);
5289
+ if (existingLoop) {
5290
+ const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
5291
+ print({ deduped: true, event, workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined, loop: publicLoop(existingLoop) }, `deduped existing loop ${existingLoop.id} (${existingLoop.name})`);
5292
+ return;
5293
+ }
5294
+ const existingWorkflow = store.findWorkflowByName(workflowBody.name);
5295
+ const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
5296
+ const loop = store.createLoop({
5297
+ ...loopInput,
5298
+ target: { type: "workflow", workflowId: workflow.id }
5299
+ });
5300
+ print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
5301
+ } finally {
5302
+ store.close();
5303
+ }
5304
+ });
4608
5305
  goal.command("show <idOrName>").description("show a goal or configured loop/workflow goal").action((idOrName) => {
4609
5306
  const store = new Store;
4610
5307
  try {
@@ -4718,11 +5415,11 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
4718
5415
  try {
4719
5416
  const run = store.requireWorkflowRun(runId);
4720
5417
  const steps = store.listWorkflowStepRuns(run.id);
4721
- const events = store.listWorkflowEvents(run.id);
5418
+ const events2 = store.listWorkflowEvents(run.id);
4722
5419
  const value = {
4723
5420
  workflowRun: publicWorkflowRun(run),
4724
5421
  steps: steps.map((step) => publicWorkflowStepRun(step)),
4725
- events: events.map(publicWorkflowEvent)
5422
+ events: events2.map(publicWorkflowEvent)
4726
5423
  };
4727
5424
  if (isJson())
4728
5425
  print(value);
@@ -4732,7 +5429,7 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
4732
5429
  const publicStep = publicWorkflowStepRun(step);
4733
5430
  console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${publicStep.error ?? ""}`);
4734
5431
  }
4735
- console.log(` events=${events.length}`);
5432
+ console.log(` events=${events2.length}`);
4736
5433
  }
4737
5434
  } finally {
4738
5435
  store.close();
@@ -4784,11 +5481,11 @@ workflows.command("runs [idOrName]").option("--limit <n>", "limit", "50").action
4784
5481
  workflows.command("events <runId>").option("--limit <n>", "limit", "200").action((runId, opts) => {
4785
5482
  const store = new Store;
4786
5483
  try {
4787
- const events = store.listWorkflowEvents(runId, Number(opts.limit));
5484
+ const events2 = store.listWorkflowEvents(runId, Number(opts.limit));
4788
5485
  if (isJson())
4789
- print(events.map(publicWorkflowEvent));
5486
+ print(events2.map(publicWorkflowEvent));
4790
5487
  else {
4791
- for (const event of events) {
5488
+ for (const event of events2) {
4792
5489
  console.log(`${String(event.sequence).padStart(3, "0")} ${event.eventType.padEnd(14)} ${event.stepId ?? "-"} ${event.createdAt}`);
4793
5490
  }
4794
5491
  }