@hasna/loops 0.3.21 → 0.3.23

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.23",
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,55 @@ 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
+ }
5665
+ function workflowSpecForPreflight(body, id = "validation") {
5666
+ const now = new Date().toISOString();
5667
+ return {
5668
+ id,
5669
+ name: body.name,
5670
+ description: body.description,
5671
+ version: body.version ?? 1,
5672
+ status: "active",
5673
+ goal: body.goal,
5674
+ steps: body.steps,
5675
+ createdAt: now,
5676
+ updatedAt: now
5677
+ };
5678
+ }
5579
5679
  function printTextOutput(value) {
5580
5680
  for (const line of textOutputBlocks(value, { indent: " " }))
5581
5681
  console.log(line);
@@ -6020,7 +6120,7 @@ function permissionModeFromOpts(opts, provider) {
6020
6120
  return mode;
6021
6121
  }
6022
6122
  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) => {
6123
+ 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
6124
  const store = new Store;
6025
6125
  try {
6026
6126
  const target = {
@@ -6031,13 +6131,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6031
6131
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
6032
6132
  account: accountFromOpts(opts)
6033
6133
  };
6034
- const loop = store.createLoop(baseCreateInput(name, opts, target));
6035
- print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
6134
+ const input = baseCreateInput(name, opts, target);
6135
+ const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
6136
+ const loop = store.createLoop(input);
6137
+ printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
6036
6138
  } finally {
6037
6139
  store.close();
6038
6140
  }
6039
6141
  });
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) => {
6142
+ 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
6143
  const provider = opts.provider;
6042
6144
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
6043
6145
  throw new Error("unsupported provider");
@@ -6063,13 +6165,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6063
6165
  allowlist: allowlistFromOpts(opts),
6064
6166
  account: accountFromOpts(opts)
6065
6167
  };
6066
- const loop = store.createLoop(baseCreateInput(name, opts, target));
6067
- print(publicLoop(loop), `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`);
6168
+ const input = baseCreateInput(name, opts, target);
6169
+ const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
6170
+ const loop = store.createLoop(input);
6171
+ printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
6068
6172
  } finally {
6069
6173
  store.close();
6070
6174
  }
6071
6175
  });
6072
- addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")))).action((name, opts) => {
6176
+ 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
6177
  const store = new Store;
6074
6178
  try {
6075
6179
  const workflow = store.requireWorkflow(opts.workflow);
@@ -6077,8 +6181,10 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
6077
6181
  type: "workflow",
6078
6182
  workflowId: workflow.id
6079
6183
  };
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}`);
6184
+ const input = baseCreateInput(name, opts, target);
6185
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
6186
+ const loop = store.createLoop(input);
6187
+ printCreatedLoop(loop, `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`, preflight);
6082
6188
  } finally {
6083
6189
  store.close();
6084
6190
  }
@@ -6119,7 +6225,7 @@ templates.command("create-workflow <id>").description("render and store a templa
6119
6225
  }
6120
6226
  });
6121
6227
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
6122
- 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("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").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) => {
6228
+ 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("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").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("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
6123
6229
  const event = await readEventEnvelopeFromStdin();
6124
6230
  const data = eventData(event);
6125
6231
  const metadata = eventMetadata(event);
@@ -6188,7 +6294,12 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
6188
6294
  leaseMs: 90 * 60000
6189
6295
  };
6190
6296
  if (opts.dryRun) {
6191
- print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
6297
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(workflowBody, "event-preflight"), {
6298
+ name: workflowBody.name,
6299
+ type: "todos-task-event-workflow",
6300
+ event: event.id
6301
+ }, {}) : undefined;
6302
+ print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput, preflight }, `dry-run ${loopName}`);
6192
6303
  return;
6193
6304
  }
6194
6305
  const store = new Store;
@@ -6207,17 +6318,23 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
6207
6318
  return;
6208
6319
  }
6209
6320
  const existingWorkflow = store.findWorkflowByName(workflowBody.name);
6321
+ const workflowPreflightSpec = existingWorkflow ?? workflowSpecForPreflight(workflowBody, "event-preflight");
6322
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflowPreflightSpec, {
6323
+ name: workflowBody.name,
6324
+ type: "todos-task-event-workflow",
6325
+ event: event.id
6326
+ }, {}) : undefined;
6210
6327
  const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
6211
6328
  const loop = store.createLoop({
6212
6329
  ...loopInput,
6213
6330
  target: { type: "workflow", workflowId: workflow.id }
6214
6331
  });
6215
- print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
6332
+ print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop), preflight }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
6216
6333
  } finally {
6217
6334
  store.close();
6218
6335
  }
6219
6336
  });
6220
- 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("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").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) => {
6337
+ 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("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").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("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
6221
6338
  const event = await readEventEnvelopeFromStdin();
6222
6339
  const data = eventData(event);
6223
6340
  const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
@@ -6267,7 +6384,12 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
6267
6384
  leaseMs: 90 * 60000
6268
6385
  };
6269
6386
  if (opts.dryRun) {
6270
- print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
6387
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(workflowBody, "event-preflight"), {
6388
+ name: workflowBody.name,
6389
+ type: "generic-event-workflow",
6390
+ event: event.id
6391
+ }, {}) : undefined;
6392
+ print({ event, workflow: workflowBody, loop: loopInput, preflight }, `dry-run ${loopName}`);
6271
6393
  return;
6272
6394
  }
6273
6395
  const store = new Store;
@@ -6279,12 +6401,18 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
6279
6401
  return;
6280
6402
  }
6281
6403
  const existingWorkflow = store.findWorkflowByName(workflowBody.name);
6404
+ const workflowPreflightSpec = existingWorkflow ?? workflowSpecForPreflight(workflowBody, "event-preflight");
6405
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflowPreflightSpec, {
6406
+ name: workflowBody.name,
6407
+ type: "generic-event-workflow",
6408
+ event: event.id
6409
+ }, {}) : undefined;
6282
6410
  const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
6283
6411
  const loop = store.createLoop({
6284
6412
  ...loopInput,
6285
6413
  target: { type: "workflow", workflowId: workflow.id }
6286
6414
  });
6287
- print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
6415
+ print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop), preflight }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
6288
6416
  } finally {
6289
6417
  store.close();
6290
6418
  }
@@ -6349,27 +6477,20 @@ machines.command("show <id>").description("resolve a machine assignment").action
6349
6477
  });
6350
6478
  workflows.command("validate <file>").description("validate a workflow JSON file without storing or running it").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables").action((file, opts) => {
6351
6479
  const body = workflowBodyFromJson(JSON.parse(readFileSync2(file, "utf8")), opts.name);
6352
- const now = new Date().toISOString();
6353
- const workflow = {
6354
- id: "validation",
6355
- name: body.name,
6356
- description: body.description,
6357
- version: body.version ?? 1,
6358
- status: "active",
6359
- goal: body.goal,
6360
- steps: body.steps,
6361
- createdAt: now,
6362
- updatedAt: now
6363
- };
6480
+ const workflow = workflowSpecForPreflight(body);
6364
6481
  const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
6365
6482
  print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
6366
6483
  });
6367
- workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").action((file, opts) => {
6484
+ workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables before storing").action((file, opts) => {
6368
6485
  const store = new Store;
6369
6486
  try {
6370
6487
  const body = workflowBodyFromJson(JSON.parse(readFileSync2(file, "utf8")), opts.name);
6488
+ const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
6371
6489
  const workflow = store.createWorkflow(body);
6372
- print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
6490
+ if (preflight !== undefined)
6491
+ print({ workflow: publicWorkflow(workflow), preflight }, `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
6492
+ else
6493
+ print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
6373
6494
  } finally {
6374
6495
  store.close();
6375
6496
  }
@@ -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.23",
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,30 @@ 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`,
63
+ `loops create workflow`, `loops workflows create`, and event-router commands such
64
+ as `loops events handle todos-task` and `loops events handle generic`. It checks
65
+ target executables and configured account profiles before loop or workflow rows
66
+ are stored, so a missing command, provider binary, OpenAccounts profile, native
67
+ Codewith auth profile, or workflow step dependency fails without creating a
68
+ scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
69
+ preflight evidence.
70
+
71
+ For shell command loops, preflight can only verify the shell plus configured
72
+ accounts because the command string is interpreted later by the shell. Use
73
+ `--no-shell` or workflow command `args` when you need executable-level
74
+ validation before storing the loop.
75
+
52
76
  Run a Claude loop every morning:
53
77
 
54
78
  ```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.23",
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",