@hasna/loops 0.3.53 → 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 +77 -4
- package/dist/cli/index.js +477 -108
- package/dist/daemon/index.js +223 -29
- package/dist/index.js +308 -75
- package/dist/lib/store.d.ts +16 -1
- package/dist/lib/store.js +202 -11
- package/dist/lib/templates.d.ts +4 -2
- package/dist/lib/workflow-spec.d.ts +8 -2
- package/dist/sdk/index.js +219 -25
- package/dist/types.d.ts +24 -8
- package/docs/USAGE.md +82 -4
- package/docs/workflows/transcript-feedback-to-loops.json +2 -2
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -314,6 +314,8 @@ function updateReadyFlags(nodes, planStatus) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// src/lib/workflow-spec.ts
|
|
317
|
+
import { readFileSync } from "fs";
|
|
318
|
+
import { isAbsolute, resolve } from "path";
|
|
317
319
|
function assertObject(value, label) {
|
|
318
320
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
319
321
|
throw new Error(`${label} must be an object`);
|
|
@@ -329,6 +331,15 @@ function optionalPositiveInteger(value, label) {
|
|
|
329
331
|
throw new Error(`${label} must be a positive integer`);
|
|
330
332
|
return value;
|
|
331
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
|
+
}
|
|
332
343
|
function optionalBoolean(value, label) {
|
|
333
344
|
if (value === undefined)
|
|
334
345
|
return;
|
|
@@ -359,6 +370,38 @@ function optionalAccountRef(value, label) {
|
|
|
359
370
|
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
360
371
|
};
|
|
361
372
|
}
|
|
373
|
+
function promptFilePath(rawPath, opts) {
|
|
374
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
|
|
375
|
+
}
|
|
376
|
+
function readPromptFile(rawPath, label, opts) {
|
|
377
|
+
assertString(rawPath, `${label}.promptFile`);
|
|
378
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
379
|
+
let prompt;
|
|
380
|
+
try {
|
|
381
|
+
prompt = readFileSync(path, "utf8");
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
384
|
+
throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
|
|
385
|
+
}
|
|
386
|
+
if (prompt.trim() === "")
|
|
387
|
+
throw new Error(`${label}.promptFile must contain a non-empty prompt`);
|
|
388
|
+
return { prompt, promptSource: { type: "file", path } };
|
|
389
|
+
}
|
|
390
|
+
function matchingPromptSource(value, prompt, opts) {
|
|
391
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
392
|
+
return;
|
|
393
|
+
if (value.type !== "file")
|
|
394
|
+
return;
|
|
395
|
+
const rawPath = value.path;
|
|
396
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "")
|
|
397
|
+
return;
|
|
398
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
399
|
+
try {
|
|
400
|
+
return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
|
|
401
|
+
} catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
362
405
|
function normalizeGoalSpec(value, label = "goal") {
|
|
363
406
|
if (value === undefined)
|
|
364
407
|
return;
|
|
@@ -381,11 +424,11 @@ function normalizeGoalSpec(value, label = "goal") {
|
|
|
381
424
|
autoExecute
|
|
382
425
|
};
|
|
383
426
|
}
|
|
384
|
-
function validateTarget(value, label) {
|
|
427
|
+
function validateTarget(value, label, opts) {
|
|
385
428
|
assertObject(value, label);
|
|
386
429
|
if (value.type === "command") {
|
|
387
430
|
assertString(value.command, `${label}.command`);
|
|
388
|
-
|
|
431
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
389
432
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
390
433
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
391
434
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
@@ -394,11 +437,17 @@ function validateTarget(value, label) {
|
|
|
394
437
|
}
|
|
395
438
|
if (value.type === "agent") {
|
|
396
439
|
assertString(value.provider, `${label}.provider`);
|
|
397
|
-
|
|
440
|
+
const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
|
|
441
|
+
const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
|
|
442
|
+
if (hasPrompt && hasPromptFile)
|
|
443
|
+
throw new Error(`${label} must use either prompt or promptFile, not both`);
|
|
444
|
+
if (!hasPrompt && !hasPromptFile)
|
|
445
|
+
throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
|
|
446
|
+
const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
|
|
398
447
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
399
448
|
if (!providers.includes(value.provider))
|
|
400
449
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
401
|
-
|
|
450
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
402
451
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
403
452
|
if (value.authProfile !== undefined) {
|
|
404
453
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
@@ -495,11 +544,14 @@ function validateTarget(value, label) {
|
|
|
495
544
|
if (value.routing.eventSource !== undefined)
|
|
496
545
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
497
546
|
}
|
|
498
|
-
|
|
547
|
+
const target = { ...value };
|
|
548
|
+
delete target.promptFile;
|
|
549
|
+
delete target.promptSource;
|
|
550
|
+
return { ...target, ...promptFields };
|
|
499
551
|
}
|
|
500
552
|
throw new Error(`${label}.type must be command or agent`);
|
|
501
553
|
}
|
|
502
|
-
function normalizeCreateWorkflowInput(input) {
|
|
554
|
+
function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
503
555
|
assertString(input.name, "workflow.name");
|
|
504
556
|
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
505
557
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
@@ -515,10 +567,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
515
567
|
...step,
|
|
516
568
|
id: step.id,
|
|
517
569
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
518
|
-
target: validateTarget(step.target, `workflow.steps[${index}].target
|
|
570
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
519
571
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
520
572
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
521
|
-
timeoutMs:
|
|
573
|
+
timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
522
574
|
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
523
575
|
};
|
|
524
576
|
});
|
|
@@ -558,7 +610,7 @@ function workflowExecutionOrder(workflow) {
|
|
|
558
610
|
visit(step);
|
|
559
611
|
return order;
|
|
560
612
|
}
|
|
561
|
-
function workflowBodyFromJson(value, fallbackName) {
|
|
613
|
+
function workflowBodyFromJson(value, fallbackName, opts = {}) {
|
|
562
614
|
assertObject(value, "workflow file");
|
|
563
615
|
const rawName = fallbackName ?? value.name;
|
|
564
616
|
assertString(rawName, "workflow.name");
|
|
@@ -570,7 +622,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
570
622
|
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
571
623
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
572
624
|
steps: value.steps
|
|
573
|
-
});
|
|
625
|
+
}, opts);
|
|
574
626
|
}
|
|
575
627
|
|
|
576
628
|
// src/lib/run-artifacts.ts
|
|
@@ -1207,13 +1259,17 @@ class Store {
|
|
|
1207
1259
|
}
|
|
1208
1260
|
createLoop(input, from = new Date) {
|
|
1209
1261
|
const now = nowIso();
|
|
1262
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1263
|
+
name: "loop-target-validation",
|
|
1264
|
+
steps: [{ id: "target", target: input.target }]
|
|
1265
|
+
}).steps[0].target;
|
|
1210
1266
|
const loop = {
|
|
1211
1267
|
id: genId(),
|
|
1212
1268
|
name: input.name,
|
|
1213
1269
|
description: input.description,
|
|
1214
1270
|
status: "active",
|
|
1215
1271
|
schedule: input.schedule,
|
|
1216
|
-
target
|
|
1272
|
+
target,
|
|
1217
1273
|
goal: input.goal,
|
|
1218
1274
|
machine: input.machine,
|
|
1219
1275
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
@@ -1260,6 +1316,17 @@ class Store {
|
|
|
1260
1316
|
const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
|
|
1261
1317
|
return row ? rowToLoop(row) : undefined;
|
|
1262
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
|
+
}
|
|
1263
1330
|
requireLoop(idOrName) {
|
|
1264
1331
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
1265
1332
|
throw new Error(`loop not found: ${idOrName}`);
|
|
@@ -1334,6 +1401,130 @@ class Store {
|
|
|
1334
1401
|
throw new Error(`loop not found after update: ${id}`);
|
|
1335
1402
|
return after;
|
|
1336
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
|
+
}
|
|
1337
1528
|
renameLoop(id, name, opts = {}) {
|
|
1338
1529
|
const current = this.getLoop(id);
|
|
1339
1530
|
if (!current)
|
|
@@ -2914,9 +3105,9 @@ class Store {
|
|
|
2914
3105
|
|
|
2915
3106
|
// src/cli/index.ts
|
|
2916
3107
|
import { createHash as createHash3, randomUUID } from "crypto";
|
|
2917
|
-
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as
|
|
3108
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync4, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2918
3109
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2919
|
-
import { join as join6, resolve as
|
|
3110
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
2920
3111
|
import { tmpdir as tmpdir2 } from "os";
|
|
2921
3112
|
import { Database as Database2 } from "bun:sqlite";
|
|
2922
3113
|
import { Command } from "commander";
|
|
@@ -3559,7 +3750,7 @@ function commandSpec(target) {
|
|
|
3559
3750
|
cwd: commandTarget.cwd,
|
|
3560
3751
|
shell: commandTarget.shell,
|
|
3561
3752
|
env: commandTarget.env,
|
|
3562
|
-
timeoutMs: commandTarget.timeoutMs
|
|
3753
|
+
timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
|
|
3563
3754
|
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
3564
3755
|
account: commandTarget.account,
|
|
3565
3756
|
accountTool: commandTarget.account?.tool
|
|
@@ -3570,7 +3761,7 @@ function commandSpec(target) {
|
|
|
3570
3761
|
command: providerCommand(agentTarget.provider),
|
|
3571
3762
|
args: agentArgs(agentTarget),
|
|
3572
3763
|
cwd: agentTarget.cwd,
|
|
3573
|
-
timeoutMs: agentTarget.timeoutMs ??
|
|
3764
|
+
timeoutMs: agentTarget.timeoutMs ?? null,
|
|
3574
3765
|
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
3575
3766
|
account: agentTarget.account,
|
|
3576
3767
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
@@ -3758,12 +3949,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3758
3949
|
if (opts.signal?.aborted)
|
|
3759
3950
|
abortHandler();
|
|
3760
3951
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3761
|
-
const timer = setTimeout(() => {
|
|
3952
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3762
3953
|
timedOut = true;
|
|
3763
3954
|
if (child.pid)
|
|
3764
3955
|
killProcessGroup(child.pid);
|
|
3765
|
-
}, spec.timeoutMs);
|
|
3766
|
-
timer
|
|
3956
|
+
}, spec.timeoutMs) : undefined;
|
|
3957
|
+
timer?.unref();
|
|
3767
3958
|
let idleTimer;
|
|
3768
3959
|
const resetIdleTimer = () => {
|
|
3769
3960
|
if (!spec.idleTimeoutMs)
|
|
@@ -3795,7 +3986,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3795
3986
|
} catch (err) {
|
|
3796
3987
|
error = err instanceof Error ? err.message : String(err);
|
|
3797
3988
|
} finally {
|
|
3798
|
-
|
|
3989
|
+
if (timer)
|
|
3990
|
+
clearTimeout(timer);
|
|
3799
3991
|
if (idleTimer)
|
|
3800
3992
|
clearTimeout(idleTimer);
|
|
3801
3993
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -3937,12 +4129,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3937
4129
|
if (opts.signal?.aborted)
|
|
3938
4130
|
abortHandler();
|
|
3939
4131
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3940
|
-
const timer = setTimeout(() => {
|
|
4132
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3941
4133
|
timedOut = true;
|
|
3942
4134
|
if (child.pid)
|
|
3943
4135
|
killProcessGroup(child.pid);
|
|
3944
|
-
}, spec.timeoutMs);
|
|
3945
|
-
timer
|
|
4136
|
+
}, spec.timeoutMs) : undefined;
|
|
4137
|
+
timer?.unref();
|
|
3946
4138
|
let idleTimer;
|
|
3947
4139
|
const resetIdleTimer = () => {
|
|
3948
4140
|
if (!spec.idleTimeoutMs)
|
|
@@ -3974,7 +4166,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3974
4166
|
} catch (err) {
|
|
3975
4167
|
error = err instanceof Error ? err.message : String(err);
|
|
3976
4168
|
} finally {
|
|
3977
|
-
|
|
4169
|
+
if (timer)
|
|
4170
|
+
clearTimeout(timer);
|
|
3978
4171
|
if (idleTimer)
|
|
3979
4172
|
clearTimeout(idleTimer);
|
|
3980
4173
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -4399,7 +4592,7 @@ ${result.stderr}`);
|
|
|
4399
4592
|
// src/lib/workflow-runner.ts
|
|
4400
4593
|
function targetWithStepAccount(step) {
|
|
4401
4594
|
const account = step.account ?? step.target.account;
|
|
4402
|
-
const timeoutMs = step.timeoutMs
|
|
4595
|
+
const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
|
|
4403
4596
|
if (!account && timeoutMs === step.target.timeoutMs)
|
|
4404
4597
|
return step.target;
|
|
4405
4598
|
return { ...step.target, account, timeoutMs };
|
|
@@ -4695,7 +4888,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4695
4888
|
})
|
|
4696
4889
|
});
|
|
4697
4890
|
}
|
|
4698
|
-
const
|
|
4891
|
+
const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
|
|
4892
|
+
const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
|
|
4699
4893
|
let workflowTimedOut = false;
|
|
4700
4894
|
const externalAbort = () => controller?.abort();
|
|
4701
4895
|
if (controller && opts.signal?.aborted)
|
|
@@ -4705,13 +4899,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4705
4899
|
const timer = controller ? setTimeout(() => {
|
|
4706
4900
|
workflowTimedOut = true;
|
|
4707
4901
|
controller.abort();
|
|
4708
|
-
},
|
|
4902
|
+
}, workflowTimeoutMs) : undefined;
|
|
4709
4903
|
timer?.unref();
|
|
4710
4904
|
try {
|
|
4711
4905
|
return await executeWorkflow(store, workflow, {
|
|
4712
4906
|
...opts,
|
|
4713
4907
|
signal: controller?.signal ?? opts.signal,
|
|
4714
|
-
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${
|
|
4908
|
+
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
|
|
4715
4909
|
loop,
|
|
4716
4910
|
loopRun: run,
|
|
4717
4911
|
scheduledFor: run.scheduledFor,
|
|
@@ -5031,13 +5225,13 @@ async function tick(deps) {
|
|
|
5031
5225
|
}
|
|
5032
5226
|
|
|
5033
5227
|
// src/daemon/control.ts
|
|
5034
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5228
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5035
5229
|
import { hostname } from "os";
|
|
5036
5230
|
import { dirname as dirname2 } from "path";
|
|
5037
5231
|
|
|
5038
5232
|
// src/daemon/loop.ts
|
|
5039
5233
|
function realSleep(ms) {
|
|
5040
|
-
return new Promise((
|
|
5234
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5041
5235
|
}
|
|
5042
5236
|
async function runLoop(opts) {
|
|
5043
5237
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -5062,7 +5256,7 @@ function readPid(path = pidFilePath()) {
|
|
|
5062
5256
|
if (!existsSync3(path))
|
|
5063
5257
|
return;
|
|
5064
5258
|
try {
|
|
5065
|
-
const pid = Number(
|
|
5259
|
+
const pid = Number(readFileSync2(path, "utf8").trim());
|
|
5066
5260
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
5067
5261
|
} catch {
|
|
5068
5262
|
return;
|
|
@@ -5487,7 +5681,11 @@ function runDoctor(store) {
|
|
|
5487
5681
|
if (loop.target.type === "workflow") {
|
|
5488
5682
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
5489
5683
|
for (const step of workflowExecutionOrder(workflow)) {
|
|
5490
|
-
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 });
|
|
5491
5689
|
}
|
|
5492
5690
|
} else {
|
|
5493
5691
|
preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
|
|
@@ -5967,7 +6165,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5967
6165
|
// package.json
|
|
5968
6166
|
var package_default = {
|
|
5969
6167
|
name: "@hasna/loops",
|
|
5970
|
-
version: "0.3.
|
|
6168
|
+
version: "0.3.55",
|
|
5971
6169
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5972
6170
|
type: "module",
|
|
5973
6171
|
main: "dist/index.js",
|
|
@@ -6057,9 +6255,9 @@ function packageVersion() {
|
|
|
6057
6255
|
|
|
6058
6256
|
// src/lib/templates.ts
|
|
6059
6257
|
import { execFileSync } from "child_process";
|
|
6060
|
-
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as
|
|
6258
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6061
6259
|
import { homedir as homedir3 } from "os";
|
|
6062
|
-
import { basename as basename3, isAbsolute, join as join5, relative, resolve } from "path";
|
|
6260
|
+
import { basename as basename3, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
|
|
6063
6261
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
6064
6262
|
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
6065
6263
|
var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
|
|
@@ -6097,7 +6295,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6097
6295
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
6098
6296
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6099
6297
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6100
|
-
{ 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." }
|
|
6101
6300
|
]
|
|
6102
6301
|
},
|
|
6103
6302
|
{
|
|
@@ -6127,7 +6326,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6127
6326
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
6128
6327
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6129
6328
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6130
|
-
{ 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." }
|
|
6131
6331
|
]
|
|
6132
6332
|
},
|
|
6133
6333
|
{
|
|
@@ -6155,7 +6355,7 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6155
6355
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
6156
6356
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
6157
6357
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
6158
|
-
{ 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." }
|
|
6159
6359
|
]
|
|
6160
6360
|
},
|
|
6161
6361
|
{
|
|
@@ -6174,7 +6374,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
6174
6374
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
6175
6375
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
6176
6376
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
6177
|
-
{ 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." }
|
|
6178
6379
|
]
|
|
6179
6380
|
},
|
|
6180
6381
|
{
|
|
@@ -6279,6 +6480,30 @@ function taskLabel(input) {
|
|
|
6279
6480
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
6280
6481
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
6281
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
|
+
}
|
|
6282
6507
|
function stableIndex(seed, size) {
|
|
6283
6508
|
let hash = 2166136261;
|
|
6284
6509
|
for (let i = 0;i < seed.length; i += 1) {
|
|
@@ -6341,7 +6566,7 @@ function normalizeWorktreeMode(mode) {
|
|
|
6341
6566
|
function defaultWorktreeRoot(root) {
|
|
6342
6567
|
if (root?.trim()) {
|
|
6343
6568
|
const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
|
|
6344
|
-
return
|
|
6569
|
+
return isAbsolute2(expanded) ? expanded : resolve2(expanded);
|
|
6345
6570
|
}
|
|
6346
6571
|
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
6347
6572
|
}
|
|
@@ -6448,7 +6673,7 @@ function worktreePlan(input, seed) {
|
|
|
6448
6673
|
const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
|
|
6449
6674
|
const worktreePath = join5(root, repoSlug, seedSlug);
|
|
6450
6675
|
const relativeCwd = relative(repoRoot, originalCwd);
|
|
6451
|
-
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !
|
|
6676
|
+
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
|
|
6452
6677
|
const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
|
|
6453
6678
|
const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
|
|
6454
6679
|
const prepareStep = {
|
|
@@ -6550,7 +6775,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
6550
6775
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
6551
6776
|
},
|
|
6552
6777
|
account: accountForRole(input, role, seed),
|
|
6553
|
-
timeoutMs:
|
|
6778
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6554
6779
|
};
|
|
6555
6780
|
}
|
|
6556
6781
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -6683,9 +6908,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
|
|
|
6683
6908
|
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
6684
6909
|
}
|
|
6685
6910
|
}
|
|
6911
|
+
function assertNoCustomTemplatePromptFiles(value, label) {
|
|
6912
|
+
if (!value || typeof value !== "object")
|
|
6913
|
+
return;
|
|
6914
|
+
if (Array.isArray(value)) {
|
|
6915
|
+
value.forEach((entry, index) => assertNoCustomTemplatePromptFiles(entry, `${label}[${index}]`));
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
6919
|
+
if (key === "promptFile") {
|
|
6920
|
+
throw new Error(`${label}.${key} is not allowed in custom templates; use direct workflow JSON for prompt-file-backed workflows`);
|
|
6921
|
+
}
|
|
6922
|
+
assertNoCustomTemplatePromptFiles(entry, `${label}.${key}`);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6686
6925
|
function assertCustomTemplateSafety(value, label) {
|
|
6687
6926
|
assertNoDangerousCustomTemplateScalars(value, label);
|
|
6688
6927
|
assertNoImplicitDangerFullAccess(value, label);
|
|
6928
|
+
assertNoCustomTemplatePromptFiles(value, label);
|
|
6689
6929
|
}
|
|
6690
6930
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
6691
6931
|
assertRecord(value, sourcePath);
|
|
@@ -6715,7 +6955,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
6715
6955
|
function readCustomTemplateFile(file) {
|
|
6716
6956
|
let parsed;
|
|
6717
6957
|
try {
|
|
6718
|
-
parsed = JSON.parse(
|
|
6958
|
+
parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
6719
6959
|
} catch (error) {
|
|
6720
6960
|
const message = error instanceof Error ? error.message : String(error);
|
|
6721
6961
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -6854,19 +7094,19 @@ function renderCustomLoopTemplate(entry, values) {
|
|
|
6854
7094
|
return workflow;
|
|
6855
7095
|
}
|
|
6856
7096
|
function validateCustomLoopTemplateFile(file) {
|
|
6857
|
-
const source =
|
|
7097
|
+
const source = resolve2(file);
|
|
6858
7098
|
const entry = readCustomTemplateFile(source);
|
|
6859
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
7099
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
|
|
6860
7100
|
assertNoTemplateCollisions([...existing, entry]);
|
|
6861
7101
|
return structuredClone(entry.summary);
|
|
6862
7102
|
}
|
|
6863
7103
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
6864
|
-
const source =
|
|
7104
|
+
const source = resolve2(file);
|
|
6865
7105
|
const entry = readCustomTemplateFile(source);
|
|
6866
7106
|
const dir = ensureCustomLoopTemplatesDir();
|
|
6867
7107
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
6868
7108
|
const replaced = existsSync4(destination);
|
|
6869
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
7109
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
6870
7110
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
6871
7111
|
if (replaced) {
|
|
6872
7112
|
const stat = lstatSync(destination);
|
|
@@ -6978,18 +7218,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
6978
7218
|
name: "Worker",
|
|
6979
7219
|
description: "Implement the todos task and record evidence.",
|
|
6980
7220
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6981
|
-
timeoutMs:
|
|
7221
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6982
7222
|
},
|
|
6983
7223
|
{
|
|
6984
7224
|
id: "verifier",
|
|
6985
7225
|
name: "Verifier",
|
|
6986
7226
|
description: "Adversarially verify worker output and update todos.",
|
|
6987
7227
|
dependsOn: ["worker"],
|
|
6988
|
-
target:
|
|
6989
|
-
|
|
6990
|
-
idleTimeoutMs: 10 * 60000
|
|
6991
|
-
},
|
|
6992
|
-
timeoutMs: 30 * 60000
|
|
7228
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
7229
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6993
7230
|
}
|
|
6994
7231
|
])
|
|
6995
7232
|
};
|
|
@@ -7150,7 +7387,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7150
7387
|
name: "Triage",
|
|
7151
7388
|
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
7152
7389
|
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
7153
|
-
timeoutMs:
|
|
7390
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7154
7391
|
},
|
|
7155
7392
|
{
|
|
7156
7393
|
id: "triage-gate",
|
|
@@ -7172,7 +7409,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7172
7409
|
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
7173
7410
|
dependsOn: ["triage-gate"],
|
|
7174
7411
|
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
7175
|
-
timeoutMs:
|
|
7412
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7176
7413
|
},
|
|
7177
7414
|
{
|
|
7178
7415
|
id: "planner-gate",
|
|
@@ -7194,18 +7431,15 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
7194
7431
|
description: "Implement the todos task according to triage and planner evidence.",
|
|
7195
7432
|
dependsOn: ["planner-gate"],
|
|
7196
7433
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
7197
|
-
timeoutMs:
|
|
7434
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7198
7435
|
},
|
|
7199
7436
|
{
|
|
7200
7437
|
id: "verifier",
|
|
7201
7438
|
name: "Verifier",
|
|
7202
7439
|
description: "Adversarially verify worker output and update todos.",
|
|
7203
7440
|
dependsOn: ["worker"],
|
|
7204
|
-
target:
|
|
7205
|
-
|
|
7206
|
-
idleTimeoutMs: 10 * 60000
|
|
7207
|
-
},
|
|
7208
|
-
timeoutMs: 30 * 60000
|
|
7441
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
7442
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7209
7443
|
}
|
|
7210
7444
|
])
|
|
7211
7445
|
};
|
|
@@ -7275,18 +7509,15 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
7275
7509
|
name: "Worker",
|
|
7276
7510
|
description: "Handle the Hasna event and record evidence.",
|
|
7277
7511
|
target: agentTarget(input, workerPrompt, "worker", seed, plan),
|
|
7278
|
-
timeoutMs:
|
|
7512
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7279
7513
|
},
|
|
7280
7514
|
{
|
|
7281
7515
|
id: "verifier",
|
|
7282
7516
|
name: "Verifier",
|
|
7283
7517
|
description: "Adversarially verify event handling.",
|
|
7284
7518
|
dependsOn: ["worker"],
|
|
7285
|
-
target:
|
|
7286
|
-
|
|
7287
|
-
idleTimeoutMs: 10 * 60000
|
|
7288
|
-
},
|
|
7289
|
-
timeoutMs: 30 * 60000
|
|
7519
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
7520
|
+
timeoutMs: agentTimeoutMs(input)
|
|
7290
7521
|
}
|
|
7291
7522
|
])
|
|
7292
7523
|
};
|
|
@@ -7298,7 +7529,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7298
7529
|
throw new Error("projectPath is required");
|
|
7299
7530
|
const seed = `${input.projectPath}:${input.objective}`;
|
|
7300
7531
|
const plan = worktreePlan(input, seed);
|
|
7301
|
-
const timeoutMs =
|
|
7532
|
+
const timeoutMs = agentTimeoutMs(input);
|
|
7302
7533
|
const workerPrompt = [
|
|
7303
7534
|
`/goal ${input.objective}`,
|
|
7304
7535
|
"",
|
|
@@ -7336,11 +7567,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
7336
7567
|
name: "Verifier",
|
|
7337
7568
|
description: "Adversarially verify the bounded objective result.",
|
|
7338
7569
|
dependsOn: ["worker"],
|
|
7339
|
-
target:
|
|
7340
|
-
|
|
7341
|
-
idleTimeoutMs: 10 * 60000
|
|
7342
|
-
},
|
|
7343
|
-
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
7570
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
7571
|
+
timeoutMs
|
|
7344
7572
|
}
|
|
7345
7573
|
])
|
|
7346
7574
|
};
|
|
@@ -7369,7 +7597,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7369
7597
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
7370
7598
|
worktreeRoot: values.worktreeRoot,
|
|
7371
7599
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7372
|
-
timeoutMs:
|
|
7600
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7373
7601
|
};
|
|
7374
7602
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
7375
7603
|
const taskId = values.taskId ?? "";
|
|
@@ -7406,6 +7634,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
7406
7634
|
worktreeMode: values.worktreeMode ?? "required",
|
|
7407
7635
|
worktreeRoot: values.worktreeRoot,
|
|
7408
7636
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7637
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
7409
7638
|
eventId: values.eventId,
|
|
7410
7639
|
eventType: values.eventType
|
|
7411
7640
|
});
|
|
@@ -7472,6 +7701,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
7472
7701
|
if (!checkCommand.trim())
|
|
7473
7702
|
throw new Error("checkCommand is required");
|
|
7474
7703
|
const seed = `${projectPath}:${checkCommand}`;
|
|
7704
|
+
const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
|
|
7705
|
+
const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
|
|
7475
7706
|
return {
|
|
7476
7707
|
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
7477
7708
|
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
@@ -7486,10 +7717,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
7486
7717
|
command: "bash",
|
|
7487
7718
|
args: ["-lc", checkCommand],
|
|
7488
7719
|
cwd: projectPath,
|
|
7489
|
-
timeoutMs
|
|
7490
|
-
idleTimeoutMs
|
|
7720
|
+
timeoutMs,
|
|
7721
|
+
idleTimeoutMs
|
|
7491
7722
|
},
|
|
7492
|
-
timeoutMs
|
|
7723
|
+
timeoutMs
|
|
7493
7724
|
}
|
|
7494
7725
|
]
|
|
7495
7726
|
};
|
|
@@ -7527,6 +7758,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7527
7758
|
worktreeMode: values.worktreeMode,
|
|
7528
7759
|
worktreeRoot: values.worktreeRoot,
|
|
7529
7760
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7761
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
7530
7762
|
eventId: values.eventId,
|
|
7531
7763
|
eventType: values.eventType
|
|
7532
7764
|
});
|
|
@@ -7558,7 +7790,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7558
7790
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
7559
7791
|
worktreeMode: values.worktreeMode,
|
|
7560
7792
|
worktreeRoot: values.worktreeRoot,
|
|
7561
|
-
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
7793
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7794
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7562
7795
|
});
|
|
7563
7796
|
}
|
|
7564
7797
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -7586,7 +7819,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
7586
7819
|
worktreeMode: values.worktreeMode,
|
|
7587
7820
|
worktreeRoot: values.worktreeRoot,
|
|
7588
7821
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
7589
|
-
timeoutMs:
|
|
7822
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
7590
7823
|
});
|
|
7591
7824
|
}
|
|
7592
7825
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -7672,12 +7905,13 @@ function validationFailed(error, context) {
|
|
|
7672
7905
|
});
|
|
7673
7906
|
process.exit(1);
|
|
7674
7907
|
}
|
|
7675
|
-
function
|
|
7908
|
+
function normalizeLoopTargetForStorage(target, context, opts = {}) {
|
|
7676
7909
|
try {
|
|
7677
|
-
workflowBodyFromJson({
|
|
7910
|
+
const workflow = workflowBodyFromJson({
|
|
7678
7911
|
name: "loop-target-validation",
|
|
7679
7912
|
steps: [{ id: "target", target }]
|
|
7680
|
-
});
|
|
7913
|
+
}, undefined, opts);
|
|
7914
|
+
return workflow.steps[0].target;
|
|
7681
7915
|
} catch (error) {
|
|
7682
7916
|
validationFailed(error, context);
|
|
7683
7917
|
}
|
|
@@ -7689,6 +7923,14 @@ function normalizeWorkflowForStorage(body, context) {
|
|
|
7689
7923
|
validationFailed(error, context);
|
|
7690
7924
|
}
|
|
7691
7925
|
}
|
|
7926
|
+
function workflowBodyFromFile(file, fallbackName, context) {
|
|
7927
|
+
try {
|
|
7928
|
+
const resolved = resolve3(file);
|
|
7929
|
+
return workflowBodyFromJson(JSON.parse(readFileSync4(resolved, "utf8")), fallbackName, { baseDir: dirname4(resolved) });
|
|
7930
|
+
} catch (error) {
|
|
7931
|
+
validationFailed(error, context);
|
|
7932
|
+
}
|
|
7933
|
+
}
|
|
7692
7934
|
function preflightLoopTarget(target, context, metadata, opts) {
|
|
7693
7935
|
try {
|
|
7694
7936
|
return preflightTarget(target, metadata, opts);
|
|
@@ -7717,6 +7959,48 @@ function workflowSpecForPreflight(body, id = "validation") {
|
|
|
7717
7959
|
updatedAt: now
|
|
7718
7960
|
};
|
|
7719
7961
|
}
|
|
7962
|
+
function publicWorkflowBody(body) {
|
|
7963
|
+
const value = publicWorkflow(workflowSpecForPreflight(body, "render"));
|
|
7964
|
+
const { id: _id, status: _status, createdAt: _createdAt, updatedAt: _updatedAt, ...bodyOnly } = value;
|
|
7965
|
+
return bodyOnly;
|
|
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
|
+
}
|
|
7720
8004
|
function printTextOutput(value) {
|
|
7721
8005
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
7722
8006
|
console.log(line);
|
|
@@ -7757,6 +8041,17 @@ function positiveDuration(raw, label) {
|
|
|
7757
8041
|
throw new Error(`${label} must be greater than zero`);
|
|
7758
8042
|
return value;
|
|
7759
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
|
+
}
|
|
7760
8055
|
function parsePolicy(opts) {
|
|
7761
8056
|
const catchUp = opts.catchUp ?? "latest";
|
|
7762
8057
|
if (!["none", "latest", "all"].includes(catchUp))
|
|
@@ -7913,7 +8208,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
7913
8208
|
return {
|
|
7914
8209
|
ok: result.status === 0,
|
|
7915
8210
|
status: result.status,
|
|
7916
|
-
stdout:
|
|
8211
|
+
stdout: readFileSync4(stdoutPath, "utf8"),
|
|
7917
8212
|
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
7918
8213
|
error: result.error ? String(result.error.message || result.error) : ""
|
|
7919
8214
|
};
|
|
@@ -7957,7 +8252,7 @@ function readRouteCursors() {
|
|
|
7957
8252
|
if (!existsSync5(path))
|
|
7958
8253
|
return {};
|
|
7959
8254
|
try {
|
|
7960
|
-
const parsed = JSON.parse(
|
|
8255
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
7961
8256
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
7962
8257
|
} catch {
|
|
7963
8258
|
return {};
|
|
@@ -8312,7 +8607,7 @@ function hasThrottleLimits(limits) {
|
|
|
8312
8607
|
function normalizeRoutePath(value) {
|
|
8313
8608
|
if (!value?.trim())
|
|
8314
8609
|
return;
|
|
8315
|
-
const resolved =
|
|
8610
|
+
const resolved = resolve3(value.trim());
|
|
8316
8611
|
let canonical = resolved;
|
|
8317
8612
|
try {
|
|
8318
8613
|
canonical = realpathSync(resolved);
|
|
@@ -8324,7 +8619,7 @@ function normalizeRoutePath(value) {
|
|
|
8324
8619
|
try {
|
|
8325
8620
|
return realpathSync(gitRoot.stdout.trim());
|
|
8326
8621
|
} catch {
|
|
8327
|
-
return
|
|
8622
|
+
return resolve3(gitRoot.stdout.trim());
|
|
8328
8623
|
}
|
|
8329
8624
|
}
|
|
8330
8625
|
return canonical;
|
|
@@ -8333,7 +8628,7 @@ function routeProjectGroup(optsGroup, data, metadata) {
|
|
|
8333
8628
|
return optsGroup?.trim() || taskEventField(data, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]) || taskEventField(metadata, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]);
|
|
8334
8629
|
}
|
|
8335
8630
|
function routeThrottleDecision(store, args) {
|
|
8336
|
-
const projectPath = normalizeRoutePath(args.projectPath) ??
|
|
8631
|
+
const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
|
|
8337
8632
|
const projectGroup = args.projectGroup?.trim() || undefined;
|
|
8338
8633
|
const counts = store.countActiveWorkflowWorkItems({ projectKey: projectPath, projectGroup });
|
|
8339
8634
|
const base = {
|
|
@@ -8358,7 +8653,7 @@ function routeThrottleDecision(store, args) {
|
|
|
8358
8653
|
return { ...base, allowed: true };
|
|
8359
8654
|
}
|
|
8360
8655
|
function routeThrottleDryRunPreview(args) {
|
|
8361
|
-
const projectPath = normalizeRoutePath(args.projectPath) ??
|
|
8656
|
+
const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
|
|
8362
8657
|
const projectGroup = args.projectGroup?.trim() || undefined;
|
|
8363
8658
|
return {
|
|
8364
8659
|
evaluated: false,
|
|
@@ -8380,7 +8675,7 @@ function todosTaskRouteTemplateId(opts) {
|
|
|
8380
8675
|
return id;
|
|
8381
8676
|
}
|
|
8382
8677
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8383
|
-
const raw = opts.eventJson ?? (opts.eventFile ?
|
|
8678
|
+
const raw = opts.eventJson ?? (opts.eventFile ? readFileSync4(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
|
|
8384
8679
|
const event = JSON.parse(raw);
|
|
8385
8680
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
8386
8681
|
throw new Error("event JSON must be an object");
|
|
@@ -8421,7 +8716,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8421
8716
|
"cwd"
|
|
8422
8717
|
]);
|
|
8423
8718
|
const projectPath = dataProjectPath ?? metadataProjectPath ?? opts.projectPath ?? process.cwd();
|
|
8424
|
-
const routeProjectPath = normalizeRoutePath(projectPath) ??
|
|
8719
|
+
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
|
|
8425
8720
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
8426
8721
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
8427
8722
|
const idempotencyKey = `todos-task:${taskId}`;
|
|
@@ -8487,6 +8782,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8487
8782
|
variant: opts.variant,
|
|
8488
8783
|
agent: opts.agent,
|
|
8489
8784
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
8785
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
8490
8786
|
permissionMode,
|
|
8491
8787
|
sandbox,
|
|
8492
8788
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -8671,7 +8967,7 @@ function routeGenericEvent(event, opts) {
|
|
|
8671
8967
|
const data = eventData(event);
|
|
8672
8968
|
const metadata = eventMetadata(event);
|
|
8673
8969
|
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();
|
|
8674
|
-
const routeProjectPath = normalizeRoutePath(projectPath) ??
|
|
8970
|
+
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
|
|
8675
8971
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
8676
8972
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
8677
8973
|
const eventSuffix = event.id.slice(0, 8);
|
|
@@ -8709,6 +9005,7 @@ function routeGenericEvent(event, opts) {
|
|
|
8709
9005
|
variant: opts.variant,
|
|
8710
9006
|
agent: opts.agent,
|
|
8711
9007
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9008
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
8712
9009
|
permissionMode,
|
|
8713
9010
|
sandbox,
|
|
8714
9011
|
manualBreakGlass: Boolean(opts.manualBreakGlass),
|
|
@@ -9001,8 +9298,8 @@ function taskMatchesDrainFilters(task, filters) {
|
|
|
9001
9298
|
const path = taskProjectPath(task);
|
|
9002
9299
|
if (!path)
|
|
9003
9300
|
return false;
|
|
9004
|
-
const normalizedPath = normalizeRoutePath(path) ??
|
|
9005
|
-
const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ??
|
|
9301
|
+
const normalizedPath = normalizeRoutePath(path) ?? resolve3(path);
|
|
9302
|
+
const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve3(filters.projectPathPrefix);
|
|
9006
9303
|
if (normalizedPath !== normalizedPrefix && !normalizedPath.startsWith(`${normalizedPrefix}/`))
|
|
9007
9304
|
return false;
|
|
9008
9305
|
}
|
|
@@ -9057,7 +9354,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
9057
9354
|
return mode;
|
|
9058
9355
|
}
|
|
9059
9356
|
var create = program.command("create").description("create loops");
|
|
9060
|
-
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) => {
|
|
9061
9358
|
const store = new Store;
|
|
9062
9359
|
try {
|
|
9063
9360
|
const target = {
|
|
@@ -9065,7 +9362,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9065
9362
|
command: opts.cmd,
|
|
9066
9363
|
cwd: opts.cwd,
|
|
9067
9364
|
shell: opts.shell,
|
|
9068
|
-
timeoutMs: opts.timeout
|
|
9365
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9069
9366
|
account: accountFromOpts(opts),
|
|
9070
9367
|
preflight: runtimePreflightFromOpts(opts)
|
|
9071
9368
|
};
|
|
@@ -9077,7 +9374,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9077
9374
|
store.close();
|
|
9078
9375
|
}
|
|
9079
9376
|
});
|
|
9080
|
-
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").
|
|
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) => {
|
|
9081
9378
|
const provider = opts.provider;
|
|
9082
9379
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
9083
9380
|
throw new Error("unsupported provider");
|
|
@@ -9087,26 +9384,26 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9087
9384
|
}
|
|
9088
9385
|
const store = new Store;
|
|
9089
9386
|
try {
|
|
9090
|
-
const target = {
|
|
9387
|
+
const target = normalizeLoopTargetForStorage({
|
|
9091
9388
|
type: "agent",
|
|
9092
9389
|
provider,
|
|
9093
9390
|
prompt: opts.prompt,
|
|
9391
|
+
promptFile: opts.promptFile,
|
|
9094
9392
|
cwd: opts.cwd,
|
|
9095
9393
|
model: opts.model,
|
|
9096
9394
|
variant: opts.variant,
|
|
9097
9395
|
agent: opts.agent,
|
|
9098
9396
|
authProfile: providerAuthProfileFromOpts(opts, provider),
|
|
9099
9397
|
addDirs: listFromRepeatedOpts(opts.addDir),
|
|
9100
|
-
timeoutMs: opts.timeout
|
|
9398
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9101
9399
|
configIsolation: opts.configIsolation,
|
|
9102
9400
|
permissionMode: permissionModeFromOpts(opts, provider),
|
|
9103
9401
|
sandbox: sandboxFromOpts(opts, provider),
|
|
9104
9402
|
allowlist: allowlistFromOpts(opts),
|
|
9105
9403
|
account: accountFromOpts(opts),
|
|
9106
9404
|
preflight: runtimePreflightFromOpts(opts)
|
|
9107
|
-
};
|
|
9405
|
+
}, { name, type: "agent", provider }, { baseDir: process.cwd() });
|
|
9108
9406
|
const input = baseCreateInput(name, opts, target);
|
|
9109
|
-
validateLoopTargetForStorage(input.target, { name, type: "agent", provider });
|
|
9110
9407
|
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
9111
9408
|
const loop = store.createLoop(input);
|
|
9112
9409
|
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
@@ -9114,13 +9411,14 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9114
9411
|
store.close();
|
|
9115
9412
|
}
|
|
9116
9413
|
});
|
|
9117
|
-
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) => {
|
|
9118
9415
|
const store = new Store;
|
|
9119
9416
|
try {
|
|
9120
9417
|
const workflow = store.requireWorkflow(opts.workflow);
|
|
9121
9418
|
const target = {
|
|
9122
9419
|
type: "workflow",
|
|
9123
9420
|
workflowId: workflow.id,
|
|
9421
|
+
timeoutMs: timeoutDuration(opts.timeout, "--timeout"),
|
|
9124
9422
|
preflight: runtimePreflightFromOpts(opts)
|
|
9125
9423
|
};
|
|
9126
9424
|
const input = baseCreateInput(name, opts, target);
|
|
@@ -9138,10 +9436,10 @@ var events = program.command("events").description("handle Hasna event envelopes
|
|
|
9138
9436
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
9139
9437
|
var goal = program.command("goal").description("inspect goal runs");
|
|
9140
9438
|
function addRouteEventOptions(command) {
|
|
9141
|
-
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");
|
|
9142
9440
|
}
|
|
9143
9441
|
function addTodosDrainOptions(command, opts = {}) {
|
|
9144
|
-
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");
|
|
9145
9443
|
if (opts.includeDryRun ?? true) {
|
|
9146
9444
|
configured = configured.option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
|
|
9147
9445
|
}
|
|
@@ -9194,6 +9492,7 @@ function routeDrainArgs(opts) {
|
|
|
9194
9492
|
add("--agent", opts.agent);
|
|
9195
9493
|
for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
|
|
9196
9494
|
add("--add-dir", dir);
|
|
9495
|
+
add("--timeout", opts.timeout);
|
|
9197
9496
|
add("--permission-mode", opts.permissionMode);
|
|
9198
9497
|
add("--sandbox", opts.sandbox);
|
|
9199
9498
|
addBool("--manual-break-glass", opts.manualBreakGlass);
|
|
@@ -9400,7 +9699,8 @@ addTemplateSourceOption(templates.command("show <id>").description("show a templ
|
|
|
9400
9699
|
});
|
|
9401
9700
|
addTemplateSourceOption(templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
|
|
9402
9701
|
const workflow = renderLoopTemplate(id, parseVars(opts.var), { source: templateSource(opts.source) });
|
|
9403
|
-
|
|
9702
|
+
const value = publicWorkflowBody(workflow);
|
|
9703
|
+
print(value, JSON.stringify(value, null, 2));
|
|
9404
9704
|
});
|
|
9405
9705
|
addTemplateSourceOption(templates.command("create-workflow <id>").description("render and store a template as a workflow").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
|
|
9406
9706
|
const store = new Store;
|
|
@@ -9504,7 +9804,7 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
|
|
|
9504
9804
|
}
|
|
9505
9805
|
});
|
|
9506
9806
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
9507
|
-
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) => {
|
|
9508
9808
|
const event = await readEventEnvelopeFromStdin();
|
|
9509
9809
|
const result = routeTodosTaskEvent(event, opts);
|
|
9510
9810
|
print(result.value, result.human);
|
|
@@ -9513,7 +9813,7 @@ var eventsDrain = events.command("drain").description("drain durable source queu
|
|
|
9513
9813
|
addTodosDrainOptions(eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops")).action((opts) => {
|
|
9514
9814
|
drainTodosTaskRoutes(opts);
|
|
9515
9815
|
});
|
|
9516
|
-
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) => {
|
|
9517
9817
|
const event = await readEventEnvelopeFromStdin();
|
|
9518
9818
|
const result = routeGenericEvent(event, opts);
|
|
9519
9819
|
print(result.value, result.human);
|
|
@@ -9577,7 +9877,7 @@ machines.command("show <id>").description("resolve a machine assignment").action
|
|
|
9577
9877
|
print(resolveLoopMachine(id));
|
|
9578
9878
|
});
|
|
9579
9879
|
workflows.command("validate <file>").description("validate a workflow JSON file without storing or running it").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables").action((file, opts) => {
|
|
9580
|
-
const body =
|
|
9880
|
+
const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
|
|
9581
9881
|
const workflow = workflowSpecForPreflight(body);
|
|
9582
9882
|
const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
|
|
9583
9883
|
print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
|
|
@@ -9585,7 +9885,7 @@ workflows.command("validate <file>").description("validate a workflow JSON file
|
|
|
9585
9885
|
workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables before storing").action((file, opts) => {
|
|
9586
9886
|
const store = new Store;
|
|
9587
9887
|
try {
|
|
9588
|
-
const body =
|
|
9888
|
+
const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
|
|
9589
9889
|
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
|
|
9590
9890
|
const workflow = store.createWorkflow(body);
|
|
9591
9891
|
if (preflight !== undefined)
|
|
@@ -9729,6 +10029,75 @@ workflows.command("recover <runId>").description("reset interrupted running work
|
|
|
9729
10029
|
store.close();
|
|
9730
10030
|
}
|
|
9731
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
|
+
});
|
|
9732
10101
|
workflows.command("archive <idOrName>").action((idOrName) => {
|
|
9733
10102
|
const store = new Store;
|
|
9734
10103
|
try {
|
|
@@ -10394,7 +10763,7 @@ daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) =>
|
|
|
10394
10763
|
console.log("");
|
|
10395
10764
|
return;
|
|
10396
10765
|
}
|
|
10397
|
-
const lines =
|
|
10766
|
+
const lines = readFileSync4(path, "utf8").trimEnd().split(`
|
|
10398
10767
|
`);
|
|
10399
10768
|
console.log(lines.slice(-Number(opts.lines)).join(`
|
|
10400
10769
|
`));
|