@hasna/loops 0.4.1 → 0.4.3

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/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 = 6;
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"]);
@@ -1523,7 +1523,7 @@ class Store {
1523
1523
  }
1524
1524
  const applied = new Set(this.db.query("SELECT id FROM schema_migrations").all().map((row) => row.id));
1525
1525
  for (const migration of this.migrations()) {
1526
- if (!migration.baseline && applied.has(migration.id))
1526
+ if (!migration.baseline && !migration.reapply && applied.has(migration.id))
1527
1527
  continue;
1528
1528
  migration.apply();
1529
1529
  if (!applied.has(migration.id)) {
@@ -1573,6 +1573,14 @@ 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
+ reapply: true,
1580
+ apply: () => {
1581
+ this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1582
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
1583
+ }
1576
1584
  }
1577
1585
  ];
1578
1586
  }
@@ -1614,6 +1622,7 @@ class Store {
1614
1622
  started_at TEXT,
1615
1623
  finished_at TEXT,
1616
1624
  claimed_by TEXT,
1625
+ claim_token TEXT,
1617
1626
  lease_expires_at TEXT,
1618
1627
  pid INTEGER,
1619
1628
  pgid INTEGER,
@@ -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(),
@@ -3965,7 +3982,7 @@ class Store {
3965
3982
  // package.json
3966
3983
  var package_default = {
3967
3984
  name: "@hasna/loops",
3968
- version: "0.4.1",
3985
+ version: "0.4.3",
3969
3986
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
3970
3987
  type: "module",
3971
3988
  main: "dist/index.js",
@@ -4005,6 +4022,22 @@ var package_default = {
4005
4022
  "./storage": {
4006
4023
  types: "./dist/lib/store.d.ts",
4007
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"
4008
4041
  }
4009
4042
  },
4010
4043
  files: [
@@ -4015,7 +4048,7 @@ var package_default = {
4015
4048
  "LICENSE"
4016
4049
  ],
4017
4050
  scripts: {
4018
- 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 --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",
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",
4019
4052
  "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
4020
4053
  typecheck: "tsc --noEmit",
4021
4054
  test: "bun test",
@@ -5796,11 +5829,12 @@ function publicLoop(loop) {
5796
5829
  target
5797
5830
  };
5798
5831
  }
5799
- function publicRun(run, showOutput = false) {
5832
+ function publicRun(run, showOutput = false, opts = {}) {
5800
5833
  return {
5801
5834
  ...run,
5802
5835
  stdout: showOutput ? run.stdout : run.stdout ? `[redacted ${run.stdout.length} chars]` : undefined,
5803
- 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
5804
5838
  };
5805
5839
  }
5806
5840
  function publicExecutorResult(result, showOutput = false) {
@@ -8319,6 +8353,627 @@ function openAutomationsRuntimeBinding(overrides = {}) {
8319
8353
  nonGoals: overrides.nonGoals ?? defaults.nonGoals
8320
8354
  };
8321
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
+ }
8322
8977
  // src/lib/templates.ts
8323
8978
  import { execFileSync } from "child_process";
8324
8979
  import { existsSync as existsSync6 } from "fs";
@@ -8403,6 +9058,7 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
8403
9058
  { name: "taskTitle", description: "Human-readable task title." },
8404
9059
  projectPathVariable(),
8405
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." },
8406
9062
  ...routeScopeVariables(),
8407
9063
  ...workerVerifierAgentVariables({ addDirs: true, branchNoun: "task/event" })
8408
9064
  ]
@@ -8443,6 +9099,8 @@ var BUILTIN_TEMPLATE_SUMMARIES = [
8443
9099
  variables: [
8444
9100
  { name: "taskId", required: true, description: "Todos task id." },
8445
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." },
8446
9104
  { name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
8447
9105
  roleAuthProfileVariable("triage"),
8448
9106
  roleAuthProfileVariable("planner"),
@@ -8573,28 +9231,29 @@ function verifierRuntimeGuidance(input) {
8573
9231
  ].join(`
8574
9232
  `);
8575
9233
  }
8576
- function todosInspectLine(todosProjectPath, taskId) {
8577
- return `- Inspect first: todos --project ${todosProjectPath} inspect ${taskId}`;
9234
+ function todosInspectLine(todosProjectPath, taskId, todosDbPath) {
9235
+ return `- Inspect first: ${todosCommand(todosProjectPath, todosDbPath)} inspect ${taskId}`;
8578
9236
  }
8579
- function todosStartLine(todosProjectPath, taskId) {
8580
- return `- Claim/start if appropriate: todos --project ${todosProjectPath} start ${taskId}`;
9237
+ function todosStartLine(todosProjectPath, taskId, todosDbPath) {
9238
+ return `- Claim/start if appropriate: ${todosCommand(todosProjectPath, todosDbPath)} start ${taskId}`;
8581
9239
  }
8582
- function todosEvidenceLine(todosProjectPath, taskId, placeholder) {
8583
- return `- Record evidence: todos --project ${todosProjectPath} comment ${taskId} "<${placeholder}>"`;
9240
+ function todosEvidenceLine(todosProjectPath, taskId, placeholder, todosDbPath) {
9241
+ return `- Record evidence: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<${placeholder}>"`;
8584
9242
  }
8585
- function todosVerificationLine(todosProjectPath, taskId) {
8586
- return `- Record verification: todos --project ${todosProjectPath} comment ${taskId} "<verification evidence or blocker>"`;
9243
+ function todosVerificationLine(todosProjectPath, taskId, todosDbPath) {
9244
+ return `- Record verification: ${todosCommand(todosProjectPath, todosDbPath)} comment ${taskId} "<verification evidence or blocker>"`;
8587
9245
  }
8588
- function todosDoneLine(todosProjectPath, taskId) {
8589
- return `- If valid and complete: todos --project ${todosProjectPath} done ${taskId}`;
9246
+ function todosDoneLine(todosProjectPath, taskId, todosDbPath) {
9247
+ return `- If valid and complete: ${todosCommand(todosProjectPath, todosDbPath)} done ${taskId}`;
8590
9248
  }
8591
- function todosExactCommandsFragment(todosProjectPath, taskId, commandLines) {
9249
+ function todosExactCommandsFragment(todosProjectPath, taskId, commandLines, todosDbPath) {
8592
9250
  return [
8593
9251
  `Todos project path: ${todosProjectPath}`,
9252
+ todosDbPath ? `Todos DB path: ${todosDbPath}` : undefined,
8594
9253
  "Use these exact todos commands so worktree cwd inference cannot attach to the wrong project:",
8595
- todosInspectLine(todosProjectPath, taskId),
9254
+ todosInspectLine(todosProjectPath, taskId, todosDbPath),
8596
9255
  ...commandLines
8597
- ];
9256
+ ].filter((line) => Boolean(line));
8598
9257
  }
8599
9258
  function worktreePrompt(plan) {
8600
9259
  if (plan.enabled) {
@@ -8631,10 +9290,19 @@ function worktreeContextFragment(plan) {
8631
9290
  function shellQuote2(value) {
8632
9291
  return `'${value.replace(/'/g, `'\\''`)}'`;
8633
9292
  }
8634
- function sourceTaskGateCommand(todosProjectPath, taskId) {
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) {
8635
9303
  return [
8636
9304
  "set -euo pipefail",
8637
- `todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(taskId)} >/dev/null`,
9305
+ `${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)} >/dev/null`,
8638
9306
  `printf "source task %s resolved in todos project %s\\n" ${shellQuote2(taskId)} ${shellQuote2(todosProjectPath)}`
8639
9307
  ].join(`
8640
9308
  `);
@@ -8694,10 +9362,10 @@ var LIFECYCLE_GATE_SCRIPT_TAIL = [
8694
9362
  "console.log(`task lifecycle ${stage} gate passed for ${task.id || task.taskId || 'task'} status=${status || 'unknown'}`);"
8695
9363
  ].join(`
8696
9364
  `);
8697
- function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker) {
9365
+ function lifecycleGateCommand(todosProjectPath, taskId, stage, goMarker, blockedMarker, todosDbPath) {
8698
9366
  return [
8699
9367
  "set -euo pipefail",
8700
- `task_json="$(todos --project ${shellQuote2(todosProjectPath)} --json inspect ${shellQuote2(taskId)})"`,
9368
+ `task_json="$(${shellTodosCommand(todosProjectPath, todosDbPath)} --json inspect ${shellQuote2(taskId)})"`,
8701
9369
  `TASK_JSON="$task_json" STAGE=${shellQuote2(stage)} bun - <<'BUN'`,
8702
9370
  LIFECYCLE_GATE_SCRIPT_HEAD,
8703
9371
  `const goMarker = ${JSON.stringify(goMarker)};`,
@@ -8713,6 +9381,7 @@ var PR_HANDOFF_SCRIPT = [
8713
9381
  "const artifactPath = process.env.OPENLOOPS_PR_HANDOFF_ARTIFACT || '';",
8714
9382
  "const taskId = process.env.OPENLOOPS_PR_HANDOFF_TASK_ID || '';",
8715
9383
  "const todosProject = process.env.OPENLOOPS_PR_HANDOFF_TODOS_PROJECT || '';",
9384
+ "const todosDbPath = process.env.OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH || '';",
8716
9385
  "const fallbackWorktree = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE || process.cwd();",
8717
9386
  "const expectedRoot = process.env.OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT || fallbackWorktree;",
8718
9387
  "const expectedBranch = process.env.OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH || '';",
@@ -8730,7 +9399,8 @@ var PR_HANDOFF_SCRIPT = [
8730
9399
  "};",
8731
9400
  "const run = (command, args, options = {}) => spawnSync(command, args, { encoding: 'utf8', ...options });",
8732
9401
  "const todosArgs = (...args) => todosProject ? ['--project', todosProject, ...args] : args;",
8733
- "const todos = (...args) => run(todosBin, todosArgs(...args));",
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 });",
8734
9404
  "const comment = (text) => {",
8735
9405
  " const result = todos('comment', taskId, text);",
8736
9406
  " if (result.status !== 0) console.error(`failed to comment original task: ${result.stderr || result.stdout || result.status}`);",
@@ -8859,6 +9529,7 @@ function prHandoffCommand(opts) {
8859
9529
  `export OPENLOOPS_PR_HANDOFF_ARTIFACT=${shellQuote2(opts.artifactPath)}`,
8860
9530
  `export OPENLOOPS_PR_HANDOFF_TASK_ID=${shellQuote2(opts.taskId)}`,
8861
9531
  `export OPENLOOPS_PR_HANDOFF_TODOS_PROJECT=${shellQuote2(opts.todosProjectPath)}`,
9532
+ opts.todosDbPath ? `export OPENLOOPS_PR_HANDOFF_TODOS_DB_PATH=${shellQuote2(opts.todosDbPath)}` : undefined,
8862
9533
  `export OPENLOOPS_PR_HANDOFF_WORKTREE=${shellQuote2(opts.worktreeCwd)}`,
8863
9534
  `export OPENLOOPS_PR_HANDOFF_WORKTREE_ROOT=${shellQuote2(opts.worktreeRoot)}`,
8864
9535
  `export OPENLOOPS_PR_HANDOFF_EXPECTED_BRANCH=${shellQuote2(opts.expectedBranch)}`,
@@ -8869,7 +9540,7 @@ function prHandoffCommand(opts) {
8869
9540
  "bun - <<'BUN'",
8870
9541
  PR_HANDOFF_SCRIPT,
8871
9542
  "BUN"
8872
- ].join(`
9543
+ ].filter((line) => Boolean(line)).join(`
8873
9544
  `);
8874
9545
  }
8875
9546
  // src/lib/templates-custom.ts
@@ -9459,12 +10130,12 @@ function commandStep(opts) {
9459
10130
  ...opts.blockedExitCodes ? { blockedExitCodes: opts.blockedExitCodes } : {}
9460
10131
  };
9461
10132
  }
9462
- function sourceTaskGateStep(todosProjectPath, taskId, plan, description) {
10133
+ function sourceTaskGateStep(todosProjectPath, taskId, plan, description, todosDbPath) {
9463
10134
  return commandStep({
9464
10135
  id: "source-task-gate",
9465
10136
  name: "Source Task Gate",
9466
10137
  description,
9467
- command: sourceTaskGateCommand(todosProjectPath, taskId),
10138
+ command: sourceTaskGateCommand(todosProjectPath, taskId, todosDbPath),
9468
10139
  cwd: plan.originalCwd,
9469
10140
  timeoutMs: 60000,
9470
10141
  blockedExitCodes: GATE_BLOCKED_EXIT_CODES
@@ -9476,7 +10147,7 @@ function lifecycleGateStep(opts) {
9476
10147
  name: opts.stage === "triage" ? "Triage Gate" : "Planner Gate",
9477
10148
  description: opts.description,
9478
10149
  dependsOn: opts.dependsOn,
9479
- 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),
9480
10151
  cwd: opts.plan.originalCwd,
9481
10152
  timeoutMs: 2 * 60000,
9482
10153
  blockedExitCodes: GATE_BLOCKED_EXIT_CODES
@@ -9485,7 +10156,7 @@ function lifecycleGateStep(opts) {
9485
10156
  function prHandoffArtifactPath(plan, taskId) {
9486
10157
  return join7(plan.cwd, ".openloops", "pr-handoff", `${slugSegment(taskId, "task")}.json`);
9487
10158
  }
9488
- function prHandoffStep(input, plan, todosProjectPath) {
10159
+ function prHandoffStep(input, plan, todosProjectPath, todosDbPath) {
9489
10160
  return commandStep({
9490
10161
  id: "pr-handoff",
9491
10162
  name: "PR Handoff",
@@ -9495,6 +10166,7 @@ function prHandoffStep(input, plan, todosProjectPath) {
9495
10166
  artifactPath: prHandoffArtifactPath(plan, input.taskId),
9496
10167
  taskId: input.taskId,
9497
10168
  todosProjectPath,
10169
+ todosDbPath,
9498
10170
  worktreeCwd: plan.cwd,
9499
10171
  worktreeRoot: plan.path ?? plan.cwd,
9500
10172
  expectedBranch: plan.branch ?? ""
@@ -9581,6 +10253,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
9581
10253
  if (!input.projectPath?.trim())
9582
10254
  throw new Error("projectPath is required");
9583
10255
  const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
10256
+ const todosDbPath = input.todosDbPath;
9584
10257
  const plan = worktreePlan(input, input.taskId);
9585
10258
  const taskContext = {
9586
10259
  taskId: input.taskId,
@@ -9591,15 +10264,17 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
9591
10264
  projectPath: input.projectPath,
9592
10265
  routeProjectPath: input.routeProjectPath,
9593
10266
  projectGroup: input.projectGroup,
10267
+ todosProjectPath: input.todosProjectPath ?? (input.todosDbPath ? todosProjectPath : undefined),
10268
+ todosDbPath,
9594
10269
  worktree: worktreeContextFragment(plan)
9595
10270
  };
9596
10271
  const workerPrompt = [
9597
10272
  ...goalHeaderFragment(`Complete todos task ${input.taskId} in ${input.projectPath}.`, "worker", "task"),
9598
10273
  worktreePrompt(plan),
9599
10274
  ...todosExactCommandsFragment(todosProjectPath, input.taskId, [
9600
- todosStartLine(todosProjectPath, input.taskId),
9601
- todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers")
9602
- ]),
10275
+ todosStartLine(todosProjectPath, input.taskId, todosDbPath),
10276
+ todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence and blockers", todosDbPath)
10277
+ ], todosDbPath),
9603
10278
  "Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
9604
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.",
9605
10280
  NO_TMUX_DISPATCH_FRAGMENT,
@@ -9612,9 +10287,9 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
9612
10287
  ...goalHeaderFragment(`Verify todos task ${input.taskId} after the worker step.`, "verifier", "task"),
9613
10288
  worktreePrompt(plan),
9614
10289
  ...todosExactCommandsFragment(todosProjectPath, input.taskId, [
9615
- todosVerificationLine(todosProjectPath, input.taskId),
9616
- todosDoneLine(todosProjectPath, input.taskId)
9617
- ]),
10290
+ todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
10291
+ todosDoneLine(todosProjectPath, input.taskId, todosDbPath)
10292
+ ], todosDbPath),
9618
10293
  adversarialReviewFragment("the task, repository state, commits, tests, and worker evidence", TASK_REVIEW_FOCUS),
9619
10294
  verifierRuntimeGuidance(input),
9620
10295
  TASK_VERIFIER_DECISION_FRAGMENT,
@@ -9629,7 +10304,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
9629
10304
  description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
9630
10305
  version: 1,
9631
10306
  steps: [
9632
- 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),
9633
10308
  ...workerVerifierSteps({
9634
10309
  input,
9635
10310
  seed: input.taskId,
@@ -9649,6 +10324,7 @@ function renderTaskLifecycleWorkflow(input) {
9649
10324
  if (!input.projectPath?.trim())
9650
10325
  throw new Error("projectPath is required");
9651
10326
  const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
10327
+ const todosDbPath = input.todosDbPath;
9652
10328
  const plan = worktreePlan(input, input.taskId);
9653
10329
  const taskContext = {
9654
10330
  taskId: input.taskId,
@@ -9660,6 +10336,7 @@ function renderTaskLifecycleWorkflow(input) {
9660
10336
  routeProjectPath: input.routeProjectPath,
9661
10337
  projectGroup: input.projectGroup,
9662
10338
  todosProjectPath,
10339
+ todosDbPath,
9663
10340
  worktree: worktreeContextFragment(plan)
9664
10341
  };
9665
10342
  const handoffArtifactPath = prHandoffArtifactPath(plan, input.taskId);
@@ -9673,8 +10350,8 @@ function renderTaskLifecycleWorkflow(input) {
9673
10350
  const shared = [
9674
10351
  worktreePrompt(plan),
9675
10352
  ...todosExactCommandsFragment(todosProjectPath, input.taskId, [
9676
- todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker")
9677
- ]),
10353
+ todosEvidenceLine(todosProjectPath, input.taskId, "concise evidence, decision, or blocker", todosDbPath)
10354
+ ], todosDbPath),
9678
10355
  NO_TMUX_DISPATCH_FRAGMENT,
9679
10356
  "Preserve unrelated user changes and keep scope tied to the task acceptance criteria.",
9680
10357
  "",
@@ -9683,7 +10360,7 @@ function renderTaskLifecycleWorkflow(input) {
9683
10360
  ].join(`
9684
10361
  `);
9685
10362
  const gateMarker = (stage, state) => `openloops:${stage}=${state} task=${input.taskId}${input.eventId ? ` event=${input.eventId}` : ""}`;
9686
- const blockTaskCommand = `todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
10363
+ const blockTaskCommand = `${todosEnvPrefix(todosDbPath)}todos --project ${todosProjectPath} update ${input.taskId} --status blocked`;
9687
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.`;
9688
10365
  const triagePrompt = [
9689
10366
  ...goalHeaderFragment(`Triage todos task ${input.taskId} for safe automated execution.`, "triage", "lifecycle"),
@@ -9711,7 +10388,7 @@ function renderTaskLifecycleWorkflow(input) {
9711
10388
  const workerPrompt = [
9712
10389
  ...goalHeaderFragment(`Complete todos task ${input.taskId} according to the planner evidence.`, "worker", "lifecycle"),
9713
10390
  shared,
9714
- todosStartLine(todosProjectPath, input.taskId),
10391
+ todosStartLine(todosProjectPath, input.taskId, todosDbPath),
9715
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.",
9716
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,
9717
10394
  WORKER_LEAVES_COMPLETION_FRAGMENT
@@ -9720,8 +10397,8 @@ function renderTaskLifecycleWorkflow(input) {
9720
10397
  const verifierPrompt = [
9721
10398
  ...goalHeaderFragment(`Verify todos task ${input.taskId} after the full lifecycle worker step.`, "verifier", "lifecycle"),
9722
10399
  shared,
9723
- todosVerificationLine(todosProjectPath, input.taskId),
9724
- todosDoneLine(todosProjectPath, input.taskId),
10400
+ todosVerificationLine(todosProjectPath, input.taskId, todosDbPath),
10401
+ todosDoneLine(todosProjectPath, input.taskId, todosDbPath),
9725
10402
  adversarialReviewFragment("triage, plan, worker evidence, repo state, commits, tests, and acceptance criteria", TASK_REVIEW_FOCUS),
9726
10403
  verifierRuntimeGuidance(input),
9727
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,
@@ -9730,7 +10407,7 @@ function renderTaskLifecycleWorkflow(input) {
9730
10407
  ].filter(Boolean).join(`
9731
10408
  `);
9732
10409
  const steps = [
9733
- 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),
9734
10411
  {
9735
10412
  id: "triage",
9736
10413
  name: "Triage",
@@ -9744,6 +10421,7 @@ function renderTaskLifecycleWorkflow(input) {
9744
10421
  description: "Stop the lifecycle before planning when triage blocked or disallowed automation.",
9745
10422
  dependsOn: ["triage"],
9746
10423
  todosProjectPath,
10424
+ todosDbPath,
9747
10425
  taskId: input.taskId,
9748
10426
  goMarker: gateMarker("triage", "go"),
9749
10427
  blockedMarker: gateMarker("triage", "blocked"),
@@ -9762,6 +10440,7 @@ function renderTaskLifecycleWorkflow(input) {
9762
10440
  description: "Stop the lifecycle before implementation when planning blocked or disallowed automation.",
9763
10441
  dependsOn: ["planner"],
9764
10442
  todosProjectPath,
10443
+ todosDbPath,
9765
10444
  taskId: input.taskId,
9766
10445
  goMarker: gateMarker("planner", "go"),
9767
10446
  blockedMarker: gateMarker("planner", "blocked"),
@@ -9777,7 +10456,7 @@ function renderTaskLifecycleWorkflow(input) {
9777
10456
  }
9778
10457
  ];
9779
10458
  if (input.prHandoff) {
9780
- steps.push(prHandoffStep(input, plan, todosProjectPath));
10459
+ steps.push(prHandoffStep(input, plan, todosProjectPath, todosDbPath));
9781
10460
  }
9782
10461
  steps.push({
9783
10462
  id: "verifier",
@@ -10002,6 +10681,7 @@ function renderLifecycleBoundedTemplate(id, values) {
10002
10681
  taskTitle: values.taskTitle,
10003
10682
  taskDescription: values.taskDescription,
10004
10683
  todosProjectPath: values.todosProjectPath ?? values.todosProject,
10684
+ todosDbPath: values.todosDbPath,
10005
10685
  triageAuthProfile: values.triageAuthProfile,
10006
10686
  plannerAuthProfile: values.plannerAuthProfile,
10007
10687
  triageAccount: accountVar(values.triageAccount, values.accountTool),
@@ -10066,6 +10746,7 @@ function renderBuiltinLoopTemplate(id, values) {
10066
10746
  taskTitle: values.taskTitle,
10067
10747
  taskDescription: values.taskDescription,
10068
10748
  todosProjectPath: values.todosProjectPath ?? values.todosProject,
10749
+ todosDbPath: values.todosDbPath,
10069
10750
  eventId: values.eventId,
10070
10751
  eventType: values.eventType
10071
10752
  });
@@ -10395,9 +11076,12 @@ export {
10395
11076
  executeLoopTarget,
10396
11077
  executeLoop,
10397
11078
  deploymentStatusLine,
11079
+ createSqliteLoopStorage,
11080
+ createPostgresStorage,
10398
11081
  createLoopsMcpServer,
10399
11082
  computeNextAfter,
10400
11083
  classifyRunFailure,
11084
+ checksumStorageSql,
10401
11085
  buildScriptInventoryReport,
10402
11086
  buildNameHygieneReport,
10403
11087
  buildHealthReport,
@@ -10406,6 +11090,10 @@ export {
10406
11090
  ValidationError,
10407
11091
  TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
10408
11092
  Store,
11093
+ SqliteLoopStorage,
11094
+ PostgresStorage,
11095
+ POSTGRES_STORAGE_MIGRATIONS,
11096
+ POSTGRES_MIGRATION_LEDGER_TABLE,
10409
11097
  LoopsClient,
10410
11098
  LoopNotFoundError,
10411
11099
  LoopArchivedError,