@hasna/loops 0.3.13 → 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 +248 -33
- package/dist/daemon/control.d.ts +1 -0
- package/dist/daemon/index.js +94 -9
- 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 +2 -2
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) {
|
|
@@ -2074,6 +2152,7 @@ class Store {
|
|
|
2074
2152
|
}
|
|
2075
2153
|
|
|
2076
2154
|
// src/cli/index.ts
|
|
2155
|
+
import { createHash } from "crypto";
|
|
2077
2156
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2078
2157
|
import { Command } from "commander";
|
|
2079
2158
|
|
|
@@ -3702,12 +3781,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
3702
3781
|
|
|
3703
3782
|
// src/lib/scheduler.ts
|
|
3704
3783
|
function manualRunScheduledFor(loop, now = new Date) {
|
|
3784
|
+
if (loop.archivedAt)
|
|
3785
|
+
return now.toISOString();
|
|
3705
3786
|
if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
|
|
3706
3787
|
return loop.retryScheduledFor ?? loop.nextRunAt;
|
|
3707
3788
|
}
|
|
3708
3789
|
return now.toISOString();
|
|
3709
3790
|
}
|
|
3710
3791
|
function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
3792
|
+
if (loop.archivedAt)
|
|
3793
|
+
return false;
|
|
3711
3794
|
if (loop.status !== "active")
|
|
3712
3795
|
return false;
|
|
3713
3796
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3715,6 +3798,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
|
3715
3798
|
return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
|
|
3716
3799
|
}
|
|
3717
3800
|
function manualRunSource(loop, scheduledFor, now = new Date) {
|
|
3801
|
+
if (loop.archivedAt)
|
|
3802
|
+
return "ad_hoc";
|
|
3718
3803
|
if (loop.status !== "active")
|
|
3719
3804
|
return "ad_hoc";
|
|
3720
3805
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3733,7 +3818,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
|
|
|
3733
3818
|
if (run.status === "running")
|
|
3734
3819
|
return;
|
|
3735
3820
|
const current = store.getLoop(loop.id);
|
|
3736
|
-
if (!current || current.status !== "active")
|
|
3821
|
+
if (!current || current.status !== "active" || current.archivedAt)
|
|
3737
3822
|
return;
|
|
3738
3823
|
if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
|
|
3739
3824
|
return;
|
|
@@ -4068,7 +4153,8 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
4068
4153
|
active: store.countLoops("active"),
|
|
4069
4154
|
paused: store.countLoops("paused"),
|
|
4070
4155
|
stopped: store.countLoops("stopped"),
|
|
4071
|
-
expired: store.countLoops("expired")
|
|
4156
|
+
expired: store.countLoops("expired"),
|
|
4157
|
+
archived: store.countLoops(undefined, { archived: true })
|
|
4072
4158
|
},
|
|
4073
4159
|
runs: {
|
|
4074
4160
|
total: store.countRuns(),
|
|
@@ -4494,7 +4580,7 @@ function runDoctor(store) {
|
|
|
4494
4580
|
// package.json
|
|
4495
4581
|
var package_default = {
|
|
4496
4582
|
name: "@hasna/loops",
|
|
4497
|
-
version: "0.3.
|
|
4583
|
+
version: "0.3.15",
|
|
4498
4584
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4499
4585
|
type: "module",
|
|
4500
4586
|
main: "dist/index.js",
|
|
@@ -4558,7 +4644,7 @@ var package_default = {
|
|
|
4558
4644
|
bun: ">=1.0.0"
|
|
4559
4645
|
},
|
|
4560
4646
|
dependencies: {
|
|
4561
|
-
"@hasna/events": "^0.1.
|
|
4647
|
+
"@hasna/events": "^0.1.9",
|
|
4562
4648
|
"@hasna/machines": "0.0.49",
|
|
4563
4649
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4564
4650
|
ai: "6.0.204",
|
|
@@ -4597,6 +4683,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
4597
4683
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4598
4684
|
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
4599
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." },
|
|
4600
4690
|
{ name: "model", description: "Provider model." },
|
|
4601
4691
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4602
4692
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
@@ -4616,6 +4706,10 @@ var TEMPLATE_SUMMARIES = [
|
|
|
4616
4706
|
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4617
4707
|
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
4618
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." },
|
|
4619
4713
|
{ name: "model", description: "Provider model." },
|
|
4620
4714
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4621
4715
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
@@ -4630,7 +4724,37 @@ function taskLabel(input) {
|
|
|
4630
4724
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
4631
4725
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
4632
4726
|
}
|
|
4633
|
-
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) {
|
|
4634
4758
|
const provider = input.provider ?? "codewith";
|
|
4635
4759
|
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
|
|
4636
4760
|
return {
|
|
@@ -4641,11 +4765,11 @@ function agentTarget(input, prompt) {
|
|
|
4641
4765
|
model: input.model,
|
|
4642
4766
|
variant: input.variant,
|
|
4643
4767
|
agent: input.agent,
|
|
4644
|
-
authProfile: provider === "codewith" ? input
|
|
4768
|
+
authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
|
|
4645
4769
|
configIsolation: "safe",
|
|
4646
4770
|
permissionMode: input.permissionMode ?? "bypass",
|
|
4647
4771
|
sandbox,
|
|
4648
|
-
account: input
|
|
4772
|
+
account: accountForRole(input, role, seed),
|
|
4649
4773
|
timeoutMs: 45 * 60000
|
|
4650
4774
|
};
|
|
4651
4775
|
}
|
|
@@ -4699,7 +4823,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4699
4823
|
id: "worker",
|
|
4700
4824
|
name: "Worker",
|
|
4701
4825
|
description: "Implement the todos task and record evidence.",
|
|
4702
|
-
target: agentTarget(input, workerPrompt),
|
|
4826
|
+
target: agentTarget(input, workerPrompt, "worker", input.taskId),
|
|
4703
4827
|
timeoutMs: 45 * 60000
|
|
4704
4828
|
},
|
|
4705
4829
|
{
|
|
@@ -4707,7 +4831,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4707
4831
|
name: "Verifier",
|
|
4708
4832
|
description: "Adversarially verify worker output and update todos.",
|
|
4709
4833
|
dependsOn: ["worker"],
|
|
4710
|
-
target: agentTarget(input, verifierPrompt),
|
|
4834
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId),
|
|
4711
4835
|
timeoutMs: 30 * 60000
|
|
4712
4836
|
}
|
|
4713
4837
|
]
|
|
@@ -4763,7 +4887,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
4763
4887
|
id: "worker",
|
|
4764
4888
|
name: "Worker",
|
|
4765
4889
|
description: "Handle the Hasna event and record evidence.",
|
|
4766
|
-
target: agentTarget(input, workerPrompt),
|
|
4890
|
+
target: agentTarget(input, workerPrompt, "worker", `${input.eventSource}:${input.eventType}:${input.eventId}`),
|
|
4767
4891
|
timeoutMs: 45 * 60000
|
|
4768
4892
|
},
|
|
4769
4893
|
{
|
|
@@ -4771,7 +4895,7 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
4771
4895
|
name: "Verifier",
|
|
4772
4896
|
description: "Adversarially verify event handling.",
|
|
4773
4897
|
dependsOn: ["worker"],
|
|
4774
|
-
target: agentTarget(input, verifierPrompt),
|
|
4898
|
+
target: agentTarget(input, verifierPrompt, "verifier", `${input.eventSource}:${input.eventType}:${input.eventId}`),
|
|
4775
4899
|
timeoutMs: 30 * 60000
|
|
4776
4900
|
}
|
|
4777
4901
|
]
|
|
@@ -4786,7 +4910,11 @@ function renderLoopTemplate(id, values) {
|
|
|
4786
4910
|
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4787
4911
|
provider: values.provider,
|
|
4788
4912
|
authProfile: values.authProfile,
|
|
4913
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
4914
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
4915
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
4789
4916
|
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4917
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
4790
4918
|
model: values.model,
|
|
4791
4919
|
variant: values.variant,
|
|
4792
4920
|
agent: values.agent,
|
|
@@ -4807,7 +4935,11 @@ function renderLoopTemplate(id, values) {
|
|
|
4807
4935
|
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4808
4936
|
provider: values.provider,
|
|
4809
4937
|
authProfile: values.authProfile,
|
|
4938
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
4939
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
4940
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
4810
4941
|
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4942
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
4811
4943
|
model: values.model,
|
|
4812
4944
|
variant: values.variant,
|
|
4813
4945
|
agent: values.agent,
|
|
@@ -4817,6 +4949,13 @@ function renderLoopTemplate(id, values) {
|
|
|
4817
4949
|
}
|
|
4818
4950
|
throw new Error(`unknown template: ${id}`);
|
|
4819
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
|
+
}
|
|
4820
4959
|
|
|
4821
4960
|
// src/cli/index.ts
|
|
4822
4961
|
var program = new Command;
|
|
@@ -4932,10 +5071,21 @@ function goalFromOpts(opts) {
|
|
|
4932
5071
|
}, "goal");
|
|
4933
5072
|
}
|
|
4934
5073
|
function accountFromOpts(opts) {
|
|
4935
|
-
if (!opts.account && opts.accountTool)
|
|
4936
|
-
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
|
+
}
|
|
4937
5077
|
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
4938
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
|
+
}
|
|
4939
5089
|
function parseVars(values) {
|
|
4940
5090
|
const vars = {};
|
|
4941
5091
|
for (const value of values ?? []) {
|
|
@@ -4956,12 +5106,21 @@ function eventData(event) {
|
|
|
4956
5106
|
return data;
|
|
4957
5107
|
return {};
|
|
4958
5108
|
}
|
|
5109
|
+
function eventMetadata(event) {
|
|
5110
|
+
const metadata = event.metadata;
|
|
5111
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata))
|
|
5112
|
+
return metadata;
|
|
5113
|
+
return {};
|
|
5114
|
+
}
|
|
4959
5115
|
function stringField(value) {
|
|
4960
5116
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
4961
5117
|
}
|
|
4962
5118
|
function slugSegment(value, fallback = "event") {
|
|
4963
5119
|
return value.toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || fallback;
|
|
4964
5120
|
}
|
|
5121
|
+
function stableSuffix(value) {
|
|
5122
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
5123
|
+
}
|
|
4965
5124
|
function taskEventField(data, keys) {
|
|
4966
5125
|
for (const key of keys) {
|
|
4967
5126
|
const direct = stringField(data[key]);
|
|
@@ -5139,15 +5298,26 @@ templates.command("create-workflow <id>").description("render and store a templa
|
|
|
5139
5298
|
}
|
|
5140
5299
|
});
|
|
5141
5300
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
5142
|
-
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) => {
|
|
5143
5302
|
const event = await readEventEnvelopeFromStdin();
|
|
5144
5303
|
const data = eventData(event);
|
|
5304
|
+
const metadata = eventMetadata(event);
|
|
5145
5305
|
const taskId = taskEventField(data, ["id", "task_id", "taskId"]);
|
|
5146
5306
|
if (!taskId)
|
|
5147
5307
|
throw new Error("todos task event is missing task id in data.id, data.task_id, data.task.id, or data.payload.id");
|
|
5148
5308
|
const taskTitle = taskEventField(data, ["title", "task_title", "taskTitle"]);
|
|
5149
5309
|
const taskDescription = taskEventField(data, ["description", "body"]);
|
|
5150
|
-
const
|
|
5310
|
+
const dataProjectPath = taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd"]);
|
|
5311
|
+
const metadataProjectPath = taskEventField(metadata, [
|
|
5312
|
+
"working_dir",
|
|
5313
|
+
"workingDir",
|
|
5314
|
+
"project_path",
|
|
5315
|
+
"projectPath",
|
|
5316
|
+
"project_canonical_path",
|
|
5317
|
+
"cwd"
|
|
5318
|
+
]);
|
|
5319
|
+
const projectPath = opts.projectPath ?? dataProjectPath ?? metadataProjectPath ?? process.cwd();
|
|
5320
|
+
const idempotencyKey = `todos-task:${taskId}:${event.type}`;
|
|
5151
5321
|
const provider = opts.provider;
|
|
5152
5322
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
|
|
5153
5323
|
throw new Error("unsupported provider");
|
|
@@ -5161,7 +5331,13 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5161
5331
|
projectPath,
|
|
5162
5332
|
provider,
|
|
5163
5333
|
authProfile,
|
|
5334
|
+
authProfilePool: splitList(opts.authProfilePool),
|
|
5335
|
+
workerAuthProfile: opts.workerAuthProfile,
|
|
5336
|
+
verifierAuthProfile: opts.verifierAuthProfile,
|
|
5164
5337
|
account: accountFromOpts(opts),
|
|
5338
|
+
accountPool: accountPoolFromOpts(opts),
|
|
5339
|
+
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
5340
|
+
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
5165
5341
|
model: opts.model,
|
|
5166
5342
|
variant: opts.variant,
|
|
5167
5343
|
agent: opts.agent,
|
|
@@ -5170,13 +5346,14 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5170
5346
|
eventId: event.id,
|
|
5171
5347
|
eventType: event.type
|
|
5172
5348
|
});
|
|
5173
|
-
const
|
|
5174
|
-
workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${
|
|
5175
|
-
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}`;
|
|
5176
|
-
const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${
|
|
5349
|
+
const idempotencySuffix = stableSuffix(idempotencyKey);
|
|
5350
|
+
workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:workflow`;
|
|
5351
|
+
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}`;
|
|
5352
|
+
const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:run`;
|
|
5353
|
+
const legacyLoopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${event.id.slice(0, 8)}:run`;
|
|
5177
5354
|
const loopInput = {
|
|
5178
5355
|
name: loopName,
|
|
5179
|
-
description: `Run ${workflowBody.name} once for task ${taskId}`,
|
|
5356
|
+
description: `Run ${workflowBody.name} once for task ${taskId}; idempotency=${idempotencyKey}; event=${event.id}`,
|
|
5180
5357
|
schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
|
|
5181
5358
|
target: { type: "workflow", workflowId: "<created-workflow-id>" },
|
|
5182
5359
|
overlap: "skip",
|
|
@@ -5185,15 +5362,22 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5185
5362
|
leaseMs: 90 * 60000
|
|
5186
5363
|
};
|
|
5187
5364
|
if (opts.dryRun) {
|
|
5188
|
-
print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5365
|
+
print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5189
5366
|
return;
|
|
5190
5367
|
}
|
|
5191
5368
|
const store = new Store;
|
|
5192
5369
|
try {
|
|
5193
|
-
const existingLoop = store.findLoopByName(loopName);
|
|
5370
|
+
const existingLoop = store.findLoopByName(loopName) ?? store.findLoopByName(legacyLoopName);
|
|
5194
5371
|
if (existingLoop) {
|
|
5195
5372
|
const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
|
|
5196
|
-
print({
|
|
5373
|
+
print({
|
|
5374
|
+
deduped: true,
|
|
5375
|
+
idempotencyKey,
|
|
5376
|
+
dedupedBy: existingLoop.name === loopName ? "idempotency" : "legacy-event-name",
|
|
5377
|
+
event,
|
|
5378
|
+
workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined,
|
|
5379
|
+
loop: publicLoop(existingLoop)
|
|
5380
|
+
}, `deduped existing loop ${existingLoop.id} (${existingLoop.name}) for event=${event.id} idempotency=${idempotencyKey}`);
|
|
5197
5381
|
return;
|
|
5198
5382
|
}
|
|
5199
5383
|
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
@@ -5202,12 +5386,12 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
5202
5386
|
...loopInput,
|
|
5203
5387
|
target: { type: "workflow", workflowId: workflow.id }
|
|
5204
5388
|
});
|
|
5205
|
-
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
5389
|
+
print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
|
|
5206
5390
|
} finally {
|
|
5207
5391
|
store.close();
|
|
5208
5392
|
}
|
|
5209
5393
|
});
|
|
5210
|
-
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) => {
|
|
5211
5395
|
const event = await readEventEnvelopeFromStdin();
|
|
5212
5396
|
const data = eventData(event);
|
|
5213
5397
|
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
|
|
@@ -5227,7 +5411,13 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
|
|
|
5227
5411
|
projectPath,
|
|
5228
5412
|
provider,
|
|
5229
5413
|
authProfile,
|
|
5414
|
+
authProfilePool: splitList(opts.authProfilePool),
|
|
5415
|
+
workerAuthProfile: opts.workerAuthProfile,
|
|
5416
|
+
verifierAuthProfile: opts.verifierAuthProfile,
|
|
5230
5417
|
account: accountFromOpts(opts),
|
|
5418
|
+
accountPool: accountPoolFromOpts(opts),
|
|
5419
|
+
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
5420
|
+
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
5231
5421
|
model: opts.model,
|
|
5232
5422
|
variant: opts.variant,
|
|
5233
5423
|
agent: opts.agent,
|
|
@@ -5494,16 +5684,19 @@ workflows.command("archive <idOrName>").action((idOrName) => {
|
|
|
5494
5684
|
store.close();
|
|
5495
5685
|
}
|
|
5496
5686
|
});
|
|
5497
|
-
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");
|
|
5498
5690
|
const store = new Store;
|
|
5499
5691
|
try {
|
|
5500
|
-
const loops = store.listLoops({ status: opts.status });
|
|
5692
|
+
const loops = store.listLoops({ status: opts.status, archived: opts.archived, includeArchived: opts.all });
|
|
5501
5693
|
if (isJson())
|
|
5502
5694
|
print(loops.map(publicLoop));
|
|
5503
5695
|
else {
|
|
5504
5696
|
for (const loop of loops) {
|
|
5505
5697
|
const machine = loop.machine ? ` machine=${loop.machine.id}` : "";
|
|
5506
|
-
|
|
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}`);
|
|
5507
5700
|
}
|
|
5508
5701
|
}
|
|
5509
5702
|
} finally {
|
|
@@ -5543,6 +5736,8 @@ function updateStatus(idOrName, status) {
|
|
|
5543
5736
|
const store = new Store;
|
|
5544
5737
|
try {
|
|
5545
5738
|
const loop = store.requireLoop(idOrName);
|
|
5739
|
+
if (loop.archivedAt)
|
|
5740
|
+
throw new Error(`loop is archived; run 'loops unarchive ${idOrName}' first`);
|
|
5546
5741
|
const updated = store.updateLoop(loop.id, { status, nextRunAt: status === "stopped" ? undefined : loop.nextRunAt });
|
|
5547
5742
|
print(publicLoop(updated), `${updated.id} ${updated.status}`);
|
|
5548
5743
|
} finally {
|
|
@@ -5558,10 +5753,30 @@ program.command("remove <idOrName>").alias("rm").action((idOrName) => {
|
|
|
5558
5753
|
store.close();
|
|
5559
5754
|
}
|
|
5560
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
|
+
});
|
|
5561
5774
|
program.command("run-now <idOrName>").option("--show-output", "show stdout/stderr").action(async (idOrName, opts) => {
|
|
5562
5775
|
const store = new Store;
|
|
5563
5776
|
try {
|
|
5564
5777
|
const loop = store.requireLoop(idOrName);
|
|
5778
|
+
if (loop.archivedAt)
|
|
5779
|
+
throw new Error(`loop is archived; run 'loops unarchive ${idOrName}' before running it`);
|
|
5565
5780
|
const runnerId = `manual:${process.pid}`;
|
|
5566
5781
|
const now = new Date;
|
|
5567
5782
|
let scheduledFor = manualRunScheduledFor(loop, now);
|