@hasna/loops 0.3.23 → 0.3.24

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
@@ -3243,6 +3243,28 @@ async function executeLoop(loop, run, opts = {}) {
3243
3243
  if (loop.target.type === "workflow") {
3244
3244
  throw new Error("workflow loop targets must be executed with executeLoopTarget");
3245
3245
  }
3246
+ if (loop.target.preflight?.beforeRun) {
3247
+ const startedAt = nowIso();
3248
+ try {
3249
+ preflightTarget(loop.target, {
3250
+ loopId: loop.id,
3251
+ loopName: loop.name,
3252
+ runId: run.id,
3253
+ scheduledFor: run.scheduledFor
3254
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3255
+ } catch (error) {
3256
+ const finishedAt = nowIso();
3257
+ return {
3258
+ status: "failed",
3259
+ stdout: "",
3260
+ stderr: "",
3261
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3262
+ startedAt,
3263
+ finishedAt,
3264
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3265
+ };
3266
+ }
3267
+ }
3246
3268
  return executeTarget(loop.target, {
3247
3269
  loopId: loop.id,
3248
3270
  loopName: loop.name,
@@ -3824,8 +3846,33 @@ function preflightWorkflow(workflow, opts = {}) {
3824
3846
  }
3825
3847
  });
3826
3848
  }
3849
+ function preflightFailureResult(error, startedAt = nowIso()) {
3850
+ const finishedAt = nowIso();
3851
+ return {
3852
+ status: "failed",
3853
+ stdout: "",
3854
+ stderr: "",
3855
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3856
+ startedAt,
3857
+ finishedAt,
3858
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3859
+ };
3860
+ }
3827
3861
  async function executeLoopTarget(store, loop, run, opts = {}) {
3828
3862
  if (loop.target.type !== "workflow") {
3863
+ if (loop.goal && loop.target.preflight?.beforeRun) {
3864
+ const startedAt = nowIso();
3865
+ try {
3866
+ preflightTarget(loop.target, {
3867
+ loopId: loop.id,
3868
+ loopName: loop.name,
3869
+ runId: run.id,
3870
+ scheduledFor: run.scheduledFor
3871
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3872
+ } catch (error) {
3873
+ return preflightFailureResult(error, startedAt);
3874
+ }
3875
+ }
3829
3876
  if (loop.goal) {
3830
3877
  return runGoal(store, loop.goal, {
3831
3878
  ...opts,
@@ -3841,6 +3888,14 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3841
3888
  return executeLoop(loop, run, opts);
3842
3889
  }
3843
3890
  const workflow = store.requireWorkflow(loop.target.workflowId);
3891
+ if (loop.target.preflight?.beforeRun) {
3892
+ const startedAt = nowIso();
3893
+ try {
3894
+ preflightWorkflow(workflow, { ...opts, machine: opts.machine ?? loop.machine });
3895
+ } catch (error) {
3896
+ return preflightFailureResult(error, startedAt);
3897
+ }
3898
+ }
3844
3899
  if (loop.goal) {
3845
3900
  return runGoal(store, loop.goal, {
3846
3901
  ...opts,
@@ -4701,6 +4756,7 @@ var CLASSIFICATIONS = [
4701
4756
  "context_length",
4702
4757
  "schema_response_format",
4703
4758
  "node_init",
4759
+ "preflight",
4704
4760
  "timeout",
4705
4761
  "sigsegv",
4706
4762
  "skipped_previous_active",
@@ -4748,6 +4804,8 @@ function classifyRunFailure(run) {
4748
4804
  classification = "timeout";
4749
4805
  else if (run.status === "skipped" && /previous run still active/.test(text))
4750
4806
  classification = "skipped_previous_active";
4807
+ else if (/runtime preflight failed|preflight failed|executable not found in path|none of required executables found|auth profile preflight failed|profile not found/.test(text))
4808
+ classification = "preflight";
4751
4809
  else if (/rate limit|too many requests|429\b|quota exceeded/.test(text))
4752
4810
  classification = "rate_limit";
4753
4811
  else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b/.test(text))
@@ -5143,7 +5201,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5143
5201
  // package.json
5144
5202
  var package_default = {
5145
5203
  name: "@hasna/loops",
5146
- version: "0.3.23",
5204
+ version: "0.3.24",
5147
5205
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5148
5206
  type: "module",
5149
5207
  main: "dist/index.js",
@@ -5803,6 +5861,9 @@ function accountPoolFromOpts(opts) {
5803
5861
  function roleAccountFromOpts(opts, profile) {
5804
5862
  return profile ? { profile, tool: opts.accountTool } : undefined;
5805
5863
  }
5864
+ function runtimePreflightFromOpts(opts) {
5865
+ return opts.preflightEachRun ? { beforeRun: true } : undefined;
5866
+ }
5806
5867
  function parseVars(values) {
5807
5868
  const vars = {};
5808
5869
  for (const value of values ?? []) {
@@ -6120,7 +6181,7 @@ function permissionModeFromOpts(opts, provider) {
6120
6181
  return mode;
6121
6182
  }
6122
6183
  var create = program.command("create").description("create loops");
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) => {
6184
+ 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-each-run", "check target executables/accounts before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
6124
6185
  const store = new Store;
6125
6186
  try {
6126
6187
  const target = {
@@ -6129,7 +6190,8 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6129
6190
  cwd: opts.cwd,
6130
6191
  shell: opts.shell,
6131
6192
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
6132
- account: accountFromOpts(opts)
6193
+ account: accountFromOpts(opts),
6194
+ preflight: runtimePreflightFromOpts(opts)
6133
6195
  };
6134
6196
  const input = baseCreateInput(name, opts, target);
6135
6197
  const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
@@ -6139,7 +6201,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6139
6201
  store.close();
6140
6202
  }
6141
6203
  });
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) => {
6204
+ 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-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
6143
6205
  const provider = opts.provider;
6144
6206
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
6145
6207
  throw new Error("unsupported provider");
@@ -6163,7 +6225,8 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6163
6225
  permissionMode: permissionModeFromOpts(opts, provider),
6164
6226
  sandbox: sandboxFromOpts(opts, provider),
6165
6227
  allowlist: allowlistFromOpts(opts),
6166
- account: accountFromOpts(opts)
6228
+ account: accountFromOpts(opts),
6229
+ preflight: runtimePreflightFromOpts(opts)
6167
6230
  };
6168
6231
  const input = baseCreateInput(name, opts, target);
6169
6232
  const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
@@ -6173,13 +6236,14 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
6173
6236
  store.close();
6174
6237
  }
6175
6238
  });
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) => {
6239
+ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight-each-run", "check workflow steps before every scheduled run").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
6177
6240
  const store = new Store;
6178
6241
  try {
6179
6242
  const workflow = store.requireWorkflow(opts.workflow);
6180
6243
  const target = {
6181
6244
  type: "workflow",
6182
- workflowId: workflow.id
6245
+ workflowId: workflow.id,
6246
+ preflight: runtimePreflightFromOpts(opts)
6183
6247
  };
6184
6248
  const input = baseCreateInput(name, opts, target);
6185
6249
  const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
@@ -3134,6 +3134,28 @@ async function executeLoop(loop, run, opts = {}) {
3134
3134
  if (loop.target.type === "workflow") {
3135
3135
  throw new Error("workflow loop targets must be executed with executeLoopTarget");
3136
3136
  }
3137
+ if (loop.target.preflight?.beforeRun) {
3138
+ const startedAt = nowIso();
3139
+ try {
3140
+ preflightTarget(loop.target, {
3141
+ loopId: loop.id,
3142
+ loopName: loop.name,
3143
+ runId: run.id,
3144
+ scheduledFor: run.scheduledFor
3145
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3146
+ } catch (error) {
3147
+ const finishedAt = nowIso();
3148
+ return {
3149
+ status: "failed",
3150
+ stdout: "",
3151
+ stderr: "",
3152
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3153
+ startedAt,
3154
+ finishedAt,
3155
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3156
+ };
3157
+ }
3158
+ }
3137
3159
  return executeTarget(loop.target, {
3138
3160
  loopId: loop.id,
3139
3161
  loopName: loop.name,
@@ -3715,8 +3737,33 @@ function preflightWorkflow(workflow, opts = {}) {
3715
3737
  }
3716
3738
  });
3717
3739
  }
3740
+ function preflightFailureResult(error, startedAt = nowIso()) {
3741
+ const finishedAt = nowIso();
3742
+ return {
3743
+ status: "failed",
3744
+ stdout: "",
3745
+ stderr: "",
3746
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3747
+ startedAt,
3748
+ finishedAt,
3749
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3750
+ };
3751
+ }
3718
3752
  async function executeLoopTarget(store, loop, run, opts = {}) {
3719
3753
  if (loop.target.type !== "workflow") {
3754
+ if (loop.goal && loop.target.preflight?.beforeRun) {
3755
+ const startedAt = nowIso();
3756
+ try {
3757
+ preflightTarget(loop.target, {
3758
+ loopId: loop.id,
3759
+ loopName: loop.name,
3760
+ runId: run.id,
3761
+ scheduledFor: run.scheduledFor
3762
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3763
+ } catch (error) {
3764
+ return preflightFailureResult(error, startedAt);
3765
+ }
3766
+ }
3720
3767
  if (loop.goal) {
3721
3768
  return runGoal(store, loop.goal, {
3722
3769
  ...opts,
@@ -3732,6 +3779,14 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3732
3779
  return executeLoop(loop, run, opts);
3733
3780
  }
3734
3781
  const workflow = store.requireWorkflow(loop.target.workflowId);
3782
+ if (loop.target.preflight?.beforeRun) {
3783
+ const startedAt = nowIso();
3784
+ try {
3785
+ preflightWorkflow(workflow, { ...opts, machine: opts.machine ?? loop.machine });
3786
+ } catch (error) {
3787
+ return preflightFailureResult(error, startedAt);
3788
+ }
3789
+ }
3735
3790
  if (loop.goal) {
3736
3791
  return runGoal(store, loop.goal, {
3737
3792
  ...opts,
@@ -4470,7 +4525,7 @@ function enableStartup(result) {
4470
4525
  // package.json
4471
4526
  var package_default = {
4472
4527
  name: "@hasna/loops",
4473
- version: "0.3.23",
4528
+ version: "0.3.24",
4474
4529
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4475
4530
  type: "module",
4476
4531
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -3124,6 +3124,28 @@ async function executeLoop(loop, run, opts = {}) {
3124
3124
  if (loop.target.type === "workflow") {
3125
3125
  throw new Error("workflow loop targets must be executed with executeLoopTarget");
3126
3126
  }
3127
+ if (loop.target.preflight?.beforeRun) {
3128
+ const startedAt = nowIso();
3129
+ try {
3130
+ preflightTarget(loop.target, {
3131
+ loopId: loop.id,
3132
+ loopName: loop.name,
3133
+ runId: run.id,
3134
+ scheduledFor: run.scheduledFor
3135
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3136
+ } catch (error) {
3137
+ const finishedAt = nowIso();
3138
+ return {
3139
+ status: "failed",
3140
+ stdout: "",
3141
+ stderr: "",
3142
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3143
+ startedAt,
3144
+ finishedAt,
3145
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3146
+ };
3147
+ }
3148
+ }
3127
3149
  return executeTarget(loop.target, {
3128
3150
  loopId: loop.id,
3129
3151
  loopName: loop.name,
@@ -3705,8 +3727,33 @@ function preflightWorkflow(workflow, opts = {}) {
3705
3727
  }
3706
3728
  });
3707
3729
  }
3730
+ function preflightFailureResult(error, startedAt = nowIso()) {
3731
+ const finishedAt = nowIso();
3732
+ return {
3733
+ status: "failed",
3734
+ stdout: "",
3735
+ stderr: "",
3736
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3737
+ startedAt,
3738
+ finishedAt,
3739
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3740
+ };
3741
+ }
3708
3742
  async function executeLoopTarget(store, loop, run, opts = {}) {
3709
3743
  if (loop.target.type !== "workflow") {
3744
+ if (loop.goal && loop.target.preflight?.beforeRun) {
3745
+ const startedAt = nowIso();
3746
+ try {
3747
+ preflightTarget(loop.target, {
3748
+ loopId: loop.id,
3749
+ loopName: loop.name,
3750
+ runId: run.id,
3751
+ scheduledFor: run.scheduledFor
3752
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3753
+ } catch (error) {
3754
+ return preflightFailureResult(error, startedAt);
3755
+ }
3756
+ }
3710
3757
  if (loop.goal) {
3711
3758
  return runGoal(store, loop.goal, {
3712
3759
  ...opts,
@@ -3722,6 +3769,14 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3722
3769
  return executeLoop(loop, run, opts);
3723
3770
  }
3724
3771
  const workflow = store.requireWorkflow(loop.target.workflowId);
3772
+ if (loop.target.preflight?.beforeRun) {
3773
+ const startedAt = nowIso();
3774
+ try {
3775
+ preflightWorkflow(workflow, { ...opts, machine: opts.machine ?? loop.machine });
3776
+ } catch (error) {
3777
+ return preflightFailureResult(error, startedAt);
3778
+ }
3779
+ }
3725
3780
  if (loop.goal) {
3726
3781
  return runGoal(store, loop.goal, {
3727
3782
  ...opts,
@@ -4796,6 +4851,7 @@ var CLASSIFICATIONS = [
4796
4851
  "context_length",
4797
4852
  "schema_response_format",
4798
4853
  "node_init",
4854
+ "preflight",
4799
4855
  "timeout",
4800
4856
  "sigsegv",
4801
4857
  "skipped_previous_active",
@@ -4843,6 +4899,8 @@ function classifyRunFailure(run) {
4843
4899
  classification = "timeout";
4844
4900
  else if (run.status === "skipped" && /previous run still active/.test(text))
4845
4901
  classification = "skipped_previous_active";
4902
+ else if (/runtime preflight failed|preflight failed|executable not found in path|none of required executables found|auth profile preflight failed|profile not found/.test(text))
4903
+ classification = "preflight";
4846
4904
  else if (/rate limit|too many requests|429\b|quota exceeded/.test(text))
4847
4905
  classification = "rate_limit";
4848
4906
  else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b/.test(text))
@@ -1,6 +1,6 @@
1
1
  import type { Loop, LoopRun } from "../types.js";
2
2
  import type { Store } from "./store.js";
3
- export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
3
+ export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
4
4
  export interface RunFailureSignal {
5
5
  classification: RunFailureClassification;
6
6
  fingerprint: string;
package/dist/sdk/index.js CHANGED
@@ -3124,6 +3124,28 @@ async function executeLoop(loop, run, opts = {}) {
3124
3124
  if (loop.target.type === "workflow") {
3125
3125
  throw new Error("workflow loop targets must be executed with executeLoopTarget");
3126
3126
  }
3127
+ if (loop.target.preflight?.beforeRun) {
3128
+ const startedAt = nowIso();
3129
+ try {
3130
+ preflightTarget(loop.target, {
3131
+ loopId: loop.id,
3132
+ loopName: loop.name,
3133
+ runId: run.id,
3134
+ scheduledFor: run.scheduledFor
3135
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3136
+ } catch (error) {
3137
+ const finishedAt = nowIso();
3138
+ return {
3139
+ status: "failed",
3140
+ stdout: "",
3141
+ stderr: "",
3142
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3143
+ startedAt,
3144
+ finishedAt,
3145
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3146
+ };
3147
+ }
3148
+ }
3127
3149
  return executeTarget(loop.target, {
3128
3150
  loopId: loop.id,
3129
3151
  loopName: loop.name,
@@ -3705,8 +3727,33 @@ function preflightWorkflow(workflow, opts = {}) {
3705
3727
  }
3706
3728
  });
3707
3729
  }
3730
+ function preflightFailureResult(error, startedAt = nowIso()) {
3731
+ const finishedAt = nowIso();
3732
+ return {
3733
+ status: "failed",
3734
+ stdout: "",
3735
+ stderr: "",
3736
+ error: `runtime preflight failed: ${error instanceof Error ? error.message : String(error)}`,
3737
+ startedAt,
3738
+ finishedAt,
3739
+ durationMs: new Date(finishedAt).getTime() - new Date(startedAt).getTime()
3740
+ };
3741
+ }
3708
3742
  async function executeLoopTarget(store, loop, run, opts = {}) {
3709
3743
  if (loop.target.type !== "workflow") {
3744
+ if (loop.goal && loop.target.preflight?.beforeRun) {
3745
+ const startedAt = nowIso();
3746
+ try {
3747
+ preflightTarget(loop.target, {
3748
+ loopId: loop.id,
3749
+ loopName: loop.name,
3750
+ runId: run.id,
3751
+ scheduledFor: run.scheduledFor
3752
+ }, { ...opts, machine: opts.machine ?? loop.machine });
3753
+ } catch (error) {
3754
+ return preflightFailureResult(error, startedAt);
3755
+ }
3756
+ }
3710
3757
  if (loop.goal) {
3711
3758
  return runGoal(store, loop.goal, {
3712
3759
  ...opts,
@@ -3722,6 +3769,14 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3722
3769
  return executeLoop(loop, run, opts);
3723
3770
  }
3724
3771
  const workflow = store.requireWorkflow(loop.target.workflowId);
3772
+ if (loop.target.preflight?.beforeRun) {
3773
+ const startedAt = nowIso();
3774
+ try {
3775
+ preflightWorkflow(workflow, { ...opts, machine: opts.machine ?? loop.machine });
3776
+ } catch (error) {
3777
+ return preflightFailureResult(error, startedAt);
3778
+ }
3779
+ }
3725
3780
  if (loop.goal) {
3726
3781
  return runGoal(store, loop.goal, {
3727
3782
  ...opts,
package/dist/types.d.ts CHANGED
@@ -40,6 +40,9 @@ export interface DynamicSchedule {
40
40
  minIntervalMs?: number;
41
41
  }
42
42
  export type ScheduleSpec = OnceSchedule | IntervalSchedule | CronSchedule | DynamicSchedule;
43
+ export interface RuntimePreflightPolicy {
44
+ beforeRun?: boolean;
45
+ }
43
46
  export interface CommandTarget {
44
47
  type: "command";
45
48
  command: string;
@@ -49,6 +52,7 @@ export interface CommandTarget {
49
52
  env?: Record<string, string>;
50
53
  timeoutMs?: number;
51
54
  account?: AccountRef;
55
+ preflight?: RuntimePreflightPolicy;
52
56
  }
53
57
  export type AgentProvider = "claude" | "cursor" | "codewith" | "aicopilot" | "opencode" | "codex";
54
58
  export type AgentConfigIsolation = "safe" | "none";
@@ -75,12 +79,14 @@ export interface AgentTarget {
75
79
  sandbox?: AgentSandbox;
76
80
  allowlist?: AgentAllowlistSpec;
77
81
  account?: AccountRef;
82
+ preflight?: RuntimePreflightPolicy;
78
83
  }
79
84
  export interface WorkflowTarget {
80
85
  type: "workflow";
81
86
  workflowId: string;
82
87
  input?: Record<string, string>;
83
88
  timeoutMs?: number;
89
+ preflight?: RuntimePreflightPolicy;
84
90
  }
85
91
  export type ExecutableTarget = CommandTarget | AgentTarget;
86
92
  export type LoopTarget = ExecutableTarget | WorkflowTarget;
package/docs/USAGE.md CHANGED
@@ -73,6 +73,12 @@ accounts because the command string is interpreted later by the shell. Use
73
73
  `--no-shell` or workflow command `args` when you need executable-level
74
74
  validation before storing the loop.
75
75
 
76
+ Use `--preflight-each-run` when a loop should repeat the same readiness check at
77
+ run time before launching expensive agent or workflow work. Runtime preflight
78
+ failures are recorded as failed loop runs with a `runtime preflight failed`
79
+ error, so health/routing checks can create follow-up tasks without spawning the
80
+ worker.
81
+
76
82
  Run a Claude loop every morning:
77
83
 
78
84
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
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",