@hasna/loops 0.3.14 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/cli/index.js +590 -26
- package/dist/daemon/control.d.ts +1 -0
- package/dist/daemon/index.js +128 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +413 -15
- package/dist/lib/health.d.ts +70 -0
- package/dist/lib/store.d.ts +8 -1
- package/dist/lib/store.js +102 -5
- package/dist/lib/templates.d.ts +12 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +139 -7
- package/dist/types.d.ts +8 -0
- package/docs/USAGE.md +66 -6
- package/package.json +1 -1
package/dist/daemon/control.d.ts
CHANGED
package/dist/daemon/index.js
CHANGED
|
@@ -328,6 +328,17 @@ function optionalPositiveInteger(value, label) {
|
|
|
328
328
|
throw new Error(`${label} must be a positive integer`);
|
|
329
329
|
return value;
|
|
330
330
|
}
|
|
331
|
+
function optionalStringArray(value, label) {
|
|
332
|
+
if (value === undefined)
|
|
333
|
+
return;
|
|
334
|
+
if (!Array.isArray(value))
|
|
335
|
+
throw new Error(`${label} must be an array`);
|
|
336
|
+
const values = value.map((entry, index) => {
|
|
337
|
+
assertString(entry, `${label}[${index}]`);
|
|
338
|
+
return entry.trim();
|
|
339
|
+
}).filter(Boolean);
|
|
340
|
+
return values.length ? values : undefined;
|
|
341
|
+
}
|
|
331
342
|
function normalizeGoalSpec(value, label = "goal") {
|
|
332
343
|
if (value === undefined)
|
|
333
344
|
return;
|
|
@@ -399,6 +410,14 @@ function validateTarget(value, label) {
|
|
|
399
410
|
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
400
411
|
}
|
|
401
412
|
}
|
|
413
|
+
if (value.allowlist !== undefined) {
|
|
414
|
+
assertObject(value.allowlist, `${label}.allowlist`);
|
|
415
|
+
optionalStringArray(value.allowlist.tools, `${label}.allowlist.tools`);
|
|
416
|
+
optionalStringArray(value.allowlist.commands, `${label}.allowlist.commands`);
|
|
417
|
+
if (value.allowlist.enforcement !== undefined && value.allowlist.enforcement !== "metadata_only") {
|
|
418
|
+
throw new Error(`${label}.allowlist.enforcement must be metadata_only`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
402
421
|
return value;
|
|
403
422
|
}
|
|
404
423
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -482,6 +501,8 @@ function rowToLoop(row) {
|
|
|
482
501
|
name: row.name,
|
|
483
502
|
description: row.description ?? undefined,
|
|
484
503
|
status: row.status,
|
|
504
|
+
archivedAt: row.archived_at ?? undefined,
|
|
505
|
+
archivedFromStatus: row.archived_from_status ? row.archived_from_status : undefined,
|
|
485
506
|
schedule: JSON.parse(row.schedule_json),
|
|
486
507
|
target: JSON.parse(row.target_json),
|
|
487
508
|
goal: row.goal_json ? JSON.parse(row.goal_json) : undefined,
|
|
@@ -691,6 +712,8 @@ class Store {
|
|
|
691
712
|
name TEXT NOT NULL,
|
|
692
713
|
description TEXT,
|
|
693
714
|
status TEXT NOT NULL,
|
|
715
|
+
archived_at TEXT,
|
|
716
|
+
archived_from_status TEXT,
|
|
694
717
|
schedule_json TEXT NOT NULL,
|
|
695
718
|
target_json TEXT NOT NULL,
|
|
696
719
|
goal_json TEXT,
|
|
@@ -891,6 +914,8 @@ class Store {
|
|
|
891
914
|
`);
|
|
892
915
|
this.addColumnIfMissing("loops", "machine_json", "TEXT");
|
|
893
916
|
this.addColumnIfMissing("loops", "goal_json", "TEXT");
|
|
917
|
+
this.addColumnIfMissing("loops", "archived_at", "TEXT");
|
|
918
|
+
this.addColumnIfMissing("loops", "archived_from_status", "TEXT");
|
|
894
919
|
this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
|
|
895
920
|
this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
|
|
896
921
|
this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
|
|
@@ -899,6 +924,7 @@ class Store {
|
|
|
899
924
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
900
925
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
901
926
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
|
|
927
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
|
|
902
928
|
}
|
|
903
929
|
addColumnIfMissing(table, column, definition) {
|
|
904
930
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
@@ -975,12 +1001,26 @@ class Store {
|
|
|
975
1001
|
}
|
|
976
1002
|
listLoops(opts = {}) {
|
|
977
1003
|
const limit = opts.limit ?? 200;
|
|
978
|
-
|
|
1004
|
+
let rows;
|
|
1005
|
+
if (opts.status && opts.archived) {
|
|
1006
|
+
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);
|
|
1007
|
+
} else if (opts.status && opts.includeArchived) {
|
|
1008
|
+
rows = this.db.query("SELECT * FROM loops WHERE status = ? ORDER BY next_run_at ASC LIMIT ?").all(opts.status, limit);
|
|
1009
|
+
} else if (opts.status) {
|
|
1010
|
+
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);
|
|
1011
|
+
} else if (opts.archived) {
|
|
1012
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NOT NULL ORDER BY archived_at DESC LIMIT ?").all(limit);
|
|
1013
|
+
} else if (opts.includeArchived) {
|
|
1014
|
+
rows = this.db.query("SELECT * FROM loops ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
1015
|
+
} else {
|
|
1016
|
+
rows = this.db.query("SELECT * FROM loops WHERE archived_at IS NULL ORDER BY status ASC, next_run_at ASC LIMIT ?").all(limit);
|
|
1017
|
+
}
|
|
979
1018
|
return rows.map(rowToLoop);
|
|
980
1019
|
}
|
|
981
1020
|
dueLoops(now) {
|
|
982
1021
|
const rows = this.db.query(`SELECT * FROM loops
|
|
983
1022
|
WHERE status = 'active'
|
|
1023
|
+
AND archived_at IS NULL
|
|
984
1024
|
AND next_run_at IS NOT NULL
|
|
985
1025
|
AND next_run_at <= ?
|
|
986
1026
|
ORDER BY next_run_at ASC`).all(now.toISOString());
|
|
@@ -1012,6 +1052,44 @@ class Store {
|
|
|
1012
1052
|
throw new Error(`loop not found after update: ${id}`);
|
|
1013
1053
|
return after;
|
|
1014
1054
|
}
|
|
1055
|
+
archiveLoop(idOrName) {
|
|
1056
|
+
const loop = this.requireLoop(idOrName);
|
|
1057
|
+
if (loop.archivedAt)
|
|
1058
|
+
return loop;
|
|
1059
|
+
const updated = nowIso();
|
|
1060
|
+
const archivedStatus = loop.status === "active" ? "paused" : loop.status;
|
|
1061
|
+
this.db.query(`UPDATE loops
|
|
1062
|
+
SET status=$status, archived_at=$archivedAt, archived_from_status=$archivedFromStatus, updated_at=$updated
|
|
1063
|
+
WHERE id=$id`).run({
|
|
1064
|
+
$id: loop.id,
|
|
1065
|
+
$status: archivedStatus,
|
|
1066
|
+
$archivedAt: updated,
|
|
1067
|
+
$archivedFromStatus: loop.status,
|
|
1068
|
+
$updated: updated
|
|
1069
|
+
});
|
|
1070
|
+
const archived = this.getLoop(loop.id);
|
|
1071
|
+
if (!archived)
|
|
1072
|
+
throw new Error(`loop not found after archive: ${loop.id}`);
|
|
1073
|
+
return archived;
|
|
1074
|
+
}
|
|
1075
|
+
unarchiveLoop(idOrName) {
|
|
1076
|
+
const loop = this.requireLoop(idOrName);
|
|
1077
|
+
if (!loop.archivedAt)
|
|
1078
|
+
return loop;
|
|
1079
|
+
const updated = nowIso();
|
|
1080
|
+
const restoredStatus = loop.archivedFromStatus ?? loop.status;
|
|
1081
|
+
this.db.query(`UPDATE loops
|
|
1082
|
+
SET status=$status, archived_at=NULL, archived_from_status=NULL, updated_at=$updated
|
|
1083
|
+
WHERE id=$id`).run({
|
|
1084
|
+
$id: loop.id,
|
|
1085
|
+
$status: restoredStatus,
|
|
1086
|
+
$updated: updated
|
|
1087
|
+
});
|
|
1088
|
+
const unarchived = this.getLoop(loop.id);
|
|
1089
|
+
if (!unarchived)
|
|
1090
|
+
throw new Error(`loop not found after unarchive: ${loop.id}`);
|
|
1091
|
+
return unarchived;
|
|
1092
|
+
}
|
|
1015
1093
|
deleteLoop(idOrName) {
|
|
1016
1094
|
const loop = this.requireLoop(idOrName);
|
|
1017
1095
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
@@ -1786,10 +1864,16 @@ class Store {
|
|
|
1786
1864
|
}
|
|
1787
1865
|
claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
|
|
1788
1866
|
const startedAt = now.toISOString();
|
|
1789
|
-
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
1790
1867
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1791
1868
|
try {
|
|
1792
1869
|
this.assertDaemonLeaseFence(opts, startedAt);
|
|
1870
|
+
const currentLoop = this.getLoop(loop.id);
|
|
1871
|
+
if (!currentLoop || currentLoop.archivedAt) {
|
|
1872
|
+
this.db.exec("COMMIT");
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
loop = currentLoop;
|
|
1876
|
+
const leaseExpiresAt = new Date(now.getTime() + loop.leaseMs).toISOString();
|
|
1793
1877
|
const existing = this.getRunBySlot(loop.id, scheduledFor);
|
|
1794
1878
|
if (existing) {
|
|
1795
1879
|
if (existing.status === "running") {
|
|
@@ -2007,7 +2091,7 @@ class Store {
|
|
|
2007
2091
|
return recovered;
|
|
2008
2092
|
}
|
|
2009
2093
|
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());
|
|
2094
|
+
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
2095
|
const expired = [];
|
|
2012
2096
|
for (const row of rows) {
|
|
2013
2097
|
const updated = this.updateLoop(row.id, { status: "expired", nextRunAt: undefined }, opts);
|
|
@@ -2016,8 +2100,21 @@ class Store {
|
|
|
2016
2100
|
}
|
|
2017
2101
|
return expired;
|
|
2018
2102
|
}
|
|
2019
|
-
countLoops(status) {
|
|
2020
|
-
|
|
2103
|
+
countLoops(status, opts = {}) {
|
|
2104
|
+
let row;
|
|
2105
|
+
if (status && opts.archived) {
|
|
2106
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NOT NULL").get(status);
|
|
2107
|
+
} else if (status && opts.includeArchived) {
|
|
2108
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ?").get(status);
|
|
2109
|
+
} else if (status) {
|
|
2110
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE status = ? AND archived_at IS NULL").get(status);
|
|
2111
|
+
} else if (opts.archived) {
|
|
2112
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NOT NULL").get();
|
|
2113
|
+
} else if (opts.includeArchived) {
|
|
2114
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops").get();
|
|
2115
|
+
} else {
|
|
2116
|
+
row = this.db.query("SELECT COUNT(*) AS count FROM loops WHERE archived_at IS NULL").get();
|
|
2117
|
+
}
|
|
2021
2118
|
return row?.count ?? 0;
|
|
2022
2119
|
}
|
|
2023
2120
|
countRuns(status) {
|
|
@@ -2387,6 +2484,16 @@ function metadataEnv(metadata) {
|
|
|
2387
2484
|
env.LOOPS_GOAL_NODE_KEY = metadata.goalNodeKey;
|
|
2388
2485
|
return env;
|
|
2389
2486
|
}
|
|
2487
|
+
function allowlistEnv(allowlist) {
|
|
2488
|
+
const env = {};
|
|
2489
|
+
if (allowlist?.tools?.length)
|
|
2490
|
+
env.LOOPS_AGENT_ALLOWED_TOOLS = allowlist.tools.join(",");
|
|
2491
|
+
if (allowlist?.commands?.length)
|
|
2492
|
+
env.LOOPS_AGENT_ALLOWED_COMMANDS = allowlist.commands.join(",");
|
|
2493
|
+
if (allowlist?.tools?.length || allowlist?.commands?.length)
|
|
2494
|
+
env.LOOPS_AGENT_ALLOWLIST_ENFORCEMENT = "metadata_only";
|
|
2495
|
+
return env;
|
|
2496
|
+
}
|
|
2390
2497
|
function providerCommand(provider) {
|
|
2391
2498
|
switch (provider) {
|
|
2392
2499
|
case "claude":
|
|
@@ -2594,7 +2701,8 @@ function commandSpec(target) {
|
|
|
2594
2701
|
account: agentTarget.account,
|
|
2595
2702
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2596
2703
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2597
|
-
stdin: agentTarget.prompt
|
|
2704
|
+
stdin: agentTarget.prompt,
|
|
2705
|
+
allowlist: agentTarget.allowlist
|
|
2598
2706
|
};
|
|
2599
2707
|
}
|
|
2600
2708
|
function executionEnv(spec, metadata, opts) {
|
|
@@ -2606,6 +2714,7 @@ function executionEnv(spec, metadata, opts) {
|
|
|
2606
2714
|
Object.assign(env, accountEnv);
|
|
2607
2715
|
}
|
|
2608
2716
|
Object.assign(env, spec.env ?? {});
|
|
2717
|
+
Object.assign(env, allowlistEnv(spec.allowlist));
|
|
2609
2718
|
env.PATH = normalizeExecutionPath(env);
|
|
2610
2719
|
Object.assign(env, metadataEnv(metadata));
|
|
2611
2720
|
return env;
|
|
@@ -2644,6 +2753,9 @@ function remoteBootstrapLines(spec, metadata) {
|
|
|
2644
2753
|
continue;
|
|
2645
2754
|
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2646
2755
|
}
|
|
2756
|
+
for (const [key, value] of Object.entries(allowlistEnv(spec.allowlist))) {
|
|
2757
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
2758
|
+
}
|
|
2647
2759
|
return lines;
|
|
2648
2760
|
}
|
|
2649
2761
|
function remoteScript(spec, metadata) {
|
|
@@ -3597,12 +3709,16 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
3597
3709
|
|
|
3598
3710
|
// src/lib/scheduler.ts
|
|
3599
3711
|
function manualRunScheduledFor(loop, now = new Date) {
|
|
3712
|
+
if (loop.archivedAt)
|
|
3713
|
+
return now.toISOString();
|
|
3600
3714
|
if (loop.status === "active" && loop.nextRunAt && new Date(loop.nextRunAt).getTime() <= now.getTime()) {
|
|
3601
3715
|
return loop.retryScheduledFor ?? loop.nextRunAt;
|
|
3602
3716
|
}
|
|
3603
3717
|
return now.toISOString();
|
|
3604
3718
|
}
|
|
3605
3719
|
function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
3720
|
+
if (loop.archivedAt)
|
|
3721
|
+
return false;
|
|
3606
3722
|
if (loop.status !== "active")
|
|
3607
3723
|
return false;
|
|
3608
3724
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3610,6 +3726,8 @@ function shouldAdvanceManualRun(loop, scheduledFor, now = new Date) {
|
|
|
3610
3726
|
return scheduledFor === (loop.retryScheduledFor ?? loop.nextRunAt);
|
|
3611
3727
|
}
|
|
3612
3728
|
function manualRunSource(loop, scheduledFor, now = new Date) {
|
|
3729
|
+
if (loop.archivedAt)
|
|
3730
|
+
return "ad_hoc";
|
|
3613
3731
|
if (loop.status !== "active")
|
|
3614
3732
|
return "ad_hoc";
|
|
3615
3733
|
if (!loop.nextRunAt || new Date(loop.nextRunAt).getTime() > now.getTime())
|
|
@@ -3628,7 +3746,7 @@ function advanceLoop(store, loop, run, finishedAt, succeeded, opts = {}) {
|
|
|
3628
3746
|
if (run.status === "running")
|
|
3629
3747
|
return;
|
|
3630
3748
|
const current = store.getLoop(loop.id);
|
|
3631
|
-
if (!current || current.status !== "active")
|
|
3749
|
+
if (!current || current.status !== "active" || current.archivedAt)
|
|
3632
3750
|
return;
|
|
3633
3751
|
if (current.retryScheduledFor && current.retryScheduledFor !== run.scheduledFor)
|
|
3634
3752
|
return;
|
|
@@ -3963,7 +4081,8 @@ function daemonStatus(store, path = pidFilePath()) {
|
|
|
3963
4081
|
active: store.countLoops("active"),
|
|
3964
4082
|
paused: store.countLoops("paused"),
|
|
3965
4083
|
stopped: store.countLoops("stopped"),
|
|
3966
|
-
expired: store.countLoops("expired")
|
|
4084
|
+
expired: store.countLoops("expired"),
|
|
4085
|
+
archived: store.countLoops(undefined, { archived: true })
|
|
3967
4086
|
},
|
|
3968
4087
|
runs: {
|
|
3969
4088
|
total: store.countRuns(),
|
|
@@ -4276,7 +4395,7 @@ function enableStartup(result) {
|
|
|
4276
4395
|
// package.json
|
|
4277
4396
|
var package_default = {
|
|
4278
4397
|
name: "@hasna/loops",
|
|
4279
|
-
version: "0.3.
|
|
4398
|
+
version: "0.3.16",
|
|
4280
4399
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4281
4400
|
type: "module",
|
|
4282
4401
|
main: "dist/index.js",
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { executeWorkflow, executeLoopTarget, preflightWorkflow } from "./lib/wor
|
|
|
10
10
|
export { workflowExecutionOrder, workflowBodyFromJson } from "./lib/workflow-spec.js";
|
|
11
11
|
export { EVENT_WORKER_VERIFIER_TEMPLATE_ID, TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID, getLoopTemplate, listLoopTemplates, renderEventWorkerVerifierWorkflow, renderLoopTemplate, renderTodosTaskWorkerVerifierWorkflow, } from "./lib/templates.js";
|
|
12
12
|
export { runDoctor } from "./lib/doctor.js";
|
|
13
|
+
export { buildHealthReport, classifyRunFailure, expectationForLoop } from "./lib/health.js";
|
|
13
14
|
export { runGoal } from "./lib/goal/runner.js";
|
|
14
15
|
export { resolveGoalModel } from "./lib/goal/model-factory.js";
|
|
15
16
|
export { isTerminal as isGoalTerminal, readyNodeKeys, rollupSummary } from "./lib/goal/status.js";
|