@hasna/loops 0.3.21 → 0.3.22

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
@@ -2833,6 +2833,7 @@ function commandSpec(target) {
2833
2833
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2834
2834
  account: agentTarget.account,
2835
2835
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2836
+ nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
2836
2837
  preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2837
2838
  stdin: agentTarget.prompt,
2838
2839
  allowlist: agentTarget.allowlist
@@ -2914,6 +2915,9 @@ function remotePreflightScript(spec, metadata) {
2914
2915
  if (spec.preflightAnyOf?.length) {
2915
2916
  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");
2916
2917
  }
2918
+ if (spec.nativeAuthProfile?.provider === "codewith") {
2919
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
2920
+ }
2917
2921
  return lines.join(`
2918
2922
  `);
2919
2923
  }
@@ -2929,6 +2933,29 @@ function transportEnv(opts) {
2929
2933
  env.PATH = normalizeExecutionPath(env);
2930
2934
  return env;
2931
2935
  }
2936
+ function preflightNativeAuthProfile(spec, env) {
2937
+ if (!spec.nativeAuthProfile)
2938
+ return;
2939
+ if (spec.nativeAuthProfile.provider !== "codewith")
2940
+ return;
2941
+ const result = spawnSync2(spec.command, ["profile", "list"], {
2942
+ encoding: "utf8",
2943
+ env,
2944
+ stdio: ["ignore", "pipe", "pipe"],
2945
+ timeout: 15000
2946
+ });
2947
+ if (result.error) {
2948
+ throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
2949
+ }
2950
+ if ((result.status ?? 1) !== 0) {
2951
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
2952
+ throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
2953
+ }
2954
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
2955
+ if (!profiles.has(spec.nativeAuthProfile.profile)) {
2956
+ throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
2957
+ }
2958
+ }
2932
2959
  function preflightRemoteSpec(spec, machine, metadata, opts) {
2933
2960
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2934
2961
  const result = spawnSync2(plan.command, plan.args, {
@@ -3070,6 +3097,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
3070
3097
  if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
3071
3098
  throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
3072
3099
  }
3100
+ preflightNativeAuthProfile(spec, env);
3073
3101
  return {
3074
3102
  command: spec.command,
3075
3103
  accountProfile: spec.account?.profile,
@@ -3111,6 +3139,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3111
3139
  durationMs: 0
3112
3140
  };
3113
3141
  }
3142
+ try {
3143
+ preflightNativeAuthProfile(spec, env);
3144
+ } catch (err) {
3145
+ return {
3146
+ status: "failed",
3147
+ stdout: "",
3148
+ stderr: "",
3149
+ error: err instanceof Error ? err.message : String(err),
3150
+ startedAt,
3151
+ finishedAt: nowIso(),
3152
+ durationMs: 0
3153
+ };
3154
+ }
3114
3155
  const child = spawn(spec.command, spec.args, {
3115
3156
  cwd: spec.cwd,
3116
3157
  env,
@@ -3767,11 +3808,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
3767
3808
  return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
3768
3809
  }
3769
3810
  function preflightWorkflow(workflow, opts = {}) {
3770
- return workflowExecutionOrder(workflow).map((step) => preflightTarget(targetWithStepAccount(step), {
3771
- workflowId: workflow.id,
3772
- workflowName: workflow.name,
3773
- workflowStepId: step.id
3774
- }, opts));
3811
+ return workflowExecutionOrder(workflow).map((step) => {
3812
+ try {
3813
+ return {
3814
+ workflowStepId: step.id,
3815
+ ...preflightTarget(targetWithStepAccount(step), {
3816
+ workflowId: workflow.id,
3817
+ workflowName: workflow.name,
3818
+ workflowStepId: step.id
3819
+ }, opts)
3820
+ };
3821
+ } catch (error) {
3822
+ const message = error instanceof Error ? error.message : String(error);
3823
+ throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
3824
+ }
3825
+ });
3775
3826
  }
3776
3827
  async function executeLoopTarget(store, loop, run, opts = {}) {
3777
3828
  if (loop.target.type !== "workflow") {
@@ -5092,7 +5143,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5092
5143
  // package.json
5093
5144
  var package_default = {
5094
5145
  name: "@hasna/loops",
5095
- version: "0.3.21",
5146
+ version: "0.3.22",
5096
5147
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5097
5148
  type: "module",
5098
5149
  main: "dist/index.js",
@@ -5576,6 +5627,41 @@ function print(value, human) {
5576
5627
  else
5577
5628
  console.log(human);
5578
5629
  }
5630
+ function printCreatedLoop(loop, human, preflight) {
5631
+ if (preflight !== undefined)
5632
+ print({ loop: publicLoop(loop), preflight }, human);
5633
+ else
5634
+ print(publicLoop(loop), human);
5635
+ }
5636
+ function preflightFailed(error, context) {
5637
+ if (!isJson())
5638
+ throw error;
5639
+ const message = error instanceof Error ? error.message : String(error);
5640
+ print({
5641
+ ok: false,
5642
+ created: false,
5643
+ preflight: {
5644
+ ok: false,
5645
+ error: redact(message, 320)
5646
+ },
5647
+ ...context
5648
+ });
5649
+ process.exit(1);
5650
+ }
5651
+ function preflightLoopTarget(target, context, metadata, opts) {
5652
+ try {
5653
+ return preflightTarget(target, metadata, opts);
5654
+ } catch (error) {
5655
+ preflightFailed(error, context);
5656
+ }
5657
+ }
5658
+ function preflightStoredWorkflow(workflow, context, opts) {
5659
+ try {
5660
+ return preflightWorkflow(workflow, opts);
5661
+ } catch (error) {
5662
+ preflightFailed(error, context);
5663
+ }
5664
+ }
5579
5665
  function printTextOutput(value) {
5580
5666
  for (const line of textOutputBlocks(value, { indent: " " }))
5581
5667
  console.log(line);
@@ -6020,7 +6106,7 @@ function permissionModeFromOpts(opts, provider) {
6020
6106
  return mode;
6021
6107
  }
6022
6108
  var create = program.command("create").description("create loops");
6023
- 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) => {
6109
+ 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").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
6024
6110
  const store = new Store;
6025
6111
  try {
6026
6112
  const target = {
@@ -6031,13 +6117,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6031
6117
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
6032
6118
  account: accountFromOpts(opts)
6033
6119
  };
6034
- const loop = store.createLoop(baseCreateInput(name, opts, target));
6035
- print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
6120
+ const input = baseCreateInput(name, opts, target);
6121
+ const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
6122
+ const loop = store.createLoop(input);
6123
+ printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
6036
6124
  } finally {
6037
6125
  store.close();
6038
6126
  }
6039
6127
  });
6040
- 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("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
6128
+ 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("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
6041
6129
  const provider = opts.provider;
6042
6130
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
6043
6131
  throw new Error("unsupported provider");
@@ -6063,13 +6151,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6063
6151
  allowlist: allowlistFromOpts(opts),
6064
6152
  account: accountFromOpts(opts)
6065
6153
  };
6066
- const loop = store.createLoop(baseCreateInput(name, opts, target));
6067
- print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
6154
+ const input = baseCreateInput(name, opts, target);
6155
+ const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
6156
+ const loop = store.createLoop(input);
6157
+ printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
6068
6158
  } finally {
6069
6159
  store.close();
6070
6160
  }
6071
6161
  });
6072
- addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")))).action((name, opts) => {
6162
+ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
6073
6163
  const store = new Store;
6074
6164
  try {
6075
6165
  const workflow = store.requireWorkflow(opts.workflow);
@@ -6077,8 +6167,10 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
6077
6167
  type: "workflow",
6078
6168
  workflowId: workflow.id
6079
6169
  };
6080
- const loop = store.createLoop(baseCreateInput(name, opts, target));
6081
- print(publicLoop(loop), `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`);
6170
+ const input = baseCreateInput(name, opts, target);
6171
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
6172
+ const loop = store.createLoop(input);
6173
+ printCreatedLoop(loop, `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`, preflight);
6082
6174
  } finally {
6083
6175
  store.close();
6084
6176
  }
@@ -2724,6 +2724,7 @@ function commandSpec(target) {
2724
2724
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2725
2725
  account: agentTarget.account,
2726
2726
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2727
+ nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
2727
2728
  preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2728
2729
  stdin: agentTarget.prompt,
2729
2730
  allowlist: agentTarget.allowlist
@@ -2805,6 +2806,9 @@ function remotePreflightScript(spec, metadata) {
2805
2806
  if (spec.preflightAnyOf?.length) {
2806
2807
  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");
2807
2808
  }
2809
+ if (spec.nativeAuthProfile?.provider === "codewith") {
2810
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
2811
+ }
2808
2812
  return lines.join(`
2809
2813
  `);
2810
2814
  }
@@ -2820,6 +2824,29 @@ function transportEnv(opts) {
2820
2824
  env.PATH = normalizeExecutionPath(env);
2821
2825
  return env;
2822
2826
  }
2827
+ function preflightNativeAuthProfile(spec, env) {
2828
+ if (!spec.nativeAuthProfile)
2829
+ return;
2830
+ if (spec.nativeAuthProfile.provider !== "codewith")
2831
+ return;
2832
+ const result = spawnSync2(spec.command, ["profile", "list"], {
2833
+ encoding: "utf8",
2834
+ env,
2835
+ stdio: ["ignore", "pipe", "pipe"],
2836
+ timeout: 15000
2837
+ });
2838
+ if (result.error) {
2839
+ throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
2840
+ }
2841
+ if ((result.status ?? 1) !== 0) {
2842
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
2843
+ throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
2844
+ }
2845
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
2846
+ if (!profiles.has(spec.nativeAuthProfile.profile)) {
2847
+ throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
2848
+ }
2849
+ }
2823
2850
  function preflightRemoteSpec(spec, machine, metadata, opts) {
2824
2851
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2825
2852
  const result = spawnSync2(plan.command, plan.args, {
@@ -2961,6 +2988,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2961
2988
  if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2962
2989
  throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2963
2990
  }
2991
+ preflightNativeAuthProfile(spec, env);
2964
2992
  return {
2965
2993
  command: spec.command,
2966
2994
  accountProfile: spec.account?.profile,
@@ -3002,6 +3030,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
3002
3030
  durationMs: 0
3003
3031
  };
3004
3032
  }
3033
+ try {
3034
+ preflightNativeAuthProfile(spec, env);
3035
+ } catch (err) {
3036
+ return {
3037
+ status: "failed",
3038
+ stdout: "",
3039
+ stderr: "",
3040
+ error: err instanceof Error ? err.message : String(err),
3041
+ startedAt,
3042
+ finishedAt: nowIso(),
3043
+ durationMs: 0
3044
+ };
3045
+ }
3005
3046
  const child = spawn(spec.command, spec.args, {
3006
3047
  cwd: spec.cwd,
3007
3048
  env,
@@ -3658,11 +3699,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
3658
3699
  return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
3659
3700
  }
3660
3701
  function preflightWorkflow(workflow, opts = {}) {
3661
- return workflowExecutionOrder(workflow).map((step) => preflightTarget(targetWithStepAccount(step), {
3662
- workflowId: workflow.id,
3663
- workflowName: workflow.name,
3664
- workflowStepId: step.id
3665
- }, opts));
3702
+ return workflowExecutionOrder(workflow).map((step) => {
3703
+ try {
3704
+ return {
3705
+ workflowStepId: step.id,
3706
+ ...preflightTarget(targetWithStepAccount(step), {
3707
+ workflowId: workflow.id,
3708
+ workflowName: workflow.name,
3709
+ workflowStepId: step.id
3710
+ }, opts)
3711
+ };
3712
+ } catch (error) {
3713
+ const message = error instanceof Error ? error.message : String(error);
3714
+ throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
3715
+ }
3716
+ });
3666
3717
  }
3667
3718
  async function executeLoopTarget(store, loop, run, opts = {}) {
3668
3719
  if (loop.target.type !== "workflow") {
@@ -4419,7 +4470,7 @@ function enableStartup(result) {
4419
4470
  // package.json
4420
4471
  var package_default = {
4421
4472
  name: "@hasna/loops",
4422
- version: "0.3.21",
4473
+ version: "0.3.22",
4423
4474
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4424
4475
  type: "module",
4425
4476
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
2714
2714
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2715
2715
  account: agentTarget.account,
2716
2716
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2717
+ nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
2717
2718
  preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2718
2719
  stdin: agentTarget.prompt,
2719
2720
  allowlist: agentTarget.allowlist
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
2795
2796
  if (spec.preflightAnyOf?.length) {
2796
2797
  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");
2797
2798
  }
2799
+ if (spec.nativeAuthProfile?.provider === "codewith") {
2800
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
2801
+ }
2798
2802
  return lines.join(`
2799
2803
  `);
2800
2804
  }
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
2810
2814
  env.PATH = normalizeExecutionPath(env);
2811
2815
  return env;
2812
2816
  }
2817
+ function preflightNativeAuthProfile(spec, env) {
2818
+ if (!spec.nativeAuthProfile)
2819
+ return;
2820
+ if (spec.nativeAuthProfile.provider !== "codewith")
2821
+ return;
2822
+ const result = spawnSync2(spec.command, ["profile", "list"], {
2823
+ encoding: "utf8",
2824
+ env,
2825
+ stdio: ["ignore", "pipe", "pipe"],
2826
+ timeout: 15000
2827
+ });
2828
+ if (result.error) {
2829
+ throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
2830
+ }
2831
+ if ((result.status ?? 1) !== 0) {
2832
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
2833
+ throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
2834
+ }
2835
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
2836
+ if (!profiles.has(spec.nativeAuthProfile.profile)) {
2837
+ throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
2838
+ }
2839
+ }
2813
2840
  function preflightRemoteSpec(spec, machine, metadata, opts) {
2814
2841
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2815
2842
  const result = spawnSync2(plan.command, plan.args, {
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2951
2978
  if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2952
2979
  throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2953
2980
  }
2981
+ preflightNativeAuthProfile(spec, env);
2954
2982
  return {
2955
2983
  command: spec.command,
2956
2984
  accountProfile: spec.account?.profile,
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
2992
3020
  durationMs: 0
2993
3021
  };
2994
3022
  }
3023
+ try {
3024
+ preflightNativeAuthProfile(spec, env);
3025
+ } catch (err) {
3026
+ return {
3027
+ status: "failed",
3028
+ stdout: "",
3029
+ stderr: "",
3030
+ error: err instanceof Error ? err.message : String(err),
3031
+ startedAt,
3032
+ finishedAt: nowIso(),
3033
+ durationMs: 0
3034
+ };
3035
+ }
2995
3036
  const child = spawn(spec.command, spec.args, {
2996
3037
  cwd: spec.cwd,
2997
3038
  env,
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
3648
3689
  return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
3649
3690
  }
3650
3691
  function preflightWorkflow(workflow, opts = {}) {
3651
- return workflowExecutionOrder(workflow).map((step) => preflightTarget(targetWithStepAccount(step), {
3652
- workflowId: workflow.id,
3653
- workflowName: workflow.name,
3654
- workflowStepId: step.id
3655
- }, opts));
3692
+ return workflowExecutionOrder(workflow).map((step) => {
3693
+ try {
3694
+ return {
3695
+ workflowStepId: step.id,
3696
+ ...preflightTarget(targetWithStepAccount(step), {
3697
+ workflowId: workflow.id,
3698
+ workflowName: workflow.name,
3699
+ workflowStepId: step.id
3700
+ }, opts)
3701
+ };
3702
+ } catch (error) {
3703
+ const message = error instanceof Error ? error.message : String(error);
3704
+ throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
3705
+ }
3706
+ });
3656
3707
  }
3657
3708
  async function executeLoopTarget(store, loop, run, opts = {}) {
3658
3709
  if (loop.target.type !== "workflow") {
package/dist/sdk/index.js CHANGED
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
2714
2714
  timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2715
2715
  account: agentTarget.account,
2716
2716
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2717
+ nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
2717
2718
  preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2718
2719
  stdin: agentTarget.prompt,
2719
2720
  allowlist: agentTarget.allowlist
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
2795
2796
  if (spec.preflightAnyOf?.length) {
2796
2797
  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");
2797
2798
  }
2799
+ if (spec.nativeAuthProfile?.provider === "codewith") {
2800
+ lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
2801
+ }
2798
2802
  return lines.join(`
2799
2803
  `);
2800
2804
  }
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
2810
2814
  env.PATH = normalizeExecutionPath(env);
2811
2815
  return env;
2812
2816
  }
2817
+ function preflightNativeAuthProfile(spec, env) {
2818
+ if (!spec.nativeAuthProfile)
2819
+ return;
2820
+ if (spec.nativeAuthProfile.provider !== "codewith")
2821
+ return;
2822
+ const result = spawnSync2(spec.command, ["profile", "list"], {
2823
+ encoding: "utf8",
2824
+ env,
2825
+ stdio: ["ignore", "pipe", "pipe"],
2826
+ timeout: 15000
2827
+ });
2828
+ if (result.error) {
2829
+ throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
2830
+ }
2831
+ if ((result.status ?? 1) !== 0) {
2832
+ const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
2833
+ throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
2834
+ }
2835
+ const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
2836
+ if (!profiles.has(spec.nativeAuthProfile.profile)) {
2837
+ throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
2838
+ }
2839
+ }
2813
2840
  function preflightRemoteSpec(spec, machine, metadata, opts) {
2814
2841
  const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
2815
2842
  const result = spawnSync2(plan.command, plan.args, {
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
2951
2978
  if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
2952
2979
  throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
2953
2980
  }
2981
+ preflightNativeAuthProfile(spec, env);
2954
2982
  return {
2955
2983
  command: spec.command,
2956
2984
  accountProfile: spec.account?.profile,
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
2992
3020
  durationMs: 0
2993
3021
  };
2994
3022
  }
3023
+ try {
3024
+ preflightNativeAuthProfile(spec, env);
3025
+ } catch (err) {
3026
+ return {
3027
+ status: "failed",
3028
+ stdout: "",
3029
+ stderr: "",
3030
+ error: err instanceof Error ? err.message : String(err),
3031
+ startedAt,
3032
+ finishedAt: nowIso(),
3033
+ durationMs: 0
3034
+ };
3035
+ }
2995
3036
  const child = spawn(spec.command, spec.args, {
2996
3037
  cwd: spec.cwd,
2997
3038
  env,
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
3648
3689
  return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
3649
3690
  }
3650
3691
  function preflightWorkflow(workflow, opts = {}) {
3651
- return workflowExecutionOrder(workflow).map((step) => preflightTarget(targetWithStepAccount(step), {
3652
- workflowId: workflow.id,
3653
- workflowName: workflow.name,
3654
- workflowStepId: step.id
3655
- }, opts));
3692
+ return workflowExecutionOrder(workflow).map((step) => {
3693
+ try {
3694
+ return {
3695
+ workflowStepId: step.id,
3696
+ ...preflightTarget(targetWithStepAccount(step), {
3697
+ workflowId: workflow.id,
3698
+ workflowName: workflow.name,
3699
+ workflowStepId: step.id
3700
+ }, opts)
3701
+ };
3702
+ } catch (error) {
3703
+ const message = error instanceof Error ? error.message : String(error);
3704
+ throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
3705
+ }
3706
+ });
3656
3707
  }
3657
3708
  async function executeLoopTarget(store, loop, run, opts = {}) {
3658
3709
  if (loop.target.type !== "workflow") {
package/docs/USAGE.md CHANGED
@@ -49,6 +49,28 @@ Run a deterministic command every minute:
49
49
  loops create command repo-status --every 1m --cmd "git status --short" --cwd /path/to/repo
50
50
  ```
51
51
 
52
+ Validate the target before storing the loop:
53
+
54
+ ```bash
55
+ loops create command repo-status \
56
+ --every 1m \
57
+ --cmd git \
58
+ --no-shell \
59
+ --preflight
60
+ ```
61
+
62
+ `--preflight` is available on `loops create command`, `loops create agent`, and
63
+ `loops create workflow`. It checks target executables and configured account
64
+ profiles before the loop row is stored, so a missing command, provider binary,
65
+ OpenAccounts profile, or workflow step dependency fails without creating a
66
+ scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
67
+ preflight evidence.
68
+
69
+ For shell command loops, preflight can only verify the shell plus configured
70
+ accounts because the command string is interpreted later by the shell. Use
71
+ `--no-shell` or workflow command `args` when you need executable-level
72
+ validation before storing the loop.
73
+
52
74
  Run a Claude loop every morning:
53
75
 
54
76
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",