@hasna/loops 0.3.54 → 0.3.55
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 +32 -4
- package/dist/cli/index.js +357 -65
- package/dist/daemon/index.js +165 -18
- package/dist/index.js +226 -55
- package/dist/lib/store.d.ts +16 -1
- package/dist/lib/store.js +147 -3
- package/dist/lib/templates.d.ts +4 -2
- package/dist/sdk/index.js +164 -17
- package/dist/types.d.ts +5 -4
- package/docs/USAGE.md +32 -4
- package/docs/workflows/transcript-feedback-to-loops.json +2 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -331,6 +331,15 @@ function optionalPositiveInteger(value, label) {
|
|
|
331
331
|
throw new Error(`${label} must be a positive integer`);
|
|
332
332
|
return value;
|
|
333
333
|
}
|
|
334
|
+
function optionalTimeoutMs(value, label) {
|
|
335
|
+
if (value === undefined)
|
|
336
|
+
return;
|
|
337
|
+
if (value === null)
|
|
338
|
+
return null;
|
|
339
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
340
|
+
throw new Error(`${label} must be a positive integer or null for unlimited`);
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
334
343
|
function optionalBoolean(value, label) {
|
|
335
344
|
if (value === undefined)
|
|
336
345
|
return;
|
|
@@ -419,7 +428,7 @@ function validateTarget(value, label, opts) {
|
|
|
419
428
|
assertObject(value, label);
|
|
420
429
|
if (value.type === "command") {
|
|
421
430
|
assertString(value.command, `${label}.command`);
|
|
422
|
-
|
|
431
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
423
432
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
424
433
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
425
434
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
@@ -438,7 +447,7 @@ function validateTarget(value, label, opts) {
|
|
|
438
447
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
439
448
|
if (!providers.includes(value.provider))
|
|
440
449
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
441
|
-
|
|
450
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
442
451
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
443
452
|
if (value.authProfile !== undefined) {
|
|
444
453
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
@@ -561,7 +570,7 @@ function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
|
561
570
|
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
562
571
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
563
572
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
564
|
-
timeoutMs:
|
|
573
|
+
timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
565
574
|
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
566
575
|
};
|
|
567
576
|
});
|
|
@@ -1307,6 +1316,17 @@ class Store {
|
|
|
1307
1316
|
const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
|
|
1308
1317
|
return row ? rowToLoop(row) : undefined;
|
|
1309
1318
|
}
|
|
1319
|
+
requireUniqueLoop(idOrName) {
|
|
1320
|
+
const byId = this.getLoop(idOrName);
|
|
1321
|
+
if (byId)
|
|
1322
|
+
return byId;
|
|
1323
|
+
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1324
|
+
if (rows.length === 0)
|
|
1325
|
+
throw new Error(`loop not found: ${idOrName}`);
|
|
1326
|
+
if (rows.length > 1)
|
|
1327
|
+
throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
|
|
1328
|
+
return rowToLoop(rows[0]);
|
|
1329
|
+
}
|
|
1310
1330
|
requireLoop(idOrName) {
|
|
1311
1331
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
1312
1332
|
throw new Error(`loop not found: ${idOrName}`);
|
|
@@ -1381,6 +1401,130 @@ class Store {
|
|
|
1381
1401
|
throw new Error(`loop not found after update: ${id}`);
|
|
1382
1402
|
return after;
|
|
1383
1403
|
}
|
|
1404
|
+
activeLoopReferenceCount(workflowId) {
|
|
1405
|
+
const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
|
|
1406
|
+
let count = 0;
|
|
1407
|
+
for (const row of rows) {
|
|
1408
|
+
try {
|
|
1409
|
+
const target = JSON.parse(row.target_json);
|
|
1410
|
+
if (target.type === "workflow" && target.workflowId === workflowId)
|
|
1411
|
+
count += 1;
|
|
1412
|
+
} catch {}
|
|
1413
|
+
}
|
|
1414
|
+
return count;
|
|
1415
|
+
}
|
|
1416
|
+
archiveWorkflowIfUnreferenced(workflowId, updated) {
|
|
1417
|
+
if (this.activeLoopReferenceCount(workflowId) > 0)
|
|
1418
|
+
return;
|
|
1419
|
+
const workflow = this.getWorkflow(workflowId);
|
|
1420
|
+
if (!workflow || workflow.status !== "active")
|
|
1421
|
+
return;
|
|
1422
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
|
|
1423
|
+
if (res.changes !== 1)
|
|
1424
|
+
return;
|
|
1425
|
+
return this.getWorkflow(workflowId);
|
|
1426
|
+
}
|
|
1427
|
+
retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
|
|
1428
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1429
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1430
|
+
try {
|
|
1431
|
+
const current = this.requireLoop(idOrName);
|
|
1432
|
+
if (current.target.type !== "workflow")
|
|
1433
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1434
|
+
if (this.hasRunningRun(current.id))
|
|
1435
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1436
|
+
const workflow = this.requireWorkflow(workflowId);
|
|
1437
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1438
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1439
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1440
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1441
|
+
WHERE id=$id
|
|
1442
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1443
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1444
|
+
))`).run({
|
|
1445
|
+
$id: current.id,
|
|
1446
|
+
$target: JSON.stringify(target),
|
|
1447
|
+
$updated: updated,
|
|
1448
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1449
|
+
$now: updated
|
|
1450
|
+
});
|
|
1451
|
+
if (res.changes !== 1)
|
|
1452
|
+
throw new Error("daemon lease lost");
|
|
1453
|
+
this.db.exec("COMMIT");
|
|
1454
|
+
const after = this.getLoop(current.id);
|
|
1455
|
+
if (!after)
|
|
1456
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1457
|
+
return after;
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
try {
|
|
1460
|
+
this.db.exec("ROLLBACK");
|
|
1461
|
+
} catch {}
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
1466
|
+
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
1467
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1468
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1469
|
+
try {
|
|
1470
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1471
|
+
if (current.target.type !== "workflow")
|
|
1472
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1473
|
+
if (this.hasRunningRun(current.id))
|
|
1474
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1475
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1476
|
+
const workflow = {
|
|
1477
|
+
id: genId(),
|
|
1478
|
+
name: normalized.name,
|
|
1479
|
+
description: normalized.description,
|
|
1480
|
+
version: normalized.version ?? 1,
|
|
1481
|
+
status: "active",
|
|
1482
|
+
goal: normalized.goal,
|
|
1483
|
+
steps: normalized.steps,
|
|
1484
|
+
createdAt: updated,
|
|
1485
|
+
updatedAt: updated
|
|
1486
|
+
};
|
|
1487
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1488
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
1489
|
+
$id: workflow.id,
|
|
1490
|
+
$name: workflow.name,
|
|
1491
|
+
$description: workflow.description ?? null,
|
|
1492
|
+
$version: workflow.version,
|
|
1493
|
+
$status: workflow.status,
|
|
1494
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
1495
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1496
|
+
$created: workflow.createdAt,
|
|
1497
|
+
$updated: workflow.updatedAt
|
|
1498
|
+
});
|
|
1499
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1500
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1501
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1502
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1503
|
+
WHERE id=$id
|
|
1504
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1505
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1506
|
+
))`).run({
|
|
1507
|
+
$id: current.id,
|
|
1508
|
+
$target: JSON.stringify(target),
|
|
1509
|
+
$updated: updated,
|
|
1510
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1511
|
+
$now: updated
|
|
1512
|
+
});
|
|
1513
|
+
if (res.changes !== 1)
|
|
1514
|
+
throw new Error("daemon lease lost");
|
|
1515
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1516
|
+
this.db.exec("COMMIT");
|
|
1517
|
+
const loop = this.getLoop(current.id);
|
|
1518
|
+
if (!loop)
|
|
1519
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1520
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1521
|
+
} catch (error) {
|
|
1522
|
+
try {
|
|
1523
|
+
this.db.exec("ROLLBACK");
|
|
1524
|
+
} catch {}
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1384
1528
|
renameLoop(id, name, opts = {}) {
|
|
1385
1529
|
const current = this.getLoop(id);
|
|
1386
1530
|
if (!current)
|
|
@@ -3606,7 +3750,7 @@ function commandSpec(target) {
|
|
|
3606
3750
|
cwd: commandTarget.cwd,
|
|
3607
3751
|
shell: commandTarget.shell,
|
|
3608
3752
|
env: commandTarget.env,
|
|
3609
|
-
timeoutMs: commandTarget.timeoutMs
|
|
3753
|
+
timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
|
|
3610
3754
|
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
3611
3755
|
account: commandTarget.account,
|
|
3612
3756
|
accountTool: commandTarget.account?.tool
|
|
@@ -3617,7 +3761,7 @@ function commandSpec(target) {
|
|
|
3617
3761
|
command: providerCommand(agentTarget.provider),
|
|
3618
3762
|
args: agentArgs(agentTarget),
|
|
3619
3763
|
cwd: agentTarget.cwd,
|
|
3620
|
-
timeoutMs: agentTarget.timeoutMs ??
|
|
3764
|
+
timeoutMs: agentTarget.timeoutMs ?? null,
|
|
3621
3765
|
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
3622
3766
|
account: agentTarget.account,
|
|
3623
3767
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
@@ -3805,12 +3949,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3805
3949
|
if (opts.signal?.aborted)
|
|
3806
3950
|
abortHandler();
|
|
3807
3951
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3808
|
-
const timer = setTimeout(() => {
|
|
3952
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3809
3953
|
timedOut = true;
|
|
3810
3954
|
if (child.pid)
|
|
3811
3955
|
killProcessGroup(child.pid);
|
|
3812
|
-
}, spec.timeoutMs);
|
|
3813
|
-
timer
|
|
3956
|
+
}, spec.timeoutMs) : undefined;
|
|
3957
|
+
timer?.unref();
|
|
3814
3958
|
let idleTimer;
|
|
3815
3959
|
const resetIdleTimer = () => {
|
|
3816
3960
|
if (!spec.idleTimeoutMs)
|
|
@@ -3842,7 +3986,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3842
3986
|
} catch (err) {
|
|
3843
3987
|
error = err instanceof Error ? err.message : String(err);
|
|
3844
3988
|
} finally {
|
|
3845
|
-
|
|
3989
|
+
if (timer)
|
|
3990
|
+
clearTimeout(timer);
|
|
3846
3991
|
if (idleTimer)
|
|
3847
3992
|
clearTimeout(idleTimer);
|
|
3848
3993
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -3984,12 +4129,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3984
4129
|
if (opts.signal?.aborted)
|
|
3985
4130
|
abortHandler();
|
|
3986
4131
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3987
|
-
const timer = setTimeout(() => {
|
|
4132
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3988
4133
|
timedOut = true;
|
|
3989
4134
|
if (child.pid)
|
|
3990
4135
|
killProcessGroup(child.pid);
|
|
3991
|
-
}, spec.timeoutMs);
|
|
3992
|
-
timer
|
|
4136
|
+
}, spec.timeoutMs) : undefined;
|
|
4137
|
+
timer?.unref();
|
|
3993
4138
|
let idleTimer;
|
|
3994
4139
|
const resetIdleTimer = () => {
|
|
3995
4140
|
if (!spec.idleTimeoutMs)
|
|
@@ -4021,7 +4166,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
4021
4166
|
} catch (err) {
|
|
4022
4167
|
error = err instanceof Error ? err.message : String(err);
|
|
4023
4168
|
} finally {
|
|
4024
|
-
|
|
4169
|
+
if (timer)
|
|
4170
|
+
clearTimeout(timer);
|
|
4025
4171
|
if (idleTimer)
|
|
4026
4172
|
clearTimeout(idleTimer);
|
|
4027
4173
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -4446,7 +4592,7 @@ ${result.stderr}`);
|
|
|
4446
4592
|
// src/lib/workflow-runner.ts
|
|
4447
4593
|
function targetWithStepAccount(step) {
|
|
4448
4594
|
const account = step.account ?? step.target.account;
|
|
4449
|
-
const timeoutMs = step.timeoutMs
|
|
4595
|
+
const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
|
|
4450
4596
|
if (!account && timeoutMs === step.target.timeoutMs)
|
|
4451
4597
|
return step.target;
|
|
4452
4598
|
return { ...step.target, account, timeoutMs };
|
|
@@ -4742,7 +4888,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4742
4888
|
})
|
|
4743
4889
|
});
|
|
4744
4890
|
}
|
|
4745
|
-
const
|
|
4891
|
+
const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
|
|
4892
|
+
const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
|
|
4746
4893
|
let workflowTimedOut = false;
|
|
4747
4894
|
const externalAbort = () => controller?.abort();
|
|
4748
4895
|
if (controller && opts.signal?.aborted)
|
|
@@ -4752,13 +4899,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4752
4899
|
const timer = controller ? setTimeout(() => {
|
|
4753
4900
|
workflowTimedOut = true;
|
|
4754
4901
|
controller.abort();
|
|
4755
|
-
},
|
|
4902
|
+
}, workflowTimeoutMs) : undefined;
|
|
4756
4903
|
timer?.unref();
|
|
4757
4904
|
try {
|
|
4758
4905
|
return await executeWorkflow(store, workflow, {
|
|
4759
4906
|
...opts,
|
|
4760
4907
|
signal: controller?.signal ?? opts.signal,
|
|
4761
|
-
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${
|
|
4908
|
+
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
|
|
4762
4909
|
loop,
|
|
4763
4910
|
loopRun: run,
|
|
4764
4911
|
scheduledFor: run.scheduledFor,
|
|
@@ -5534,7 +5681,11 @@ function runDoctor(store) {
|
|
|
5534
5681
|
if (loop.target.type === "workflow") {
|
|
5535
5682
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
5536
5683
|
for (const step of workflowExecutionOrder(workflow)) {
|
|
5537
|
-
preflightTarget({
|
|
5684
|
+
preflightTarget({
|
|
5685
|
+
...step.target,
|
|
5686
|
+
account: step.account ?? step.target.account,
|
|
5687
|
+
timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
|
|
5688
|
+
}, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
|
|
5538
5689
|
}
|
|
5539
5690
|
} else {
|
|
5540
5691
|
preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
|
|
@@ -6014,7 +6165,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
6014
6165
|
// package.json
|
|
6015
6166
|
var package_default = {
|
|
6016
6167
|
name: "@hasna/loops",
|
|
6017
|
-
version: "0.3.
|
|
6168
|
+
version: "0.3.55",
|
|
6018
6169
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
6019
6170
|
type: "module",
|
|
6020
6171
|
main: "dist/index.js",
|
|
@@ -6144,7 +6295,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6144
6295
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
6145
6296
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6146
6297
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6147
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
|
|
6298
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
|
|
6299
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6148
6300
|
]
|
|
6149
6301
|
},
|
|
6150
6302
|
{
|
|
@@ -6174,7 +6326,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6174
6326
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
6175
6327
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6176
6328
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6177
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
|
|
6329
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
|
|
6330
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6178
6331
|
]
|
|
6179
6332
|
},
|
|
6180
6333
|
{
|
|
@@ -6202,7 +6355,7 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6202
6355
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6203
6356
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6204
6357
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
6205
|
-
{ name: "timeoutMs", default: "
|
|
6358
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6206
6359
|
]
|
|
6207
6360
|
},
|
|
6208
6361
|
{
|
|
@@ -6221,7 +6374,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6221
6374
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
6222
6375
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6223
6376
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6224
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
6377
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
6378
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
6225
6379
|
]
|
|
6226
6380
|
},
|
|
6227
6381
|
{
|
|
@@ -6326,6 +6480,30 @@ function taskLabel(input) {
|
|
|
6326
6480
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
6327
6481
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
6328
6482
|
}
|
|
6483
|
+
var UNLIMITED_AGENT_TIMEOUT_MS = null;
|
|
6484
|
+
function agentTimeoutMs(input) {
|
|
6485
|
+
return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
|
|
6486
|
+
}
|
|
6487
|
+
function parseTemplateTimeoutMs(raw) {
|
|
6488
|
+
if (raw === undefined || raw.trim() === "")
|
|
6489
|
+
return;
|
|
6490
|
+
const normalized = raw.trim().toLowerCase();
|
|
6491
|
+
if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
|
|
6492
|
+
return null;
|
|
6493
|
+
const value = Number(raw);
|
|
6494
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
6495
|
+
throw new Error("timeoutMs must be a positive integer number of milliseconds, or unlimited/none/null");
|
|
6496
|
+
}
|
|
6497
|
+
return value;
|
|
6498
|
+
}
|
|
6499
|
+
function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
|
|
6500
|
+
if (raw === undefined || raw.trim() === "")
|
|
6501
|
+
return fallbackMs;
|
|
6502
|
+
const value = Number(raw);
|
|
6503
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
6504
|
+
throw new Error(`${label} must be a positive integer number of milliseconds`);
|
|
6505
|
+
return value;
|
|
6506
|
+
}
|
|
6329
6507
|
function stableIndex(seed, size) {
|
|
6330
6508
|
let hash = 2166136261;
|
|
6331
6509
|
for (let i = 0;i < seed.length; i += 1) {
|
|
@@ -6597,7 +6775,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6597
6775
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
6598
6776
|
},
|
|
6599
6777
|
account: accountForRole(input, role, seed),
|
|
6600
|
-
timeoutMs:
|
|
6778
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6601
6779
|
};
|
|
6602
6780
|
}
|
|
6603
6781
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -7040,18 +7218,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
7040
7218
|
name: "Worker",
|
|
7041
7219
|
description: "Implement the todos task and record evidence.",
|
|
7042
7220
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7043
|
-
timeoutMs:
|
|
7221
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7044
7222
|
},
|
|
7045
7223
|
{
|
|
7046
7224
|
id: "verifier",
|
|
7047
7225
|
name: "Verifier",
|
|
7048
7226
|
description: "Adversarially verify worker output and update todos.",
|
|
7049
7227
|
dependsOn: ["worker"],
|
|
7050
|
-
target:
|
|
7051
|
-
|
|
7052
|
-
idleTimeoutMs: 10 * 60000
|
|
7053
|
-
},
|
|
7054
|
-
timeoutMs: 30 * 60000
|
|
7228
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
7229
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7055
7230
|
}
|
|
7056
7231
|
])
|
|
7057
7232
|
};
|
|
@@ -7212,7 +7387,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7212
7387
|
name: "Triage",
|
|
7213
7388
|
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
7214
7389
|
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
7215
|
-
timeoutMs:
|
|
7390
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7216
7391
|
},
|
|
7217
7392
|
{
|
|
7218
7393
|
id: "triage-gate",
|
|
@@ -7234,7 +7409,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7234
7409
|
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
7235
7410
|
dependsOn: ["triage-gate"],
|
|
7236
7411
|
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
7237
|
-
timeoutMs:
|
|
7412
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7238
7413
|
},
|
|
7239
7414
|
{
|
|
7240
7415
|
id: "planner-gate",
|
|
@@ -7256,18 +7431,15 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7256
7431
|
description: "Implement the todos task according to triage and planner evidence.",
|
|
7257
7432
|
dependsOn: ["planner-gate"],
|
|
7258
7433
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7259
|
-
timeoutMs:
|
|
7434
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7260
7435
|
},
|
|
7261
7436
|
{
|
|
7262
7437
|
id: "verifier",
|
|
7263
7438
|
name: "Verifier",
|
|
7264
7439
|
description: "Adversarially verify worker output and update todos.",
|
|
7265
7440
|
dependsOn: ["worker"],
|
|
7266
|
-
target:
|
|
7267
|
-
|
|
7268
|
-
idleTimeoutMs: 10 * 60000
|
|
7269
|
-
},
|
|
7270
|
-
timeoutMs: 30 * 60000
|
|
7441
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
7442
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7271
7443
|
}
|
|
7272
7444
|
])
|
|
7273
7445
|
};
|
|
@@ -7337,18 +7509,15 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
7337
7509
|
name: "Worker",
|
|
7338
7510
|
description: "Handle the Hasna event and record evidence.",
|
|
7339
7511
|
target: agentTarget(input, workerPrompt, "worker", seed, plan),
|
|
7340
|
-
timeoutMs:
|
|
7512
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7341
7513
|
},
|
|
7342
7514
|
{
|
|
7343
7515
|
id: "verifier",
|
|
7344
7516
|
name: "Verifier",
|
|
7345
7517
|
description: "Adversarially verify event handling.",
|
|
7346
7518
|
dependsOn: ["worker"],
|
|
7347
|
-
target:
|
|
7348
|
-
|
|
7349
|
-
idleTimeoutMs: 10 * 60000
|
|
7350
|
-
},
|
|
7351
|
-
timeoutMs: 30 * 60000
|
|
7519
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
7520
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7352
7521
|
}
|
|
7353
7522
|
])
|
|
7354
7523
|
};
|
|
@@ -7360,7 +7529,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7360
7529
|
throw new Error("projectPath is required");
|
|
7361
7530
|
const seed = `${input.projectPath}:${input.objective}`;
|
|
7362
7531
|
const plan = worktreePlan(input, seed);
|
|
7363
|
-
const timeoutMs =
|
|
7532
|
+
const timeoutMs = agentTimeoutMs(input);
|
|
7364
7533
|
const workerPrompt = [
|
|
7365
7534
|
`/goal ${input.objective}`,
|
|
7366
7535
|
"",
|
|
@@ -7398,11 +7567,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7398
7567
|
name: "Verifier",
|
|
7399
7568
|
description: "Adversarially verify the bounded objective result.",
|
|
7400
7569
|
dependsOn: ["worker"],
|
|
7401
|
-
target:
|
|
7402
|
-
|
|
7403
|
-
idleTimeoutMs: 10 * 60000
|
|
7404
|
-
},
|
|
7405
|
-
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
7570
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
7571
|
+
timeoutMs
|
|
7406
7572
|
}
|
|
7407
7573
|
])
|
|
7408
7574
|
};
|
|
@@ -7431,7 +7597,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7431
7597
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
7432
7598
|
worktreeRoot: values.worktreeRoot,
|
|
7433
7599
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7434
|
-
timeoutMs:
|
|
7600
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7435
7601
|
};
|
|
7436
7602
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
7437
7603
|
const taskId = values.taskId ?? "";
|
|
@@ -7468,6 +7634,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7468
7634
|
worktreeMode: values.worktreeMode ?? "required",
|
|
7469
7635
|
worktreeRoot: values.worktreeRoot,
|
|
7470
7636
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7637
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
7471
7638
|
eventId: values.eventId,
|
|
7472
7639
|
eventType: values.eventType
|
|
7473
7640
|
});
|
|
@@ -7534,6 +7701,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
7534
7701
|
if (!checkCommand.trim())
|
|
7535
7702
|
throw new Error("checkCommand is required");
|
|
7536
7703
|
const seed = `${projectPath}:${checkCommand}`;
|
|
7704
|
+
const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
|
|
7705
|
+
const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
|
|
7537
7706
|
return {
|
|
7538
7707
|
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
7539
7708
|
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
@@ -7548,10 +7717,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
7548
7717
|
command: "bash",
|
|
7549
7718
|
args: ["-lc", checkCommand],
|
|
7550
7719
|
cwd: projectPath,
|
|
7551
|
-
timeoutMs
|
|
7552
|
-
idleTimeoutMs
|
|
7720
|
+
timeoutMs,
|
|
7721
|
+
idleTimeoutMs
|
|
7553
7722
|
},
|
|
7554
|
-
timeoutMs
|
|
7723
|
+
timeoutMs
|
|
7555
7724
|
}
|
|
7556
7725
|
]
|
|
7557
7726
|
};
|
|
@@ -7589,6 +7758,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7589
7758
|
worktreeMode: values.worktreeMode,
|
|
7590
7759
|
worktreeRoot: values.worktreeRoot,
|
|
7591
7760
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7761
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
7592
7762
|
eventId: values.eventId,
|
|
7593
7763
|
eventType: values.eventType
|
|
7594
7764
|
});
|
|
@@ -7620,7 +7790,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7620
7790
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
7621
7791
|
worktreeMode: values.worktreeMode,
|
|
7622
7792
|
worktreeRoot: values.worktreeRoot,
|
|
7623
|
-
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
7793
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7794
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7624
7795
|
});
|
|
7625
7796
|
}
|
|
7626
7797
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -7648,7 +7819,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7648
7819
|
worktreeMode: values.worktreeMode,
|
|
7649
7820
|
worktreeRoot: values.worktreeRoot,
|
|
7650
7821
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7651
|
-
timeoutMs:
|
|
7822
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7652
7823
|
});
|
|
7653
7824
|
}
|
|
7654
7825
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -7793,6 +7964,43 @@ function publicWorkflowBody(body) {
|
|
|
7793
7964
|
const { id: _id, status: _status, createdAt: _createdAt, updatedAt: _updatedAt, ...bodyOnly } = value;
|
|
7794
7965
|
return bodyOnly;
|
|
7795
7966
|
}
|
|
7967
|
+
function workflowWithAgentTimeouts(workflow, timeoutMs, opts = {}) {
|
|
7968
|
+
let changed = false;
|
|
7969
|
+
const agentStepIds = [];
|
|
7970
|
+
const steps = workflow.steps.map((step) => {
|
|
7971
|
+
if (step.target.type !== "agent")
|
|
7972
|
+
return step;
|
|
7973
|
+
agentStepIds.push(step.id);
|
|
7974
|
+
const target = { ...step.target, timeoutMs };
|
|
7975
|
+
if (timeoutMs === null && target.idleTimeoutMs !== undefined) {
|
|
7976
|
+
delete target.idleTimeoutMs;
|
|
7977
|
+
changed = true;
|
|
7978
|
+
}
|
|
7979
|
+
if (step.timeoutMs !== timeoutMs || step.target.timeoutMs !== timeoutMs)
|
|
7980
|
+
changed = true;
|
|
7981
|
+
return {
|
|
7982
|
+
...step,
|
|
7983
|
+
target,
|
|
7984
|
+
timeoutMs
|
|
7985
|
+
};
|
|
7986
|
+
});
|
|
7987
|
+
return {
|
|
7988
|
+
body: {
|
|
7989
|
+
name: opts.name ?? workflow.name,
|
|
7990
|
+
description: workflow.description,
|
|
7991
|
+
version: workflow.version,
|
|
7992
|
+
goal: workflow.goal,
|
|
7993
|
+
steps
|
|
7994
|
+
},
|
|
7995
|
+
changed,
|
|
7996
|
+
agentStepIds
|
|
7997
|
+
};
|
|
7998
|
+
}
|
|
7999
|
+
function workflowTimeoutMigrationName(workflow, timeoutMs) {
|
|
8000
|
+
const policy = timeoutMs === null ? "agent-timeout-unlimited" : `agent-timeout-${timeoutMs}ms`;
|
|
8001
|
+
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8002
|
+
return `${workflow.name}-${policy}-${suffix}`;
|
|
8003
|
+
}
|
|
7796
8004
|
function printTextOutput(value) {
|
|
7797
8005
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
7798
8006
|
console.log(line);
|
|
@@ -7833,6 +8041,17 @@ function positiveDuration(raw, label) {
|
|
|
7833
8041
|
throw new Error(`${label} must be greater than zero`);
|
|
7834
8042
|
return value;
|
|
7835
8043
|
}
|
|
8044
|
+
function timeoutDuration(raw, label) {
|
|
8045
|
+
if (raw === undefined)
|
|
8046
|
+
return;
|
|
8047
|
+
const normalized = raw.trim().toLowerCase();
|
|
8048
|
+
if (["none", "unlimited", "null", "never", "off", "false"].includes(normalized))
|
|
8049
|
+
return null;
|
|
8050
|
+
const value = parseDuration(raw);
|
|
8051
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
8052
|
+
throw new Error(`${label} must be a duration greater than zero, or none/unlimited`);
|
|
8053
|
+
return value;
|
|
8054
|
+
}
|
|
7836
8055
|
function parsePolicy(opts) {
|
|
7837
8056
|
const catchUp = opts.catchUp ?? "latest";
|
|
7838
8057
|
if (!["none", "latest", "all"].includes(catchUp))
|
|
@@ -8563,6 +8782,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8563
8782
|
variant: opts.variant,
|
|
8564
8783
|
agent: opts.agent,
|
|
8565
8784
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
8785
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
8566
8786
|
permissionMode,
|
|
8567
8787
|
sandbox,
|
|
8568
8788
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -8785,6 +9005,7 @@ function routeGenericEvent(event, opts) {
|
|
|
8785
9005
|
variant: opts.variant,
|
|
8786
9006
|
agent: opts.agent,
|
|
8787
9007
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9008
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
8788
9009
|
permissionMode,
|
|
8789
9010
|
sandbox,
|
|
8790
9011
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -9133,7 +9354,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
9133
9354
|
return mode;
|
|
9134
9355
|
}
|
|
9135
9356
|
var create = program.command("create").description("create loops");
|
|
9136
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell").option("--preflight-each-run", "check target executables/accounts before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
9357
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout; use none/unlimited for no timeout").option("--no-shell", "execute without a shell").option("--preflight-each-run", "check target executables/accounts before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
9137
9358
|
const store = new Store;
|
|
9138
9359
|
try {
|
|
9139
9360
|
const target = {
|
|
@@ -9141,7 +9362,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9141
9362
|
command: opts.cmd,
|
|
9142
9363
|
cwd: opts.cwd,
|
|
9143
9364
|
shell: opts.shell,
|
|
9144
|
-
timeoutMs: opts.timeout
|
|
9365
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9145
9366
|
account: accountFromOpts(opts),
|
|
9146
9367
|
preflight: runtimePreflightFromOpts(opts)
|
|
9147
9368
|
};
|
|
@@ -9153,7 +9374,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9153
9374
|
store.close();
|
|
9154
9375
|
}
|
|
9155
9376
|
});
|
|
9156
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").option("--prompt <prompt>", "agent prompt").option("--prompt-file <file>", "read the agent prompt from a markdown/text file").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
9377
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").option("--prompt <prompt>", "agent prompt").option("--prompt-file <file>", "read the agent prompt from a markdown/text file").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "run timeout; use none/unlimited for no timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
9157
9378
|
const provider = opts.provider;
|
|
9158
9379
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
9159
9380
|
throw new Error("unsupported provider");
|
|
@@ -9174,7 +9395,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9174
9395
|
agent: opts.agent,
|
|
9175
9396
|
authProfile: providerAuthProfileFromOpts(opts, provider),
|
|
9176
9397
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9177
|
-
timeoutMs: opts.timeout
|
|
9398
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9178
9399
|
configIsolation: opts.configIsolation,
|
|
9179
9400
|
permissionMode: permissionModeFromOpts(opts, provider),
|
|
9180
9401
|
sandbox: sandboxFromOpts(opts, provider),
|
|
@@ -9190,13 +9411,14 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9190
9411
|
store.close();
|
|
9191
9412
|
}
|
|
9192
9413
|
});
|
|
9193
|
-
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight-each-run", "check workflow steps before every scheduled run").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
|
|
9414
|
+
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--timeout <duration>", "workflow run timeout; use none/unlimited for no workflow-level timeout").option("--preflight-each-run", "check workflow steps before every scheduled run").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
|
|
9194
9415
|
const store = new Store;
|
|
9195
9416
|
try {
|
|
9196
9417
|
const workflow = store.requireWorkflow(opts.workflow);
|
|
9197
9418
|
const target = {
|
|
9198
9419
|
type: "workflow",
|
|
9199
9420
|
workflowId: workflow.id,
|
|
9421
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9200
9422
|
preflight: runtimePreflightFromOpts(opts)
|
|
9201
9423
|
};
|
|
9202
9424
|
const input = baseCreateInput(name, opts, target);
|
|
@@ -9214,10 +9436,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
9214
9436
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
9215
9437
|
var goal = program.command("goal").description("inspect goal runs");
|
|
9216
9438
|
function addRouteEventOptions(command) {
|
|
9217
|
-
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("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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");
|
|
9439
|
+
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("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").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");
|
|
9218
9440
|
}
|
|
9219
9441
|
function addTodosDrainOptions(command, opts = {}) {
|
|
9220
|
-
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("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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");
|
|
9442
|
+
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("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").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");
|
|
9221
9443
|
if (opts.includeDryRun ?? true) {
|
|
9222
9444
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
9223
9445
|
}
|
|
@@ -9270,6 +9492,7 @@ function routeDrainArgs(opts) {
|
|
|
9270
9492
|
add("--agent", opts.agent);
|
|
9271
9493
|
for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
|
|
9272
9494
|
add("--add-dir", dir);
|
|
9495
|
+
add("--timeout", opts.timeout);
|
|
9273
9496
|
add("--permission-mode", opts.permissionMode);
|
|
9274
9497
|
add("--sandbox", opts.sandbox);
|
|
9275
9498
|
addBool("--manual-break-glass", opts.manualBreakGlass);
|
|
@@ -9581,7 +9804,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9581
9804
|
}
|
|
9582
9805
|
});
|
|
9583
9806
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
9584
|
-
eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
9807
|
+
eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--template <id>", "todos-task route workflow template: todos-task-worker-verifier or task-lifecycle", TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID).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("--triage-auth-profile <profile>", "provider-native auth profile for triage step").option("--planner-auth-profile <profile>", "provider-native auth profile for planner step").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("--triage-account <profile>", "OpenAccounts profile for triage step").option("--planner-account <profile>", "OpenAccounts profile for planner step").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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").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", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
9585
9808
|
const event = await readEventEnvelopeFromStdin();
|
|
9586
9809
|
const result = routeTodosTaskEvent(event, opts);
|
|
9587
9810
|
print(result.value, result.human);
|
|
@@ -9590,7 +9813,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
|
|
|
9590
9813
|
addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
|
|
9591
9814
|
drainTodosTaskRoutes(opts);
|
|
9592
9815
|
});
|
|
9593
|
-
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) => {
|
|
9816
|
+
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("--timeout <duration>", "agent step timeout; use none/unlimited for no timeout").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) => {
|
|
9594
9817
|
const event = await readEventEnvelopeFromStdin();
|
|
9595
9818
|
const result = routeGenericEvent(event, opts);
|
|
9596
9819
|
print(result.value, result.human);
|
|
@@ -9806,6 +10029,75 @@ workflows.command("recover <runId>").description("reset interrupted running work
|
|
|
9806
10029
|
store.close();
|
|
9807
10030
|
}
|
|
9808
10031
|
});
|
|
10032
|
+
workflows.command("migrate-agent-timeouts").description("append-only migrate active workflow loops to a new agent timeout policy").option("--loop <idOrName>", "migrate only one loop instead of all active workflow loops").option("--timeout <duration>", "agent timeout policy; use none/unlimited for no timeout", "none").option("--apply", "create new workflow specs and retarget eligible loops").option("--archive-old", "archive old workflow specs after retargeting when no active loops still reference them").action((opts) => {
|
|
10033
|
+
const store = new Store;
|
|
10034
|
+
try {
|
|
10035
|
+
const timeoutMs = timeoutDuration(opts.timeout, "--timeout") ?? null;
|
|
10036
|
+
const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
|
|
10037
|
+
const rows = [];
|
|
10038
|
+
for (const loop of candidateLoops) {
|
|
10039
|
+
if (loop.archivedAt) {
|
|
10040
|
+
rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
|
|
10041
|
+
continue;
|
|
10042
|
+
}
|
|
10043
|
+
if (loop.target.type !== "workflow") {
|
|
10044
|
+
rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
|
|
10045
|
+
continue;
|
|
10046
|
+
}
|
|
10047
|
+
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
10048
|
+
const nextWorkflowName = workflowTimeoutMigrationName(workflow, timeoutMs);
|
|
10049
|
+
const migration = workflowWithAgentTimeouts(workflow, timeoutMs, { name: nextWorkflowName });
|
|
10050
|
+
if (migration.agentStepIds.length === 0) {
|
|
10051
|
+
rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no agent steps" });
|
|
10052
|
+
continue;
|
|
10053
|
+
}
|
|
10054
|
+
if (!migration.changed) {
|
|
10055
|
+
rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "agent timeout policy already matches" });
|
|
10056
|
+
continue;
|
|
10057
|
+
}
|
|
10058
|
+
if (store.hasRunningRun(loop.id)) {
|
|
10059
|
+
rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
|
|
10060
|
+
continue;
|
|
10061
|
+
}
|
|
10062
|
+
if (!opts.apply) {
|
|
10063
|
+
rows.push({
|
|
10064
|
+
loop: publicLoop(loop),
|
|
10065
|
+
workflow: publicWorkflow(workflow),
|
|
10066
|
+
status: "would_migrate",
|
|
10067
|
+
agentStepIds: migration.agentStepIds,
|
|
10068
|
+
nextWorkflowName,
|
|
10069
|
+
timeoutMs
|
|
10070
|
+
});
|
|
10071
|
+
continue;
|
|
10072
|
+
}
|
|
10073
|
+
const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
|
|
10074
|
+
workflowTimeoutMs: timeoutMs,
|
|
10075
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
10076
|
+
});
|
|
10077
|
+
rows.push({
|
|
10078
|
+
loop: publicLoop(migrated.loop),
|
|
10079
|
+
previousWorkflow: publicWorkflow(migrated.previousWorkflow),
|
|
10080
|
+
workflow: publicWorkflow(migrated.workflow),
|
|
10081
|
+
archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
|
|
10082
|
+
status: "migrated",
|
|
10083
|
+
agentStepIds: migration.agentStepIds,
|
|
10084
|
+
timeoutMs
|
|
10085
|
+
});
|
|
10086
|
+
}
|
|
10087
|
+
const summary = {
|
|
10088
|
+
apply: Boolean(opts.apply),
|
|
10089
|
+
timeoutMs,
|
|
10090
|
+
total: rows.length,
|
|
10091
|
+
migrated: rows.filter((row) => row.status === "migrated").length,
|
|
10092
|
+
wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
|
|
10093
|
+
blocked: rows.filter((row) => row.status === "blocked").length,
|
|
10094
|
+
skipped: rows.filter((row) => row.status === "skipped").length
|
|
10095
|
+
};
|
|
10096
|
+
print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
|
|
10097
|
+
} finally {
|
|
10098
|
+
store.close();
|
|
10099
|
+
}
|
|
10100
|
+
});
|
|
9809
10101
|
workflows.command("archive <idOrName>").action((idOrName) => {
|
|
9810
10102
|
const store = new Store;
|
|
9811
10103
|
try {
|