@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/index.js CHANGED
@@ -368,6 +368,35 @@ function validateTarget(value, label) {
368
368
  if (value.provider !== "codewith")
369
369
  throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
370
370
  }
371
+ if (value.variant !== undefined)
372
+ assertString(value.variant, `${label}.variant`);
373
+ if (value.permissionMode !== undefined) {
374
+ assertString(value.permissionMode, `${label}.permissionMode`);
375
+ const permissionModes = ["default", "plan", "auto", "bypass"];
376
+ if (!permissionModes.includes(value.permissionMode)) {
377
+ throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
378
+ }
379
+ if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
380
+ throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
381
+ }
382
+ if (value.permissionMode === "auto" && value.provider !== "claude") {
383
+ throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
384
+ }
385
+ }
386
+ if (value.sandbox !== undefined) {
387
+ assertString(value.sandbox, `${label}.sandbox`);
388
+ const codexLike = ["read-only", "workspace-write", "danger-full-access"];
389
+ const cursorLike = ["enabled", "disabled"];
390
+ if (["codewith", "codex"].includes(value.provider)) {
391
+ if (!codexLike.includes(value.sandbox))
392
+ throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
393
+ } else if (value.provider === "cursor") {
394
+ if (!cursorLike.includes(value.sandbox))
395
+ throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
396
+ } else {
397
+ throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
398
+ }
399
+ }
371
400
  return value;
372
401
  }
373
402
  throw new Error(`${label}.type must be command or agent`);
@@ -2353,7 +2382,7 @@ function providerCommand(provider) {
2353
2382
  case "claude":
2354
2383
  return "claude";
2355
2384
  case "cursor":
2356
- return "cursor-agent";
2385
+ return "sh";
2357
2386
  case "codewith":
2358
2387
  return "codewith";
2359
2388
  case "aicopilot":
@@ -2364,22 +2393,107 @@ function providerCommand(provider) {
2364
2393
  return "codex";
2365
2394
  }
2366
2395
  }
2396
+ function codewithLikeSandbox(target) {
2397
+ const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
2398
+ if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
2399
+ throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
2400
+ }
2401
+ return sandbox;
2402
+ }
2403
+ function configStringValue(value) {
2404
+ return JSON.stringify(value);
2405
+ }
2406
+ function assertStringOption(value, label) {
2407
+ if (value !== undefined && typeof value !== "string")
2408
+ throw new Error(`${label} must be a string`);
2409
+ }
2410
+ function assertSupportedAgentOptions(target) {
2411
+ assertStringOption(target.variant, `${target.provider}.variant`);
2412
+ assertStringOption(target.model, `${target.provider}.model`);
2413
+ assertStringOption(target.agent, `${target.provider}.agent`);
2414
+ assertStringOption(target.authProfile, `${target.provider}.authProfile`);
2415
+ if (target.authProfile !== undefined && target.provider !== "codewith") {
2416
+ throw new Error(`${target.provider}.authProfile is supported only for codewith`);
2417
+ }
2418
+ if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
2419
+ throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
2420
+ }
2421
+ if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
2422
+ throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
2423
+ }
2424
+ if (["codewith", "codex"].includes(target.provider)) {
2425
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2426
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2427
+ }
2428
+ if (target.sandbox)
2429
+ codewithLikeSandbox(target);
2430
+ return;
2431
+ }
2432
+ if (target.provider === "claude") {
2433
+ if (target.sandbox !== undefined)
2434
+ throw new Error("claude.sandbox is not supported");
2435
+ return;
2436
+ }
2437
+ if (target.provider === "cursor") {
2438
+ if (target.permissionMode === "auto")
2439
+ throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
2440
+ if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
2441
+ throw new Error("cursor.sandbox must be enabled or disabled");
2442
+ }
2443
+ return;
2444
+ }
2445
+ if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
2446
+ throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
2447
+ }
2448
+ if (target.sandbox !== undefined)
2449
+ throw new Error(`${target.provider}.sandbox is not supported`);
2450
+ }
2367
2451
  function agentArgs(target) {
2452
+ assertSupportedAgentOptions(target);
2368
2453
  const isolation = target.configIsolation ?? "safe";
2454
+ const permissionMode = target.permissionMode ?? "default";
2369
2455
  const args = [];
2370
2456
  switch (target.provider) {
2371
2457
  case "claude":
2372
2458
  if (isolation === "safe")
2373
2459
  args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
2460
+ if (permissionMode !== "default") {
2461
+ const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
2462
+ if (mode)
2463
+ args.push("--permission-mode", mode);
2464
+ }
2374
2465
  args.push("-p", "--output-format", "json");
2375
2466
  if (target.model)
2376
2467
  args.push("--model", target.model);
2468
+ if (target.variant)
2469
+ args.push("--effort", target.variant);
2377
2470
  if (target.agent)
2378
2471
  args.push("--agent", target.agent);
2379
2472
  args.push(...target.extraArgs ?? []);
2380
2473
  return args;
2381
2474
  case "cursor":
2382
- args.push("agent", "-p");
2475
+ args.push("-c", [
2476
+ "set -eu",
2477
+ "if command -v cursor >/dev/null 2>&1; then",
2478
+ ' exec cursor agent "$@"',
2479
+ "elif command -v agent >/dev/null 2>&1; then",
2480
+ ' exec agent "$@"',
2481
+ "else",
2482
+ " echo 'Executable not found in PATH: cursor agent or agent' >&2",
2483
+ " exit 127",
2484
+ "fi"
2485
+ ].join(`
2486
+ `), "openloops-cursor", "-p");
2487
+ if (permissionMode === "plan")
2488
+ args.push("--mode", "plan");
2489
+ if (permissionMode === "bypass")
2490
+ args.push("--force");
2491
+ const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
2492
+ if (cursorSandbox) {
2493
+ if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
2494
+ throw new Error("cursor sandbox must be enabled or disabled");
2495
+ args.push("--sandbox", cursorSandbox);
2496
+ }
2383
2497
  if (target.model)
2384
2498
  args.push("--model", target.model);
2385
2499
  if (target.agent)
@@ -2387,7 +2501,10 @@ function agentArgs(target) {
2387
2501
  args.push(...target.extraArgs ?? []);
2388
2502
  return args;
2389
2503
  case "codewith":
2390
- args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : [], "--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", "workspace-write", "--skip-git-repo-check");
2504
+ args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
2505
+ if (target.variant)
2506
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2507
+ args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
2391
2508
  if (isolation === "safe")
2392
2509
  args.push("--ignore-rules");
2393
2510
  if (target.cwd)
@@ -2399,7 +2516,9 @@ function agentArgs(target) {
2399
2516
  args.push(...target.extraArgs ?? []);
2400
2517
  return args;
2401
2518
  case "codex":
2402
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", "workspace-write");
2519
+ if (target.variant)
2520
+ args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
2521
+ args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
2403
2522
  if (isolation === "safe")
2404
2523
  args.push("--ignore-rules");
2405
2524
  if (target.cwd)
@@ -2412,10 +2531,14 @@ function agentArgs(target) {
2412
2531
  args.push("run", "--format", "json");
2413
2532
  if (isolation === "safe")
2414
2533
  args.push("--pure");
2534
+ if (permissionMode === "bypass")
2535
+ args.push("--dangerously-skip-permissions");
2415
2536
  if (target.cwd)
2416
2537
  args.push("--dir", target.cwd);
2417
2538
  if (target.model)
2418
2539
  args.push("--model", target.model);
2540
+ if (target.variant)
2541
+ args.push("--variant", target.variant);
2419
2542
  if (target.agent)
2420
2543
  args.push("--agent", target.agent);
2421
2544
  args.push(...target.extraArgs ?? []);
@@ -2424,10 +2547,14 @@ function agentArgs(target) {
2424
2547
  args.push("run", "--format", "json");
2425
2548
  if (isolation === "safe")
2426
2549
  args.push("--pure");
2550
+ if (permissionMode === "bypass")
2551
+ args.push("--dangerously-skip-permissions");
2427
2552
  if (target.cwd)
2428
2553
  args.push("--dir", target.cwd);
2429
2554
  if (target.model)
2430
2555
  args.push("--model", target.model);
2556
+ if (target.variant)
2557
+ args.push("--variant", target.variant);
2431
2558
  if (target.agent)
2432
2559
  args.push("--agent", target.agent);
2433
2560
  args.push(...target.extraArgs ?? []);
@@ -2456,6 +2583,7 @@ function commandSpec(target) {
2456
2583
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2457
2584
  account: agentTarget.account,
2458
2585
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2586
+ preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2459
2587
  stdin: agentTarget.prompt
2460
2588
  };
2461
2589
  }
@@ -2523,11 +2651,15 @@ function remoteScript(spec, metadata) {
2523
2651
  `;
2524
2652
  }
2525
2653
  function remotePreflightScript(spec, metadata) {
2526
- return [
2654
+ const lines = [
2527
2655
  ...remoteBootstrapLines(spec, metadata),
2528
2656
  "command -v bash >/dev/null 2>&1",
2529
2657
  `command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
2530
- ].join(`
2658
+ ];
2659
+ if (spec.preflightAnyOf?.length) {
2660
+ 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");
2661
+ }
2662
+ return lines.join(`
2531
2663
  `);
2532
2664
  }
2533
2665
  function transportEnv(opts) {
@@ -2680,6 +2812,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2680
2812
  if (!spec.shell && !executableExists(spec.command, env)) {
2681
2813
  throw new Error(commandNotFoundMessage(spec.command, env));
2682
2814
  }
2815
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2816
+ throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2817
+ }
2683
2818
  return {
2684
2819
  command: spec.command,
2685
2820
  accountProfile: spec.account?.profile,
@@ -2710,6 +2845,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
2710
2845
  durationMs: 0
2711
2846
  };
2712
2847
  }
2848
+ if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2849
+ return {
2850
+ status: "failed",
2851
+ stdout,
2852
+ stderr,
2853
+ error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
2854
+ startedAt,
2855
+ finishedAt: nowIso(),
2856
+ durationMs: 0
2857
+ };
2858
+ }
2713
2859
  const child = spawn(spec.command, spec.args, {
2714
2860
  cwd: spec.cwd,
2715
2861
  env,
@@ -3813,6 +3959,241 @@ class LoopsClient {
3813
3959
  function loops(opts = {}) {
3814
3960
  return new LoopsClient(opts);
3815
3961
  }
3962
+ // src/lib/templates.ts
3963
+ var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
3964
+ var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
3965
+ var TEMPLATE_SUMMARIES = [
3966
+ {
3967
+ id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
3968
+ name: "Todos Task Worker + Verifier",
3969
+ 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.",
3970
+ kind: "workflow",
3971
+ variables: [
3972
+ { name: "taskId", required: true, description: "Todos task id to execute." },
3973
+ { name: "taskTitle", description: "Human-readable task title." },
3974
+ { name: "projectPath", required: true, description: "Repository or project working directory." },
3975
+ { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3976
+ { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
3977
+ { name: "model", description: "Provider model." },
3978
+ { name: "variant", description: "Provider reasoning/model effort variant." },
3979
+ { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
3980
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
3981
+ ]
3982
+ },
3983
+ {
3984
+ id: EVENT_WORKER_VERIFIER_TEMPLATE_ID,
3985
+ name: "Hasna Event Worker + Verifier",
3986
+ 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.",
3987
+ kind: "workflow",
3988
+ variables: [
3989
+ { name: "eventId", required: true, description: "Hasna event id." },
3990
+ { name: "eventType", required: true, description: "Hasna event type." },
3991
+ { name: "eventSource", required: true, description: "Hasna event source." },
3992
+ { name: "eventJson", required: true, description: "Full event envelope JSON." },
3993
+ { name: "projectPath", required: true, description: "Repository or project working directory." },
3994
+ { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3995
+ { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
3996
+ { name: "model", description: "Provider model." },
3997
+ { name: "variant", description: "Provider reasoning/model effort variant." },
3998
+ { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
3999
+ { name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
4000
+ ]
4001
+ }
4002
+ ];
4003
+ function compactJson(value) {
4004
+ return JSON.stringify(value);
4005
+ }
4006
+ function taskLabel(input) {
4007
+ const head = input.taskTitle?.trim() || input.taskId;
4008
+ return head.length > 160 ? `${head.slice(0, 157)}...` : head;
4009
+ }
4010
+ function agentTarget(input, prompt) {
4011
+ const provider = input.provider ?? "codewith";
4012
+ const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4013
+ return {
4014
+ type: "agent",
4015
+ provider,
4016
+ prompt,
4017
+ cwd: input.projectPath,
4018
+ model: input.model,
4019
+ variant: input.variant,
4020
+ agent: input.agent,
4021
+ authProfile: provider === "codewith" ? input.authProfile : undefined,
4022
+ configIsolation: "safe",
4023
+ permissionMode: input.permissionMode ?? "bypass",
4024
+ sandbox,
4025
+ account: input.account,
4026
+ timeoutMs: 45 * 60000
4027
+ };
4028
+ }
4029
+ function listLoopTemplates() {
4030
+ return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
4031
+ }
4032
+ function getLoopTemplate(id) {
4033
+ return listLoopTemplates().find((template) => template.id === id || template.name === id);
4034
+ }
4035
+ function renderTodosTaskWorkerVerifierWorkflow(input) {
4036
+ if (!input.taskId?.trim())
4037
+ throw new Error("taskId is required");
4038
+ if (!input.projectPath?.trim())
4039
+ throw new Error("projectPath is required");
4040
+ const taskContext = {
4041
+ taskId: input.taskId,
4042
+ taskTitle: input.taskTitle,
4043
+ taskDescription: input.taskDescription,
4044
+ eventId: input.eventId,
4045
+ eventType: input.eventType,
4046
+ projectPath: input.projectPath
4047
+ };
4048
+ const workerPrompt = [
4049
+ `/goal Complete todos task ${input.taskId} in ${input.projectPath}.`,
4050
+ "",
4051
+ "You are the worker agent for a task-triggered OpenLoops workflow.",
4052
+ "Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
4053
+ "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.",
4054
+ "Do not mark the task complete unless the work is genuinely done and validated.",
4055
+ "",
4056
+ `Task context JSON: ${compactJson(taskContext)}`
4057
+ ].join(`
4058
+ `);
4059
+ const verifierPrompt = [
4060
+ `/goal Verify todos task ${input.taskId} after the worker step.`,
4061
+ "",
4062
+ "You are the verifier agent for a task-triggered OpenLoops workflow.",
4063
+ "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.",
4064
+ "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.",
4065
+ "Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
4066
+ "",
4067
+ `Task context JSON: ${compactJson(taskContext)}`
4068
+ ].join(`
4069
+ `);
4070
+ return {
4071
+ name: `todos-task-${input.taskId.slice(0, 8)}-worker-verifier`,
4072
+ description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
4073
+ version: 1,
4074
+ steps: [
4075
+ {
4076
+ id: "worker",
4077
+ name: "Worker",
4078
+ description: "Implement the todos task and record evidence.",
4079
+ target: agentTarget(input, workerPrompt),
4080
+ timeoutMs: 45 * 60000
4081
+ },
4082
+ {
4083
+ id: "verifier",
4084
+ name: "Verifier",
4085
+ description: "Adversarially verify worker output and update todos.",
4086
+ dependsOn: ["worker"],
4087
+ target: agentTarget(input, verifierPrompt),
4088
+ timeoutMs: 30 * 60000
4089
+ }
4090
+ ]
4091
+ };
4092
+ }
4093
+ function renderEventWorkerVerifierWorkflow(input) {
4094
+ if (!input.eventId?.trim())
4095
+ throw new Error("eventId is required");
4096
+ if (!input.eventType?.trim())
4097
+ throw new Error("eventType is required");
4098
+ if (!input.eventSource?.trim())
4099
+ throw new Error("eventSource is required");
4100
+ if (!input.eventJson?.trim())
4101
+ throw new Error("eventJson is required");
4102
+ if (!input.projectPath?.trim())
4103
+ throw new Error("projectPath is required");
4104
+ const eventContext = {
4105
+ eventId: input.eventId,
4106
+ eventType: input.eventType,
4107
+ eventSource: input.eventSource,
4108
+ eventSubject: input.eventSubject,
4109
+ eventMessage: input.eventMessage,
4110
+ projectPath: input.projectPath
4111
+ };
4112
+ const workerPrompt = [
4113
+ `/goal Handle Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}) in ${input.projectPath}.`,
4114
+ "",
4115
+ "You are the worker agent for an event-triggered OpenLoops workflow.",
4116
+ "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.",
4117
+ "If the event is informational or does not require action, record that finding and stop without making changes.",
4118
+ "",
4119
+ `Event context JSON: ${compactJson(eventContext)}`,
4120
+ `Full event envelope JSON: ${input.eventJson}`
4121
+ ].join(`
4122
+ `);
4123
+ const verifierPrompt = [
4124
+ `/goal Verify handling of Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}).`,
4125
+ "",
4126
+ "You are the verifier agent for an event-triggered OpenLoops workflow.",
4127
+ "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.",
4128
+ "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.",
4129
+ "",
4130
+ `Event context JSON: ${compactJson(eventContext)}`,
4131
+ `Full event envelope JSON: ${input.eventJson}`
4132
+ ].join(`
4133
+ `);
4134
+ return {
4135
+ name: `event-${input.eventSource}-${input.eventType}-${input.eventId.slice(0, 8)}-worker-verifier`.replace(/[^a-zA-Z0-9._:-]+/g, "-"),
4136
+ description: `Event-triggered worker/verifier workflow for ${input.eventSource}/${input.eventType}`,
4137
+ version: 1,
4138
+ steps: [
4139
+ {
4140
+ id: "worker",
4141
+ name: "Worker",
4142
+ description: "Handle the Hasna event and record evidence.",
4143
+ target: agentTarget(input, workerPrompt),
4144
+ timeoutMs: 45 * 60000
4145
+ },
4146
+ {
4147
+ id: "verifier",
4148
+ name: "Verifier",
4149
+ description: "Adversarially verify event handling.",
4150
+ dependsOn: ["worker"],
4151
+ target: agentTarget(input, verifierPrompt),
4152
+ timeoutMs: 30 * 60000
4153
+ }
4154
+ ]
4155
+ };
4156
+ }
4157
+ function renderLoopTemplate(id, values) {
4158
+ if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
4159
+ return renderTodosTaskWorkerVerifierWorkflow({
4160
+ taskId: values.taskId ?? "",
4161
+ taskTitle: values.taskTitle,
4162
+ taskDescription: values.taskDescription,
4163
+ projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4164
+ provider: values.provider,
4165
+ authProfile: values.authProfile,
4166
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4167
+ model: values.model,
4168
+ variant: values.variant,
4169
+ agent: values.agent,
4170
+ permissionMode: values.permissionMode,
4171
+ sandbox: values.sandbox,
4172
+ eventId: values.eventId,
4173
+ eventType: values.eventType
4174
+ });
4175
+ }
4176
+ if (id === EVENT_WORKER_VERIFIER_TEMPLATE_ID) {
4177
+ return renderEventWorkerVerifierWorkflow({
4178
+ eventId: values.eventId ?? "",
4179
+ eventType: values.eventType ?? "",
4180
+ eventSource: values.eventSource ?? "",
4181
+ eventSubject: values.eventSubject,
4182
+ eventMessage: values.eventMessage,
4183
+ eventJson: values.eventJson ?? "",
4184
+ projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4185
+ provider: values.provider,
4186
+ authProfile: values.authProfile,
4187
+ account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4188
+ model: values.model,
4189
+ variant: values.variant,
4190
+ agent: values.agent,
4191
+ permissionMode: values.permissionMode,
4192
+ sandbox: values.sandbox
4193
+ });
4194
+ }
4195
+ throw new Error(`unknown template: ${id}`);
4196
+ }
3816
4197
  // src/lib/doctor.ts
3817
4198
  import { spawnSync as spawnSync3 } from "child_process";
3818
4199
  import { accessSync as accessSync2, constants as constants2 } from "fs";
@@ -3945,17 +4326,35 @@ async function stopDaemon(opts = {}) {
3945
4326
  // src/lib/doctor.ts
3946
4327
  var PROVIDER_COMMANDS = [
3947
4328
  "claude",
3948
- "cursor-agent",
4329
+ "cursor agent",
3949
4330
  "codewith",
3950
4331
  "aicopilot",
3951
4332
  "opencode",
3952
4333
  "codex"
3953
4334
  ];
3954
4335
  function hasCommand(command) {
4336
+ if (command === "cursor agent") {
4337
+ return hasCommand("cursor") || hasCommand("agent");
4338
+ }
3955
4339
  const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
3956
4340
  return (result.status ?? 1) === 0;
3957
4341
  }
3958
4342
  function commandVersion(command) {
4343
+ if (command === "cursor agent") {
4344
+ const cursorResult = spawnSync3("cursor", ["agent", "--version"], {
4345
+ encoding: "utf8",
4346
+ stdio: ["ignore", "pipe", "pipe"]
4347
+ });
4348
+ if ((cursorResult.status ?? 1) === 0)
4349
+ return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
4350
+ const agentResult = spawnSync3("agent", ["--version"], {
4351
+ encoding: "utf8",
4352
+ stdio: ["ignore", "pipe", "pipe"]
4353
+ });
4354
+ if ((agentResult.status ?? 1) === 0)
4355
+ return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
4356
+ return;
4357
+ }
3959
4358
  const result = spawnSync3(command, ["--version"], {
3960
4359
  encoding: "utf8",
3961
4360
  stdio: ["ignore", "pipe", "pipe"]
@@ -4040,6 +4439,9 @@ export {
4040
4439
  rollupSummary,
4041
4440
  resolveLoopMachine,
4042
4441
  resolveGoalModel,
4442
+ renderTodosTaskWorkerVerifierWorkflow,
4443
+ renderLoopTemplate,
4444
+ renderEventWorkerVerifierWorkflow,
4043
4445
  refreshLoopMachine,
4044
4446
  readyNodeKeys,
4045
4447
  preflightWorkflow,
@@ -4049,13 +4451,17 @@ export {
4049
4451
  nextCronRun,
4050
4452
  loops,
4051
4453
  listOpenMachines,
4454
+ listLoopTemplates,
4052
4455
  isTerminal as isGoalTerminal,
4053
4456
  initialNextRun,
4457
+ getLoopTemplate,
4054
4458
  executeWorkflow,
4055
4459
  executeTarget,
4056
4460
  executeLoopTarget,
4057
4461
  executeLoop,
4058
4462
  computeNextAfter,
4463
+ TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
4059
4464
  Store,
4060
- LoopsClient
4465
+ LoopsClient,
4466
+ EVENT_WORKER_VERIFIER_TEMPLATE_ID
4061
4467
  };
package/dist/lib/store.js CHANGED
@@ -368,6 +368,35 @@ function validateTarget(value, label) {
368
368
  if (value.provider !== "codewith")
369
369
  throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
370
370
  }
371
+ if (value.variant !== undefined)
372
+ assertString(value.variant, `${label}.variant`);
373
+ if (value.permissionMode !== undefined) {
374
+ assertString(value.permissionMode, `${label}.permissionMode`);
375
+ const permissionModes = ["default", "plan", "auto", "bypass"];
376
+ if (!permissionModes.includes(value.permissionMode)) {
377
+ throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
378
+ }
379
+ if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
380
+ throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
381
+ }
382
+ if (value.permissionMode === "auto" && value.provider !== "claude") {
383
+ throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
384
+ }
385
+ }
386
+ if (value.sandbox !== undefined) {
387
+ assertString(value.sandbox, `${label}.sandbox`);
388
+ const codexLike = ["read-only", "workspace-write", "danger-full-access"];
389
+ const cursorLike = ["enabled", "disabled"];
390
+ if (["codewith", "codex"].includes(value.provider)) {
391
+ if (!codexLike.includes(value.sandbox))
392
+ throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
393
+ } else if (value.provider === "cursor") {
394
+ if (!cursorLike.includes(value.sandbox))
395
+ throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
396
+ } else {
397
+ throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
398
+ }
399
+ }
371
400
  return value;
372
401
  }
373
402
  throw new Error(`${label}.type must be command or agent`);
@@ -0,0 +1,41 @@
1
+ import type { AccountRef, AgentPermissionMode, AgentProvider, AgentSandbox, CreateWorkflowInput, LoopTemplateSummary } from "../types.js";
2
+ export declare const TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
3
+ export declare const EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4
+ export interface TodosTaskWorkflowTemplateInput {
5
+ taskId: string;
6
+ taskTitle?: string;
7
+ taskDescription?: string;
8
+ projectPath: string;
9
+ provider?: AgentProvider;
10
+ authProfile?: string;
11
+ account?: AccountRef;
12
+ model?: string;
13
+ variant?: string;
14
+ agent?: string;
15
+ permissionMode?: AgentPermissionMode;
16
+ sandbox?: AgentSandbox;
17
+ eventId?: string;
18
+ eventType?: string;
19
+ }
20
+ export interface EventWorkflowTemplateInput {
21
+ eventId: string;
22
+ eventType: string;
23
+ eventSource: string;
24
+ eventSubject?: string;
25
+ eventMessage?: string;
26
+ eventJson: string;
27
+ projectPath: string;
28
+ provider?: AgentProvider;
29
+ authProfile?: string;
30
+ account?: AccountRef;
31
+ model?: string;
32
+ variant?: string;
33
+ agent?: string;
34
+ permissionMode?: AgentPermissionMode;
35
+ sandbox?: AgentSandbox;
36
+ }
37
+ export declare function listLoopTemplates(): LoopTemplateSummary[];
38
+ export declare function getLoopTemplate(id: string): LoopTemplateSummary | undefined;
39
+ export declare function renderTodosTaskWorkerVerifierWorkflow(input: TodosTaskWorkflowTemplateInput): CreateWorkflowInput;
40
+ export declare function renderEventWorkerVerifierWorkflow(input: EventWorkflowTemplateInput): CreateWorkflowInput;
41
+ export declare function renderLoopTemplate(id: string, values: Record<string, string | undefined>): CreateWorkflowInput;