@hasna/loops 0.3.14 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -326,6 +326,17 @@ function optionalPositiveInteger(value, label) {
326
326
  throw new Error(`${label} must be a positive integer`);
327
327
  return value;
328
328
  }
329
+ function optionalStringArray(value, label) {
330
+ if (value === undefined)
331
+ return;
332
+ if (!Array.isArray(value))
333
+ throw new Error(`${label} must be an array`);
334
+ const values = value.map((entry, index) => {
335
+ assertString(entry, `${label}[${index}]`);
336
+ return entry.trim();
337
+ }).filter(Boolean);
338
+ return values.length ? values : undefined;
339
+ }
329
340
  function normalizeGoalSpec(value, label = "goal") {
330
341
  if (value === undefined)
331
342
  return;
@@ -397,6 +408,14 @@ function validateTarget(value, label) {
397
408
  throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
398
409
  }
399
410
  }
411
+ if (value.allowlist !== undefined) {
412
+ assertObject(value.allowlist, `${label}.allowlist`);
413
+ optionalStringArray(value.allowlist.tools, `${label}.allowlist.tools`);
414
+ optionalStringArray(value.allowlist.commands, `${label}.allowlist.commands`);
415
+ if (value.allowlist.enforcement !== undefined && value.allowlist.enforcement !== "metadata_only") {
416
+ throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
417
+ }
418
+ }
400
419
  return value;
401
420
  }
402
421
  throw new Error(`${label}.type must be command or agent`);
@@ -480,6 +499,8 @@ function rowToLoop(row) {
480
499
  name: row.name,
481
500
  description: row.description ?? undefined,
482
501
  status: row.status,
502
+ archivedAt: row.archived_at ?? undefined,
503
+ archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
483
504
  schedule: JSON.parse(row.schedule_json),
484
505
  target: JSON.parse(row.target_json),
485
506
  goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
@@ -689,6 +710,8 @@ class Store {
689
710
  name TEXT NOT NULL,
690
711
  description TEXT,
691
712
  status TEXT NOT NULL,
713
+ archived_at TEXT,
714
+ archived_from_status TEXT,
692
715
  schedule_json TEXT NOT NULL,
693
716
  target_json TEXT NOT NULL,
694
717
  goal_json TEXT,
@@ -889,6 +912,8 @@ class Store {
889
912
  `);
890
913
  this.addColumnIfMissing("loops", "machine_json", "TEXT");
891
914
  this.addColumnIfMissing("loops", "goal_json", "TEXT");
915
+ this.addColumnIfMissing("loops", "archived_at", "TEXT");
916
+ this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
892
917
  this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
893
918
  this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
894
919
  this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
@@ -897,6 +922,7 @@ class Store {
897
922
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
898
923
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
899
924
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
925
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
900
926
  }
901
927
  addColumnIfMissing(table, column, definition) {
902
928
  const columns = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -973,12 +999,26 @@ class Store {
973
999
  }
974
1000
  listLoops(opts = {}) {
975
1001
  const limit = opts.limit ?? 200;
976
- const rows = opts.status ? this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
1002
+ let rows;
1003
+ if (opts.status && opts.archived) {
1004
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NOT NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
1005
+ } else if (opts.status && opts.includeArchived) {
1006
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
1007
+ } else if (opts.status) {
1008
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? AND archived_at IS NULL ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
1009
+ } else if (opts.archived) {
1010
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
1011
+ } else if (opts.includeArchived) {
1012
+ rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
1013
+ } else {
1014
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
1015
+ }
977
1016
  return rows.map(rowToLoop);
978
1017
  }
979
1018
  dueLoops(now) {
980
1019
  const rows = this.db.query(`SELECT * FROM loops
981
1020
  WHERE status = 'active'
1021
+ AND archived_at IS NULL
982
1022
  AND next_run_at IS NOT NULL
983
1023
  AND next_run_at <= ?
984
1024
  ORDER BY next_run_at ASC`).all(now.toISOString());
@@ -1010,6 +1050,44 @@ class Store {
1010
1050
  throw new Error(`loop not found after update: ${id}`);
1011
1051
  return after;
1012
1052
  }
1053
+ archiveLoop(idOrName) {
1054
+ const loop = this.requireLoop(idOrName);
1055
+ if (loop.archivedAt)
1056
+ return loop;
1057
+ const updated = nowIso();
1058
+ const archivedStatus = loop.status === "active" ? "paused" : loop.status;
1059
+ this.db.query(`UPDATE loops
1060
+ SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
1061
+ WHERE id=$id`).run({
1062
+ $id: loop.id,
1063
+ $status: archivedStatus,
1064
+ $archivedAt: updated,
1065
+ $archivedFromStatus: loop.status,
1066
+ $updated: updated
1067
+ });
1068
+ const archived = this.getLoop(loop.id);
1069
+ if (!archived)
1070
+ throw new Error(`loop not found after archive: ${loop.id}`);
1071
+ return archived;
1072
+ }
1073
+ unarchiveLoop(idOrName) {
1074
+ const loop = this.requireLoop(idOrName);
1075
+ if (!loop.archivedAt)
1076
+ return loop;
1077
+ const updated = nowIso();
1078
+ const restoredStatus = loop.archivedFromStatus ?? loop.status;
1079
+ this.db.query(`UPDATE loops
1080
+ SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
1081
+ WHERE id=$id`).run({
1082
+ $id: loop.id,
1083
+ $status: restoredStatus,
1084
+ $updated: updated
1085
+ });
1086
+ const unarchived = this.getLoop(loop.id);
1087
+ if (!unarchived)
1088
+ throw new Error(`loop not found after unarchive: ${loop.id}`);
1089
+ return unarchived;
1090
+ }
1013
1091
  deleteLoop(idOrName) {
1014
1092
  const loop = this.requireLoop(idOrName);
1015
1093
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
@@ -1784,10 +1862,16 @@ class Store {
1784
1862
  }
1785
1863
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
1786
1864
  const startedAt = now.toISOString();
1787
- const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1788
1865
  this.db.exec("BEGIN IMMEDIATE");
1789
1866
  try {
1790
1867
  this.assertDaemonLeaseFence(opts, startedAt);
1868
+ const currentLoop = this.getLoop(loop.id);
1869
+ if (!currentLoop || currentLoop.archivedAt) {
1870
+ this.db.exec("COMMIT");
1871
+ return;
1872
+ }
1873
+ loop = currentLoop;
1874
+ const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1791
1875
  const existing = this.getRunBySlot(loop.id, scheduledFor);
1792
1876
  if (existing) {
1793
1877
  if (existing.status === "running") {
@@ -2005,7 +2089,7 @@ class Store {
2005
2089
  return recovered;
2006
2090
  }
2007
2091
  expireLoops(now = new Date, opts = {}) {
2008
- const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2092
+ const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND archived_at IS NULL AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2009
2093
  const expired = [];
2010
2094
  for (const row of rows) {
2011
2095
  const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
@@ -2014,8 +2098,21 @@ class Store {
2014
2098
  }
2015
2099
  return expired;
2016
2100
  }
2017
- countLoops(status) {
2018
- const row = status ? this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status) : this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2101
+ countLoops(status, opts = {}) {
2102
+ let row;
2103
+ if (status && opts.archived) {
2104
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
2105
+ } else if (status && opts.includeArchived) {
2106
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
2107
+ } else if (status) {
2108
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
2109
+ } else if (opts.archived) {
2110
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
2111
+ } else if (opts.includeArchived) {
2112
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2113
+ } else {
2114
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
2115
+ }
2019
2116
  return row?.count ?? 0;
2020
2117
  }
2021
2118
  countRuns(status) {
@@ -2377,6 +2474,16 @@ function metadataEnv(metadata) {
2377
2474
  env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
2378
2475
  return env;
2379
2476
  }
2477
+ function allowlistEnv(allowlist) {
2478
+ const env = {};
2479
+ if (allowlist?.tools?.length)
2480
+ env.LOOPS_AGENT_ALLOWED_TOOLS = allowlist.tools.join(",");
2481
+ if (allowlist?.commands?.length)
2482
+ env.LOOPS_AGENT_ALLOWED_COMMANDS = allowlist.commands.join(",");
2483
+ if (allowlist?.tools?.length || allowlist?.commands?.length)
2484
+ env.LOOPS_AGENT_ALLOWLIST_ENFORCEMENT = "metadata_only";
2485
+ return env;
2486
+ }
2380
2487
  function providerCommand(provider) {
2381
2488
  switch (provider) {
2382
2489
  case "claude":
@@ -2584,7 +2691,8 @@ function commandSpec(target) {
2584
2691
  account: agentTarget.account,
2585
2692
  accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
2586
2693
  preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
2587
- stdin: agentTarget.prompt
2694
+ stdin: agentTarget.prompt,
2695
+ allowlist: agentTarget.allowlist
2588
2696
  };
2589
2697
  }
2590
2698
  function executionEnv(spec, metadata, opts) {
@@ -2596,6 +2704,7 @@ function executionEnv(spec, metadata, opts) {
2596
2704
  Object.assign(env, accountEnv);
2597
2705
  }
2598
2706
  Object.assign(env, spec.env ?? {});
2707
+ Object.assign(env, allowlistEnv(spec.allowlist));
2599
2708
  env.PATH = normalizeExecutionPath(env);
2600
2709
  Object.assign(env, metadataEnv(metadata));
2601
2710
  return env;
@@ -2634,6 +2743,9 @@ function remoteBootstrapLines(spec, metadata) {
2634
2743
  continue;
2635
2744
  lines.push(`export ${key}=${shellQuote(value)}`);
2636
2745
  }
2746
+ for (const [key, value] of Object.entries(allowlistEnv(spec.allowlist))) {
2747
+ lines.push(`export ${key}=${shellQuote(value)}`);
2748
+ }
2637
2749
  return lines;
2638
2750
  }
2639
2751
  function remoteScript(spec, metadata) {
@@ -3587,12 +3699,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3587
3699
 
3588
3700
  // src/lib/scheduler.ts
3589
3701
  function manualRunScheduledFor(loop, now = new Date) {
3702
+ if (loop.archivedAt)
3703
+ return now.toISOString();
3590
3704
  if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
3591
3705
  return loop.retryScheduledFor ?? loop.nextRunAt;
3592
3706
  }
3593
3707
  return now.toISOString();
3594
3708
  }
3595
3709
  function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3710
+ if (loop.archivedAt)
3711
+ return false;
3596
3712
  if (loop.status !== "active")
3597
3713
  return false;
3598
3714
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3600,6 +3716,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3600
3716
  return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
3601
3717
  }
3602
3718
  function manualRunSource(loop, scheduledFor, now = new Date) {
3719
+ if (loop.archivedAt)
3720
+ return "ad_hoc";
3603
3721
  if (loop.status !== "active")
3604
3722
  return "ad_hoc";
3605
3723
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3618,7 +3736,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
3618
3736
  if (run.status === "running")
3619
3737
  return;
3620
3738
  const current = store.getLoop(loop.id);
3621
- if (!current || current.status !== "active")
3739
+ if (!current || current.status !== "active" || current.archivedAt)
3622
3740
  return;
3623
3741
  if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
3624
3742
  return;
@@ -3903,16 +4021,28 @@ class LoopsClient {
3903
4021
  }
3904
4022
  pause(idOrName) {
3905
4023
  const loop = this.get(idOrName);
4024
+ if (loop.archivedAt)
4025
+ throw new Error(`loop is archived; unarchive it before pausing: ${idOrName}`);
3906
4026
  return this.store.updateLoop(loop.id, { status: "paused" });
3907
4027
  }
3908
4028
  resume(idOrName) {
3909
4029
  const loop = this.get(idOrName);
4030
+ if (loop.archivedAt)
4031
+ throw new Error(`loop is archived; unarchive it before resuming: ${idOrName}`);
3910
4032
  return this.store.updateLoop(loop.id, { status: "active" });
3911
4033
  }
3912
4034
  stop(idOrName) {
3913
4035
  const loop = this.get(idOrName);
4036
+ if (loop.archivedAt)
4037
+ throw new Error(`loop is archived; unarchive it before stopping: ${idOrName}`);
3914
4038
  return this.store.updateLoop(loop.id, { status: "stopped", nextRunAt: undefined });
3915
4039
  }
4040
+ archive(idOrName) {
4041
+ return this.store.archiveLoop(idOrName);
4042
+ }
4043
+ unarchive(idOrName) {
4044
+ return this.store.unarchiveLoop(idOrName);
4045
+ }
3916
4046
  delete(idOrName) {
3917
4047
  return this.store.deleteLoop(idOrName);
3918
4048
  }
@@ -3931,6 +4061,8 @@ class LoopsClient {
3931
4061
  }
3932
4062
  async runNow(idOrName) {
3933
4063
  const loop = this.get(idOrName);
4064
+ if (loop.archivedAt)
4065
+ throw new Error(`loop is archived; unarchive it before running: ${idOrName}`);
3934
4066
  const now = new Date;
3935
4067
  let scheduledFor = manualRunScheduledFor(loop, now);
3936
4068
  let shouldAdvance = shouldAdvanceManualRun(loop, scheduledFor, now);
@@ -3974,6 +4106,10 @@ var TEMPLATE_SUMMARIES = [
3974
4106
  { name: "projectPath", required: true, description: "Repository or project working directory." },
3975
4107
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3976
4108
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4109
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4110
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4111
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4112
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
3977
4113
  { name: "model", description: "Provider model." },
3978
4114
  { name: "variant", description: "Provider reasoning/model effort variant." },
3979
4115
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -3993,6 +4129,10 @@ var TEMPLATE_SUMMARIES = [
3993
4129
  { name: "projectPath", required: true, description: "Repository or project working directory." },
3994
4130
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
3995
4131
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4132
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4133
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4134
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4135
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
3996
4136
  { name: "model", description: "Provider model." },
3997
4137
  { name: "variant", description: "Provider reasoning/model effort variant." },
3998
4138
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -4007,7 +4147,37 @@ function taskLabel(input) {
4007
4147
  const head = input.taskTitle?.trim() || input.taskId;
4008
4148
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
4009
4149
  }
4010
- function agentTarget(input, prompt) {
4150
+ function stableIndex(seed, size) {
4151
+ let hash = 2166136261;
4152
+ for (let i = 0;i < seed.length; i += 1) {
4153
+ hash ^= seed.charCodeAt(i);
4154
+ hash = Math.imul(hash, 16777619);
4155
+ }
4156
+ return Math.abs(hash >>> 0) % size;
4157
+ }
4158
+ function rolePoolValue(pool, seed, role) {
4159
+ if (!pool?.length)
4160
+ return;
4161
+ const workerIndex = stableIndex(seed, pool.length);
4162
+ if (role === "worker" || pool.length === 1)
4163
+ return pool[workerIndex];
4164
+ return pool[(workerIndex + 1) % pool.length];
4165
+ }
4166
+ function authProfileForRole(input, role, seed) {
4167
+ if (role === "worker" && input.workerAuthProfile)
4168
+ return input.workerAuthProfile;
4169
+ if (role === "verifier" && input.verifierAuthProfile)
4170
+ return input.verifierAuthProfile;
4171
+ return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
4172
+ }
4173
+ function accountForRole(input, role, seed) {
4174
+ if (role === "worker" && input.workerAccount)
4175
+ return input.workerAccount;
4176
+ if (role === "verifier" && input.verifierAccount)
4177
+ return input.verifierAccount;
4178
+ return rolePoolValue(input.accountPool, seed, role) ?? input.account;
4179
+ }
4180
+ function agentTarget(input, prompt, role, seed) {
4011
4181
  const provider = input.provider ?? "codewith";
4012
4182
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4013
4183
  return {
@@ -4018,11 +4188,11 @@ function agentTarget(input, prompt) {
4018
4188
  model: input.model,
4019
4189
  variant: input.variant,
4020
4190
  agent: input.agent,
4021
- authProfile: provider === "codewith" ? input.authProfile : undefined,
4191
+ authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
4022
4192
  configIsolation: "safe",
4023
4193
  permissionMode: input.permissionMode ?? "bypass",
4024
4194
  sandbox,
4025
- account: input.account,
4195
+ account: accountForRole(input, role, seed),
4026
4196
  timeoutMs: 45 * 60000
4027
4197
  };
4028
4198
  }
@@ -4076,7 +4246,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4076
4246
  id: "worker",
4077
4247
  name: "Worker",
4078
4248
  description: "Implement the todos task and record evidence.",
4079
- target: agentTarget(input, workerPrompt),
4249
+ target: agentTarget(input, workerPrompt, "worker", input.taskId),
4080
4250
  timeoutMs: 45 * 60000
4081
4251
  },
4082
4252
  {
@@ -4084,7 +4254,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4084
4254
  name: "Verifier",
4085
4255
  description: "Adversarially verify worker output and update todos.",
4086
4256
  dependsOn: ["worker"],
4087
- target: agentTarget(input, verifierPrompt),
4257
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
4088
4258
  timeoutMs: 30 * 60000
4089
4259
  }
4090
4260
  ]
@@ -4140,7 +4310,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4140
4310
  id: "worker",
4141
4311
  name: "Worker",
4142
4312
  description: "Handle the Hasna event and record evidence.",
4143
- target: agentTarget(input, workerPrompt),
4313
+ target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4144
4314
  timeoutMs: 45 * 60000
4145
4315
  },
4146
4316
  {
@@ -4148,7 +4318,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4148
4318
  name: "Verifier",
4149
4319
  description: "Adversarially verify event handling.",
4150
4320
  dependsOn: ["worker"],
4151
- target: agentTarget(input, verifierPrompt),
4321
+ target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4152
4322
  timeoutMs: 30 * 60000
4153
4323
  }
4154
4324
  ]
@@ -4163,7 +4333,11 @@ function renderLoopTemplate(id, values) {
4163
4333
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4164
4334
  provider: values.provider,
4165
4335
  authProfile: values.authProfile,
4336
+ authProfilePool: listVar(values.authProfilePool),
4337
+ workerAuthProfile: values.workerAuthProfile,
4338
+ verifierAuthProfile: values.verifierAuthProfile,
4166
4339
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4340
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4167
4341
  model: values.model,
4168
4342
  variant: values.variant,
4169
4343
  agent: values.agent,
@@ -4184,7 +4358,11 @@ function renderLoopTemplate(id, values) {
4184
4358
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4185
4359
  provider: values.provider,
4186
4360
  authProfile: values.authProfile,
4361
+ authProfilePool: listVar(values.authProfilePool),
4362
+ workerAuthProfile: values.workerAuthProfile,
4363
+ verifierAuthProfile: values.verifierAuthProfile,
4187
4364
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4365
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4188
4366
  model: values.model,
4189
4367
  variant: values.variant,
4190
4368
  agent: values.agent,
@@ -4194,6 +4372,13 @@ function renderLoopTemplate(id, values) {
4194
4372
  }
4195
4373
  throw new Error(`unknown template: ${id}`);
4196
4374
  }
4375
+ function listVar(value) {
4376
+ const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
4377
+ return values?.length ? values : undefined;
4378
+ }
4379
+ function accountPoolVar(value, tool) {
4380
+ return listVar(value)?.map((profile) => ({ profile, tool }));
4381
+ }
4197
4382
  // src/lib/doctor.ts
4198
4383
  import { spawnSync as spawnSync3 } from "child_process";
4199
4384
  import { accessSync as accessSync2, constants as constants2 } from "fs";
@@ -4269,7 +4454,8 @@ function daemonStatus(store, path = pidFilePath()) {
4269
4454
  active: store.countLoops("active"),
4270
4455
  paused: store.countLoops("paused"),
4271
4456
  stopped: store.countLoops("stopped"),
4272
- expired: store.countLoops("expired")
4457
+ expired: store.countLoops("expired"),
4458
+ archived: store.countLoops(undefined, { archived: true })
4273
4459
  },
4274
4460
  runs: {
4275
4461
  total: store.countRuns(),
@@ -4430,6 +4616,215 @@ function runDoctor(store) {
4430
4616
  checks
4431
4617
  };
4432
4618
  }
4619
+ // src/lib/health.ts
4620
+ import { createHash } from "crypto";
4621
+ var EVIDENCE_CHARS = 2000;
4622
+ var CLASSIFICATIONS = [
4623
+ "rate_limit",
4624
+ "auth",
4625
+ "model_not_found",
4626
+ "context_length",
4627
+ "schema_response_format",
4628
+ "node_init",
4629
+ "timeout",
4630
+ "sigsegv",
4631
+ "skipped_previous_active",
4632
+ "unknown"
4633
+ ];
4634
+ function bounded(value, limit = EVIDENCE_CHARS) {
4635
+ if (!value)
4636
+ return;
4637
+ if (value.length <= limit)
4638
+ return value;
4639
+ return `${value.slice(0, limit)}
4640
+ [truncated ${value.length - limit} chars]`;
4641
+ }
4642
+ function searchableText(run) {
4643
+ return [run.error, run.stderr, run.stdout].filter(Boolean).join(`
4644
+ `).toLowerCase();
4645
+ }
4646
+ function stableFingerprint(parts) {
4647
+ return createHash("sha256").update(parts.join(`
4648
+ `)).digest("hex").slice(0, 16);
4649
+ }
4650
+ function healthRun(run) {
4651
+ return {
4652
+ ...run,
4653
+ error: bounded(run.error),
4654
+ stdout: bounded(run.stdout),
4655
+ stderr: bounded(run.stderr)
4656
+ };
4657
+ }
4658
+ function classifyRunFailure(run) {
4659
+ if (run.status === "succeeded" || run.status === "running")
4660
+ return;
4661
+ const text = searchableText(run);
4662
+ let classification = "unknown";
4663
+ if (run.status === "timed_out")
4664
+ classification = "timeout";
4665
+ else if (run.status === "skipped" && /previous run still active/.test(text))
4666
+ classification = "skipped_previous_active";
4667
+ else if (/rate limit|too many requests|429\b|quota exceeded/.test(text))
4668
+ classification = "rate_limit";
4669
+ else if (/unauthorized|authentication|auth\b|api key|invalid token|permission denied|401\b|403\b/.test(text))
4670
+ classification = "auth";
4671
+ else if (/model .*not found|model_not_found|unknown model|invalid model|404.*model/.test(text))
4672
+ classification = "model_not_found";
4673
+ else if (/context length|context_length|context window|maximum context|token limit|too many tokens/.test(text))
4674
+ classification = "context_length";
4675
+ else if (/response_format|json schema|schema validation|invalid schema|structured output/.test(text))
4676
+ classification = "schema_response_format";
4677
+ else if (/cannot find module|module not found|node:internal|bun: command not found|node: command not found|npm err!|err_module_not_found/.test(text))
4678
+ classification = "node_init";
4679
+ else if (/sigsegv|segmentation fault|signal 11/.test(text))
4680
+ classification = "sigsegv";
4681
+ return {
4682
+ classification,
4683
+ fingerprint: stableFingerprint([
4684
+ run.loopId,
4685
+ run.loopName,
4686
+ run.status,
4687
+ classification,
4688
+ String(run.exitCode ?? ""),
4689
+ (run.error ?? run.stderr ?? run.stdout ?? "").slice(0, 500)
4690
+ ]),
4691
+ evidence: {
4692
+ error: bounded(run.error),
4693
+ stdout: bounded(run.stdout),
4694
+ stderr: bounded(run.stderr),
4695
+ exitCode: run.exitCode
4696
+ }
4697
+ };
4698
+ }
4699
+ function targetRoute(loop) {
4700
+ if (loop.target.type === "agent") {
4701
+ return {
4702
+ source: "openloops",
4703
+ kind: "loop_expectation",
4704
+ loopId: loop.id,
4705
+ loopName: loop.name,
4706
+ cwd: loop.target.cwd,
4707
+ provider: loop.target.provider
4708
+ };
4709
+ }
4710
+ if (loop.target.type === "command") {
4711
+ return {
4712
+ source: "openloops",
4713
+ kind: "loop_expectation",
4714
+ loopId: loop.id,
4715
+ loopName: loop.name,
4716
+ cwd: loop.target.cwd
4717
+ };
4718
+ }
4719
+ return {
4720
+ source: "openloops",
4721
+ kind: "loop_expectation",
4722
+ loopId: loop.id,
4723
+ loopName: loop.name
4724
+ };
4725
+ }
4726
+ function recommendedTask(loop, run, failure, route) {
4727
+ const title = `BUG: open-loops loop failure - ${loop.name}`;
4728
+ const description = [
4729
+ `OpenLoops expectation failed for loop ${loop.name} (${loop.id}).`,
4730
+ `Run: ${run.id}`,
4731
+ `Status: ${run.status}`,
4732
+ `Classification: ${failure.classification}`,
4733
+ `Fingerprint: ${failure.fingerprint}`,
4734
+ route.cwd ? `Route cwd: ${route.cwd}` : undefined,
4735
+ route.provider ? `Provider: ${route.provider}` : undefined,
4736
+ failure.evidence.error ? `Error:
4737
+ ${failure.evidence.error}` : undefined,
4738
+ failure.evidence.stderr ? `Stderr:
4739
+ ${failure.evidence.stderr}` : undefined
4740
+ ].filter(Boolean).join(`
4741
+
4742
+ `);
4743
+ const dedupeKey = `openloops:${loop.id}:${failure.fingerprint}`;
4744
+ const tags = ["bug", "openloops", "loop-health", failure.classification];
4745
+ const priority = failure.classification === "auth" || failure.classification === "rate_limit" ? "high" : "medium";
4746
+ return {
4747
+ title,
4748
+ description,
4749
+ priority,
4750
+ tags,
4751
+ dedupeKey,
4752
+ search: { query: dedupeKey },
4753
+ compatibilityFallback: {
4754
+ search: ["todos", "search", dedupeKey, "--json"],
4755
+ add: ["todos", "add", title, "--description", description, "--tag", tags.join(","), "--priority", priority],
4756
+ comment: ["todos", "comment", "<task-id>", description]
4757
+ },
4758
+ futureNativeUpsert: {
4759
+ command: "todos upsert",
4760
+ fields: {
4761
+ title,
4762
+ description,
4763
+ priority,
4764
+ tags,
4765
+ dedupeKey,
4766
+ routeSource: route.source,
4767
+ routeKind: route.kind,
4768
+ routeLoopId: route.loopId,
4769
+ routeLoopName: route.loopName
4770
+ }
4771
+ }
4772
+ };
4773
+ }
4774
+ function expectationForLoop(store, loop) {
4775
+ const latestRun = store.listRuns({ loopId: loop.id, limit: 1 })[0];
4776
+ const route = targetRoute(loop);
4777
+ if (!latestRun) {
4778
+ return {
4779
+ loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
4780
+ ok: true,
4781
+ check: { id: "latest-run-succeeded", status: "warn", message: "loop has no recorded runs yet" },
4782
+ route
4783
+ };
4784
+ }
4785
+ if (latestRun.status === "succeeded") {
4786
+ return {
4787
+ loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
4788
+ ok: true,
4789
+ check: { id: "latest-run-succeeded", status: "pass", message: "latest run succeeded" },
4790
+ latestRun: healthRun(latestRun),
4791
+ route
4792
+ };
4793
+ }
4794
+ const failure = classifyRunFailure(latestRun);
4795
+ return {
4796
+ loop: { id: loop.id, name: loop.name, status: loop.status, nextRunAt: loop.nextRunAt },
4797
+ ok: false,
4798
+ check: { id: "latest-run-succeeded", status: "fail", message: `latest run is ${latestRun.status}` },
4799
+ latestRun: healthRun(latestRun),
4800
+ failure,
4801
+ route,
4802
+ recommendedTask: failure ? recommendedTask(loop, latestRun, failure, route) : undefined
4803
+ };
4804
+ }
4805
+ function buildHealthReport(store, opts = {}) {
4806
+ const loops2 = store.listLoops({ includeArchived: opts.includeArchived, limit: opts.limit ?? 200 });
4807
+ const expectations = loops2.map((loop) => expectationForLoop(store, loop));
4808
+ const classifications = Object.fromEntries(CLASSIFICATIONS.map((key) => [key, 0]));
4809
+ for (const expectation of expectations) {
4810
+ if (expectation.failure)
4811
+ classifications[expectation.failure.classification] += 1;
4812
+ }
4813
+ const unhealthy = expectations.filter((expectation) => !expectation.ok).length;
4814
+ const warnings = expectations.filter((expectation) => expectation.check.status === "warn").length;
4815
+ return {
4816
+ ok: unhealthy === 0,
4817
+ generatedAt: new Date().toISOString(),
4818
+ summary: {
4819
+ loops: expectations.length,
4820
+ healthy: expectations.length - unhealthy,
4821
+ unhealthy,
4822
+ warnings
4823
+ },
4824
+ classifications,
4825
+ expectations
4826
+ };
4827
+ }
4433
4828
  export {
4434
4829
  workflowExecutionOrder,
4435
4830
  workflowBodyFromJson,
@@ -4455,11 +4850,14 @@ export {
4455
4850
  isTerminal as isGoalTerminal,
4456
4851
  initialNextRun,
4457
4852
  getLoopTemplate,
4853
+ expectationForLoop,
4458
4854
  executeWorkflow,
4459
4855
  executeTarget,
4460
4856
  executeLoopTarget,
4461
4857
  executeLoop,
4462
4858
  computeNextAfter,
4859
+ classifyRunFailure,
4860
+ buildHealthReport,
4463
4861
  TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
4464
4862
  Store,
4465
4863
  LoopsClient,