@hasna/loops 0.4.0 → 0.4.2
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/CHANGELOG.md +18 -0
- package/README.md +75 -33
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.js +1290 -0
- package/dist/cli/index.js +738 -201
- package/dist/daemon/index.js +64 -15
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1005 -155
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +276 -0
- package/dist/lib/route/todos-cli.d.ts +1 -0
- package/dist/lib/route/types.d.ts +10 -0
- package/dist/lib/storage/contract.d.ts +133 -0
- package/dist/lib/storage/contract.js +1 -0
- package/dist/lib/storage/index.d.ts +6 -0
- package/dist/lib/storage/index.js +4612 -0
- package/dist/lib/storage/postgres-schema.d.ts +4 -0
- package/dist/lib/storage/postgres-schema.js +328 -0
- package/dist/lib/storage/postgres.d.ts +22 -0
- package/dist/lib/storage/postgres.js +423 -0
- package/dist/lib/storage/sqlite.d.ts +84 -0
- package/dist/lib/storage/sqlite.js +4187 -0
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +27 -10
- package/dist/lib/template-kit.d.ts +10 -8
- package/dist/lib/templates.d.ts +1 -0
- package/dist/mcp/index.js +64 -15
- package/dist/runner/index.d.ts +32 -0
- package/dist/runner/index.js +2037 -0
- package/dist/sdk/index.js +33 -15
- package/dist/types.d.ts +1 -1
- package/docs/DEPLOYMENT_MODES.md +148 -0
- package/docs/USAGE.md +66 -32
- package/package.json +34 -3
package/dist/index.js
CHANGED
|
@@ -1203,7 +1203,7 @@ function discardWorkflowRunManifest(staged) {
|
|
|
1203
1203
|
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
1204
1204
|
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
1205
1205
|
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
1206
|
-
var SCHEMA_USER_VERSION =
|
|
1206
|
+
var SCHEMA_USER_VERSION = 7;
|
|
1207
1207
|
var TERMINAL_RUN_STATUSES = ["succeeded", "failed", "timed_out", "abandoned", "skipped"];
|
|
1208
1208
|
var PRUNE_BATCH_SIZE = 400;
|
|
1209
1209
|
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
|
|
@@ -1573,6 +1573,13 @@ class Store {
|
|
|
1573
1573
|
this.addColumnIfMissing("loop_runs", "pgid", "INTEGER");
|
|
1574
1574
|
this.addColumnIfMissing("loop_runs", "process_started_at", "TEXT");
|
|
1575
1575
|
}
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
id: "0007_run_claim_tokens",
|
|
1579
|
+
apply: () => {
|
|
1580
|
+
this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
|
|
1581
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
|
|
1582
|
+
}
|
|
1576
1583
|
}
|
|
1577
1584
|
];
|
|
1578
1585
|
}
|
|
@@ -1614,6 +1621,7 @@ class Store {
|
|
|
1614
1621
|
started_at TEXT,
|
|
1615
1622
|
finished_at TEXT,
|
|
1616
1623
|
claimed_by TEXT,
|
|
1624
|
+
claim_token TEXT,
|
|
1617
1625
|
lease_expires_at TEXT,
|
|
1618
1626
|
pid INTEGER,
|
|
1619
1627
|
pgid INTEGER,
|
|
@@ -1632,6 +1640,7 @@ class Store {
|
|
|
1632
1640
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
1633
1641
|
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
1634
1642
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
1643
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
1635
1644
|
|
|
1636
1645
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
1637
1646
|
id TEXT PRIMARY KEY,
|
|
@@ -3503,6 +3512,7 @@ class Store {
|
|
|
3503
3512
|
}
|
|
3504
3513
|
claimRun(loop, scheduledFor, runnerId, now = new Date, opts = {}) {
|
|
3505
3514
|
const startedAt = now.toISOString();
|
|
3515
|
+
const claimToken = opts.claimToken ?? genId();
|
|
3506
3516
|
this.db.exec("BEGIN IMMEDIATE");
|
|
3507
3517
|
try {
|
|
3508
3518
|
this.assertDaemonLeaseFence(opts, startedAt);
|
|
@@ -3525,12 +3535,13 @@ class Store {
|
|
|
3525
3535
|
return;
|
|
3526
3536
|
}
|
|
3527
3537
|
const res3 = this.db.query(`UPDATE loop_runs SET status='running', started_at=$started, finished_at=NULL,
|
|
3528
|
-
claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
|
|
3538
|
+
claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
|
|
3529
3539
|
duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
|
|
3530
3540
|
WHERE id=$id AND status='running' AND lease_expires_at <= $now`).run({
|
|
3531
3541
|
$id: existing.id,
|
|
3532
3542
|
$started: startedAt,
|
|
3533
3543
|
$claimedBy: runnerId,
|
|
3544
|
+
$claimToken: claimToken,
|
|
3534
3545
|
$lease: leaseExpiresAt,
|
|
3535
3546
|
$updated: startedAt,
|
|
3536
3547
|
$now: startedAt
|
|
@@ -3539,7 +3550,7 @@ class Store {
|
|
|
3539
3550
|
if (res3.changes !== 1)
|
|
3540
3551
|
return;
|
|
3541
3552
|
const run3 = this.getRun(existing.id);
|
|
3542
|
-
return run3 ? { run: run3, loop } : undefined;
|
|
3553
|
+
return run3 ? { run: run3, loop, claimToken } : undefined;
|
|
3543
3554
|
}
|
|
3544
3555
|
if (existing.status === "succeeded" || existing.status === "skipped") {
|
|
3545
3556
|
this.db.exec("COMMIT");
|
|
@@ -3547,7 +3558,7 @@ class Store {
|
|
|
3547
3558
|
}
|
|
3548
3559
|
const attempt = existing.attempt + 1;
|
|
3549
3560
|
const res2 = this.db.query(`UPDATE loop_runs SET attempt=$attempt, status='running', started_at=$started, finished_at=NULL,
|
|
3550
|
-
claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
|
|
3561
|
+
claimed_by=$claimedBy, claim_token=$claimToken, lease_expires_at=$lease, pid=NULL, pgid=NULL, process_started_at=NULL, exit_code=NULL,
|
|
3551
3562
|
duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
|
|
3552
3563
|
WHERE id=$id
|
|
3553
3564
|
AND status IN ('failed', 'timed_out', 'abandoned')
|
|
@@ -3556,6 +3567,7 @@ class Store {
|
|
|
3556
3567
|
$attempt: attempt,
|
|
3557
3568
|
$started: startedAt,
|
|
3558
3569
|
$claimedBy: runnerId,
|
|
3570
|
+
$claimToken: claimToken,
|
|
3559
3571
|
$lease: leaseExpiresAt,
|
|
3560
3572
|
$updated: startedAt,
|
|
3561
3573
|
$maxAttempts: loop.maxAttempts
|
|
@@ -3564,12 +3576,12 @@ class Store {
|
|
|
3564
3576
|
if (res2.changes !== 1)
|
|
3565
3577
|
return;
|
|
3566
3578
|
const run2 = this.getRun(existing.id);
|
|
3567
|
-
return run2 ? { run: run2, loop } : undefined;
|
|
3579
|
+
return run2 ? { run: run2, loop, claimToken } : undefined;
|
|
3568
3580
|
}
|
|
3569
3581
|
const id = genId();
|
|
3570
3582
|
const res = this.db.query(`INSERT OR IGNORE INTO loop_runs (id, loop_id, loop_name, scheduled_for, attempt, status, started_at, finished_at,
|
|
3571
|
-
claimed_by, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
|
|
3572
|
-
VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $lease,
|
|
3583
|
+
claimed_by, claim_token, lease_expires_at, pid, exit_code, duration_ms, stdout, stderr, error, created_at, updated_at)
|
|
3584
|
+
VALUES ($id, $loopId, $loopName, $scheduledFor, 1, 'running', $started, NULL, $claimedBy, $claimToken, $lease,
|
|
3573
3585
|
NULL, NULL, NULL, NULL, NULL, NULL, $created, $updated)`).run({
|
|
3574
3586
|
$id: id,
|
|
3575
3587
|
$loopId: loop.id,
|
|
@@ -3577,6 +3589,7 @@ class Store {
|
|
|
3577
3589
|
$scheduledFor: scheduledFor,
|
|
3578
3590
|
$started: startedAt,
|
|
3579
3591
|
$claimedBy: runnerId,
|
|
3592
|
+
$claimToken: claimToken,
|
|
3580
3593
|
$lease: leaseExpiresAt,
|
|
3581
3594
|
$created: startedAt,
|
|
3582
3595
|
$updated: startedAt
|
|
@@ -3585,7 +3598,7 @@ class Store {
|
|
|
3585
3598
|
if (res.changes !== 1)
|
|
3586
3599
|
return;
|
|
3587
3600
|
const run = this.getRun(id);
|
|
3588
|
-
return run ? { run, loop } : undefined;
|
|
3601
|
+
return run ? { run, loop, claimToken } : undefined;
|
|
3589
3602
|
} catch (error) {
|
|
3590
3603
|
try {
|
|
3591
3604
|
this.db.exec("ROLLBACK");
|
|
@@ -3608,16 +3621,18 @@ class Store {
|
|
|
3608
3621
|
$error: error ?? null,
|
|
3609
3622
|
$updated: finishedAt,
|
|
3610
3623
|
$claimedBy: opts.claimedBy ?? null,
|
|
3624
|
+
$claimToken: opts.claimToken ?? null,
|
|
3611
3625
|
$now: (opts.now ?? new Date).toISOString(),
|
|
3612
3626
|
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
3613
3627
|
};
|
|
3614
3628
|
return this.transact(() => {
|
|
3615
|
-
const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3629
|
+
const res = opts.claimedBy ? this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3616
3630
|
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
|
|
3617
3631
|
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
|
|
3632
|
+
AND ($claimToken IS NULL OR claim_token=$claimToken)
|
|
3618
3633
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3619
3634
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3620
|
-
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3635
|
+
))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
|
|
3621
3636
|
duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
|
|
3622
3637
|
const run = this.getRun(id);
|
|
3623
3638
|
if (!run)
|
|
@@ -3645,11 +3660,13 @@ class Store {
|
|
|
3645
3660
|
const expiresAt = new Date(now.getTime() + leaseMs).toISOString();
|
|
3646
3661
|
const res = this.db.query(`UPDATE loop_runs SET lease_expires_at=$expires, updated_at=$updated
|
|
3647
3662
|
WHERE id=$id AND status='running' AND claimed_by=$claimedBy AND lease_expires_at > $now
|
|
3663
|
+
AND ($claimToken IS NULL OR claim_token=$claimToken)
|
|
3648
3664
|
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
3649
3665
|
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
3650
3666
|
))`).run({
|
|
3651
3667
|
$id: id,
|
|
3652
3668
|
$claimedBy: claimedBy,
|
|
3669
|
+
$claimToken: opts.claimToken ?? null,
|
|
3653
3670
|
$expires: expiresAt,
|
|
3654
3671
|
$updated: now.toISOString(),
|
|
3655
3672
|
$now: now.toISOString(),
|
|
@@ -3962,6 +3979,273 @@ class Store {
|
|
|
3962
3979
|
this.db.close();
|
|
3963
3980
|
}
|
|
3964
3981
|
}
|
|
3982
|
+
// package.json
|
|
3983
|
+
var package_default = {
|
|
3984
|
+
name: "@hasna/loops",
|
|
3985
|
+
version: "0.4.2",
|
|
3986
|
+
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
3987
|
+
type: "module",
|
|
3988
|
+
main: "dist/index.js",
|
|
3989
|
+
types: "dist/index.d.ts",
|
|
3990
|
+
bin: {
|
|
3991
|
+
loops: "dist/cli/index.js",
|
|
3992
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
3993
|
+
"loops-api": "dist/api/index.js",
|
|
3994
|
+
"loops-runner": "dist/runner/index.js",
|
|
3995
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
3996
|
+
},
|
|
3997
|
+
exports: {
|
|
3998
|
+
".": {
|
|
3999
|
+
types: "./dist/index.d.ts",
|
|
4000
|
+
import: "./dist/index.js"
|
|
4001
|
+
},
|
|
4002
|
+
"./sdk": {
|
|
4003
|
+
types: "./dist/sdk/index.d.ts",
|
|
4004
|
+
import: "./dist/sdk/index.js"
|
|
4005
|
+
},
|
|
4006
|
+
"./mcp": {
|
|
4007
|
+
types: "./dist/mcp/index.d.ts",
|
|
4008
|
+
import: "./dist/mcp/index.js"
|
|
4009
|
+
},
|
|
4010
|
+
"./api": {
|
|
4011
|
+
types: "./dist/api/index.d.ts",
|
|
4012
|
+
import: "./dist/api/index.js"
|
|
4013
|
+
},
|
|
4014
|
+
"./runner": {
|
|
4015
|
+
types: "./dist/runner/index.d.ts",
|
|
4016
|
+
import: "./dist/runner/index.js"
|
|
4017
|
+
},
|
|
4018
|
+
"./mode": {
|
|
4019
|
+
types: "./dist/lib/mode.d.ts",
|
|
4020
|
+
import: "./dist/lib/mode.js"
|
|
4021
|
+
},
|
|
4022
|
+
"./storage": {
|
|
4023
|
+
types: "./dist/lib/store.d.ts",
|
|
4024
|
+
import: "./dist/lib/store.js"
|
|
4025
|
+
},
|
|
4026
|
+
"./storage/contract": {
|
|
4027
|
+
types: "./dist/lib/storage/contract.d.ts",
|
|
4028
|
+
import: "./dist/lib/storage/contract.js"
|
|
4029
|
+
},
|
|
4030
|
+
"./storage/sqlite": {
|
|
4031
|
+
types: "./dist/lib/storage/sqlite.d.ts",
|
|
4032
|
+
import: "./dist/lib/storage/sqlite.js"
|
|
4033
|
+
},
|
|
4034
|
+
"./storage/postgres": {
|
|
4035
|
+
types: "./dist/lib/storage/postgres.d.ts",
|
|
4036
|
+
import: "./dist/lib/storage/postgres.js"
|
|
4037
|
+
},
|
|
4038
|
+
"./storage/postgres-schema": {
|
|
4039
|
+
types: "./dist/lib/storage/postgres-schema.d.ts",
|
|
4040
|
+
import: "./dist/lib/storage/postgres-schema.js"
|
|
4041
|
+
}
|
|
4042
|
+
},
|
|
4043
|
+
files: [
|
|
4044
|
+
"dist",
|
|
4045
|
+
"README.md",
|
|
4046
|
+
"CHANGELOG.md",
|
|
4047
|
+
"docs",
|
|
4048
|
+
"LICENSE"
|
|
4049
|
+
],
|
|
4050
|
+
scripts: {
|
|
4051
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
4052
|
+
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
4053
|
+
typecheck: "tsc --noEmit",
|
|
4054
|
+
test: "bun test",
|
|
4055
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
4056
|
+
prepare: "test -d dist || bun run build",
|
|
4057
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
4058
|
+
"dev:daemon": "bun run src/daemon/index.ts",
|
|
4059
|
+
prepublishOnly: "bun run build && bun run test:boundary"
|
|
4060
|
+
},
|
|
4061
|
+
keywords: [
|
|
4062
|
+
"loops",
|
|
4063
|
+
"scheduler",
|
|
4064
|
+
"daemon",
|
|
4065
|
+
"agents",
|
|
4066
|
+
"claude",
|
|
4067
|
+
"cursor",
|
|
4068
|
+
"codewith",
|
|
4069
|
+
"opencode",
|
|
4070
|
+
"cli"
|
|
4071
|
+
],
|
|
4072
|
+
repository: {
|
|
4073
|
+
type: "git",
|
|
4074
|
+
url: "git+https://github.com/hasna/loops.git"
|
|
4075
|
+
},
|
|
4076
|
+
homepage: "https://github.com/hasna/loops#readme",
|
|
4077
|
+
bugs: {
|
|
4078
|
+
url: "https://github.com/hasna/loops/issues"
|
|
4079
|
+
},
|
|
4080
|
+
author: "Hasna <andrei@hasna.com>",
|
|
4081
|
+
license: "Apache-2.0",
|
|
4082
|
+
engines: {
|
|
4083
|
+
bun: ">=1.0.0"
|
|
4084
|
+
},
|
|
4085
|
+
dependencies: {
|
|
4086
|
+
"@hasna/events": "^0.1.9",
|
|
4087
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4088
|
+
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4089
|
+
ai: "6.0.204",
|
|
4090
|
+
commander: "^13.1.0",
|
|
4091
|
+
zod: "4.4.3"
|
|
4092
|
+
},
|
|
4093
|
+
optionalDependencies: {
|
|
4094
|
+
"@hasna/machines": "0.0.49"
|
|
4095
|
+
},
|
|
4096
|
+
devDependencies: {
|
|
4097
|
+
"@types/bun": "latest",
|
|
4098
|
+
typescript: "^5.7.3"
|
|
4099
|
+
},
|
|
4100
|
+
publishConfig: {
|
|
4101
|
+
registry: "https://registry.npmjs.org",
|
|
4102
|
+
access: "public"
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
|
|
4106
|
+
// src/lib/version.ts
|
|
4107
|
+
function packageVersion() {
|
|
4108
|
+
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
4109
|
+
throw new Error("package.json version is missing");
|
|
4110
|
+
return package_default.version;
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// src/lib/mode.ts
|
|
4114
|
+
var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
|
|
4115
|
+
var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
|
|
4116
|
+
var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
|
|
4117
|
+
var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
|
|
4118
|
+
var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
|
|
4119
|
+
var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
|
|
4120
|
+
var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
|
|
4121
|
+
var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
|
|
4122
|
+
function envValue(env, keys) {
|
|
4123
|
+
for (const key of keys) {
|
|
4124
|
+
const value = env[key]?.trim();
|
|
4125
|
+
if (value)
|
|
4126
|
+
return { key, value };
|
|
4127
|
+
}
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
function normalizeLoopDeploymentMode(value) {
|
|
4131
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
4132
|
+
if (normalized === "local")
|
|
4133
|
+
return "local";
|
|
4134
|
+
if (normalized === "self_hosted" || normalized === "selfhosted")
|
|
4135
|
+
return "self_hosted";
|
|
4136
|
+
if (normalized === "cloud" || normalized === "saas")
|
|
4137
|
+
return "cloud";
|
|
4138
|
+
throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
|
|
4139
|
+
}
|
|
4140
|
+
function resolveLoopDeploymentMode(env = process.env) {
|
|
4141
|
+
const explicitMode = envValue(env, MODE_ENV_KEYS);
|
|
4142
|
+
if (explicitMode) {
|
|
4143
|
+
return {
|
|
4144
|
+
deploymentMode: normalizeLoopDeploymentMode(explicitMode.value),
|
|
4145
|
+
source: explicitMode.key
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
|
|
4149
|
+
if (cloudApiUrl)
|
|
4150
|
+
return { deploymentMode: "cloud", source: cloudApiUrl.key };
|
|
4151
|
+
const apiUrl = envValue(env, API_URL_ENV_KEYS);
|
|
4152
|
+
if (apiUrl)
|
|
4153
|
+
return { deploymentMode: "self_hosted", source: apiUrl.key };
|
|
4154
|
+
const databaseUrl = envValue(env, DATABASE_URL_ENV_KEYS);
|
|
4155
|
+
if (databaseUrl)
|
|
4156
|
+
return { deploymentMode: "self_hosted", source: databaseUrl.key };
|
|
4157
|
+
return { deploymentMode: "local", source: "default" };
|
|
4158
|
+
}
|
|
4159
|
+
function loopControlPlaneConfig(env = process.env) {
|
|
4160
|
+
return {
|
|
4161
|
+
apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
|
|
4162
|
+
cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
|
|
4163
|
+
databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
|
|
4164
|
+
apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
|
|
4165
|
+
cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
|
|
4166
|
+
authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
function sourceOfTruthForMode(mode) {
|
|
4170
|
+
if (mode === "local")
|
|
4171
|
+
return "local_sqlite";
|
|
4172
|
+
if (mode === "self_hosted")
|
|
4173
|
+
return "self_hosted_control_plane";
|
|
4174
|
+
return "cloud_control_plane";
|
|
4175
|
+
}
|
|
4176
|
+
function displayControlPlaneUrl(value) {
|
|
4177
|
+
if (!value)
|
|
4178
|
+
return;
|
|
4179
|
+
try {
|
|
4180
|
+
const url = new URL(value);
|
|
4181
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/g, "");
|
|
4182
|
+
return `${url.origin}${path}`;
|
|
4183
|
+
} catch {
|
|
4184
|
+
return "[invalid-url]";
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
function buildDeploymentStatus(opts = {}) {
|
|
4188
|
+
const env = opts.env ?? process.env;
|
|
4189
|
+
const active = resolveLoopDeploymentMode(env);
|
|
4190
|
+
const deploymentMode = opts.perspective ?? active.deploymentMode;
|
|
4191
|
+
const config = loopControlPlaneConfig(env);
|
|
4192
|
+
const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
|
|
4193
|
+
const apiUrl = displayControlPlaneUrl(rawApiUrl);
|
|
4194
|
+
const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
|
|
4195
|
+
const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
|
|
4196
|
+
const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
|
|
4197
|
+
const warnings = [];
|
|
4198
|
+
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
4199
|
+
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
4200
|
+
}
|
|
4201
|
+
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
4202
|
+
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
4203
|
+
}
|
|
4204
|
+
if (deploymentMode === "cloud" && !config.cloudApiUrl) {
|
|
4205
|
+
warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
|
|
4206
|
+
}
|
|
4207
|
+
if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
|
|
4208
|
+
warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
|
|
4209
|
+
}
|
|
4210
|
+
if (opts.perspective && opts.perspective !== active.deploymentMode) {
|
|
4211
|
+
warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
|
|
4212
|
+
}
|
|
4213
|
+
return {
|
|
4214
|
+
packageVersion: packageVersion(),
|
|
4215
|
+
deploymentMode,
|
|
4216
|
+
activeDeploymentMode: active.deploymentMode,
|
|
4217
|
+
active: deploymentMode === active.deploymentMode,
|
|
4218
|
+
deploymentModeSource: active.source,
|
|
4219
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
4220
|
+
localStore: {
|
|
4221
|
+
role: deploymentMode === "local" ? "authoritative" : "cache_and_spool"
|
|
4222
|
+
},
|
|
4223
|
+
controlPlane: {
|
|
4224
|
+
kind: controlPlaneKind,
|
|
4225
|
+
configured: controlPlaneConfigured,
|
|
4226
|
+
apiUrl,
|
|
4227
|
+
databaseUrlPresent: config.databaseUrlPresent,
|
|
4228
|
+
authTokenPresent: deploymentAuthTokenPresent
|
|
4229
|
+
},
|
|
4230
|
+
runner: {
|
|
4231
|
+
required: deploymentMode !== "local",
|
|
4232
|
+
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
4233
|
+
},
|
|
4234
|
+
warnings
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
function deploymentStatusLine(status) {
|
|
4238
|
+
const configured = status.controlPlane.kind === "none" ? "none" : String(status.controlPlane.configured);
|
|
4239
|
+
const active = status.active ? "active" : `active=${status.activeDeploymentMode}`;
|
|
4240
|
+
return [
|
|
4241
|
+
`deploymentMode=${status.deploymentMode}`,
|
|
4242
|
+
active,
|
|
4243
|
+
`source=${status.deploymentModeSource}`,
|
|
4244
|
+
`truth=${status.sourceOfTruth}`,
|
|
4245
|
+
`local=${status.localStore.role}`,
|
|
4246
|
+
`control_plane=${configured}`
|
|
4247
|
+
].join(" ");
|
|
4248
|
+
}
|
|
3965
4249
|
|
|
3966
4250
|
// src/mcp/index.ts
|
|
3967
4251
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -5545,11 +5829,12 @@ function publicLoop(loop) {
|
|
|
5545
5829
|
target
|
|
5546
5830
|
};
|
|
5547
5831
|
}
|
|
5548
|
-
function publicRun(run, showOutput = false) {
|
|
5832
|
+
function publicRun(run, showOutput = false, opts = {}) {
|
|
5549
5833
|
return {
|
|
5550
5834
|
...run,
|
|
5551
5835
|
stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
|
|
5552
|
-
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined
|
|
5836
|
+
stderr: showOutput ? run.stderr : run.stderr ? `[redacted ${run.stderr.length} chars]` : undefined,
|
|
5837
|
+
error: opts.redactError ? redact(run.error) : run.error
|
|
5553
5838
|
};
|
|
5554
5839
|
}
|
|
5555
5840
|
function publicExecutorResult(result, showOutput = false) {
|
|
@@ -7323,105 +7608,6 @@ async function tick(deps) {
|
|
|
7323
7608
|
}
|
|
7324
7609
|
return { claimed, completed, skipped, recovered, expired };
|
|
7325
7610
|
}
|
|
7326
|
-
// package.json
|
|
7327
|
-
var package_default = {
|
|
7328
|
-
name: "@hasna/loops",
|
|
7329
|
-
version: "0.4.0",
|
|
7330
|
-
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7331
|
-
type: "module",
|
|
7332
|
-
main: "dist/index.js",
|
|
7333
|
-
types: "dist/index.d.ts",
|
|
7334
|
-
bin: {
|
|
7335
|
-
loops: "dist/cli/index.js",
|
|
7336
|
-
"loops-daemon": "dist/daemon/index.js",
|
|
7337
|
-
"loops-mcp": "dist/mcp/index.js"
|
|
7338
|
-
},
|
|
7339
|
-
exports: {
|
|
7340
|
-
".": {
|
|
7341
|
-
types: "./dist/index.d.ts",
|
|
7342
|
-
import: "./dist/index.js"
|
|
7343
|
-
},
|
|
7344
|
-
"./sdk": {
|
|
7345
|
-
types: "./dist/sdk/index.d.ts",
|
|
7346
|
-
import: "./dist/sdk/index.js"
|
|
7347
|
-
},
|
|
7348
|
-
"./mcp": {
|
|
7349
|
-
types: "./dist/mcp/index.d.ts",
|
|
7350
|
-
import: "./dist/mcp/index.js"
|
|
7351
|
-
},
|
|
7352
|
-
"./storage": {
|
|
7353
|
-
types: "./dist/lib/store.d.ts",
|
|
7354
|
-
import: "./dist/lib/store.js"
|
|
7355
|
-
}
|
|
7356
|
-
},
|
|
7357
|
-
files: [
|
|
7358
|
-
"dist",
|
|
7359
|
-
"README.md",
|
|
7360
|
-
"CHANGELOG.md",
|
|
7361
|
-
"docs",
|
|
7362
|
-
"LICENSE"
|
|
7363
|
-
],
|
|
7364
|
-
scripts: {
|
|
7365
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
7366
|
-
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7367
|
-
typecheck: "tsc --noEmit",
|
|
7368
|
-
test: "bun test",
|
|
7369
|
-
prepare: "test -d dist || bun run build",
|
|
7370
|
-
"dev:cli": "bun run src/cli/index.ts",
|
|
7371
|
-
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7372
|
-
prepublishOnly: "bun run build"
|
|
7373
|
-
},
|
|
7374
|
-
keywords: [
|
|
7375
|
-
"loops",
|
|
7376
|
-
"scheduler",
|
|
7377
|
-
"daemon",
|
|
7378
|
-
"agents",
|
|
7379
|
-
"claude",
|
|
7380
|
-
"cursor",
|
|
7381
|
-
"codewith",
|
|
7382
|
-
"opencode",
|
|
7383
|
-
"cli"
|
|
7384
|
-
],
|
|
7385
|
-
repository: {
|
|
7386
|
-
type: "git",
|
|
7387
|
-
url: "git+https://github.com/hasna/loops.git"
|
|
7388
|
-
},
|
|
7389
|
-
homepage: "https://github.com/hasna/loops#readme",
|
|
7390
|
-
bugs: {
|
|
7391
|
-
url: "https://github.com/hasna/loops/issues"
|
|
7392
|
-
},
|
|
7393
|
-
author: "Hasna <andrei@hasna.com>",
|
|
7394
|
-
license: "Apache-2.0",
|
|
7395
|
-
engines: {
|
|
7396
|
-
bun: ">=1.0.0"
|
|
7397
|
-
},
|
|
7398
|
-
dependencies: {
|
|
7399
|
-
"@hasna/events": "^0.1.9",
|
|
7400
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
7401
|
-
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
7402
|
-
ai: "6.0.204",
|
|
7403
|
-
commander: "^13.1.0",
|
|
7404
|
-
zod: "4.4.3"
|
|
7405
|
-
},
|
|
7406
|
-
optionalDependencies: {
|
|
7407
|
-
"@hasna/machines": "0.0.49"
|
|
7408
|
-
},
|
|
7409
|
-
devDependencies: {
|
|
7410
|
-
"@types/bun": "latest",
|
|
7411
|
-
typescript: "^5.7.3"
|
|
7412
|
-
},
|
|
7413
|
-
publishConfig: {
|
|
7414
|
-
registry: "https://registry.npmjs.org",
|
|
7415
|
-
access: "public"
|
|
7416
|
-
}
|
|
7417
|
-
};
|
|
7418
|
-
|
|
7419
|
-
// src/lib/version.ts
|
|
7420
|
-
function packageVersion() {
|
|
7421
|
-
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
7422
|
-
throw new Error("package.json version is missing");
|
|
7423
|
-
return package_default.version;
|
|
7424
|
-
}
|
|
7425
7611
|
|
|
7426
7612
|
// src/mcp/index.ts
|
|
7427
7613
|
var LOOP_STATUSES = ["active", "paused", "stopped", "expired"];
|
|
@@ -8141,15 +8327,15 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
8141
8327
|
failCommand: "automations queue fail",
|
|
8142
8328
|
eventHandoff: {
|
|
8143
8329
|
envelopeCommand: "automations webhooks event",
|
|
8144
|
-
handlerCommand: "loops
|
|
8145
|
-
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json
|
|
8330
|
+
handlerCommand: "loops routes create generic",
|
|
8331
|
+
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json routes create generic",
|
|
8146
8332
|
boundary: "Use only for explicit event-envelope workflow handoff. OpenAutomations still owns deterministic automation materialization and queue state; OpenLoops owns workflow invocation."
|
|
8147
8333
|
},
|
|
8148
8334
|
requiredEnvironment: ["HASNA_AUTOMATIONS_DIR"],
|
|
8149
8335
|
guarantees: [
|
|
8150
8336
|
"OpenAutomations owns automation specs, run materialization, queue state, DLQ, replay, idempotency, and approvals.",
|
|
8151
8337
|
"OpenLoops may execute claimed actions through explicit command or SDK handoff only.",
|
|
8152
|
-
"OpenLoops may consume exported event envelopes only through explicit
|
|
8338
|
+
"OpenLoops may consume exported event envelopes only through explicit routes create commands.",
|
|
8153
8339
|
"Workers must complete or fail actions by action id and runner id so OpenAutomations can enforce queue leases."
|
|
8154
8340
|
],
|
|
8155
8341
|
nonGoals: [
|
|
@@ -8167,6 +8353,627 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
8167
8353
|
nonGoals: overrides.nonGoals ?? defaults.nonGoals
|
|
8168
8354
|
};
|
|
8169
8355
|
}
|
|
8356
|
+
|
|
8357
|
+
// src/lib/storage/sqlite.ts
|
|
8358
|
+
class SqliteLoopStorage {
|
|
8359
|
+
store;
|
|
8360
|
+
backend = "sqlite";
|
|
8361
|
+
supportsRemoteRunners = false;
|
|
8362
|
+
constructor(store = new Store) {
|
|
8363
|
+
this.store = store;
|
|
8364
|
+
}
|
|
8365
|
+
async close() {
|
|
8366
|
+
this.store.close();
|
|
8367
|
+
}
|
|
8368
|
+
async call(method, ...args) {
|
|
8369
|
+
return this.store[method](...args);
|
|
8370
|
+
}
|
|
8371
|
+
createLoop(...args) {
|
|
8372
|
+
return this.call("createLoop", ...args);
|
|
8373
|
+
}
|
|
8374
|
+
getLoop(...args) {
|
|
8375
|
+
return this.call("getLoop", ...args);
|
|
8376
|
+
}
|
|
8377
|
+
findLoopByName(...args) {
|
|
8378
|
+
return this.call("findLoopByName", ...args);
|
|
8379
|
+
}
|
|
8380
|
+
requireLoop(...args) {
|
|
8381
|
+
return this.call("requireLoop", ...args);
|
|
8382
|
+
}
|
|
8383
|
+
listLoops(...args) {
|
|
8384
|
+
return this.call("listLoops", ...args);
|
|
8385
|
+
}
|
|
8386
|
+
dueLoops(...args) {
|
|
8387
|
+
return this.call("dueLoops", ...args);
|
|
8388
|
+
}
|
|
8389
|
+
updateLoop(...args) {
|
|
8390
|
+
return this.call("updateLoop", ...args);
|
|
8391
|
+
}
|
|
8392
|
+
renameLoop(...args) {
|
|
8393
|
+
return this.call("renameLoop", ...args);
|
|
8394
|
+
}
|
|
8395
|
+
archiveLoop(...args) {
|
|
8396
|
+
return this.call("archiveLoop", ...args);
|
|
8397
|
+
}
|
|
8398
|
+
unarchiveLoop(...args) {
|
|
8399
|
+
return this.call("unarchiveLoop", ...args);
|
|
8400
|
+
}
|
|
8401
|
+
deleteLoop(...args) {
|
|
8402
|
+
return this.call("deleteLoop", ...args);
|
|
8403
|
+
}
|
|
8404
|
+
createWorkflow(...args) {
|
|
8405
|
+
return this.call("createWorkflow", ...args);
|
|
8406
|
+
}
|
|
8407
|
+
getWorkflow(...args) {
|
|
8408
|
+
return this.call("getWorkflow", ...args);
|
|
8409
|
+
}
|
|
8410
|
+
listWorkflows(...args) {
|
|
8411
|
+
return this.call("listWorkflows", ...args);
|
|
8412
|
+
}
|
|
8413
|
+
countWorkflows(...args) {
|
|
8414
|
+
return this.call("countWorkflows", ...args);
|
|
8415
|
+
}
|
|
8416
|
+
archiveWorkflow(...args) {
|
|
8417
|
+
return this.call("archiveWorkflow", ...args);
|
|
8418
|
+
}
|
|
8419
|
+
createWorkflowInvocation(...args) {
|
|
8420
|
+
return this.call("createWorkflowInvocation", ...args);
|
|
8421
|
+
}
|
|
8422
|
+
getWorkflowInvocation(...args) {
|
|
8423
|
+
return this.call("getWorkflowInvocation", ...args);
|
|
8424
|
+
}
|
|
8425
|
+
listWorkflowInvocations(...args) {
|
|
8426
|
+
return this.call("listWorkflowInvocations", ...args);
|
|
8427
|
+
}
|
|
8428
|
+
upsertWorkflowWorkItem(...args) {
|
|
8429
|
+
return this.call("upsertWorkflowWorkItem", ...args);
|
|
8430
|
+
}
|
|
8431
|
+
getWorkflowWorkItem(...args) {
|
|
8432
|
+
return this.call("getWorkflowWorkItem", ...args);
|
|
8433
|
+
}
|
|
8434
|
+
listWorkflowWorkItems(...args) {
|
|
8435
|
+
return this.call("listWorkflowWorkItems", ...args);
|
|
8436
|
+
}
|
|
8437
|
+
countActiveWorkflowWorkItems(...args) {
|
|
8438
|
+
return this.call("countActiveWorkflowWorkItems", ...args);
|
|
8439
|
+
}
|
|
8440
|
+
admitWorkflowWorkItem(...args) {
|
|
8441
|
+
return this.call("admitWorkflowWorkItem", ...args);
|
|
8442
|
+
}
|
|
8443
|
+
createGoal(...args) {
|
|
8444
|
+
return this.call("createGoal", ...args);
|
|
8445
|
+
}
|
|
8446
|
+
getGoal(...args) {
|
|
8447
|
+
return this.call("getGoal", ...args);
|
|
8448
|
+
}
|
|
8449
|
+
listGoals(...args) {
|
|
8450
|
+
return this.call("listGoals", ...args);
|
|
8451
|
+
}
|
|
8452
|
+
createGoalPlanNodes(...args) {
|
|
8453
|
+
return this.call("createGoalPlanNodes", ...args);
|
|
8454
|
+
}
|
|
8455
|
+
listGoalPlanNodes(...args) {
|
|
8456
|
+
return this.call("listGoalPlanNodes", ...args);
|
|
8457
|
+
}
|
|
8458
|
+
updateGoalStatus(...args) {
|
|
8459
|
+
return this.call("updateGoalStatus", ...args);
|
|
8460
|
+
}
|
|
8461
|
+
updateGoalPlanNode(...args) {
|
|
8462
|
+
return this.call("updateGoalPlanNode", ...args);
|
|
8463
|
+
}
|
|
8464
|
+
recordGoalEvent(...args) {
|
|
8465
|
+
return this.call("recordGoalEvent", ...args);
|
|
8466
|
+
}
|
|
8467
|
+
listGoalRuns(...args) {
|
|
8468
|
+
return this.call("listGoalRuns", ...args);
|
|
8469
|
+
}
|
|
8470
|
+
createWorkflowRun(...args) {
|
|
8471
|
+
return this.call("createWorkflowRun", ...args);
|
|
8472
|
+
}
|
|
8473
|
+
getWorkflowRun(...args) {
|
|
8474
|
+
return this.call("getWorkflowRun", ...args);
|
|
8475
|
+
}
|
|
8476
|
+
listWorkflowRuns(...args) {
|
|
8477
|
+
return this.call("listWorkflowRuns", ...args);
|
|
8478
|
+
}
|
|
8479
|
+
listWorkflowStepRuns(...args) {
|
|
8480
|
+
return this.call("listWorkflowStepRuns", ...args);
|
|
8481
|
+
}
|
|
8482
|
+
getWorkflowStepRun(...args) {
|
|
8483
|
+
return this.call("getWorkflowStepRun", ...args);
|
|
8484
|
+
}
|
|
8485
|
+
startWorkflowStepRun(...args) {
|
|
8486
|
+
return this.call("startWorkflowStepRun", ...args);
|
|
8487
|
+
}
|
|
8488
|
+
recoverWorkflowRun(...args) {
|
|
8489
|
+
return this.call("recoverWorkflowRun", ...args);
|
|
8490
|
+
}
|
|
8491
|
+
finalizeWorkflowStepRun(...args) {
|
|
8492
|
+
return this.call("finalizeWorkflowStepRun", ...args);
|
|
8493
|
+
}
|
|
8494
|
+
finalizeWorkflowRun(...args) {
|
|
8495
|
+
return this.call("finalizeWorkflowRun", ...args);
|
|
8496
|
+
}
|
|
8497
|
+
appendWorkflowEvent(...args) {
|
|
8498
|
+
return this.call("appendWorkflowEvent", ...args);
|
|
8499
|
+
}
|
|
8500
|
+
listWorkflowEvents(...args) {
|
|
8501
|
+
return this.call("listWorkflowEvents", ...args);
|
|
8502
|
+
}
|
|
8503
|
+
recordRunProcess(...args) {
|
|
8504
|
+
return this.call("recordRunProcess", ...args);
|
|
8505
|
+
}
|
|
8506
|
+
createSkippedRun(...args) {
|
|
8507
|
+
return this.call("createSkippedRun", ...args);
|
|
8508
|
+
}
|
|
8509
|
+
getRun(...args) {
|
|
8510
|
+
return this.call("getRun", ...args);
|
|
8511
|
+
}
|
|
8512
|
+
getRunBySlot(...args) {
|
|
8513
|
+
return this.call("getRunBySlot", ...args);
|
|
8514
|
+
}
|
|
8515
|
+
claimRun(...args) {
|
|
8516
|
+
return this.call("claimRun", ...args);
|
|
8517
|
+
}
|
|
8518
|
+
finalizeRun(...args) {
|
|
8519
|
+
return this.call("finalizeRun", ...args);
|
|
8520
|
+
}
|
|
8521
|
+
heartbeatRunLease(...args) {
|
|
8522
|
+
return this.call("heartbeatRunLease", ...args);
|
|
8523
|
+
}
|
|
8524
|
+
listRuns(...args) {
|
|
8525
|
+
return this.call("listRuns", ...args);
|
|
8526
|
+
}
|
|
8527
|
+
recoverExpiredRunLeases(...args) {
|
|
8528
|
+
return this.call("recoverExpiredRunLeases", ...args);
|
|
8529
|
+
}
|
|
8530
|
+
recoverExpiredRunLeasesDetailed(...args) {
|
|
8531
|
+
return this.call("recoverExpiredRunLeasesDetailed", ...args);
|
|
8532
|
+
}
|
|
8533
|
+
countLoops(...args) {
|
|
8534
|
+
return this.call("countLoops", ...args);
|
|
8535
|
+
}
|
|
8536
|
+
countRuns(...args) {
|
|
8537
|
+
return this.call("countRuns", ...args);
|
|
8538
|
+
}
|
|
8539
|
+
pruneHistory(...args) {
|
|
8540
|
+
return this.call("pruneHistory", ...args);
|
|
8541
|
+
}
|
|
8542
|
+
acquireDaemonLease(...args) {
|
|
8543
|
+
return this.call("acquireDaemonLease", ...args);
|
|
8544
|
+
}
|
|
8545
|
+
heartbeatDaemonLease(...args) {
|
|
8546
|
+
return this.call("heartbeatDaemonLease", ...args);
|
|
8547
|
+
}
|
|
8548
|
+
releaseDaemonLease(...args) {
|
|
8549
|
+
return this.call("releaseDaemonLease", ...args);
|
|
8550
|
+
}
|
|
8551
|
+
getDaemonLease(...args) {
|
|
8552
|
+
return this.call("getDaemonLease", ...args);
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
function createSqliteLoopStorage(path) {
|
|
8556
|
+
return new SqliteLoopStorage(new Store(path));
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8559
|
+
// src/lib/storage/postgres-schema.ts
|
|
8560
|
+
import { createHash as createHash3 } from "crypto";
|
|
8561
|
+
var POSTGRES_MIGRATION_LEDGER_TABLE = "open_loops_schema_migrations";
|
|
8562
|
+
function checksumStorageSql(sql) {
|
|
8563
|
+
const normalized = sql.trim().replace(/\r\n/g, `
|
|
8564
|
+
`);
|
|
8565
|
+
return `sha256:${createHash3("sha256").update(normalized).digest("hex")}`;
|
|
8566
|
+
}
|
|
8567
|
+
function migration(id, sql) {
|
|
8568
|
+
return Object.freeze({ id, sql: sql.trim(), checksum: checksumStorageSql(sql) });
|
|
8569
|
+
}
|
|
8570
|
+
var POSTGRES_STORAGE_MIGRATIONS = Object.freeze([
|
|
8571
|
+
migration("0001_core_runtime", `
|
|
8572
|
+
CREATE TABLE IF NOT EXISTS loops (
|
|
8573
|
+
id TEXT PRIMARY KEY,
|
|
8574
|
+
name TEXT NOT NULL,
|
|
8575
|
+
description TEXT,
|
|
8576
|
+
status TEXT NOT NULL,
|
|
8577
|
+
archived_at TIMESTAMPTZ,
|
|
8578
|
+
archived_from_status TEXT,
|
|
8579
|
+
schedule_json JSONB NOT NULL,
|
|
8580
|
+
target_json JSONB NOT NULL,
|
|
8581
|
+
goal_json JSONB,
|
|
8582
|
+
machine_json JSONB,
|
|
8583
|
+
next_run_at TIMESTAMPTZ,
|
|
8584
|
+
retry_scheduled_for TIMESTAMPTZ,
|
|
8585
|
+
catch_up TEXT NOT NULL,
|
|
8586
|
+
catch_up_limit INTEGER NOT NULL,
|
|
8587
|
+
overlap TEXT NOT NULL,
|
|
8588
|
+
max_attempts INTEGER NOT NULL,
|
|
8589
|
+
retry_delay_ms INTEGER NOT NULL,
|
|
8590
|
+
lease_ms INTEGER NOT NULL,
|
|
8591
|
+
expires_at TIMESTAMPTZ,
|
|
8592
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8593
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8594
|
+
);
|
|
8595
|
+
CREATE INDEX IF NOT EXISTS idx_loops_status_next ON loops(status, next_run_at);
|
|
8596
|
+
CREATE INDEX IF NOT EXISTS idx_loops_name ON loops(name);
|
|
8597
|
+
|
|
8598
|
+
CREATE TABLE IF NOT EXISTS loop_runs (
|
|
8599
|
+
id TEXT PRIMARY KEY,
|
|
8600
|
+
loop_id TEXT NOT NULL REFERENCES loops(id) ON DELETE CASCADE,
|
|
8601
|
+
loop_name TEXT NOT NULL,
|
|
8602
|
+
scheduled_for TIMESTAMPTZ NOT NULL,
|
|
8603
|
+
attempt INTEGER NOT NULL,
|
|
8604
|
+
status TEXT NOT NULL,
|
|
8605
|
+
started_at TIMESTAMPTZ,
|
|
8606
|
+
finished_at TIMESTAMPTZ,
|
|
8607
|
+
claimed_by TEXT,
|
|
8608
|
+
claim_token TEXT,
|
|
8609
|
+
lease_expires_at TIMESTAMPTZ,
|
|
8610
|
+
pid INTEGER,
|
|
8611
|
+
pgid INTEGER,
|
|
8612
|
+
process_started_at TIMESTAMPTZ,
|
|
8613
|
+
exit_code INTEGER,
|
|
8614
|
+
duration_ms INTEGER,
|
|
8615
|
+
stdout TEXT,
|
|
8616
|
+
stderr TEXT,
|
|
8617
|
+
error TEXT,
|
|
8618
|
+
goal_run_id TEXT,
|
|
8619
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8620
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8621
|
+
UNIQUE(loop_id, scheduled_for)
|
|
8622
|
+
);
|
|
8623
|
+
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
8624
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
8625
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
8626
|
+
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
8627
|
+
CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
|
|
8628
|
+
|
|
8629
|
+
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
8630
|
+
id TEXT PRIMARY KEY,
|
|
8631
|
+
pid INTEGER NOT NULL,
|
|
8632
|
+
hostname TEXT NOT NULL,
|
|
8633
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
8634
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
8635
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8636
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8637
|
+
);
|
|
8638
|
+
`),
|
|
8639
|
+
migration("0002_workflows_goals", `
|
|
8640
|
+
CREATE TABLE IF NOT EXISTS workflow_specs (
|
|
8641
|
+
id TEXT PRIMARY KEY,
|
|
8642
|
+
name TEXT NOT NULL,
|
|
8643
|
+
description TEXT,
|
|
8644
|
+
version INTEGER NOT NULL,
|
|
8645
|
+
status TEXT NOT NULL,
|
|
8646
|
+
goal_json JSONB,
|
|
8647
|
+
steps_json JSONB NOT NULL,
|
|
8648
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8649
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8650
|
+
);
|
|
8651
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_status_name ON workflow_specs(status, name);
|
|
8652
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflows_name_active ON workflow_specs(name) WHERE status = 'active';
|
|
8653
|
+
|
|
8654
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
8655
|
+
id TEXT PRIMARY KEY,
|
|
8656
|
+
workflow_id TEXT NOT NULL REFERENCES workflow_specs(id) ON DELETE CASCADE,
|
|
8657
|
+
workflow_name TEXT NOT NULL,
|
|
8658
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
8659
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
|
|
8660
|
+
invocation_id TEXT,
|
|
8661
|
+
work_item_id TEXT,
|
|
8662
|
+
scheduled_for TIMESTAMPTZ,
|
|
8663
|
+
idempotency_key TEXT,
|
|
8664
|
+
manifest_path TEXT,
|
|
8665
|
+
status TEXT NOT NULL,
|
|
8666
|
+
started_at TIMESTAMPTZ,
|
|
8667
|
+
finished_at TIMESTAMPTZ,
|
|
8668
|
+
duration_ms INTEGER,
|
|
8669
|
+
error TEXT,
|
|
8670
|
+
goal_run_id TEXT,
|
|
8671
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8672
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8673
|
+
);
|
|
8674
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_runs_idempotency ON workflow_runs(workflow_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
8675
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created ON workflow_runs(workflow_id, created_at DESC);
|
|
8676
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
|
|
8677
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
8678
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_invocation ON workflow_runs(invocation_id);
|
|
8679
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_work_item ON workflow_runs(work_item_id);
|
|
8680
|
+
|
|
8681
|
+
CREATE TABLE IF NOT EXISTS workflow_invocations (
|
|
8682
|
+
id TEXT PRIMARY KEY,
|
|
8683
|
+
workflow_id TEXT,
|
|
8684
|
+
template_id TEXT,
|
|
8685
|
+
source_kind TEXT NOT NULL,
|
|
8686
|
+
source_id TEXT,
|
|
8687
|
+
source_dedupe_key TEXT,
|
|
8688
|
+
source_json JSONB NOT NULL,
|
|
8689
|
+
subject_kind TEXT NOT NULL,
|
|
8690
|
+
subject_id TEXT,
|
|
8691
|
+
subject_path TEXT,
|
|
8692
|
+
subject_url TEXT,
|
|
8693
|
+
subject_json JSONB NOT NULL,
|
|
8694
|
+
intent TEXT NOT NULL,
|
|
8695
|
+
scope_json JSONB,
|
|
8696
|
+
output_policy_json JSONB,
|
|
8697
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8698
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8699
|
+
);
|
|
8700
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_source ON workflow_invocations(source_kind, source_id);
|
|
8701
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_subject ON workflow_invocations(subject_kind, subject_id, subject_path);
|
|
8702
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_invocations_dedupe ON workflow_invocations(source_kind, source_dedupe_key) WHERE source_dedupe_key IS NOT NULL;
|
|
8703
|
+
|
|
8704
|
+
CREATE TABLE IF NOT EXISTS workflow_work_items (
|
|
8705
|
+
id TEXT PRIMARY KEY,
|
|
8706
|
+
route_key TEXT NOT NULL,
|
|
8707
|
+
idempotency_key TEXT NOT NULL,
|
|
8708
|
+
invocation_id TEXT NOT NULL REFERENCES workflow_invocations(id) ON DELETE CASCADE,
|
|
8709
|
+
source_type TEXT NOT NULL,
|
|
8710
|
+
source_ref TEXT NOT NULL,
|
|
8711
|
+
subject_ref TEXT NOT NULL,
|
|
8712
|
+
project_key TEXT,
|
|
8713
|
+
project_group TEXT,
|
|
8714
|
+
priority INTEGER NOT NULL,
|
|
8715
|
+
status TEXT NOT NULL,
|
|
8716
|
+
attempts INTEGER NOT NULL,
|
|
8717
|
+
next_attempt_at TIMESTAMPTZ,
|
|
8718
|
+
lease_expires_at TIMESTAMPTZ,
|
|
8719
|
+
workflow_id TEXT REFERENCES workflow_specs(id) ON DELETE SET NULL,
|
|
8720
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
8721
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE SET NULL,
|
|
8722
|
+
last_reason TEXT,
|
|
8723
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8724
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8725
|
+
UNIQUE(route_key, idempotency_key)
|
|
8726
|
+
);
|
|
8727
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_status_next ON workflow_work_items(status, next_attempt_at, priority DESC, created_at ASC);
|
|
8728
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
8729
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
8730
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
8731
|
+
|
|
8732
|
+
CREATE TABLE IF NOT EXISTS workflow_step_runs (
|
|
8733
|
+
id TEXT PRIMARY KEY,
|
|
8734
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8735
|
+
step_id TEXT NOT NULL,
|
|
8736
|
+
sequence INTEGER NOT NULL,
|
|
8737
|
+
status TEXT NOT NULL,
|
|
8738
|
+
started_at TIMESTAMPTZ,
|
|
8739
|
+
finished_at TIMESTAMPTZ,
|
|
8740
|
+
exit_code INTEGER,
|
|
8741
|
+
pid INTEGER,
|
|
8742
|
+
duration_ms INTEGER,
|
|
8743
|
+
stdout TEXT,
|
|
8744
|
+
stderr TEXT,
|
|
8745
|
+
error TEXT,
|
|
8746
|
+
account_profile TEXT,
|
|
8747
|
+
account_tool TEXT,
|
|
8748
|
+
goal_run_id TEXT,
|
|
8749
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8750
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8751
|
+
UNIQUE(workflow_run_id, step_id)
|
|
8752
|
+
);
|
|
8753
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_run_sequence ON workflow_step_runs(workflow_run_id, sequence);
|
|
8754
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_step_runs_status ON workflow_step_runs(status);
|
|
8755
|
+
|
|
8756
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
8757
|
+
id TEXT PRIMARY KEY,
|
|
8758
|
+
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8759
|
+
sequence INTEGER NOT NULL,
|
|
8760
|
+
event_type TEXT NOT NULL,
|
|
8761
|
+
step_id TEXT,
|
|
8762
|
+
payload_json JSONB,
|
|
8763
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8764
|
+
UNIQUE(workflow_run_id, sequence)
|
|
8765
|
+
);
|
|
8766
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
|
|
8767
|
+
|
|
8768
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
8769
|
+
id TEXT PRIMARY KEY,
|
|
8770
|
+
plan_id TEXT NOT NULL,
|
|
8771
|
+
objective TEXT NOT NULL,
|
|
8772
|
+
status TEXT NOT NULL,
|
|
8773
|
+
token_budget INTEGER,
|
|
8774
|
+
tokens_used INTEGER NOT NULL,
|
|
8775
|
+
time_used_seconds INTEGER NOT NULL,
|
|
8776
|
+
auto_execute TEXT NOT NULL,
|
|
8777
|
+
max_tokens INTEGER,
|
|
8778
|
+
source_type TEXT,
|
|
8779
|
+
source_id TEXT,
|
|
8780
|
+
loop_id TEXT,
|
|
8781
|
+
loop_run_id TEXT,
|
|
8782
|
+
workflow_id TEXT,
|
|
8783
|
+
workflow_run_id TEXT,
|
|
8784
|
+
workflow_step_id TEXT,
|
|
8785
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8786
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8787
|
+
);
|
|
8788
|
+
CREATE INDEX IF NOT EXISTS idx_goals_status_updated ON goals(status, updated_at DESC);
|
|
8789
|
+
CREATE INDEX IF NOT EXISTS idx_goals_loop_run ON goals(loop_run_id);
|
|
8790
|
+
CREATE INDEX IF NOT EXISTS idx_goals_workflow_run ON goals(workflow_run_id);
|
|
8791
|
+
CREATE INDEX IF NOT EXISTS idx_goals_source ON goals(source_type, source_id);
|
|
8792
|
+
|
|
8793
|
+
CREATE TABLE IF NOT EXISTS goal_plan_nodes (
|
|
8794
|
+
id TEXT PRIMARY KEY,
|
|
8795
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
8796
|
+
plan_id TEXT NOT NULL,
|
|
8797
|
+
key TEXT NOT NULL,
|
|
8798
|
+
sequence INTEGER NOT NULL,
|
|
8799
|
+
priority INTEGER NOT NULL,
|
|
8800
|
+
objective TEXT NOT NULL,
|
|
8801
|
+
status TEXT NOT NULL,
|
|
8802
|
+
ready BOOLEAN NOT NULL,
|
|
8803
|
+
token_budget INTEGER,
|
|
8804
|
+
tokens_used INTEGER NOT NULL,
|
|
8805
|
+
time_used_seconds INTEGER NOT NULL,
|
|
8806
|
+
depends_on_json JSONB NOT NULL,
|
|
8807
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8808
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8809
|
+
UNIQUE(plan_id, key)
|
|
8810
|
+
);
|
|
8811
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_goal_sequence ON goal_plan_nodes(goal_id, sequence);
|
|
8812
|
+
CREATE INDEX IF NOT EXISTS idx_goal_plan_nodes_status ON goal_plan_nodes(status);
|
|
8813
|
+
|
|
8814
|
+
CREATE TABLE IF NOT EXISTS goal_runs (
|
|
8815
|
+
id TEXT PRIMARY KEY,
|
|
8816
|
+
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
|
|
8817
|
+
plan_id TEXT NOT NULL,
|
|
8818
|
+
loop_id TEXT,
|
|
8819
|
+
loop_run_id TEXT,
|
|
8820
|
+
workflow_id TEXT,
|
|
8821
|
+
workflow_run_id TEXT,
|
|
8822
|
+
workflow_step_id TEXT,
|
|
8823
|
+
turn INTEGER NOT NULL,
|
|
8824
|
+
phase TEXT NOT NULL,
|
|
8825
|
+
status TEXT NOT NULL,
|
|
8826
|
+
node_key TEXT,
|
|
8827
|
+
tokens_used INTEGER NOT NULL,
|
|
8828
|
+
evidence_json JSONB,
|
|
8829
|
+
raw_response_json JSONB,
|
|
8830
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8831
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8832
|
+
);
|
|
8833
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_goal_created ON goal_runs(goal_id, created_at);
|
|
8834
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_loop_run ON goal_runs(loop_run_id);
|
|
8835
|
+
CREATE INDEX IF NOT EXISTS idx_goal_runs_workflow_run ON goal_runs(workflow_run_id);
|
|
8836
|
+
`),
|
|
8837
|
+
migration("0003_remote_runners_and_audit", `
|
|
8838
|
+
CREATE TABLE IF NOT EXISTS runner_machines (
|
|
8839
|
+
id TEXT PRIMARY KEY,
|
|
8840
|
+
hostname TEXT NOT NULL,
|
|
8841
|
+
labels_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
8842
|
+
capabilities_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
8843
|
+
status TEXT NOT NULL,
|
|
8844
|
+
last_seen_at TIMESTAMPTZ NOT NULL,
|
|
8845
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8846
|
+
updated_at TIMESTAMPTZ NOT NULL
|
|
8847
|
+
);
|
|
8848
|
+
CREATE INDEX IF NOT EXISTS idx_runner_machines_status_seen ON runner_machines(status, last_seen_at DESC);
|
|
8849
|
+
|
|
8850
|
+
CREATE TABLE IF NOT EXISTS runner_leases (
|
|
8851
|
+
id TEXT PRIMARY KEY,
|
|
8852
|
+
runner_id TEXT NOT NULL REFERENCES runner_machines(id) ON DELETE CASCADE,
|
|
8853
|
+
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE CASCADE,
|
|
8854
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
8855
|
+
claim_token TEXT NOT NULL,
|
|
8856
|
+
status TEXT NOT NULL,
|
|
8857
|
+
heartbeat_at TIMESTAMPTZ NOT NULL,
|
|
8858
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
8859
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
8860
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
8861
|
+
CHECK (loop_run_id IS NOT NULL OR workflow_run_id IS NOT NULL)
|
|
8862
|
+
);
|
|
8863
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_loop_run ON runner_leases(loop_run_id) WHERE loop_run_id IS NOT NULL AND status = 'active';
|
|
8864
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_runner_leases_active_workflow_run ON runner_leases(workflow_run_id) WHERE workflow_run_id IS NOT NULL AND status = 'active';
|
|
8865
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_runner_status ON runner_leases(runner_id, status, expires_at);
|
|
8866
|
+
CREATE INDEX IF NOT EXISTS idx_runner_leases_claim_token ON runner_leases(claim_token);
|
|
8867
|
+
|
|
8868
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
8869
|
+
id TEXT PRIMARY KEY,
|
|
8870
|
+
actor TEXT NOT NULL,
|
|
8871
|
+
action TEXT NOT NULL,
|
|
8872
|
+
subject_type TEXT NOT NULL,
|
|
8873
|
+
subject_id TEXT NOT NULL,
|
|
8874
|
+
metadata_json JSONB,
|
|
8875
|
+
created_at TIMESTAMPTZ NOT NULL
|
|
8876
|
+
);
|
|
8877
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_subject ON audit_events(subject_type, subject_id, created_at DESC);
|
|
8878
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action, created_at DESC);
|
|
8879
|
+
`)
|
|
8880
|
+
]);
|
|
8881
|
+
|
|
8882
|
+
// src/lib/storage/postgres.ts
|
|
8883
|
+
class PostgresStorage {
|
|
8884
|
+
executor;
|
|
8885
|
+
backend = "postgres";
|
|
8886
|
+
migrations;
|
|
8887
|
+
constructor(executor, migrations = POSTGRES_STORAGE_MIGRATIONS) {
|
|
8888
|
+
this.executor = executor;
|
|
8889
|
+
this.migrations = migrations;
|
|
8890
|
+
}
|
|
8891
|
+
async listAppliedMigrations() {
|
|
8892
|
+
await this.ensureLedger();
|
|
8893
|
+
return this.readAppliedMigrations();
|
|
8894
|
+
}
|
|
8895
|
+
async migrate(opts = {}) {
|
|
8896
|
+
const dryRun = opts.dryRun === true;
|
|
8897
|
+
if (!dryRun)
|
|
8898
|
+
await this.ensureLedger();
|
|
8899
|
+
const applied = dryRun ? await this.tryReadAppliedMigrations() : await this.readAppliedMigrations();
|
|
8900
|
+
const knownMigrationIds = new Set(this.migrations.map((migration2) => migration2.id));
|
|
8901
|
+
for (const row of applied) {
|
|
8902
|
+
if (!knownMigrationIds.has(row.id)) {
|
|
8903
|
+
throw new Error(`Postgres migration ${row.id} is not recognized by this binary`);
|
|
8904
|
+
}
|
|
8905
|
+
}
|
|
8906
|
+
const appliedById = new Map(applied.map((row) => [row.id, row]));
|
|
8907
|
+
const plan = this.migrations.map((migration2) => ({
|
|
8908
|
+
migration: migration2,
|
|
8909
|
+
state: appliedById.has(migration2.id) ? "already_applied" : "pending"
|
|
8910
|
+
}));
|
|
8911
|
+
for (const migration2 of this.migrations) {
|
|
8912
|
+
const existing = appliedById.get(migration2.id);
|
|
8913
|
+
if (existing && existing.checksum !== migration2.checksum) {
|
|
8914
|
+
throw new Error(`Postgres migration checksum mismatch for ${migration2.id}`);
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8917
|
+
if (dryRun)
|
|
8918
|
+
return { backend: this.backend, dryRun, applied, plan };
|
|
8919
|
+
const run = async () => {
|
|
8920
|
+
for (const item of plan) {
|
|
8921
|
+
if (item.state === "already_applied")
|
|
8922
|
+
continue;
|
|
8923
|
+
await this.executor.execute(item.migration.sql);
|
|
8924
|
+
await this.executor.execute(`INSERT INTO ${POSTGRES_MIGRATION_LEDGER_TABLE} (id, checksum, applied_at) VALUES ($1, $2, NOW())`, [item.migration.id, item.migration.checksum]);
|
|
8925
|
+
}
|
|
8926
|
+
};
|
|
8927
|
+
if (this.executor.transaction)
|
|
8928
|
+
await this.executor.transaction(run);
|
|
8929
|
+
else
|
|
8930
|
+
await run();
|
|
8931
|
+
return {
|
|
8932
|
+
backend: this.backend,
|
|
8933
|
+
dryRun,
|
|
8934
|
+
applied: await this.readAppliedMigrations(),
|
|
8935
|
+
plan
|
|
8936
|
+
};
|
|
8937
|
+
}
|
|
8938
|
+
async close() {
|
|
8939
|
+
await this.executor.close?.();
|
|
8940
|
+
}
|
|
8941
|
+
async ensureLedger() {
|
|
8942
|
+
await this.executor.execute(`
|
|
8943
|
+
CREATE TABLE IF NOT EXISTS ${POSTGRES_MIGRATION_LEDGER_TABLE} (
|
|
8944
|
+
id TEXT PRIMARY KEY,
|
|
8945
|
+
checksum TEXT NOT NULL,
|
|
8946
|
+
applied_at TIMESTAMPTZ NOT NULL
|
|
8947
|
+
);
|
|
8948
|
+
`);
|
|
8949
|
+
}
|
|
8950
|
+
async readAppliedMigrations() {
|
|
8951
|
+
const rows = await this.executor.query(`SELECT id, checksum, applied_at FROM ${POSTGRES_MIGRATION_LEDGER_TABLE} ORDER BY id ASC`);
|
|
8952
|
+
return rows.map((row) => ({
|
|
8953
|
+
id: row.id,
|
|
8954
|
+
checksum: row.checksum,
|
|
8955
|
+
appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : row.applied_at
|
|
8956
|
+
}));
|
|
8957
|
+
}
|
|
8958
|
+
async tryReadAppliedMigrations() {
|
|
8959
|
+
try {
|
|
8960
|
+
return await this.readAppliedMigrations();
|
|
8961
|
+
} catch (error) {
|
|
8962
|
+
if (isMissingLedgerError(error))
|
|
8963
|
+
return [];
|
|
8964
|
+
throw error;
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
function createPostgresStorage(executor) {
|
|
8969
|
+
return new PostgresStorage(executor);
|
|
8970
|
+
}
|
|
8971
|
+
function isMissingLedgerError(error) {
|
|
8972
|
+
if (!(error instanceof Error))
|
|
8973
|
+
return false;
|
|
8974
|
+
const message = error.message.toLowerCase();
|
|
8975
|
+
return message.includes(POSTGRES_MIGRATION_LEDGER_TABLE.toLowerCase()) && (message.includes("does not exist") || message.includes("no such table") || message.includes("undefined_table"));
|
|
8976
|
+
}
|
|
8170
8977
|
// src/lib/templates.ts
|
|
8171
8978
|
import { execFileSync } from "child_process";
|
|
8172
8979
|
import { existsSync as existsSync6 } from "fs";
|
|
@@ -8251,6 +9058,7 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
|
|
|
8251
9058
|
{ name: "taskTitle", description: "Human-readable task title." },
|
|
8252
9059
|
projectPathVariable(),
|
|
8253
9060
|
{ name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
|
|
9061
|
+
{ name: "todosDbPath", description: "Optional exact Todos DB path used via TODOS_DB_PATH/HASNA_TODOS_DB_PATH." },
|
|
8254
9062
|
...routeScopeVariables(),
|
|
8255
9063
|
...workerVerifierAgentVariables({ addDirs: true, branchNoun: "task/event" })
|
|
8256
9064
|
]
|
|
@@ -8291,6 +9099,8 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
|
|
|
8291
9099
|
variables: [
|
|
8292
9100
|
{ name: "taskId", required: true, description: "Todos task id." },
|
|
8293
9101
|
projectPathVariable(),
|
|
9102
|
+
{ name: "todosProjectPath", description: "Todos storage project path used in lifecycle commands." },
|
|
9103
|
+
{ name: "todosDbPath", description: "Optional exact Todos DB path used via TODOS_DB_PATH/HASNA_TODOS_DB_PATH." },
|
|
8294
9104
|
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
8295
9105
|
roleAuthProfileVariable("triage"),
|
|
8296
9106
|
roleAuthProfileVariable("planner"),
|
|
@@ -8421,28 +9231,29 @@ function verifierRuntimeGuidance(input) {
|
|
|
8421
9231
|
].join(`
|
|
8422
9232
|
`);
|
|
8423
9233
|
}
|
|
8424
|
-
function todosInspectLine(todosProjectPath, taskId) {
|
|
8425
|
-
return `- Inspect first:
|
|
9234
|
+
function todosInspectLine(todosProjectPath, taskId, todosDbPath) {
|
|
9235
|
+
return `- Inspect first: ${todosCommand(todosProjectPath, todosDbPath)} inspect ${taskId}`;
|
|
8426
9236
|
}
|
|
8427
|
-
function todosStartLine(todosProjectPath, taskId) {
|
|
8428
|
-
return `- Claim/start if appropriate:
|
|
9237
|
+
function todosStartLine(todosProjectPath, taskId, todosDbPath) {
|
|
9238
|
+
return `- Claim/start if appropriate: ${todosCommand(todosProjectPath, todosDbPath)} start ${taskId}`;
|
|
8429
9239
|
}
|
|
8430
|
-
function todosEvidenceLine(todosProjectPath, taskId, placeholder) {
|
|
8431
|
-
return `- Record evidence:
|
|
9240
|
+
function todosEvidenceLine(todosProjectPath, taskId, placeholder, todosDbPath) {
|
|
9241
|
+
return `- Record evidence: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<${placeholder}>"`;
|
|
8432
9242
|
}
|
|
8433
|
-
function todosVerificationLine(todosProjectPath, taskId) {
|
|
8434
|
-
return `- Record verification:
|
|
9243
|
+
function todosVerificationLine(todosProjectPath, taskId, todosDbPath) {
|
|
9244
|
+
return `- Record verification: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<verification evidence or blocker>"`;
|
|
8435
9245
|
}
|
|
8436
|
-
function todosDoneLine(todosProjectPath, taskId) {
|
|
8437
|
-
return `- If valid and complete:
|
|
9246
|
+
function todosDoneLine(todosProjectPath, taskId, todosDbPath) {
|
|
9247
|
+
return `- If valid and complete: ${todosCommand(todosProjectPath, todosDbPath)} done ${taskId}`;
|
|
8438
9248
|
}
|
|
8439
|
-
function todosExactCommandsFragment(todosProjectPath, taskId, commandLines) {
|
|
9249
|
+
function todosExactCommandsFragment(todosProjectPath, taskId, commandLines, todosDbPath) {
|
|
8440
9250
|
return [
|
|
8441
9251
|
`Todos project path: ${todosProjectPath}`,
|
|
9252
|
+
todosDbPath ? `Todos DB path: ${todosDbPath}` : undefined,
|
|
8442
9253
|
"Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
|
|
8443
|
-
todosInspectLine(todosProjectPath, taskId),
|
|
9254
|
+
todosInspectLine(todosProjectPath, taskId, todosDbPath),
|
|
8444
9255
|
...commandLines
|
|
8445
|
-
];
|
|
9256
|
+
].filter((line) => Boolean(line));
|
|
8446
9257
|
}
|
|
8447
9258
|
function worktreePrompt(plan) {
|
|
8448
9259
|
if (plan.enabled) {
|
|
@@ -8479,10 +9290,19 @@ function worktreeContextFragment(plan) {
|
|
|
8479
9290
|
function shellQuote2(value) {
|
|
8480
9291
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8481
9292
|
}
|
|
8482
|
-
function
|
|
9293
|
+
function todosEnvPrefix(todosDbPath) {
|
|
9294
|
+
return todosDbPath ? `TODOS_DB_PATH=${shellQuote2(todosDbPath)} HASNA_TODOS_DB_PATH=${shellQuote2(todosDbPath)} ` : "";
|
|
9295
|
+
}
|
|
9296
|
+
function todosCommand(todosProjectPath, todosDbPath) {
|
|
9297
|
+
return `${todosEnvPrefix(todosDbPath)}todos --project ${todosProjectPath}`;
|
|
9298
|
+
}
|
|
9299
|
+
function shellTodosCommand(todosProjectPath, todosDbPath) {
|
|
9300
|
+
return `${todosEnvPrefix(todosDbPath)}todos --project ${shellQuote2(todosProjectPath)}`;
|
|
9301
|
+
}
|
|
9302
|
+
function sourceTaskGateCommand(todosProjectPath, taskId, todosDbPath) {
|
|
8483
9303
|
return [
|
|
8484
9304
|
"set -euo pipefail",
|
|
8485
|
-
|
|
9305
|
+
`${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)} >/dev/null`,
|
|
8486
9306
|
`printf "source task %s resolved in todos project %s\\n" ${shellQuote2(taskId)} ${shellQuote2(todosProjectPath)}`
|
|
8487
9307
|
].join(`
|
|
8488
9308
|
`);
|
|
@@ -8542,10 +9362,10 @@ var LIFECYCLE_GATE_SCRIPT_TAIL = [
|
|
|
8542
9362
|
"console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);"
|
|
8543
9363
|
].join(`
|
|
8544
9364
|
`);
|
|
8545
|
-
function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker) {
|
|
9365
|
+
function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker, todosDbPath) {
|
|
8546
9366
|
return [
|
|
8547
9367
|
"set -euo pipefail",
|
|
8548
|
-
`task_json="$(
|
|
9368
|
+
`task_json="$(${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)})"`,
|
|
8549
9369
|
`TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
|
|
8550
9370
|
LIFECYCLE_GATE_SCRIPT_HEAD,
|
|
8551
9371
|
`const goMarker = ${JSON.stringify(goMarker)};`,
|
|
@@ -8561,6 +9381,7 @@ var PR_HANDOFF_SCRIPT = [
|
|
|
8561
9381
|
"const artifactPath = process.env.OPENLOOPS_PR_HANDOFF_ARTIFACT || '';",
|
|
8562
9382
|
"const taskId = process.env.OPENLOOPS_PR_HANDOFF_TASK_ID || '';",
|
|
8563
9383
|
"const todosProject = process.env.OPENLOOPS_PR_HANDOFF_TODOS_PROJECT || '';",
|
|
9384
|
+
"const todosDbPath = process.env.OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH || '';",
|
|
8564
9385
|
"const fallbackWorktree = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE || process.cwd();",
|
|
8565
9386
|
"const expectedRoot = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT || fallbackWorktree;",
|
|
8566
9387
|
"const expectedBranch = process.env.OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH || '';",
|
|
@@ -8578,7 +9399,8 @@ var PR_HANDOFF_SCRIPT = [
|
|
|
8578
9399
|
"};",
|
|
8579
9400
|
"const run = (command, args, options = {}) => spawnSync(command, args, { encoding: 'utf8', ...options });",
|
|
8580
9401
|
"const todosArgs = (...args) => todosProject ? ['--project', todosProject, ...args] : args;",
|
|
8581
|
-
"const
|
|
9402
|
+
"const todosEnv = todosDbPath ? { ...process.env, TODOS_DB_PATH: todosDbPath, HASNA_TODOS_DB_PATH: todosDbPath } : process.env;",
|
|
9403
|
+
"const todos = (...args) => run(todosBin, todosArgs(...args), { env: todosEnv });",
|
|
8582
9404
|
"const comment = (text) => {",
|
|
8583
9405
|
" const result = todos('comment', taskId, text);",
|
|
8584
9406
|
" if (result.status !== 0) console.error(`failed to comment original task: ${result.stderr || result.stdout || result.status}`);",
|
|
@@ -8707,6 +9529,7 @@ function prHandoffCommand(opts) {
|
|
|
8707
9529
|
`export OPENLOOPS_PR_HANDOFF_ARTIFACT=${shellQuote2(opts.artifactPath)}`,
|
|
8708
9530
|
`export OPENLOOPS_PR_HANDOFF_TASK_ID=${shellQuote2(opts.taskId)}`,
|
|
8709
9531
|
`export OPENLOOPS_PR_HANDOFF_TODOS_PROJECT=${shellQuote2(opts.todosProjectPath)}`,
|
|
9532
|
+
opts.todosDbPath ? `export OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH=${shellQuote2(opts.todosDbPath)}` : undefined,
|
|
8710
9533
|
`export OPENLOOPS_PR_HANDOFF_WORKTREE=${shellQuote2(opts.worktreeCwd)}`,
|
|
8711
9534
|
`export OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT=${shellQuote2(opts.worktreeRoot)}`,
|
|
8712
9535
|
`export OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH=${shellQuote2(opts.expectedBranch)}`,
|
|
@@ -8717,7 +9540,7 @@ function prHandoffCommand(opts) {
|
|
|
8717
9540
|
"bun - <<'BUN'",
|
|
8718
9541
|
PR_HANDOFF_SCRIPT,
|
|
8719
9542
|
"BUN"
|
|
8720
|
-
].join(`
|
|
9543
|
+
].filter((line) => Boolean(line)).join(`
|
|
8721
9544
|
`);
|
|
8722
9545
|
}
|
|
8723
9546
|
// src/lib/templates-custom.ts
|
|
@@ -9307,12 +10130,12 @@ function commandStep(opts) {
|
|
|
9307
10130
|
...opts.blockedExitCodes ? { blockedExitCodes: opts.blockedExitCodes } : {}
|
|
9308
10131
|
};
|
|
9309
10132
|
}
|
|
9310
|
-
function sourceTaskGateStep(todosProjectPath, taskId, plan, description) {
|
|
10133
|
+
function sourceTaskGateStep(todosProjectPath, taskId, plan, description, todosDbPath) {
|
|
9311
10134
|
return commandStep({
|
|
9312
10135
|
id: "source-task-gate",
|
|
9313
10136
|
name: "Source Task Gate",
|
|
9314
10137
|
description,
|
|
9315
|
-
command: sourceTaskGateCommand(todosProjectPath, taskId),
|
|
10138
|
+
command: sourceTaskGateCommand(todosProjectPath, taskId, todosDbPath),
|
|
9316
10139
|
cwd: plan.originalCwd,
|
|
9317
10140
|
timeoutMs: 60000,
|
|
9318
10141
|
blockedExitCodes: GATE_BLOCKED_EXIT_CODES
|
|
@@ -9324,7 +10147,7 @@ function lifecycleGateStep(opts) {
|
|
|
9324
10147
|
name: opts.stage === "triage" ? "Triage Gate" : "Planner Gate",
|
|
9325
10148
|
description: opts.description,
|
|
9326
10149
|
dependsOn: opts.dependsOn,
|
|
9327
|
-
command: lifecycleGateCommand(opts.todosProjectPath, opts.taskId, opts.stage, opts.goMarker, opts.blockedMarker),
|
|
10150
|
+
command: lifecycleGateCommand(opts.todosProjectPath, opts.taskId, opts.stage, opts.goMarker, opts.blockedMarker, opts.todosDbPath),
|
|
9328
10151
|
cwd: opts.plan.originalCwd,
|
|
9329
10152
|
timeoutMs: 2 * 60000,
|
|
9330
10153
|
blockedExitCodes: GATE_BLOCKED_EXIT_CODES
|
|
@@ -9333,7 +10156,7 @@ function lifecycleGateStep(opts) {
|
|
|
9333
10156
|
function prHandoffArtifactPath(plan, taskId) {
|
|
9334
10157
|
return join7(plan.cwd, ".openloops", "pr-handoff", `${slugSegment(taskId, "task")}.json`);
|
|
9335
10158
|
}
|
|
9336
|
-
function prHandoffStep(input, plan, todosProjectPath) {
|
|
10159
|
+
function prHandoffStep(input, plan, todosProjectPath, todosDbPath) {
|
|
9337
10160
|
return commandStep({
|
|
9338
10161
|
id: "pr-handoff",
|
|
9339
10162
|
name: "PR Handoff",
|
|
@@ -9343,6 +10166,7 @@ function prHandoffStep(input, plan, todosProjectPath) {
|
|
|
9343
10166
|
artifactPath: prHandoffArtifactPath(plan, input.taskId),
|
|
9344
10167
|
taskId: input.taskId,
|
|
9345
10168
|
todosProjectPath,
|
|
10169
|
+
todosDbPath,
|
|
9346
10170
|
worktreeCwd: plan.cwd,
|
|
9347
10171
|
worktreeRoot: plan.path ?? plan.cwd,
|
|
9348
10172
|
expectedBranch: plan.branch ?? ""
|
|
@@ -9429,6 +10253,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9429
10253
|
if (!input.projectPath?.trim())
|
|
9430
10254
|
throw new Error("projectPath is required");
|
|
9431
10255
|
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
10256
|
+
const todosDbPath = input.todosDbPath;
|
|
9432
10257
|
const plan = worktreePlan(input, input.taskId);
|
|
9433
10258
|
const taskContext = {
|
|
9434
10259
|
taskId: input.taskId,
|
|
@@ -9439,15 +10264,17 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9439
10264
|
projectPath: input.projectPath,
|
|
9440
10265
|
routeProjectPath: input.routeProjectPath,
|
|
9441
10266
|
projectGroup: input.projectGroup,
|
|
10267
|
+
todosProjectPath: input.todosProjectPath ?? (input.todosDbPath ? todosProjectPath : undefined),
|
|
10268
|
+
todosDbPath,
|
|
9442
10269
|
worktree: worktreeContextFragment(plan)
|
|
9443
10270
|
};
|
|
9444
10271
|
const workerPrompt = [
|
|
9445
10272
|
...goalHeaderFragment(`Complete todos task ${input.taskId} in ${input.projectPath}.`, "worker", "task"),
|
|
9446
10273
|
worktreePrompt(plan),
|
|
9447
10274
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9448
|
-
todosStartLine(todosProjectPath, input.taskId),
|
|
9449
|
-
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers")
|
|
9450
|
-
]),
|
|
10275
|
+
todosStartLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10276
|
+
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers", todosDbPath)
|
|
10277
|
+
], todosDbPath),
|
|
9451
10278
|
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
9452
10279
|
"Inspect the repository/project state, implement only the task scope, run focused validation, preserve unrelated user changes, and update the task with comments, evidence, changed files, commits, and blockers.",
|
|
9453
10280
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
@@ -9460,9 +10287,9 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9460
10287
|
...goalHeaderFragment(`Verify todos task ${input.taskId} after the worker step.`, "verifier", "task"),
|
|
9461
10288
|
worktreePrompt(plan),
|
|
9462
10289
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9463
|
-
todosVerificationLine(todosProjectPath, input.taskId),
|
|
9464
|
-
todosDoneLine(todosProjectPath, input.taskId)
|
|
9465
|
-
]),
|
|
10290
|
+
todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10291
|
+
todosDoneLine(todosProjectPath, input.taskId, todosDbPath)
|
|
10292
|
+
], todosDbPath),
|
|
9466
10293
|
adversarialReviewFragment("the task, repository state, commits, tests, and worker evidence", TASK_REVIEW_FOCUS),
|
|
9467
10294
|
verifierRuntimeGuidance(input),
|
|
9468
10295
|
TASK_VERIFIER_DECISION_FRAGMENT,
|
|
@@ -9477,7 +10304,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
9477
10304
|
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
9478
10305
|
version: 1,
|
|
9479
10306
|
steps: [
|
|
9480
|
-
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before worker execution when the source todos task is not resolvable."),
|
|
10307
|
+
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before worker execution when the source todos task is not resolvable.", todosDbPath),
|
|
9481
10308
|
...workerVerifierSteps({
|
|
9482
10309
|
input,
|
|
9483
10310
|
seed: input.taskId,
|
|
@@ -9497,6 +10324,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9497
10324
|
if (!input.projectPath?.trim())
|
|
9498
10325
|
throw new Error("projectPath is required");
|
|
9499
10326
|
const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
|
|
10327
|
+
const todosDbPath = input.todosDbPath;
|
|
9500
10328
|
const plan = worktreePlan(input, input.taskId);
|
|
9501
10329
|
const taskContext = {
|
|
9502
10330
|
taskId: input.taskId,
|
|
@@ -9508,6 +10336,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9508
10336
|
routeProjectPath: input.routeProjectPath,
|
|
9509
10337
|
projectGroup: input.projectGroup,
|
|
9510
10338
|
todosProjectPath,
|
|
10339
|
+
todosDbPath,
|
|
9511
10340
|
worktree: worktreeContextFragment(plan)
|
|
9512
10341
|
};
|
|
9513
10342
|
const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
|
|
@@ -9521,8 +10350,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9521
10350
|
const shared = [
|
|
9522
10351
|
worktreePrompt(plan),
|
|
9523
10352
|
...todosExactCommandsFragment(todosProjectPath, input.taskId, [
|
|
9524
|
-
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker")
|
|
9525
|
-
]),
|
|
10353
|
+
todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker", todosDbPath)
|
|
10354
|
+
], todosDbPath),
|
|
9526
10355
|
NO_TMUX_DISPATCH_FRAGMENT,
|
|
9527
10356
|
"Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
|
|
9528
10357
|
"",
|
|
@@ -9531,7 +10360,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9531
10360
|
].join(`
|
|
9532
10361
|
`);
|
|
9533
10362
|
const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
|
|
9534
|
-
const blockTaskCommand =
|
|
10363
|
+
const blockTaskCommand = `${todosEnvPrefix(todosDbPath)}todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
|
|
9535
10364
|
const gateStopFragment = (stage, stops) => `The deterministic ${stage} gate will stop ${stops} unless the latest ${stage} marker is the exact go marker and the task has no blocked/completed/done/cancelled/failed/archived/no-auto/manual/approval-required state.`;
|
|
9536
10365
|
const triagePrompt = [
|
|
9537
10366
|
...goalHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
|
|
@@ -9559,7 +10388,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9559
10388
|
const workerPrompt = [
|
|
9560
10389
|
...goalHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
|
|
9561
10390
|
shared,
|
|
9562
|
-
todosStartLine(todosProjectPath, input.taskId),
|
|
10391
|
+
todosStartLine(todosProjectPath, input.taskId, todosDbPath),
|
|
9563
10392
|
"Read the triage and planner comments first. Implement only the scoped task, run focused validation, and record changed files, commits, evidence, blockers, and residual risks.",
|
|
9564
10393
|
input.prHandoff ? `When only GitHub network access is blocked after a successful commit/validation, record the handoff artifact at ${handoffArtifactPath} instead of repeatedly retrying push/PR creation.` : undefined,
|
|
9565
10394
|
WORKER_LEAVES_COMPLETION_FRAGMENT
|
|
@@ -9568,8 +10397,8 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9568
10397
|
const verifierPrompt = [
|
|
9569
10398
|
...goalHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
|
|
9570
10399
|
shared,
|
|
9571
|
-
todosVerificationLine(todosProjectPath, input.taskId),
|
|
9572
|
-
todosDoneLine(todosProjectPath, input.taskId),
|
|
10400
|
+
todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
|
|
10401
|
+
todosDoneLine(todosProjectPath, input.taskId, todosDbPath),
|
|
9573
10402
|
adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
|
|
9574
10403
|
verifierRuntimeGuidance(input),
|
|
9575
10404
|
input.prHandoff ? `If ${handoffArtifactPath} exists and there is no PR URL evidence, verify that the PR handoff step queued or completed a bounded handoff; leave the original task open or blocked until PR evidence is recorded.` : undefined,
|
|
@@ -9578,7 +10407,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9578
10407
|
].filter(Boolean).join(`
|
|
9579
10408
|
`);
|
|
9580
10409
|
const steps = [
|
|
9581
|
-
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before lifecycle agents execute when the source todos task is not resolvable."),
|
|
10410
|
+
sourceTaskGateStep(todosProjectPath, input.taskId, plan, "Fail before lifecycle agents execute when the source todos task is not resolvable.", todosDbPath),
|
|
9582
10411
|
{
|
|
9583
10412
|
id: "triage",
|
|
9584
10413
|
name: "Triage",
|
|
@@ -9592,6 +10421,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9592
10421
|
description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
|
|
9593
10422
|
dependsOn: ["triage"],
|
|
9594
10423
|
todosProjectPath,
|
|
10424
|
+
todosDbPath,
|
|
9595
10425
|
taskId: input.taskId,
|
|
9596
10426
|
goMarker: gateMarker("triage", "go"),
|
|
9597
10427
|
blockedMarker: gateMarker("triage", "blocked"),
|
|
@@ -9610,6 +10440,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9610
10440
|
description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
|
|
9611
10441
|
dependsOn: ["planner"],
|
|
9612
10442
|
todosProjectPath,
|
|
10443
|
+
todosDbPath,
|
|
9613
10444
|
taskId: input.taskId,
|
|
9614
10445
|
goMarker: gateMarker("planner", "go"),
|
|
9615
10446
|
blockedMarker: gateMarker("planner", "blocked"),
|
|
@@ -9625,7 +10456,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
9625
10456
|
}
|
|
9626
10457
|
];
|
|
9627
10458
|
if (input.prHandoff) {
|
|
9628
|
-
steps.push(prHandoffStep(input, plan, todosProjectPath));
|
|
10459
|
+
steps.push(prHandoffStep(input, plan, todosProjectPath, todosDbPath));
|
|
9629
10460
|
}
|
|
9630
10461
|
steps.push({
|
|
9631
10462
|
id: "verifier",
|
|
@@ -9850,6 +10681,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
9850
10681
|
taskTitle: values.taskTitle,
|
|
9851
10682
|
taskDescription: values.taskDescription,
|
|
9852
10683
|
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
10684
|
+
todosDbPath: values.todosDbPath,
|
|
9853
10685
|
triageAuthProfile: values.triageAuthProfile,
|
|
9854
10686
|
plannerAuthProfile: values.plannerAuthProfile,
|
|
9855
10687
|
triageAccount: accountVar(values.triageAccount, values.accountTool),
|
|
@@ -9914,6 +10746,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
9914
10746
|
taskTitle: values.taskTitle,
|
|
9915
10747
|
taskDescription: values.taskDescription,
|
|
9916
10748
|
todosProjectPath: values.todosProjectPath ?? values.todosProject,
|
|
10749
|
+
todosDbPath: values.todosDbPath,
|
|
9917
10750
|
eventId: values.eventId,
|
|
9918
10751
|
eventType: values.eventType
|
|
9919
10752
|
});
|
|
@@ -9955,6 +10788,7 @@ function renderLoopTemplate(id, values, opts = {}) {
|
|
|
9955
10788
|
}
|
|
9956
10789
|
// src/lib/hygiene.ts
|
|
9957
10790
|
import { basename as basename4 } from "path";
|
|
10791
|
+
import { homedir as homedir4 } from "os";
|
|
9958
10792
|
var PROVIDER_TOKENS = new Set([
|
|
9959
10793
|
"codewith",
|
|
9960
10794
|
"claude",
|
|
@@ -9969,11 +10803,14 @@ var PROVIDER_TOKENS = new Set([
|
|
|
9969
10803
|
var REPO_GENERIC_TOKENS = new Set(["repo", "repoops"]);
|
|
9970
10804
|
var CADENCE_SUFFIX_TOKENS = new Set(["hourly", "daily", "weekly", "monthly"]);
|
|
9971
10805
|
var CADENCE_SUFFIX_PATTERN = /^(?:every-?)?\d+(?:s|m|h|d|w)$/;
|
|
10806
|
+
function userHome() {
|
|
10807
|
+
return process.env.HOME || homedir4();
|
|
10808
|
+
}
|
|
9972
10809
|
function slugify(value) {
|
|
9973
10810
|
return value.normalize("NFKD").replace(/[^\w\s.-]/g, "-").replace(/[_\s.:/]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
9974
10811
|
}
|
|
9975
10812
|
function repoSlugFromCwd(cwd) {
|
|
9976
|
-
if (!cwd || cwd ===
|
|
10813
|
+
if (!cwd || cwd === userHome())
|
|
9977
10814
|
return "";
|
|
9978
10815
|
if (cwd.includes("/.hasna/loops/"))
|
|
9979
10816
|
return "";
|
|
@@ -10156,7 +10993,7 @@ function commandText(loop) {
|
|
|
10156
10993
|
return [loop.target.command, ...loop.target.args ?? []].join(" ");
|
|
10157
10994
|
}
|
|
10158
10995
|
function scriptNeedles(scriptsDir) {
|
|
10159
|
-
const home =
|
|
10996
|
+
const home = userHome();
|
|
10160
10997
|
const normalized = scriptsDir.replace(/\/+$/g, "");
|
|
10161
10998
|
const values = [
|
|
10162
10999
|
normalized,
|
|
@@ -10174,7 +11011,7 @@ function scriptNeedles(scriptsDir) {
|
|
|
10174
11011
|
return [...new Set(values)];
|
|
10175
11012
|
}
|
|
10176
11013
|
function buildScriptInventoryReport(store, opts = {}) {
|
|
10177
|
-
const scriptsDir = opts.scriptsDir ?? `${
|
|
11014
|
+
const scriptsDir = opts.scriptsDir ?? `${userHome()}/.hasna/loops/scripts`;
|
|
10178
11015
|
const needles = scriptNeedles(scriptsDir);
|
|
10179
11016
|
const loops2 = managedLoops(store, { includeInactive: opts.includeInactive, includeStopped: true, limit: opts.limit });
|
|
10180
11017
|
const scriptBacked = loops2.map((loop) => {
|
|
@@ -10210,6 +11047,7 @@ export {
|
|
|
10210
11047
|
runDoctor,
|
|
10211
11048
|
rollupSummary,
|
|
10212
11049
|
resolveLoopMachine,
|
|
11050
|
+
resolveLoopDeploymentMode,
|
|
10213
11051
|
resolveGoalModel,
|
|
10214
11052
|
renderTodosTaskWorkerVerifierWorkflow,
|
|
10215
11053
|
renderLoopTemplate,
|
|
@@ -10222,8 +11060,10 @@ export {
|
|
|
10222
11060
|
parseDuration,
|
|
10223
11061
|
parseCron,
|
|
10224
11062
|
openAutomationsRuntimeBinding,
|
|
11063
|
+
normalizeLoopDeploymentMode,
|
|
10225
11064
|
nextCronRun,
|
|
10226
11065
|
loops,
|
|
11066
|
+
loopControlPlaneConfig,
|
|
10227
11067
|
listToolsForCli,
|
|
10228
11068
|
listOpenMachines,
|
|
10229
11069
|
listLoopTemplates,
|
|
@@ -10235,19 +11075,29 @@ export {
|
|
|
10235
11075
|
executeTarget,
|
|
10236
11076
|
executeLoopTarget,
|
|
10237
11077
|
executeLoop,
|
|
11078
|
+
deploymentStatusLine,
|
|
11079
|
+
createSqliteLoopStorage,
|
|
11080
|
+
createPostgresStorage,
|
|
10238
11081
|
createLoopsMcpServer,
|
|
10239
11082
|
computeNextAfter,
|
|
10240
11083
|
classifyRunFailure,
|
|
11084
|
+
checksumStorageSql,
|
|
10241
11085
|
buildScriptInventoryReport,
|
|
10242
11086
|
buildNameHygieneReport,
|
|
10243
11087
|
buildHealthReport,
|
|
10244
11088
|
buildDuplicateOverlapReport,
|
|
11089
|
+
buildDeploymentStatus,
|
|
10245
11090
|
ValidationError,
|
|
10246
11091
|
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
10247
11092
|
Store,
|
|
11093
|
+
SqliteLoopStorage,
|
|
11094
|
+
PostgresStorage,
|
|
11095
|
+
POSTGRES_STORAGE_MIGRATIONS,
|
|
11096
|
+
POSTGRES_MIGRATION_LEDGER_TABLE,
|
|
10248
11097
|
LoopsClient,
|
|
10249
11098
|
LoopNotFoundError,
|
|
10250
11099
|
LoopArchivedError,
|
|
11100
|
+
LOOP_DEPLOYMENT_MODES,
|
|
10251
11101
|
LOOPS_MCP_TOOLS,
|
|
10252
11102
|
EVENT_WORKER_VERIFIER_TEMPLATE_ID,
|
|
10253
11103
|
CodedError,
|