@hasna/loops 0.3.14 → 0.3.15

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/README.md CHANGED
@@ -219,7 +219,7 @@ loops templates render todos-task-worker-verifier \
219
219
  --var taskTitle="Fix parser" \
220
220
  --var projectPath=/path/to/repo \
221
221
  --var provider=codewith \
222
- --var authProfile=account005 \
222
+ --var authProfilePool=account004,account005,account006 \
223
223
  --var sandbox=danger-full-access
224
224
  loops templates create-workflow todos-task-worker-verifier \
225
225
  --var taskId=<task-id> \
@@ -239,7 +239,7 @@ schedules a deduped one-shot workflow loop:
239
239
  ```bash
240
240
  cat task-created-event.json | loops events handle todos-task \
241
241
  --provider codewith \
242
- --auth-profile account005 \
242
+ --auth-profile-pool account004,account005,account006 \
243
243
  --permission-mode bypass \
244
244
  --sandbox danger-full-access
245
245
  ```
@@ -250,7 +250,7 @@ handler:
250
250
  ```bash
251
251
  cat event.json | loops events handle generic \
252
252
  --provider codewith \
253
- --auth-profile account005 \
253
+ --auth-profile-pool account004,account005,account006 \
254
254
  --permission-mode bypass \
255
255
  --sandbox danger-full-access \
256
256
  --project-path /path/to/repo
@@ -258,9 +258,11 @@ cat event.json | loops events handle generic \
258
258
 
259
259
  This is the intended deterministic-to-agentic path: a producer creates a todos
260
260
  task, `@hasna/events` delivers `task.created`, OpenLoops creates a worker and a
261
- verifier workflow, and the workflow updates todos with evidence. Use
262
- `--dry-run` to inspect the rendered workflow and loop input without storing
263
- anything.
261
+ verifier workflow, and the workflow updates todos with evidence. Use account
262
+ pools so worker and verifier steps do not burn the same profile; OpenLoops picks
263
+ deterministically and uses a different verifier profile when the pool has at
264
+ least two entries. Use `--dry-run` to inspect the rendered workflow and loop
265
+ input without storing anything.
264
266
 
265
267
  ## Transcript-Driven Loops
266
268
 
@@ -283,12 +285,24 @@ loops runs <id-or-name>
283
285
  loops pause <id-or-name>
284
286
  loops resume <id-or-name>
285
287
  loops stop <id-or-name>
288
+ loops archive <id-or-name>
289
+ loops unarchive <id-or-name>
286
290
  loops remove <id-or-name>
287
291
  loops run-now <id-or-name>
288
292
  ```
289
293
 
290
294
  Use `--json` for machine-readable output. Prompt bodies and run stdout/stderr are redacted by default in status output. `loops run-now` exits non-zero when the recorded run fails or times out.
291
295
 
296
+ Archive loops when retiring old automation but preserving history:
297
+
298
+ ```bash
299
+ loops archive <id-or-name>
300
+ loops list --archived
301
+ loops list --all
302
+ ```
303
+
304
+ Archived loops are hidden from the default `loops list`, excluded from daemon scheduling and doctor preflight, and cannot be run manually until restored with `loops unarchive`. `loops remove` deletes the loop record; prefer `archive` for superseded loops that may need audit history.
305
+
292
306
  `loops run-now` reports the manual run source:
293
307
 
294
308
  - `source=ad_hoc`: the loop was not due yet, so OpenLoops created a one-off manual slot. This is a single immediate attempt and does not schedule retries or consume the future scheduled slot.
package/dist/cli/index.js CHANGED
@@ -482,6 +482,8 @@ function rowToLoop(row) {
482
482
  name: row.name,
483
483
  description: row.description ?? undefined,
484
484
  status: row.status,
485
+ archivedAt: row.archived_at ?? undefined,
486
+ archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
485
487
  schedule: JSON.parse(row.schedule_json),
486
488
  target: JSON.parse(row.target_json),
487
489
  goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
@@ -691,6 +693,8 @@ class Store {
691
693
  name TEXT NOT NULL,
692
694
  description TEXT,
693
695
  status TEXT NOT NULL,
696
+ archived_at TEXT,
697
+ archived_from_status TEXT,
694
698
  schedule_json TEXT NOT NULL,
695
699
  target_json TEXT NOT NULL,
696
700
  goal_json TEXT,
@@ -891,6 +895,8 @@ class Store {
891
895
  `);
892
896
  this.addColumnIfMissing("loops", "machine_json", "TEXT");
893
897
  this.addColumnIfMissing("loops", "goal_json", "TEXT");
898
+ this.addColumnIfMissing("loops", "archived_at", "TEXT");
899
+ this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
894
900
  this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
895
901
  this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
896
902
  this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
@@ -899,6 +905,7 @@ class Store {
899
905
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
900
906
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
901
907
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
908
+ this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
902
909
  }
903
910
  addColumnIfMissing(table, column, definition) {
904
911
  const columns = this.db.query(`PRAGMA table_info(${table})`).all();
@@ -975,12 +982,26 @@ class Store {
975
982
  }
976
983
  listLoops(opts = {}) {
977
984
  const limit = opts.limit ?? 200;
978
- 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);
985
+ let rows;
986
+ if (opts.status && opts.archived) {
987
+ 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);
988
+ } else if (opts.status && opts.includeArchived) {
989
+ rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
990
+ } else if (opts.status) {
991
+ 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);
992
+ } else if (opts.archived) {
993
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
994
+ } else if (opts.includeArchived) {
995
+ rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
996
+ } else {
997
+ rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
998
+ }
979
999
  return rows.map(rowToLoop);
980
1000
  }
981
1001
  dueLoops(now) {
982
1002
  const rows = this.db.query(`SELECT * FROM loops
983
1003
  WHERE status = 'active'
1004
+ AND archived_at IS NULL
984
1005
  AND next_run_at IS NOT NULL
985
1006
  AND next_run_at <= ?
986
1007
  ORDER BY next_run_at ASC`).all(now.toISOString());
@@ -1012,6 +1033,44 @@ class Store {
1012
1033
  throw new Error(`loop not found after update: ${id}`);
1013
1034
  return after;
1014
1035
  }
1036
+ archiveLoop(idOrName) {
1037
+ const loop = this.requireLoop(idOrName);
1038
+ if (loop.archivedAt)
1039
+ return loop;
1040
+ const updated = nowIso();
1041
+ const archivedStatus = loop.status === "active" ? "paused" : loop.status;
1042
+ this.db.query(`UPDATE loops
1043
+ SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
1044
+ WHERE id=$id`).run({
1045
+ $id: loop.id,
1046
+ $status: archivedStatus,
1047
+ $archivedAt: updated,
1048
+ $archivedFromStatus: loop.status,
1049
+ $updated: updated
1050
+ });
1051
+ const archived = this.getLoop(loop.id);
1052
+ if (!archived)
1053
+ throw new Error(`loop not found after archive: ${loop.id}`);
1054
+ return archived;
1055
+ }
1056
+ unarchiveLoop(idOrName) {
1057
+ const loop = this.requireLoop(idOrName);
1058
+ if (!loop.archivedAt)
1059
+ return loop;
1060
+ const updated = nowIso();
1061
+ const restoredStatus = loop.archivedFromStatus ?? loop.status;
1062
+ this.db.query(`UPDATE loops
1063
+ SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
1064
+ WHERE id=$id`).run({
1065
+ $id: loop.id,
1066
+ $status: restoredStatus,
1067
+ $updated: updated
1068
+ });
1069
+ const unarchived = this.getLoop(loop.id);
1070
+ if (!unarchived)
1071
+ throw new Error(`loop not found after unarchive: ${loop.id}`);
1072
+ return unarchived;
1073
+ }
1015
1074
  deleteLoop(idOrName) {
1016
1075
  const loop = this.requireLoop(idOrName);
1017
1076
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
@@ -1786,10 +1845,16 @@ class Store {
1786
1845
  }
1787
1846
  claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
1788
1847
  const startedAt = now.toISOString();
1789
- const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1790
1848
  this.db.exec("BEGIN IMMEDIATE");
1791
1849
  try {
1792
1850
  this.assertDaemonLeaseFence(opts, startedAt);
1851
+ const currentLoop = this.getLoop(loop.id);
1852
+ if (!currentLoop || currentLoop.archivedAt) {
1853
+ this.db.exec("COMMIT");
1854
+ return;
1855
+ }
1856
+ loop = currentLoop;
1857
+ const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
1793
1858
  const existing = this.getRunBySlot(loop.id, scheduledFor);
1794
1859
  if (existing) {
1795
1860
  if (existing.status === "running") {
@@ -2007,7 +2072,7 @@ class Store {
2007
2072
  return recovered;
2008
2073
  }
2009
2074
  expireLoops(now = new Date, opts = {}) {
2010
- const rows = this.db.query("SELECT * FROM loops WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?").all(now.toISOString());
2075
+ 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());
2011
2076
  const expired = [];
2012
2077
  for (const row of rows) {
2013
2078
  const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
@@ -2016,8 +2081,21 @@ class Store {
2016
2081
  }
2017
2082
  return expired;
2018
2083
  }
2019
- countLoops(status) {
2020
- 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();
2084
+ countLoops(status, opts = {}) {
2085
+ let row;
2086
+ if (status && opts.archived) {
2087
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
2088
+ } else if (status && opts.includeArchived) {
2089
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
2090
+ } else if (status) {
2091
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
2092
+ } else if (opts.archived) {
2093
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
2094
+ } else if (opts.includeArchived) {
2095
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
2096
+ } else {
2097
+ row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
2098
+ }
2021
2099
  return row?.count ?? 0;
2022
2100
  }
2023
2101
  countRuns(status) {
@@ -3703,12 +3781,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
3703
3781
 
3704
3782
  // src/lib/scheduler.ts
3705
3783
  function manualRunScheduledFor(loop, now = new Date) {
3784
+ if (loop.archivedAt)
3785
+ return now.toISOString();
3706
3786
  if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
3707
3787
  return loop.retryScheduledFor ?? loop.nextRunAt;
3708
3788
  }
3709
3789
  return now.toISOString();
3710
3790
  }
3711
3791
  function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3792
+ if (loop.archivedAt)
3793
+ return false;
3712
3794
  if (loop.status !== "active")
3713
3795
  return false;
3714
3796
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3716,6 +3798,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
3716
3798
  return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
3717
3799
  }
3718
3800
  function manualRunSource(loop, scheduledFor, now = new Date) {
3801
+ if (loop.archivedAt)
3802
+ return "ad_hoc";
3719
3803
  if (loop.status !== "active")
3720
3804
  return "ad_hoc";
3721
3805
  if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
@@ -3734,7 +3818,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
3734
3818
  if (run.status === "running")
3735
3819
  return;
3736
3820
  const current = store.getLoop(loop.id);
3737
- if (!current || current.status !== "active")
3821
+ if (!current || current.status !== "active" || current.archivedAt)
3738
3822
  return;
3739
3823
  if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
3740
3824
  return;
@@ -4069,7 +4153,8 @@ function daemonStatus(store, path = pidFilePath()) {
4069
4153
  active: store.countLoops("active"),
4070
4154
  paused: store.countLoops("paused"),
4071
4155
  stopped: store.countLoops("stopped"),
4072
- expired: store.countLoops("expired")
4156
+ expired: store.countLoops("expired"),
4157
+ archived: store.countLoops(undefined, { archived: true })
4073
4158
  },
4074
4159
  runs: {
4075
4160
  total: store.countRuns(),
@@ -4495,7 +4580,7 @@ function runDoctor(store) {
4495
4580
  // package.json
4496
4581
  var package_default = {
4497
4582
  name: "@hasna/loops",
4498
- version: "0.3.14",
4583
+ version: "0.3.15",
4499
4584
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4500
4585
  type: "module",
4501
4586
  main: "dist/index.js",
@@ -4598,6 +4683,10 @@ var TEMPLATE_SUMMARIES = [
4598
4683
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4599
4684
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4600
4685
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4686
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4687
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4688
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4689
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4601
4690
  { name: "model", description: "Provider model." },
4602
4691
  { name: "variant", description: "Provider reasoning/model effort variant." },
4603
4692
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -4617,6 +4706,10 @@ var TEMPLATE_SUMMARIES = [
4617
4706
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4618
4707
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
4619
4708
  { name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
4709
+ { name: "authProfilePool", description: "Comma-separated provider-native auth profiles; worker/verifier are selected deterministically." },
4710
+ { name: "workerAuthProfile", description: "Provider-native auth profile for the worker step." },
4711
+ { name: "verifierAuthProfile", description: "Provider-native auth profile for the verifier step." },
4712
+ { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4620
4713
  { name: "model", description: "Provider model." },
4621
4714
  { name: "variant", description: "Provider reasoning/model effort variant." },
4622
4715
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
@@ -4631,7 +4724,37 @@ function taskLabel(input) {
4631
4724
  const head = input.taskTitle?.trim() || input.taskId;
4632
4725
  return head.length > 160 ? `${head.slice(0, 157)}...` : head;
4633
4726
  }
4634
- function agentTarget(input, prompt) {
4727
+ function stableIndex(seed, size) {
4728
+ let hash = 2166136261;
4729
+ for (let i = 0;i < seed.length; i += 1) {
4730
+ hash ^= seed.charCodeAt(i);
4731
+ hash = Math.imul(hash, 16777619);
4732
+ }
4733
+ return Math.abs(hash >>> 0) % size;
4734
+ }
4735
+ function rolePoolValue(pool, seed, role) {
4736
+ if (!pool?.length)
4737
+ return;
4738
+ const workerIndex = stableIndex(seed, pool.length);
4739
+ if (role === "worker" || pool.length === 1)
4740
+ return pool[workerIndex];
4741
+ return pool[(workerIndex + 1) % pool.length];
4742
+ }
4743
+ function authProfileForRole(input, role, seed) {
4744
+ if (role === "worker" && input.workerAuthProfile)
4745
+ return input.workerAuthProfile;
4746
+ if (role === "verifier" && input.verifierAuthProfile)
4747
+ return input.verifierAuthProfile;
4748
+ return rolePoolValue(input.authProfilePool, seed, role) ?? input.authProfile;
4749
+ }
4750
+ function accountForRole(input, role, seed) {
4751
+ if (role === "worker" && input.workerAccount)
4752
+ return input.workerAccount;
4753
+ if (role === "verifier" && input.verifierAccount)
4754
+ return input.verifierAccount;
4755
+ return rolePoolValue(input.accountPool, seed, role) ?? input.account;
4756
+ }
4757
+ function agentTarget(input, prompt, role, seed) {
4635
4758
  const provider = input.provider ?? "codewith";
4636
4759
  const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
4637
4760
  return {
@@ -4642,11 +4765,11 @@ function agentTarget(input, prompt) {
4642
4765
  model: input.model,
4643
4766
  variant: input.variant,
4644
4767
  agent: input.agent,
4645
- authProfile: provider === "codewith" ? input.authProfile : undefined,
4768
+ authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
4646
4769
  configIsolation: "safe",
4647
4770
  permissionMode: input.permissionMode ?? "bypass",
4648
4771
  sandbox,
4649
- account: input.account,
4772
+ account: accountForRole(input, role, seed),
4650
4773
  timeoutMs: 45 * 60000
4651
4774
  };
4652
4775
  }
@@ -4700,7 +4823,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4700
4823
  id: "worker",
4701
4824
  name: "Worker",
4702
4825
  description: "Implement the todos task and record evidence.",
4703
- target: agentTarget(input, workerPrompt),
4826
+ target: agentTarget(input, workerPrompt, "worker", input.taskId),
4704
4827
  timeoutMs: 45 * 60000
4705
4828
  },
4706
4829
  {
@@ -4708,7 +4831,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
4708
4831
  name: "Verifier",
4709
4832
  description: "Adversarially verify worker output and update todos.",
4710
4833
  dependsOn: ["worker"],
4711
- target: agentTarget(input, verifierPrompt),
4834
+ target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
4712
4835
  timeoutMs: 30 * 60000
4713
4836
  }
4714
4837
  ]
@@ -4764,7 +4887,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4764
4887
  id: "worker",
4765
4888
  name: "Worker",
4766
4889
  description: "Handle the Hasna event and record evidence.",
4767
- target: agentTarget(input, workerPrompt),
4890
+ target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4768
4891
  timeoutMs: 45 * 60000
4769
4892
  },
4770
4893
  {
@@ -4772,7 +4895,7 @@ function renderEventWorkerVerifierWorkflow(input) {
4772
4895
  name: "Verifier",
4773
4896
  description: "Adversarially verify event handling.",
4774
4897
  dependsOn: ["worker"],
4775
- target: agentTarget(input, verifierPrompt),
4898
+ target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
4776
4899
  timeoutMs: 30 * 60000
4777
4900
  }
4778
4901
  ]
@@ -4787,7 +4910,11 @@ function renderLoopTemplate(id, values) {
4787
4910
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4788
4911
  provider: values.provider,
4789
4912
  authProfile: values.authProfile,
4913
+ authProfilePool: listVar(values.authProfilePool),
4914
+ workerAuthProfile: values.workerAuthProfile,
4915
+ verifierAuthProfile: values.verifierAuthProfile,
4790
4916
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4917
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4791
4918
  model: values.model,
4792
4919
  variant: values.variant,
4793
4920
  agent: values.agent,
@@ -4808,7 +4935,11 @@ function renderLoopTemplate(id, values) {
4808
4935
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
4809
4936
  provider: values.provider,
4810
4937
  authProfile: values.authProfile,
4938
+ authProfilePool: listVar(values.authProfilePool),
4939
+ workerAuthProfile: values.workerAuthProfile,
4940
+ verifierAuthProfile: values.verifierAuthProfile,
4811
4941
  account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
4942
+ accountPool: accountPoolVar(values.accountPool, values.accountTool),
4812
4943
  model: values.model,
4813
4944
  variant: values.variant,
4814
4945
  agent: values.agent,
@@ -4818,6 +4949,13 @@ function renderLoopTemplate(id, values) {
4818
4949
  }
4819
4950
  throw new Error(`unknown template: ${id}`);
4820
4951
  }
4952
+ function listVar(value) {
4953
+ const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
4954
+ return values?.length ? values : undefined;
4955
+ }
4956
+ function accountPoolVar(value, tool) {
4957
+ return listVar(value)?.map((profile) => ({ profile, tool }));
4958
+ }
4821
4959
 
4822
4960
  // src/cli/index.ts
4823
4961
  var program = new Command;
@@ -4933,10 +5071,21 @@ function goalFromOpts(opts) {
4933
5071
  }, "goal");
4934
5072
  }
4935
5073
  function accountFromOpts(opts) {
4936
- if (!opts.account && opts.accountTool)
4937
- throw new Error("--account-tool requires --account");
5074
+ if (!opts.account && opts.accountTool && !opts.accountPool && !opts.workerAccount && !opts.verifierAccount) {
5075
+ throw new Error("--account-tool requires --account, --account-pool, --worker-account, or --verifier-account");
5076
+ }
4938
5077
  return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
4939
5078
  }
5079
+ function splitList(value) {
5080
+ const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
5081
+ return values?.length ? values : undefined;
5082
+ }
5083
+ function accountPoolFromOpts(opts) {
5084
+ return splitList(opts.accountPool)?.map((profile) => ({ profile, tool: opts.accountTool }));
5085
+ }
5086
+ function roleAccountFromOpts(opts, profile) {
5087
+ return profile ? { profile, tool: opts.accountTool } : undefined;
5088
+ }
4940
5089
  function parseVars(values) {
4941
5090
  const vars = {};
4942
5091
  for (const value of values ?? []) {
@@ -5149,7 +5298,7 @@ templates.command("create-workflow <id>").description("render and store a templa
5149
5298
  }
5150
5299
  });
5151
5300
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
5152
- 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("--account <profile>", "OpenAccounts profile name").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) => {
5301
+ 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) => {
5153
5302
  const event = await readEventEnvelopeFromStdin();
5154
5303
  const data = eventData(event);
5155
5304
  const metadata = eventMetadata(event);
@@ -5182,7 +5331,13 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
5182
5331
  projectPath,
5183
5332
  provider,
5184
5333
  authProfile,
5334
+ authProfilePool: splitList(opts.authProfilePool),
5335
+ workerAuthProfile: opts.workerAuthProfile,
5336
+ verifierAuthProfile: opts.verifierAuthProfile,
5185
5337
  account: accountFromOpts(opts),
5338
+ accountPool: accountPoolFromOpts(opts),
5339
+ workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
5340
+ verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
5186
5341
  model: opts.model,
5187
5342
  variant: opts.variant,
5188
5343
  agent: opts.agent,
@@ -5236,7 +5391,7 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
5236
5391
  store.close();
5237
5392
  }
5238
5393
  });
5239
- 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("--account <profile>", "OpenAccounts profile name").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) => {
5394
+ 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) => {
5240
5395
  const event = await readEventEnvelopeFromStdin();
5241
5396
  const data = eventData(event);
5242
5397
  const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
@@ -5256,7 +5411,13 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
5256
5411
  projectPath,
5257
5412
  provider,
5258
5413
  authProfile,
5414
+ authProfilePool: splitList(opts.authProfilePool),
5415
+ workerAuthProfile: opts.workerAuthProfile,
5416
+ verifierAuthProfile: opts.verifierAuthProfile,
5259
5417
  account: accountFromOpts(opts),
5418
+ accountPool: accountPoolFromOpts(opts),
5419
+ workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
5420
+ verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
5260
5421
  model: opts.model,
5261
5422
  variant: opts.variant,
5262
5423
  agent: opts.agent,
@@ -5523,16 +5684,19 @@ workflows.command("archive <idOrName>").action((idOrName) => {
5523
5684
  store.close();
5524
5685
  }
5525
5686
  });
5526
- program.command("list").alias("ls").option("--status <status>", "filter by status").action((opts) => {
5687
+ program.command("list").alias("ls").option("--status <status>", "filter by status").option("--archived", "show only archived loops").option("--all", "include archived loops").action((opts) => {
5688
+ if (opts.archived && opts.all)
5689
+ throw new Error("use either --archived or --all, not both");
5527
5690
  const store = new Store;
5528
5691
  try {
5529
- const loops = store.listLoops({ status: opts.status });
5692
+ const loops = store.listLoops({ status: opts.status, archived: opts.archived, includeArchived: opts.all });
5530
5693
  if (isJson())
5531
5694
  print(loops.map(publicLoop));
5532
5695
  else {
5533
5696
  for (const loop of loops) {
5534
5697
  const machine = loop.machine ? ` machine=${loop.machine.id}` : "";
5535
- console.log(`${loop.id} ${loop.status.padEnd(7)} next=${loop.nextRunAt ?? "-"} ${loop.name}${machine}`);
5698
+ const archive = loop.archivedAt ? ` archived=${loop.archivedAt} from=${loop.archivedFromStatus ?? "-"}` : "";
5699
+ console.log(`${loop.id} ${loop.status.padEnd(7)} next=${loop.nextRunAt ?? "-"} ${loop.name}${machine}${archive}`);
5536
5700
  }
5537
5701
  }
5538
5702
  } finally {
@@ -5572,6 +5736,8 @@ function updateStatus(idOrName, status) {
5572
5736
  const store = new Store;
5573
5737
  try {
5574
5738
  const loop = store.requireLoop(idOrName);
5739
+ if (loop.archivedAt)
5740
+ throw new Error(`loop is archived; run 'loops unarchive ${idOrName}' first`);
5575
5741
  const updated = store.updateLoop(loop.id, { status, nextRunAt: status === "stopped" ? undefined : loop.nextRunAt });
5576
5742
  print(publicLoop(updated), `${updated.id} ${updated.status}`);
5577
5743
  } finally {
@@ -5587,10 +5753,30 @@ program.command("remove <idOrName>").alias("rm").action((idOrName) => {
5587
5753
  store.close();
5588
5754
  }
5589
5755
  });
5756
+ program.command("archive <idOrName>").description("archive a loop without deleting history").action((idOrName) => {
5757
+ const store = new Store;
5758
+ try {
5759
+ const loop = store.archiveLoop(idOrName);
5760
+ print(publicLoop(loop), `${loop.id} archived`);
5761
+ } finally {
5762
+ store.close();
5763
+ }
5764
+ });
5765
+ program.command("unarchive <idOrName>").alias("restore").description("restore an archived loop").action((idOrName) => {
5766
+ const store = new Store;
5767
+ try {
5768
+ const loop = store.unarchiveLoop(idOrName);
5769
+ print(publicLoop(loop), `${loop.id} ${loop.status}`);
5770
+ } finally {
5771
+ store.close();
5772
+ }
5773
+ });
5590
5774
  program.command("run-now <idOrName>").option("--show-output", "show stdout/stderr").action(async (idOrName, opts) => {
5591
5775
  const store = new Store;
5592
5776
  try {
5593
5777
  const loop = store.requireLoop(idOrName);
5778
+ if (loop.archivedAt)
5779
+ throw new Error(`loop is archived; run 'loops unarchive ${idOrName}' before running it`);
5594
5780
  const runnerId = `manual:${process.pid}`;
5595
5781
  const now = new Date;
5596
5782
  let scheduledFor = manualRunScheduledFor(loop, now);
@@ -18,6 +18,7 @@ export interface DaemonStatus extends DaemonProcessState {
18
18
  paused: number;
19
19
  stopped: number;
20
20
  expired: number;
21
+ archived: number;
21
22
  };
22
23
  runs: {
23
24
  total: number;