@hasna/loops 0.3.47 → 0.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/dist/cli/index.js +357 -349
- package/dist/daemon/index.js +223 -40
- package/dist/index.js +223 -58
- package/dist/lib/store.d.ts +13 -2
- package/dist/lib/store.js +206 -32
- package/dist/sdk/index.js +222 -39
- package/docs/USAGE.md +44 -5
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -329,6 +329,13 @@ function optionalPositiveInteger(value, label) {
|
|
|
329
329
|
throw new Error(`${label} must be a positive integer`);
|
|
330
330
|
return value;
|
|
331
331
|
}
|
|
332
|
+
function optionalBoolean(value, label) {
|
|
333
|
+
if (value === undefined)
|
|
334
|
+
return;
|
|
335
|
+
if (typeof value !== "boolean")
|
|
336
|
+
throw new Error(`${label} must be a boolean`);
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
332
339
|
function optionalStringArray(value, label) {
|
|
333
340
|
if (value === undefined)
|
|
334
341
|
return;
|
|
@@ -340,6 +347,18 @@ function optionalStringArray(value, label) {
|
|
|
340
347
|
}).filter(Boolean);
|
|
341
348
|
return values.length ? values : undefined;
|
|
342
349
|
}
|
|
350
|
+
function optionalAccountRef(value, label) {
|
|
351
|
+
if (value === undefined)
|
|
352
|
+
return;
|
|
353
|
+
assertObject(value, label);
|
|
354
|
+
assertString(value.profile, `${label}.profile`);
|
|
355
|
+
if (value.tool !== undefined)
|
|
356
|
+
assertString(value.tool, `${label}.tool`);
|
|
357
|
+
return {
|
|
358
|
+
profile: value.profile.trim(),
|
|
359
|
+
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
360
|
+
};
|
|
361
|
+
}
|
|
343
362
|
function normalizeGoalSpec(value, label = "goal") {
|
|
344
363
|
if (value === undefined)
|
|
345
364
|
return;
|
|
@@ -386,8 +405,21 @@ function validateTarget(value, label) {
|
|
|
386
405
|
if (value.provider !== "codewith")
|
|
387
406
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
388
407
|
}
|
|
408
|
+
if (value.configIsolation !== undefined) {
|
|
409
|
+
assertString(value.configIsolation, `${label}.configIsolation`);
|
|
410
|
+
if (!["safe", "none"].includes(value.configIsolation))
|
|
411
|
+
throw new Error(`${label}.configIsolation must be safe or none`);
|
|
412
|
+
}
|
|
413
|
+
if (value.model !== undefined)
|
|
414
|
+
assertString(value.model, `${label}.model`);
|
|
389
415
|
if (value.variant !== undefined)
|
|
390
416
|
assertString(value.variant, `${label}.variant`);
|
|
417
|
+
if (value.agent !== undefined)
|
|
418
|
+
assertString(value.agent, `${label}.agent`);
|
|
419
|
+
if (value.provider === "cursor" && value.variant !== undefined)
|
|
420
|
+
throw new Error(`${label}.variant is not supported for provider cursor`);
|
|
421
|
+
if (value.provider === "codex" && value.agent !== undefined)
|
|
422
|
+
throw new Error(`${label}.agent is not supported for provider codex`);
|
|
391
423
|
optionalStringArray(value.addDirs, `${label}.addDirs`);
|
|
392
424
|
if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
|
|
393
425
|
throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
|
|
@@ -484,8 +516,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
484
516
|
id: step.id,
|
|
485
517
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
486
518
|
target: validateTarget(step.target, `workflow.steps[${index}].target`),
|
|
487
|
-
dependsOn: step.dependsOn ?? [],
|
|
488
|
-
continueOnFailure: step.continueOnFailure ?? false
|
|
519
|
+
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
520
|
+
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
521
|
+
timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
522
|
+
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
489
523
|
};
|
|
490
524
|
});
|
|
491
525
|
for (const step of steps) {
|
|
@@ -586,6 +620,11 @@ function writeWorkflowRunManifest(args) {
|
|
|
586
620
|
}
|
|
587
621
|
|
|
588
622
|
// src/lib/store.ts
|
|
623
|
+
var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
|
|
624
|
+
var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
|
|
625
|
+
var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
|
|
626
|
+
var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
|
|
627
|
+
var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
|
|
589
628
|
function rowToLoop(row) {
|
|
590
629
|
return {
|
|
591
630
|
id: row.id,
|
|
@@ -852,6 +891,7 @@ class Store {
|
|
|
852
891
|
ensurePrivateStorePath(file);
|
|
853
892
|
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
|
|
854
893
|
this.db = new Database(file);
|
|
894
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
855
895
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
856
896
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
857
897
|
if (file !== ":memory:")
|
|
@@ -915,6 +955,7 @@ class Store {
|
|
|
915
955
|
);
|
|
916
956
|
CREATE INDEX IF NOT EXISTS idx_runs_loop ON loop_runs(loop_id, created_at);
|
|
917
957
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
|
|
958
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
|
|
918
959
|
CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
|
|
919
960
|
|
|
920
961
|
CREATE TABLE IF NOT EXISTS daemon_lease (
|
|
@@ -1242,39 +1283,51 @@ class Store {
|
|
|
1242
1283
|
}
|
|
1243
1284
|
return rows.map(rowToLoop);
|
|
1244
1285
|
}
|
|
1245
|
-
dueLoops(now) {
|
|
1286
|
+
dueLoops(now, limit = 500) {
|
|
1246
1287
|
const rows = this.db.query(`SELECT * FROM loops
|
|
1247
1288
|
WHERE status = 'active'
|
|
1248
1289
|
AND archived_at IS NULL
|
|
1249
1290
|
AND next_run_at IS NOT NULL
|
|
1250
1291
|
AND next_run_at <= ?
|
|
1251
|
-
ORDER BY next_run_at ASC
|
|
1292
|
+
ORDER BY next_run_at ASC
|
|
1293
|
+
LIMIT ?`).all(now.toISOString(), limit);
|
|
1252
1294
|
return rows.map(rowToLoop);
|
|
1253
1295
|
}
|
|
1254
1296
|
updateLoop(id, patch, opts = {}) {
|
|
1255
|
-
const current = this.getLoop(id);
|
|
1256
|
-
if (!current)
|
|
1257
|
-
throw new Error(`loop not found: ${id}`);
|
|
1258
1297
|
const updated = (opts.now ?? new Date).toISOString();
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1298
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1299
|
+
try {
|
|
1300
|
+
const current = this.getLoop(id);
|
|
1301
|
+
if (!current)
|
|
1302
|
+
throw new Error(`loop not found: ${id}`);
|
|
1303
|
+
const merged = { ...current, ...patch, updatedAt: updated };
|
|
1304
|
+
const res = this.db.query(`UPDATE loops SET status=$status, next_run_at=$nextRun, retry_scheduled_for=$retrySlot,
|
|
1305
|
+
expires_at=$expiresAt, updated_at=$updated
|
|
1306
|
+
WHERE id=$id
|
|
1307
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1308
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1309
|
+
))`).run({
|
|
1310
|
+
$id: id,
|
|
1311
|
+
$status: merged.status,
|
|
1312
|
+
$nextRun: merged.nextRunAt ?? null,
|
|
1313
|
+
$retrySlot: merged.retryScheduledFor ?? null,
|
|
1314
|
+
$expiresAt: merged.expiresAt ?? null,
|
|
1315
|
+
$updated: merged.updatedAt,
|
|
1316
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1317
|
+
$now: updated
|
|
1318
|
+
});
|
|
1319
|
+
if (res.changes !== 1)
|
|
1320
|
+
throw new Error("daemon lease lost");
|
|
1321
|
+
if (patch.status && patch.status !== "active") {
|
|
1322
|
+
const status = patch.status === "paused" ? "deferred" : "cancelled";
|
|
1323
|
+
this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
|
|
1324
|
+
}
|
|
1325
|
+
this.db.exec("COMMIT");
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
try {
|
|
1328
|
+
this.db.exec("ROLLBACK");
|
|
1329
|
+
} catch {}
|
|
1330
|
+
throw error;
|
|
1278
1331
|
}
|
|
1279
1332
|
const after = this.getLoop(id);
|
|
1280
1333
|
if (!after)
|
|
@@ -1392,10 +1445,23 @@ class Store {
|
|
|
1392
1445
|
})();
|
|
1393
1446
|
}
|
|
1394
1447
|
listWorkflows(opts = {}) {
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1448
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
1449
|
+
let rows;
|
|
1450
|
+
if (opts.status && opts.limit !== undefined) {
|
|
1451
|
+
rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?").all(opts.status, opts.limit, offset);
|
|
1452
|
+
} else if (opts.status) {
|
|
1453
|
+
rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT -1 OFFSET ?").all(opts.status, offset);
|
|
1454
|
+
} else if (opts.limit !== undefined) {
|
|
1455
|
+
rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ? OFFSET ?").all(opts.limit, offset);
|
|
1456
|
+
} else {
|
|
1457
|
+
rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT -1 OFFSET ?").all(offset);
|
|
1458
|
+
}
|
|
1397
1459
|
return rows.map(rowToWorkflow);
|
|
1398
1460
|
}
|
|
1461
|
+
countWorkflows(opts = {}) {
|
|
1462
|
+
const row = opts.status ? this.db.query("SELECT COUNT(*) AS count FROM workflow_specs WHERE status = ?").get(opts.status) : this.db.query("SELECT COUNT(*) AS count FROM workflow_specs").get();
|
|
1463
|
+
return row?.count ?? 0;
|
|
1464
|
+
}
|
|
1399
1465
|
archiveWorkflow(idOrName) {
|
|
1400
1466
|
const workflow = this.requireWorkflow(idOrName);
|
|
1401
1467
|
const updated = nowIso();
|
|
@@ -1405,6 +1471,64 @@ class Store {
|
|
|
1405
1471
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
1406
1472
|
return archived;
|
|
1407
1473
|
}
|
|
1474
|
+
generatedRouteArchiveContext(args) {
|
|
1475
|
+
if (!args.loopId || !args.workItemId)
|
|
1476
|
+
return;
|
|
1477
|
+
const workItem = this.getWorkflowWorkItem(args.workItemId);
|
|
1478
|
+
if (!workItem || !GENERATED_ROUTE_KEYS.has(workItem.routeKey))
|
|
1479
|
+
return;
|
|
1480
|
+
const invocation = this.getWorkflowInvocation(workItem.invocationId);
|
|
1481
|
+
if (!invocation?.templateId || !GENERATED_ROUTE_TEMPLATE_IDS.has(invocation.templateId))
|
|
1482
|
+
return;
|
|
1483
|
+
const loop = this.getLoop(args.loopId);
|
|
1484
|
+
if (!loop || loop.schedule.type !== "once" || loop.target.type !== "workflow" || loop.target.workflowId !== args.workflowId)
|
|
1485
|
+
return;
|
|
1486
|
+
const input = loop.target.input ?? {};
|
|
1487
|
+
if (input.workflowWorkItemId && input.workflowWorkItemId !== workItem.id)
|
|
1488
|
+
return;
|
|
1489
|
+
if (input.workflowInvocationId && input.workflowInvocationId !== invocation.id)
|
|
1490
|
+
return;
|
|
1491
|
+
if (workItem.workflowId && workItem.workflowId !== args.workflowId)
|
|
1492
|
+
return;
|
|
1493
|
+
const workflow = this.getWorkflow(args.workflowId);
|
|
1494
|
+
if (!workflow)
|
|
1495
|
+
return;
|
|
1496
|
+
return { workflow, loop, workItem, invocation };
|
|
1497
|
+
}
|
|
1498
|
+
maybeArchiveGeneratedRouteWorkflow(args) {
|
|
1499
|
+
const context = this.generatedRouteArchiveContext(args);
|
|
1500
|
+
if (!context)
|
|
1501
|
+
return;
|
|
1502
|
+
const { workflow, loop, workItem } = context;
|
|
1503
|
+
if (!workflow || workflow.status !== "active")
|
|
1504
|
+
return;
|
|
1505
|
+
const nonTerminal = this.db.query(`SELECT COUNT(*) AS count FROM workflow_runs
|
|
1506
|
+
WHERE workflow_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).get(args.workflowId)?.count ?? 0;
|
|
1507
|
+
if (nonTerminal > 0)
|
|
1508
|
+
return;
|
|
1509
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(args.updated, args.workflowId);
|
|
1510
|
+
if (res.changes === 1 && args.workflowRunId) {
|
|
1511
|
+
this.appendWorkflowEvent(args.workflowRunId, "workflow_archived", undefined, {
|
|
1512
|
+
workflowId: args.workflowId,
|
|
1513
|
+
loopId: loop.id,
|
|
1514
|
+
workItemId: workItem.id,
|
|
1515
|
+
routeKey: workItem.routeKey,
|
|
1516
|
+
reason: "terminal generated one-shot route workflow"
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, updated) {
|
|
1521
|
+
const run = this.getWorkflowRun(workflowRunId);
|
|
1522
|
+
if (!run)
|
|
1523
|
+
return;
|
|
1524
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
1525
|
+
workflowId: run.workflowId,
|
|
1526
|
+
loopId: run.loopId,
|
|
1527
|
+
workItemId: run.workItemId,
|
|
1528
|
+
workflowRunId,
|
|
1529
|
+
updated
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1408
1532
|
createWorkflowInvocation(input) {
|
|
1409
1533
|
const now = nowIso();
|
|
1410
1534
|
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
@@ -2190,6 +2314,7 @@ class Store {
|
|
|
2190
2314
|
if (changed) {
|
|
2191
2315
|
const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
|
|
2192
2316
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
|
|
2317
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, finishedAt);
|
|
2193
2318
|
}
|
|
2194
2319
|
this.db.exec("COMMIT");
|
|
2195
2320
|
} catch (error) {
|
|
@@ -2217,6 +2342,7 @@ class Store {
|
|
|
2217
2342
|
WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
|
|
2218
2343
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
|
|
2219
2344
|
this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
|
|
2345
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, now);
|
|
2220
2346
|
}
|
|
2221
2347
|
this.db.exec("COMMIT");
|
|
2222
2348
|
return this.requireWorkflowRun(workflowRunId);
|
|
@@ -2469,8 +2595,20 @@ class Store {
|
|
|
2469
2595
|
throw new Error(`run not found after finalize: ${id}`);
|
|
2470
2596
|
if (opts.claimedBy && res.changes !== 1)
|
|
2471
2597
|
return run;
|
|
2472
|
-
if (res.changes === 1)
|
|
2598
|
+
if (res.changes === 1) {
|
|
2473
2599
|
this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
|
|
2600
|
+
const loop = this.getLoop(run.loopId);
|
|
2601
|
+
const itemStatus = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
|
|
2602
|
+
if (loop?.target.type === "workflow" && itemStatus && itemStatus !== "admitted") {
|
|
2603
|
+
const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
|
|
2604
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
2605
|
+
workflowId: loop.target.workflowId,
|
|
2606
|
+
loopId: loop.id,
|
|
2607
|
+
workItemId,
|
|
2608
|
+
updated: finishedAt
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2474
2612
|
return run;
|
|
2475
2613
|
}
|
|
2476
2614
|
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
|
|
@@ -2505,14 +2643,40 @@ class Store {
|
|
|
2505
2643
|
}
|
|
2506
2644
|
return rows.map(rowToRun);
|
|
2507
2645
|
}
|
|
2646
|
+
deferLiveExpiredRun(id, now, opts = {}) {
|
|
2647
|
+
const updated = now.toISOString();
|
|
2648
|
+
const deferredUntil = new Date(now.getTime() + LIVE_EXPIRED_RUN_GRACE_MS).toISOString();
|
|
2649
|
+
this.db.query(`UPDATE loop_runs SET lease_expires_at=$deferredUntil, updated_at=$updated
|
|
2650
|
+
WHERE id=$id AND status='running' AND lease_expires_at <= $now
|
|
2651
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
2652
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
2653
|
+
))`).run({
|
|
2654
|
+
$id: id,
|
|
2655
|
+
$deferredUntil: deferredUntil,
|
|
2656
|
+
$updated: updated,
|
|
2657
|
+
$now: updated,
|
|
2658
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2508
2661
|
recoverExpiredRunLeases(now = new Date, opts = {}) {
|
|
2509
|
-
const
|
|
2662
|
+
const limit = Math.max(1, Math.min(1000, Math.floor(opts.limit ?? DEFAULT_RECOVERY_BATCH_LIMIT)));
|
|
2663
|
+
const scanLimit = Math.max(limit, Math.min(5000, Math.floor(opts.scanLimit ?? limit * DEFAULT_RECOVERY_SCAN_MULTIPLIER)));
|
|
2664
|
+
const rows = this.db.query(`SELECT * FROM loop_runs
|
|
2665
|
+
WHERE status = 'running' AND lease_expires_at <= ?
|
|
2666
|
+
ORDER BY lease_expires_at ASC
|
|
2667
|
+
LIMIT ?`).all(now.toISOString(), scanLimit);
|
|
2510
2668
|
const recovered = [];
|
|
2511
2669
|
for (const row of rows) {
|
|
2512
|
-
if (
|
|
2670
|
+
if (recovered.length >= limit)
|
|
2671
|
+
break;
|
|
2672
|
+
if (row.pid && isProcessAlive(row.pid)) {
|
|
2673
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2513
2674
|
continue;
|
|
2514
|
-
|
|
2675
|
+
}
|
|
2676
|
+
if (this.hasLiveWorkflowStepProcesses(row.id)) {
|
|
2677
|
+
this.deferLiveExpiredRun(row.id, now, opts);
|
|
2515
2678
|
continue;
|
|
2679
|
+
}
|
|
2516
2680
|
const finished = now.toISOString();
|
|
2517
2681
|
this.db.exec("BEGIN IMMEDIATE");
|
|
2518
2682
|
try {
|
|
@@ -2565,6 +2729,7 @@ class Store {
|
|
|
2565
2729
|
loopRunId: row.id
|
|
2566
2730
|
});
|
|
2567
2731
|
this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
|
|
2732
|
+
this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRow.id, finished);
|
|
2568
2733
|
}
|
|
2569
2734
|
const loop = this.getLoop(row.loop_id);
|
|
2570
2735
|
const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
|
|
@@ -2572,6 +2737,15 @@ class Store {
|
|
|
2572
2737
|
const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
2573
2738
|
const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
|
|
2574
2739
|
this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
|
|
2740
|
+
if (loop?.target.type === "workflow" && itemStatus !== "admitted") {
|
|
2741
|
+
const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
|
|
2742
|
+
this.maybeArchiveGeneratedRouteWorkflow({
|
|
2743
|
+
workflowId: loop.target.workflowId,
|
|
2744
|
+
loopId: loop.id,
|
|
2745
|
+
workItemId,
|
|
2746
|
+
updated: finished
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2575
2749
|
}
|
|
2576
2750
|
this.db.exec("COMMIT");
|
|
2577
2751
|
} catch (error) {
|
|
@@ -3025,6 +3199,7 @@ var AUTH_ENV_KEYS = [
|
|
|
3025
3199
|
"CODEWITH_HOME",
|
|
3026
3200
|
"CODEX_HOME",
|
|
3027
3201
|
"CURSOR_CONFIG_DIR",
|
|
3202
|
+
"CURSOR_API_KEY",
|
|
3028
3203
|
"OPENCODE_CONFIG_DIR",
|
|
3029
3204
|
"AICOPILOT_CONFIG_DIR",
|
|
3030
3205
|
"ANTHROPIC_API_KEY",
|
|
@@ -3154,6 +3329,10 @@ function assertSupportedAgentOptions(target) {
|
|
|
3154
3329
|
assertStringOption(target.model, `${target.provider}.model`);
|
|
3155
3330
|
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
3156
3331
|
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
3332
|
+
assertStringOption(target.configIsolation, `${target.provider}.configIsolation`);
|
|
3333
|
+
if (target.configIsolation !== undefined && target.configIsolation !== "safe" && target.configIsolation !== "none") {
|
|
3334
|
+
throw new Error(`${target.provider}.configIsolation must be safe or none`);
|
|
3335
|
+
}
|
|
3157
3336
|
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
3158
3337
|
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
3159
3338
|
}
|
|
@@ -3166,6 +3345,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3166
3345
|
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
3167
3346
|
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
3168
3347
|
}
|
|
3348
|
+
if (target.provider === "codex" && target.agent !== undefined)
|
|
3349
|
+
throw new Error("codex.agent is not supported");
|
|
3169
3350
|
if (["codewith", "codex"].includes(target.provider)) {
|
|
3170
3351
|
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
3171
3352
|
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
@@ -3180,6 +3361,8 @@ function assertSupportedAgentOptions(target) {
|
|
|
3180
3361
|
return;
|
|
3181
3362
|
}
|
|
3182
3363
|
if (target.provider === "cursor") {
|
|
3364
|
+
if (target.variant !== undefined)
|
|
3365
|
+
throw new Error("cursor.variant is not supported");
|
|
3183
3366
|
if (target.permissionMode === "auto")
|
|
3184
3367
|
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
3185
3368
|
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
@@ -3221,10 +3404,8 @@ function agentArgs(target) {
|
|
|
3221
3404
|
"set -eu",
|
|
3222
3405
|
"if command -v agent >/dev/null 2>&1; then",
|
|
3223
3406
|
' exec agent "$@"',
|
|
3224
|
-
"elif command -v cursor >/dev/null 2>&1; then",
|
|
3225
|
-
' exec cursor agent "$@"',
|
|
3226
3407
|
"else",
|
|
3227
|
-
" echo 'Executable not found in PATH:
|
|
3408
|
+
" echo 'Executable not found in PATH: agent' >&2",
|
|
3228
3409
|
" exit 127",
|
|
3229
3410
|
"fi"
|
|
3230
3411
|
].join(`
|
|
@@ -3265,7 +3446,7 @@ function agentArgs(target) {
|
|
|
3265
3446
|
case "codex":
|
|
3266
3447
|
if (target.variant)
|
|
3267
3448
|
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
3268
|
-
args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3449
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
3269
3450
|
if (isolation === "safe")
|
|
3270
3451
|
args.push("--ignore-rules");
|
|
3271
3452
|
if (target.cwd)
|
|
@@ -3335,7 +3516,7 @@ function commandSpec(target) {
|
|
|
3335
3516
|
account: agentTarget.account,
|
|
3336
3517
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
3337
3518
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
3338
|
-
preflightAnyOf: agentTarget.provider === "cursor" ? ["
|
|
3519
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["agent"] : undefined,
|
|
3339
3520
|
stdin: agentTarget.prompt,
|
|
3340
3521
|
allowlist: agentTarget.allowlist
|
|
3341
3522
|
};
|
|
@@ -4718,12 +4899,14 @@ function claimDueRuns(deps) {
|
|
|
4718
4899
|
const claimed = [];
|
|
4719
4900
|
const skipped = [];
|
|
4720
4901
|
const maxClaims = Math.max(0, deps.maxClaims ?? Number.POSITIVE_INFINITY);
|
|
4902
|
+
if (maxClaims === 0)
|
|
4903
|
+
return { claims, claimed, completed: [], skipped, recovered, expired };
|
|
4721
4904
|
for (const loop of deps.store.dueLoops(now)) {
|
|
4722
|
-
if (claims.length >= maxClaims)
|
|
4905
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4723
4906
|
break;
|
|
4724
4907
|
const plan = dueSlots(loop, now);
|
|
4725
4908
|
for (const slot of plan.slots) {
|
|
4726
|
-
if (claims.length >= maxClaims)
|
|
4909
|
+
if (claims.length + skipped.length >= maxClaims)
|
|
4727
4910
|
break;
|
|
4728
4911
|
const run = claimSlot(deps, loop, slot);
|
|
4729
4912
|
if (!run)
|
|
@@ -5179,35 +5362,17 @@ import { spawnSync as spawnSync4 } from "child_process";
|
|
|
5179
5362
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
5180
5363
|
var PROVIDER_COMMANDS = [
|
|
5181
5364
|
"claude",
|
|
5182
|
-
"
|
|
5365
|
+
"agent",
|
|
5183
5366
|
"codewith",
|
|
5184
5367
|
"aicopilot",
|
|
5185
5368
|
"opencode",
|
|
5186
5369
|
"codex"
|
|
5187
5370
|
];
|
|
5188
5371
|
function hasCommand(command) {
|
|
5189
|
-
if (command === "cursor agent") {
|
|
5190
|
-
return hasCommand("cursor") || hasCommand("agent");
|
|
5191
|
-
}
|
|
5192
5372
|
const result = spawnSync4("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
5193
5373
|
return (result.status ?? 1) === 0;
|
|
5194
5374
|
}
|
|
5195
5375
|
function commandVersion(command) {
|
|
5196
|
-
if (command === "cursor agent") {
|
|
5197
|
-
const cursorResult = spawnSync4("cursor", ["agent", "--version"], {
|
|
5198
|
-
encoding: "utf8",
|
|
5199
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5200
|
-
});
|
|
5201
|
-
if ((cursorResult.status ?? 1) === 0)
|
|
5202
|
-
return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
|
|
5203
|
-
const agentResult = spawnSync4("agent", ["--version"], {
|
|
5204
|
-
encoding: "utf8",
|
|
5205
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5206
|
-
});
|
|
5207
|
-
if ((agentResult.status ?? 1) === 0)
|
|
5208
|
-
return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
|
|
5209
|
-
return;
|
|
5210
|
-
}
|
|
5211
5376
|
const result = spawnSync4(command, ["--version"], {
|
|
5212
5377
|
encoding: "utf8",
|
|
5213
5378
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -5743,7 +5908,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5743
5908
|
// package.json
|
|
5744
5909
|
var package_default = {
|
|
5745
5910
|
name: "@hasna/loops",
|
|
5746
|
-
version: "0.3.
|
|
5911
|
+
version: "0.3.49",
|
|
5747
5912
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5748
5913
|
type: "module",
|
|
5749
5914
|
main: "dist/index.js",
|
|
@@ -6834,6 +6999,13 @@ function validateLoopTargetForStorage(target, context) {
|
|
|
6834
6999
|
validationFailed(error, context);
|
|
6835
7000
|
}
|
|
6836
7001
|
}
|
|
7002
|
+
function normalizeWorkflowForStorage(body, context) {
|
|
7003
|
+
try {
|
|
7004
|
+
return workflowBodyFromJson(body);
|
|
7005
|
+
} catch (error) {
|
|
7006
|
+
validationFailed(error, context);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
6837
7009
|
function preflightLoopTarget(target, context, metadata, opts) {
|
|
6838
7010
|
try {
|
|
6839
7011
|
return preflightTarget(target, metadata, opts);
|
|
@@ -6886,6 +7058,14 @@ function positiveInteger(raw, label) {
|
|
|
6886
7058
|
throw new Error(`${label} must be a positive integer`);
|
|
6887
7059
|
return value;
|
|
6888
7060
|
}
|
|
7061
|
+
function nonNegativeInteger(raw, label) {
|
|
7062
|
+
if (raw === undefined)
|
|
7063
|
+
return;
|
|
7064
|
+
const value = Number(raw);
|
|
7065
|
+
if (!Number.isInteger(value) || value < 0)
|
|
7066
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
7067
|
+
return value;
|
|
7068
|
+
}
|
|
6889
7069
|
function positiveDuration(raw, label) {
|
|
6890
7070
|
if (raw === undefined)
|
|
6891
7071
|
return;
|
|
@@ -7367,6 +7547,23 @@ function taskRouteEligibility(data, metadata) {
|
|
|
7367
7547
|
}
|
|
7368
7548
|
return { eligible: true, tags };
|
|
7369
7549
|
}
|
|
7550
|
+
function skippedDrainTask(task, event, reason) {
|
|
7551
|
+
const taskId = taskField(task, ["id", "task_id", "taskId"]) ?? event?.subject ?? "unknown";
|
|
7552
|
+
return {
|
|
7553
|
+
kind: "skipped",
|
|
7554
|
+
value: {
|
|
7555
|
+
skipped: true,
|
|
7556
|
+
reason,
|
|
7557
|
+
taskId,
|
|
7558
|
+
event,
|
|
7559
|
+
routeError: true
|
|
7560
|
+
},
|
|
7561
|
+
human: `skipped task ${taskId}: ${reason}`
|
|
7562
|
+
};
|
|
7563
|
+
}
|
|
7564
|
+
function isSkippableDrainRouteError(message) {
|
|
7565
|
+
return message.startsWith("worktreeMode=required but projectPath is not an existing git repository:");
|
|
7566
|
+
}
|
|
7370
7567
|
function generatedRouteSandboxPreflight(workflow) {
|
|
7371
7568
|
const checks = [];
|
|
7372
7569
|
for (const step of workflow.steps) {
|
|
@@ -7533,7 +7730,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
7533
7730
|
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
|
|
7534
7731
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
7535
7732
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
7536
|
-
const idempotencyKey = `todos-task:${taskId}
|
|
7733
|
+
const idempotencyKey = `todos-task:${taskId}`;
|
|
7537
7734
|
const idempotencySuffix = stableSuffix(idempotencyKey);
|
|
7538
7735
|
const namePrefix = opts.namePrefix ?? "event:todos-task";
|
|
7539
7736
|
const workflowName = `${namePrefix}:${taskId.slice(0, 8)}:${idempotencySuffix}:workflow`;
|
|
@@ -7571,7 +7768,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
7571
7768
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
7572
7769
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
7573
7770
|
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
7574
|
-
|
|
7771
|
+
let workflowBody = renderTodosTaskWorkerVerifierWorkflow({
|
|
7575
7772
|
taskId,
|
|
7576
7773
|
taskTitle,
|
|
7577
7774
|
taskDescription,
|
|
@@ -7603,6 +7800,11 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
7603
7800
|
});
|
|
7604
7801
|
workflowBody.name = workflowName;
|
|
7605
7802
|
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
|
|
7803
|
+
workflowBody = normalizeWorkflowForStorage(workflowBody, {
|
|
7804
|
+
name: workflowName,
|
|
7805
|
+
type: "todos-task-event-workflow",
|
|
7806
|
+
event: event.id
|
|
7807
|
+
});
|
|
7606
7808
|
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
7607
7809
|
const invocationInput = {
|
|
7608
7810
|
templateId: "todos-task-worker-verifier",
|
|
@@ -7782,7 +7984,7 @@ function routeGenericEvent(event, opts) {
|
|
|
7782
7984
|
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode ?? "bypass" }, provider);
|
|
7783
7985
|
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
7784
7986
|
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
7785
|
-
|
|
7987
|
+
let workflowBody = renderEventWorkerVerifierWorkflow({
|
|
7786
7988
|
eventId: event.id,
|
|
7787
7989
|
eventType: event.type,
|
|
7788
7990
|
eventSource: event.source,
|
|
@@ -7814,6 +8016,11 @@ function routeGenericEvent(event, opts) {
|
|
|
7814
8016
|
});
|
|
7815
8017
|
workflowBody.name = workflowName;
|
|
7816
8018
|
workflowBody.description = `Event-triggered worker/verifier workflow for ${event.source}/${event.type}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
|
|
8019
|
+
workflowBody = normalizeWorkflowForStorage(workflowBody, {
|
|
8020
|
+
name: workflowName,
|
|
8021
|
+
type: "generic-event-workflow",
|
|
8022
|
+
event: event.id
|
|
8023
|
+
});
|
|
7817
8024
|
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
7818
8025
|
const invocationInput = {
|
|
7819
8026
|
templateId: "event-worker-verifier",
|
|
@@ -8230,8 +8437,12 @@ var goal = program.command("goal").description("inspect goal runs");
|
|
|
8230
8437
|
function addRouteEventOptions(command) {
|
|
8231
8438
|
return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
|
|
8232
8439
|
}
|
|
8233
|
-
function addTodosDrainOptions(command) {
|
|
8234
|
-
|
|
8440
|
+
function addTodosDrainOptions(command, opts = {}) {
|
|
8441
|
+
let configured = command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", opts.preflightDescription ?? "check generated workflow steps before storing workflow loops");
|
|
8442
|
+
if (opts.includeDryRun ?? true) {
|
|
8443
|
+
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
8444
|
+
}
|
|
8445
|
+
return configured;
|
|
8235
8446
|
}
|
|
8236
8447
|
function routeEventByKind(kind, event, opts) {
|
|
8237
8448
|
if (kind === "todos-task")
|
|
@@ -8312,8 +8523,17 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8312
8523
|
for (const task of candidates) {
|
|
8313
8524
|
if (created >= maxDispatch)
|
|
8314
8525
|
break;
|
|
8315
|
-
|
|
8316
|
-
|
|
8526
|
+
let event;
|
|
8527
|
+
let result;
|
|
8528
|
+
try {
|
|
8529
|
+
event = taskDrainEvent(task);
|
|
8530
|
+
result = routeTodosTaskEvent(event, opts);
|
|
8531
|
+
} catch (error) {
|
|
8532
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8533
|
+
if (!isSkippableDrainRouteError(message))
|
|
8534
|
+
throw error;
|
|
8535
|
+
result = skippedDrainTask(task, event, redact(message, 640) ?? "route task failed");
|
|
8536
|
+
}
|
|
8317
8537
|
results.push(result);
|
|
8318
8538
|
if (result.kind === "created" && !opts.dryRun)
|
|
8319
8539
|
created += 1;
|
|
@@ -8374,6 +8594,57 @@ function drainTodosTaskRoutes(opts) {
|
|
|
8374
8594
|
} : { ...report, evidencePath };
|
|
8375
8595
|
print(output, `drained todos ready queue: considered=${report.considered} created=${report.created} deduped=${report.deduped} throttled=${report.throttled} skipped=${report.skipped}`);
|
|
8376
8596
|
}
|
|
8597
|
+
function formatTemplateVariable(template, name) {
|
|
8598
|
+
const variable = template.variables.find((entry) => entry.name === name);
|
|
8599
|
+
const placeholder = variable?.default ? variable.default : `<${name}>`;
|
|
8600
|
+
return ` --var ${name}=${placeholder}`;
|
|
8601
|
+
}
|
|
8602
|
+
function printTemplateDetails(template) {
|
|
8603
|
+
console.log(`${template.id} (${template.kind})`);
|
|
8604
|
+
console.log(template.name);
|
|
8605
|
+
console.log("");
|
|
8606
|
+
console.log(template.description);
|
|
8607
|
+
console.log("");
|
|
8608
|
+
console.log("Variables:");
|
|
8609
|
+
const nameWidth = Math.max(...template.variables.map((variable) => variable.name.length), 4);
|
|
8610
|
+
for (const variable of template.variables) {
|
|
8611
|
+
const required = variable.required ? "required" : "optional";
|
|
8612
|
+
const defaultValue = variable.default ? ` default=${variable.default}` : "";
|
|
8613
|
+
const description = variable.description ? ` ${variable.description}` : "";
|
|
8614
|
+
console.log(` ${variable.name.padEnd(nameWidth)} ${required}${defaultValue}${description}`);
|
|
8615
|
+
}
|
|
8616
|
+
const requiredVariables = template.variables.filter((variable) => variable.required).map((variable) => variable.name);
|
|
8617
|
+
const hintVariables = requiredVariables.length ? requiredVariables : template.variables.slice(0, 2).map((variable) => variable.name);
|
|
8618
|
+
const renderArgs = hintVariables.map((name) => formatTemplateVariable(template, name));
|
|
8619
|
+
const renderHint = renderArgs.length ? ` \\
|
|
8620
|
+
${renderArgs.join(" \\\n")}` : "";
|
|
8621
|
+
console.log("");
|
|
8622
|
+
console.log("Usage:");
|
|
8623
|
+
console.log(` loops templates render ${template.id}${renderHint}`);
|
|
8624
|
+
if (template.kind === "workflow")
|
|
8625
|
+
console.log(` loops templates create-workflow ${template.id}${renderHint}`);
|
|
8626
|
+
}
|
|
8627
|
+
function workflowStatusFromOpts(status, all) {
|
|
8628
|
+
if (all) {
|
|
8629
|
+
if (status && status !== "active")
|
|
8630
|
+
throw new Error("use either --all or --status, not both");
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
const value = status ?? "active";
|
|
8634
|
+
if (value === "all")
|
|
8635
|
+
return;
|
|
8636
|
+
if (value === "active" || value === "archived")
|
|
8637
|
+
return value;
|
|
8638
|
+
throw new Error("--status must be active, archived, or all");
|
|
8639
|
+
}
|
|
8640
|
+
function printWorkflowListWarning(args) {
|
|
8641
|
+
if (args.shown + args.offset >= args.total && args.offset === 0)
|
|
8642
|
+
return;
|
|
8643
|
+
const scope = args.status ?? "all";
|
|
8644
|
+
const nextOffset = args.offset + args.shown;
|
|
8645
|
+
const next = args.limit && nextOffset < args.total ? ` next page: --limit ${args.limit} --offset ${nextOffset}` : "";
|
|
8646
|
+
console.error(`showing ${args.offset + args.shown} of ${args.total} ${scope} workflows.${next}`);
|
|
8647
|
+
}
|
|
8377
8648
|
templates.command("list").alias("ls").description("list built-in OpenLoops templates").action(() => {
|
|
8378
8649
|
const values = listLoopTemplates();
|
|
8379
8650
|
if (isJson())
|
|
@@ -8388,7 +8659,10 @@ templates.command("show <id>").description("show a built-in template").action((i
|
|
|
8388
8659
|
const template = getLoopTemplate(id);
|
|
8389
8660
|
if (!template)
|
|
8390
8661
|
throw new Error(`template not found: ${id}`);
|
|
8391
|
-
|
|
8662
|
+
if (isJson())
|
|
8663
|
+
print(template);
|
|
8664
|
+
else
|
|
8665
|
+
printTemplateDetails(template);
|
|
8392
8666
|
});
|
|
8393
8667
|
templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, []).action((id, opts) => {
|
|
8394
8668
|
const workflow = renderLoopTemplate(id, parseVars(opts.var));
|
|
@@ -8472,7 +8746,10 @@ addTodosDrainOptions(routes.command("drain <kind>").description("drain a durable
|
|
|
8472
8746
|
throw new Error("route drain currently supports kind todos-task");
|
|
8473
8747
|
drainTodosTaskRoutes(opts);
|
|
8474
8748
|
});
|
|
8475
|
-
addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>").description("schedule a deterministic route drain loop")
|
|
8749
|
+
addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>").description("schedule a deterministic route drain loop"), {
|
|
8750
|
+
includeDryRun: false,
|
|
8751
|
+
preflightDescription: "forward generated workflow preflight checks into each future drain run"
|
|
8752
|
+
})).action((kind, name, opts) => {
|
|
8476
8753
|
if (kind !== "todos-task")
|
|
8477
8754
|
throw new Error("route schedule currently supports kind todos-task");
|
|
8478
8755
|
const store = new Store;
|
|
@@ -8499,288 +8776,13 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
8499
8776
|
print(result.value, result.human);
|
|
8500
8777
|
});
|
|
8501
8778
|
var eventsDrain = events.command("drain").description("drain durable source queues into bounded OpenLoops workflows");
|
|
8502
|
-
eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")
|
|
8503
|
-
|
|
8504
|
-
const todosProject = opts.todosProject ?? defaultLoopsProject();
|
|
8505
|
-
const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
|
|
8506
|
-
const taskListFilter = resolveTaskListFilter(todosProject, opts.taskList);
|
|
8507
|
-
const candidateLimit = positiveInteger(opts.limit ?? "50", "--limit") ?? 50;
|
|
8508
|
-
const hasPostFilters = Boolean(opts.todosProjectId || taskListFilter || opts.projectPathPrefix || requiredTags.length);
|
|
8509
|
-
const defaultScanLimit = hasPostFilters ? Math.max(candidateLimit, 500) : candidateLimit;
|
|
8510
|
-
const scanLimit = positiveInteger(opts.scanLimit ?? String(defaultScanLimit), "--scan-limit") ?? defaultScanLimit;
|
|
8511
|
-
const ready = loadReadyTodosTasks(opts, scanLimit);
|
|
8512
|
-
const filteredCandidates = ready.filter((task) => taskMatchesDrainFilters(task, {
|
|
8513
|
-
projectId: opts.todosProjectId,
|
|
8514
|
-
taskListId: taskListFilter,
|
|
8515
|
-
projectPathPrefix: opts.projectPathPrefix,
|
|
8516
|
-
tags: requiredTags
|
|
8517
|
-
}));
|
|
8518
|
-
const candidates = filteredCandidates.slice(0, candidateLimit);
|
|
8519
|
-
const results = [];
|
|
8520
|
-
let created = 0;
|
|
8521
|
-
for (const task of candidates) {
|
|
8522
|
-
if (created >= maxDispatch)
|
|
8523
|
-
break;
|
|
8524
|
-
const event = taskDrainEvent(task);
|
|
8525
|
-
const result = routeTodosTaskEvent(event, opts);
|
|
8526
|
-
results.push(result);
|
|
8527
|
-
if (result.kind === "created" && !opts.dryRun)
|
|
8528
|
-
created += 1;
|
|
8529
|
-
if (result.kind === "created" && opts.dryRun)
|
|
8530
|
-
created += 1;
|
|
8531
|
-
}
|
|
8532
|
-
const report = {
|
|
8533
|
-
drainedAt: new Date().toISOString(),
|
|
8534
|
-
todosProject,
|
|
8535
|
-
todosProjectId: opts.todosProjectId,
|
|
8536
|
-
taskList: opts.taskList,
|
|
8537
|
-
taskListId: taskListFilter,
|
|
8538
|
-
projectPathPrefix: opts.projectPathPrefix,
|
|
8539
|
-
tags: requiredTags,
|
|
8540
|
-
limit: candidateLimit,
|
|
8541
|
-
scanLimit,
|
|
8542
|
-
filtersApplied: hasPostFilters,
|
|
8543
|
-
scanned: ready.length,
|
|
8544
|
-
candidates: candidates.length,
|
|
8545
|
-
filteredCandidates: filteredCandidates.length,
|
|
8546
|
-
scanExhausted: ready.length >= scanLimit && filteredCandidates.length < candidateLimit,
|
|
8547
|
-
considered: results.length,
|
|
8548
|
-
created: results.filter((result) => result.kind === "created" && !result.value.deduped).length,
|
|
8549
|
-
deduped: results.filter((result) => result.kind === "deduped").length,
|
|
8550
|
-
throttled: results.filter((result) => result.kind === "throttled").length,
|
|
8551
|
-
skipped: results.filter((result) => result.kind === "skipped").length,
|
|
8552
|
-
maxDispatch,
|
|
8553
|
-
source: "todos ready",
|
|
8554
|
-
dryRun: Boolean(opts.dryRun),
|
|
8555
|
-
results: results.map((result) => ({ kind: result.kind, ...result.value }))
|
|
8556
|
-
};
|
|
8557
|
-
const evidencePath = writeRouteEvidence("todos-task-drain", report, opts.evidenceDir);
|
|
8558
|
-
const output = opts.compact ? {
|
|
8559
|
-
drainedAt: report.drainedAt,
|
|
8560
|
-
todosProject: report.todosProject,
|
|
8561
|
-
todosProjectId: report.todosProjectId,
|
|
8562
|
-
taskList: report.taskList,
|
|
8563
|
-
taskListId: report.taskListId,
|
|
8564
|
-
projectPathPrefix: report.projectPathPrefix,
|
|
8565
|
-
tags: report.tags,
|
|
8566
|
-
limit: report.limit,
|
|
8567
|
-
scanLimit: report.scanLimit,
|
|
8568
|
-
filtersApplied: report.filtersApplied,
|
|
8569
|
-
scanned: report.scanned,
|
|
8570
|
-
candidates: report.candidates,
|
|
8571
|
-
filteredCandidates: report.filteredCandidates,
|
|
8572
|
-
scanExhausted: report.scanExhausted,
|
|
8573
|
-
considered: report.considered,
|
|
8574
|
-
created: report.created,
|
|
8575
|
-
deduped: report.deduped,
|
|
8576
|
-
throttled: report.throttled,
|
|
8577
|
-
skipped: report.skipped,
|
|
8578
|
-
maxDispatch: report.maxDispatch,
|
|
8579
|
-
source: report.source,
|
|
8580
|
-
dryRun: report.dryRun,
|
|
8581
|
-
evidencePath,
|
|
8582
|
-
results: results.map(compactDrainResult)
|
|
8583
|
-
} : { ...report, evidencePath };
|
|
8584
|
-
print(output, `drained todos ready queue: considered=${report.considered} created=${report.created} deduped=${report.deduped} throttled=${report.throttled} skipped=${report.skipped}`);
|
|
8779
|
+
addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
|
|
8780
|
+
drainTodosTaskRoutes(opts);
|
|
8585
8781
|
});
|
|
8586
8782
|
eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
8587
8783
|
const event = await readEventEnvelopeFromStdin();
|
|
8588
|
-
const
|
|
8589
|
-
|
|
8590
|
-
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? taskEventField(metadata, ["working_dir", "workingDir", "project_path", "projectPath", "project_canonical_path", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
|
|
8591
|
-
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
|
|
8592
|
-
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
8593
|
-
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
8594
|
-
const eventSuffix = event.id.slice(0, 8);
|
|
8595
|
-
const source = slugSegment2(event.source, "source");
|
|
8596
|
-
const type = slugSegment2(event.type, "type");
|
|
8597
|
-
const workflowName = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:workflow`;
|
|
8598
|
-
const loopName = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:run`;
|
|
8599
|
-
const idempotencyKey = `generic-event:${event.source}:${event.type}:${event.id}`;
|
|
8600
|
-
const provider = opts.provider;
|
|
8601
|
-
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
|
|
8602
|
-
throw new Error("unsupported provider");
|
|
8603
|
-
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode }, provider);
|
|
8604
|
-
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
8605
|
-
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
8606
|
-
const workflowBody = renderEventWorkerVerifierWorkflow({
|
|
8607
|
-
eventId: event.id,
|
|
8608
|
-
eventType: event.type,
|
|
8609
|
-
eventSource: event.source,
|
|
8610
|
-
eventSubject: stringField(event.subject),
|
|
8611
|
-
eventMessage: stringField(event.message),
|
|
8612
|
-
eventJson: JSON.stringify(event),
|
|
8613
|
-
projectPath,
|
|
8614
|
-
routeProjectPath,
|
|
8615
|
-
projectGroup,
|
|
8616
|
-
provider,
|
|
8617
|
-
authProfile,
|
|
8618
|
-
authProfilePool: splitList(opts.authProfilePool),
|
|
8619
|
-
workerAuthProfile: opts.workerAuthProfile,
|
|
8620
|
-
verifierAuthProfile: opts.verifierAuthProfile,
|
|
8621
|
-
account: accountFromOpts(opts),
|
|
8622
|
-
accountPool: accountPoolFromOpts(opts),
|
|
8623
|
-
workerAccount: roleAccountFromOpts(opts, opts.workerAccount),
|
|
8624
|
-
verifierAccount: roleAccountFromOpts(opts, opts.verifierAccount),
|
|
8625
|
-
model: opts.model,
|
|
8626
|
-
variant: opts.variant,
|
|
8627
|
-
agent: opts.agent,
|
|
8628
|
-
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
8629
|
-
permissionMode,
|
|
8630
|
-
sandbox,
|
|
8631
|
-
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8632
|
-
worktreeMode: opts.worktreeMode,
|
|
8633
|
-
worktreeRoot: opts.worktreeRoot,
|
|
8634
|
-
worktreeBranchPrefix: opts.worktreeBranchPrefix
|
|
8635
|
-
});
|
|
8636
|
-
workflowBody.name = workflowName;
|
|
8637
|
-
workflowBody.description = `Event-triggered worker/verifier workflow for ${event.source}/${event.type}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
|
|
8638
|
-
const sandboxPreflight = generatedRouteSandboxPreflight(workflowBody);
|
|
8639
|
-
const invocationInput = {
|
|
8640
|
-
templateId: "event-worker-verifier",
|
|
8641
|
-
sourceRef: {
|
|
8642
|
-
kind: "event",
|
|
8643
|
-
id: event.id,
|
|
8644
|
-
dedupeKey: idempotencyKey,
|
|
8645
|
-
raw: { source: event.source, type: event.type }
|
|
8646
|
-
},
|
|
8647
|
-
subjectRef: {
|
|
8648
|
-
kind: "event",
|
|
8649
|
-
id: stringField(event.subject) ?? event.id,
|
|
8650
|
-
path: routeProjectPath,
|
|
8651
|
-
raw: { message: stringField(event.message) }
|
|
8652
|
-
},
|
|
8653
|
-
intent: "route",
|
|
8654
|
-
scope: {
|
|
8655
|
-
projectPath: routeProjectPath,
|
|
8656
|
-
projectGroup,
|
|
8657
|
-
worktreePolicy: opts.worktreeMode ?? "auto",
|
|
8658
|
-
permissions: permissionMode,
|
|
8659
|
-
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
8660
|
-
accountPolicy: opts.authProfilePool || opts.accountPool ? "pool" : "single",
|
|
8661
|
-
concurrencyGroup: projectGroup ?? routeProjectPath
|
|
8662
|
-
},
|
|
8663
|
-
outputPolicy: {
|
|
8664
|
-
report: "always",
|
|
8665
|
-
createTask: "on_failure"
|
|
8666
|
-
}
|
|
8667
|
-
};
|
|
8668
|
-
const workItemInput = {
|
|
8669
|
-
routeKey: "generic-event",
|
|
8670
|
-
idempotencyKey,
|
|
8671
|
-
invocationId: "<created-invocation-id>",
|
|
8672
|
-
sourceType: event.type,
|
|
8673
|
-
sourceRef: event.id,
|
|
8674
|
-
subjectRef: stringField(event.subject) ?? event.id,
|
|
8675
|
-
projectKey: routeProjectPath,
|
|
8676
|
-
projectGroup,
|
|
8677
|
-
priority: 0,
|
|
8678
|
-
status: "queued"
|
|
8679
|
-
};
|
|
8680
|
-
const loopInput = {
|
|
8681
|
-
name: loopName,
|
|
8682
|
-
description: `Run ${workflowBody.name} once for event ${event.id}; idempotency=${idempotencyKey}`,
|
|
8683
|
-
schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
|
|
8684
|
-
target: { type: "workflow", workflowId: "<created-workflow-id>", input: {} },
|
|
8685
|
-
overlap: "skip",
|
|
8686
|
-
maxAttempts: 1,
|
|
8687
|
-
retryDelayMs: 60000,
|
|
8688
|
-
leaseMs: 90 * 60000
|
|
8689
|
-
};
|
|
8690
|
-
if (opts.dryRun) {
|
|
8691
|
-
const throttle = hasThrottleLimits(throttleLimits) ? routeThrottleDryRunPreview({ projectPath: routeProjectPath, projectGroup, limits: throttleLimits }) : undefined;
|
|
8692
|
-
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(workflowBody, "event-preflight"), {
|
|
8693
|
-
name: workflowBody.name,
|
|
8694
|
-
type: "generic-event-workflow",
|
|
8695
|
-
event: event.id
|
|
8696
|
-
}, {}) : undefined;
|
|
8697
|
-
print({ event, idempotencyKey, invocation: invocationInput, workItem: workItemInput, workflow: workflowBody, loop: loopInput, throttle, sandboxPreflight, preflight }, `dry-run ${loopName}`);
|
|
8698
|
-
return;
|
|
8699
|
-
}
|
|
8700
|
-
const store = new Store;
|
|
8701
|
-
try {
|
|
8702
|
-
const workflowPreflightSpec = workflowSpecForPreflight(workflowBody, "event-preflight");
|
|
8703
|
-
generatedRouteSandboxPreflight(workflowPreflightSpec);
|
|
8704
|
-
const preflight = opts.preflight ? preflightStoredWorkflow(workflowPreflightSpec, {
|
|
8705
|
-
name: workflowBody.name,
|
|
8706
|
-
type: "generic-event-workflow",
|
|
8707
|
-
event: event.id
|
|
8708
|
-
}, {}) : undefined;
|
|
8709
|
-
const outcome = store.writeTransaction(() => {
|
|
8710
|
-
const invocation = store.createWorkflowInvocation(invocationInput);
|
|
8711
|
-
const existingItem = store.findWorkflowWorkItem("generic-event", idempotencyKey);
|
|
8712
|
-
if (existingItem?.loopId && ["admitted", "running", "succeeded"].includes(existingItem.status)) {
|
|
8713
|
-
const existingLoop = store.getLoop(existingItem.loopId);
|
|
8714
|
-
const existingWorkflow = existingItem.workflowId ? store.getWorkflow(existingItem.workflowId) : undefined;
|
|
8715
|
-
return { kind: "deduped", existingItem, existingLoop, existingWorkflow, invocation };
|
|
8716
|
-
}
|
|
8717
|
-
const throttle = hasThrottleLimits(throttleLimits) ? routeThrottleDecision(store, { projectPath: routeProjectPath, projectGroup, limits: throttleLimits }) : undefined;
|
|
8718
|
-
const workItem = store.upsertWorkflowWorkItem({
|
|
8719
|
-
...workItemInput,
|
|
8720
|
-
invocationId: invocation.id,
|
|
8721
|
-
status: throttle && !throttle.allowed ? "deferred" : "queued",
|
|
8722
|
-
lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
|
|
8723
|
-
});
|
|
8724
|
-
if (throttle && !throttle.allowed)
|
|
8725
|
-
return { kind: "throttled", invocation, workItem, throttle };
|
|
8726
|
-
const workflow = routeWorkflowForStorage(store, workflowBody);
|
|
8727
|
-
const loop = store.createLoop({
|
|
8728
|
-
...loopInput,
|
|
8729
|
-
target: {
|
|
8730
|
-
type: "workflow",
|
|
8731
|
-
workflowId: workflow.id,
|
|
8732
|
-
input: {
|
|
8733
|
-
workflowInvocationId: invocation.id,
|
|
8734
|
-
workflowWorkItemId: workItem.id
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
|
-
});
|
|
8738
|
-
const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by generic-event route" });
|
|
8739
|
-
return { kind: "created", invocation, workItem: admitted, workflow, loop, throttle };
|
|
8740
|
-
});
|
|
8741
|
-
if (outcome.kind === "deduped") {
|
|
8742
|
-
print({
|
|
8743
|
-
deduped: true,
|
|
8744
|
-
idempotencyKey,
|
|
8745
|
-
dedupedBy: "work-item",
|
|
8746
|
-
event,
|
|
8747
|
-
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8748
|
-
workItem: publicWorkflowWorkItem(outcome.existingItem),
|
|
8749
|
-
workflow: outcome.existingWorkflow ? publicWorkflow(outcome.existingWorkflow) : undefined,
|
|
8750
|
-
loop: outcome.existingLoop ? publicLoop(outcome.existingLoop) : undefined
|
|
8751
|
-
}, `deduped existing work item ${outcome.existingItem.id} for event=${event.id} idempotency=${idempotencyKey}`);
|
|
8752
|
-
return;
|
|
8753
|
-
}
|
|
8754
|
-
if (outcome.kind === "throttled") {
|
|
8755
|
-
print({
|
|
8756
|
-
skipped: true,
|
|
8757
|
-
queuedAtSource: true,
|
|
8758
|
-
reason: outcome.throttle.reason,
|
|
8759
|
-
idempotencyKey,
|
|
8760
|
-
event,
|
|
8761
|
-
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8762
|
-
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
8763
|
-
throttle: outcome.throttle,
|
|
8764
|
-
workflow: workflowBody,
|
|
8765
|
-
loop: loopInput
|
|
8766
|
-
}, `skipped event ${event.id}: ${outcome.throttle.reason}`);
|
|
8767
|
-
return;
|
|
8768
|
-
}
|
|
8769
|
-
print({
|
|
8770
|
-
deduped: false,
|
|
8771
|
-
idempotencyKey,
|
|
8772
|
-
event,
|
|
8773
|
-
invocation: publicWorkflowInvocation(outcome.invocation),
|
|
8774
|
-
workItem: publicWorkflowWorkItem(outcome.workItem),
|
|
8775
|
-
workflow: publicWorkflow(outcome.workflow),
|
|
8776
|
-
loop: publicLoop(outcome.loop),
|
|
8777
|
-
throttle: outcome.throttle,
|
|
8778
|
-
sandboxPreflight,
|
|
8779
|
-
preflight
|
|
8780
|
-
}, `created ${outcome.loop.id} (${outcome.loop.name}) workflow=${outcome.workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
|
|
8781
|
-
} finally {
|
|
8782
|
-
store.close();
|
|
8783
|
-
}
|
|
8784
|
+
const result = routeGenericEvent(event, opts);
|
|
8785
|
+
print(result.value, result.human);
|
|
8784
8786
|
});
|
|
8785
8787
|
goal.command("show <idOrName>").description("show a goal or configured loop/workflow goal").action((idOrName) => {
|
|
8786
8788
|
const store = new Store;
|
|
@@ -8860,10 +8862,14 @@ workflows.command("create <file>").description("validate and store a workflow JS
|
|
|
8860
8862
|
store.close();
|
|
8861
8863
|
}
|
|
8862
8864
|
});
|
|
8863
|
-
workflows.command("list").alias("ls").option("--status <status>", "active or
|
|
8865
|
+
workflows.command("list").alias("ls").option("--status <status>", "active, archived, or all", "active").option("--all", "include active and archived workflows").option("--limit <n>", "maximum rows to print; omitted means all matching workflows").option("--offset <n>", "number of matching rows to skip before printing", "0").action((opts) => {
|
|
8864
8866
|
const store = new Store;
|
|
8865
8867
|
try {
|
|
8866
|
-
const
|
|
8868
|
+
const status = workflowStatusFromOpts(opts.status, opts.all);
|
|
8869
|
+
const limit = positiveInteger(opts.limit, "--limit");
|
|
8870
|
+
const offset = nonNegativeInteger(opts.offset, "--offset") ?? 0;
|
|
8871
|
+
const workflowsList = store.listWorkflows({ status, limit, offset });
|
|
8872
|
+
const total = store.countWorkflows({ status });
|
|
8867
8873
|
if (isJson())
|
|
8868
8874
|
print(workflowsList.map(publicWorkflow));
|
|
8869
8875
|
else {
|
|
@@ -8871,6 +8877,8 @@ workflows.command("list").alias("ls").option("--status <status>", "active or arc
|
|
|
8871
8877
|
console.log(`${workflow.id} ${workflow.status.padEnd(8)} steps=${workflow.steps.length} ${workflow.name}`);
|
|
8872
8878
|
}
|
|
8873
8879
|
}
|
|
8880
|
+
if (limit !== undefined || offset > 0)
|
|
8881
|
+
printWorkflowListWarning({ shown: workflowsList.length, total, status, offset, limit });
|
|
8874
8882
|
} finally {
|
|
8875
8883
|
store.close();
|
|
8876
8884
|
}
|