@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 +20 -6
- package/dist/cli/index.js +208 -22
- package/dist/daemon/control.d.ts +1 -0
- package/dist/daemon/index.js +93 -8
- package/dist/index.js +166 -14
- package/dist/lib/store.d.ts +8 -1
- package/dist/lib/store.js +83 -5
- package/dist/lib/templates.d.ts +12 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +104 -6
- package/dist/types.d.ts +2 -0
- package/docs/USAGE.md +20 -6
- package/package.json +1 -1
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
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|