@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/index.js
CHANGED
|
@@ -312,6 +312,8 @@ function updateReadyFlags(nodes, planStatus) {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// src/lib/workflow-spec.ts
|
|
315
|
+
import { readFileSync } from "fs";
|
|
316
|
+
import { isAbsolute, resolve } from "path";
|
|
315
317
|
function assertObject(value, label) {
|
|
316
318
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
317
319
|
throw new Error(`${label} must be an object`);
|
|
@@ -327,6 +329,15 @@ function optionalPositiveInteger(value, label) {
|
|
|
327
329
|
throw new Error(`${label} must be a positive integer`);
|
|
328
330
|
return value;
|
|
329
331
|
}
|
|
332
|
+
function optionalTimeoutMs(value, label) {
|
|
333
|
+
if (value === undefined)
|
|
334
|
+
return;
|
|
335
|
+
if (value === null)
|
|
336
|
+
return null;
|
|
337
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
338
|
+
throw new Error(`${label} must be a positive integer or null for unlimited`);
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
330
341
|
function optionalBoolean(value, label) {
|
|
331
342
|
if (value === undefined)
|
|
332
343
|
return;
|
|
@@ -357,6 +368,38 @@ function optionalAccountRef(value, label) {
|
|
|
357
368
|
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
358
369
|
};
|
|
359
370
|
}
|
|
371
|
+
function promptFilePath(rawPath, opts) {
|
|
372
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
|
|
373
|
+
}
|
|
374
|
+
function readPromptFile(rawPath, label, opts) {
|
|
375
|
+
assertString(rawPath, `${label}.promptFile`);
|
|
376
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
377
|
+
let prompt;
|
|
378
|
+
try {
|
|
379
|
+
prompt = readFileSync(path, "utf8");
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
382
|
+
throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
|
|
383
|
+
}
|
|
384
|
+
if (prompt.trim() === "")
|
|
385
|
+
throw new Error(`${label}.promptFile must contain a non-empty prompt`);
|
|
386
|
+
return { prompt, promptSource: { type: "file", path } };
|
|
387
|
+
}
|
|
388
|
+
function matchingPromptSource(value, prompt, opts) {
|
|
389
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
390
|
+
return;
|
|
391
|
+
if (value.type !== "file")
|
|
392
|
+
return;
|
|
393
|
+
const rawPath = value.path;
|
|
394
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "")
|
|
395
|
+
return;
|
|
396
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
397
|
+
try {
|
|
398
|
+
return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
|
|
399
|
+
} catch {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
360
403
|
function normalizeGoalSpec(value, label = "goal") {
|
|
361
404
|
if (value === undefined)
|
|
362
405
|
return;
|
|
@@ -379,11 +422,11 @@ function normalizeGoalSpec(value, label = "goal") {
|
|
|
379
422
|
autoExecute
|
|
380
423
|
};
|
|
381
424
|
}
|
|
382
|
-
function validateTarget(value, label) {
|
|
425
|
+
function validateTarget(value, label, opts) {
|
|
383
426
|
assertObject(value, label);
|
|
384
427
|
if (value.type === "command") {
|
|
385
428
|
assertString(value.command, `${label}.command`);
|
|
386
|
-
|
|
429
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
387
430
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
388
431
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
389
432
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
@@ -392,11 +435,17 @@ function validateTarget(value, label) {
|
|
|
392
435
|
}
|
|
393
436
|
if (value.type === "agent") {
|
|
394
437
|
assertString(value.provider, `${label}.provider`);
|
|
395
|
-
|
|
438
|
+
const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
|
|
439
|
+
const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
|
|
440
|
+
if (hasPrompt && hasPromptFile)
|
|
441
|
+
throw new Error(`${label} must use either prompt or promptFile, not both`);
|
|
442
|
+
if (!hasPrompt && !hasPromptFile)
|
|
443
|
+
throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
|
|
444
|
+
const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
|
|
396
445
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
397
446
|
if (!providers.includes(value.provider))
|
|
398
447
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
399
|
-
|
|
448
|
+
optionalTimeoutMs(value.timeoutMs, `${label}.timeoutMs`);
|
|
400
449
|
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
401
450
|
if (value.authProfile !== undefined) {
|
|
402
451
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
@@ -493,11 +542,14 @@ function validateTarget(value, label) {
|
|
|
493
542
|
if (value.routing.eventSource !== undefined)
|
|
494
543
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
495
544
|
}
|
|
496
|
-
|
|
545
|
+
const target = { ...value };
|
|
546
|
+
delete target.promptFile;
|
|
547
|
+
delete target.promptSource;
|
|
548
|
+
return { ...target, ...promptFields };
|
|
497
549
|
}
|
|
498
550
|
throw new Error(`${label}.type must be command or agent`);
|
|
499
551
|
}
|
|
500
|
-
function normalizeCreateWorkflowInput(input) {
|
|
552
|
+
function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
501
553
|
assertString(input.name, "workflow.name");
|
|
502
554
|
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
503
555
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
@@ -513,10 +565,10 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
513
565
|
...step,
|
|
514
566
|
id: step.id,
|
|
515
567
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
516
|
-
target: validateTarget(step.target, `workflow.steps[${index}].target
|
|
568
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
517
569
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
518
570
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
519
|
-
timeoutMs:
|
|
571
|
+
timeoutMs: optionalTimeoutMs(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
520
572
|
account: optionalAccountRef(step.account, `workflow.steps[${index}].account`)
|
|
521
573
|
};
|
|
522
574
|
});
|
|
@@ -556,7 +608,7 @@ function workflowExecutionOrder(workflow) {
|
|
|
556
608
|
visit(step);
|
|
557
609
|
return order;
|
|
558
610
|
}
|
|
559
|
-
function workflowBodyFromJson(value, fallbackName) {
|
|
611
|
+
function workflowBodyFromJson(value, fallbackName, opts = {}) {
|
|
560
612
|
assertObject(value, "workflow file");
|
|
561
613
|
const rawName = fallbackName ?? value.name;
|
|
562
614
|
assertString(rawName, "workflow.name");
|
|
@@ -568,7 +620,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
568
620
|
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
569
621
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
570
622
|
steps: value.steps
|
|
571
|
-
});
|
|
623
|
+
}, opts);
|
|
572
624
|
}
|
|
573
625
|
|
|
574
626
|
// src/lib/run-artifacts.ts
|
|
@@ -1205,13 +1257,17 @@ class Store {
|
|
|
1205
1257
|
}
|
|
1206
1258
|
createLoop(input, from = new Date) {
|
|
1207
1259
|
const now = nowIso();
|
|
1260
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1261
|
+
name: "loop-target-validation",
|
|
1262
|
+
steps: [{ id: "target", target: input.target }]
|
|
1263
|
+
}).steps[0].target;
|
|
1208
1264
|
const loop = {
|
|
1209
1265
|
id: genId(),
|
|
1210
1266
|
name: input.name,
|
|
1211
1267
|
description: input.description,
|
|
1212
1268
|
status: "active",
|
|
1213
1269
|
schedule: input.schedule,
|
|
1214
|
-
target
|
|
1270
|
+
target,
|
|
1215
1271
|
goal: input.goal,
|
|
1216
1272
|
machine: input.machine,
|
|
1217
1273
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
@@ -1258,6 +1314,17 @@ class Store {
|
|
|
1258
1314
|
const row = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 1").get(name);
|
|
1259
1315
|
return row ? rowToLoop(row) : undefined;
|
|
1260
1316
|
}
|
|
1317
|
+
requireUniqueLoop(idOrName) {
|
|
1318
|
+
const byId = this.getLoop(idOrName);
|
|
1319
|
+
if (byId)
|
|
1320
|
+
return byId;
|
|
1321
|
+
const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
|
|
1322
|
+
if (rows.length === 0)
|
|
1323
|
+
throw new Error(`loop not found: ${idOrName}`);
|
|
1324
|
+
if (rows.length > 1)
|
|
1325
|
+
throw new Error(`ambiguous loop name: ${idOrName}; use a loop id`);
|
|
1326
|
+
return rowToLoop(rows[0]);
|
|
1327
|
+
}
|
|
1261
1328
|
requireLoop(idOrName) {
|
|
1262
1329
|
return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
|
|
1263
1330
|
throw new Error(`loop not found: ${idOrName}`);
|
|
@@ -1332,6 +1399,130 @@ class Store {
|
|
|
1332
1399
|
throw new Error(`loop not found after update: ${id}`);
|
|
1333
1400
|
return after;
|
|
1334
1401
|
}
|
|
1402
|
+
activeLoopReferenceCount(workflowId) {
|
|
1403
|
+
const rows = this.db.query("SELECT target_json FROM loops WHERE archived_at IS NULL").all();
|
|
1404
|
+
let count = 0;
|
|
1405
|
+
for (const row of rows) {
|
|
1406
|
+
try {
|
|
1407
|
+
const target = JSON.parse(row.target_json);
|
|
1408
|
+
if (target.type === "workflow" && target.workflowId === workflowId)
|
|
1409
|
+
count += 1;
|
|
1410
|
+
} catch {}
|
|
1411
|
+
}
|
|
1412
|
+
return count;
|
|
1413
|
+
}
|
|
1414
|
+
archiveWorkflowIfUnreferenced(workflowId, updated) {
|
|
1415
|
+
if (this.activeLoopReferenceCount(workflowId) > 0)
|
|
1416
|
+
return;
|
|
1417
|
+
const workflow = this.getWorkflow(workflowId);
|
|
1418
|
+
if (!workflow || workflow.status !== "active")
|
|
1419
|
+
return;
|
|
1420
|
+
const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(updated, workflowId);
|
|
1421
|
+
if (res.changes !== 1)
|
|
1422
|
+
return;
|
|
1423
|
+
return this.getWorkflow(workflowId);
|
|
1424
|
+
}
|
|
1425
|
+
retargetWorkflowLoop(idOrName, workflowId, opts = {}) {
|
|
1426
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1427
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1428
|
+
try {
|
|
1429
|
+
const current = this.requireLoop(idOrName);
|
|
1430
|
+
if (current.target.type !== "workflow")
|
|
1431
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1432
|
+
if (this.hasRunningRun(current.id))
|
|
1433
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1434
|
+
const workflow = this.requireWorkflow(workflowId);
|
|
1435
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1436
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1437
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1438
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1439
|
+
WHERE id=$id
|
|
1440
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1441
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1442
|
+
))`).run({
|
|
1443
|
+
$id: current.id,
|
|
1444
|
+
$target: JSON.stringify(target),
|
|
1445
|
+
$updated: updated,
|
|
1446
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1447
|
+
$now: updated
|
|
1448
|
+
});
|
|
1449
|
+
if (res.changes !== 1)
|
|
1450
|
+
throw new Error("daemon lease lost");
|
|
1451
|
+
this.db.exec("COMMIT");
|
|
1452
|
+
const after = this.getLoop(current.id);
|
|
1453
|
+
if (!after)
|
|
1454
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1455
|
+
return after;
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
try {
|
|
1458
|
+
this.db.exec("ROLLBACK");
|
|
1459
|
+
} catch {}
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
createAndRetargetWorkflowLoop(idOrName, workflowInput, opts = {}) {
|
|
1464
|
+
const normalized = normalizeCreateWorkflowInput(workflowInput);
|
|
1465
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1466
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1467
|
+
try {
|
|
1468
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1469
|
+
if (current.target.type !== "workflow")
|
|
1470
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1471
|
+
if (this.hasRunningRun(current.id))
|
|
1472
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1473
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1474
|
+
const workflow = {
|
|
1475
|
+
id: genId(),
|
|
1476
|
+
name: normalized.name,
|
|
1477
|
+
description: normalized.description,
|
|
1478
|
+
version: normalized.version ?? 1,
|
|
1479
|
+
status: "active",
|
|
1480
|
+
goal: normalized.goal,
|
|
1481
|
+
steps: normalized.steps,
|
|
1482
|
+
createdAt: updated,
|
|
1483
|
+
updatedAt: updated
|
|
1484
|
+
};
|
|
1485
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1486
|
+
VALUES ($id, $name, $description, $version, $status, $goal, $steps, $created, $updated)`).run({
|
|
1487
|
+
$id: workflow.id,
|
|
1488
|
+
$name: workflow.name,
|
|
1489
|
+
$description: workflow.description ?? null,
|
|
1490
|
+
$version: workflow.version,
|
|
1491
|
+
$status: workflow.status,
|
|
1492
|
+
$goal: workflow.goal ? JSON.stringify(workflow.goal) : null,
|
|
1493
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1494
|
+
$created: workflow.createdAt,
|
|
1495
|
+
$updated: workflow.updatedAt
|
|
1496
|
+
});
|
|
1497
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1498
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1499
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1500
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1501
|
+
WHERE id=$id
|
|
1502
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1503
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1504
|
+
))`).run({
|
|
1505
|
+
$id: current.id,
|
|
1506
|
+
$target: JSON.stringify(target),
|
|
1507
|
+
$updated: updated,
|
|
1508
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1509
|
+
$now: updated
|
|
1510
|
+
});
|
|
1511
|
+
if (res.changes !== 1)
|
|
1512
|
+
throw new Error("daemon lease lost");
|
|
1513
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1514
|
+
this.db.exec("COMMIT");
|
|
1515
|
+
const loop = this.getLoop(current.id);
|
|
1516
|
+
if (!loop)
|
|
1517
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1518
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
try {
|
|
1521
|
+
this.db.exec("ROLLBACK");
|
|
1522
|
+
} catch {}
|
|
1523
|
+
throw error;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1335
1526
|
renameLoop(id, name, opts = {}) {
|
|
1336
1527
|
const current = this.getLoop(id);
|
|
1337
1528
|
if (!current)
|
|
@@ -3433,7 +3624,7 @@ function commandSpec(target) {
|
|
|
3433
3624
|
cwd: commandTarget.cwd,
|
|
3434
3625
|
shell: commandTarget.shell,
|
|
3435
3626
|
env: commandTarget.env,
|
|
3436
|
-
timeoutMs: commandTarget.timeoutMs
|
|
3627
|
+
timeoutMs: commandTarget.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : commandTarget.timeoutMs,
|
|
3437
3628
|
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
3438
3629
|
account: commandTarget.account,
|
|
3439
3630
|
accountTool: commandTarget.account?.tool
|
|
@@ -3444,7 +3635,7 @@ function commandSpec(target) {
|
|
|
3444
3635
|
command: providerCommand(agentTarget.provider),
|
|
3445
3636
|
args: agentArgs(agentTarget),
|
|
3446
3637
|
cwd: agentTarget.cwd,
|
|
3447
|
-
timeoutMs: agentTarget.timeoutMs ??
|
|
3638
|
+
timeoutMs: agentTarget.timeoutMs ?? null,
|
|
3448
3639
|
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
3449
3640
|
account: agentTarget.account,
|
|
3450
3641
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
@@ -3632,12 +3823,12 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3632
3823
|
if (opts.signal?.aborted)
|
|
3633
3824
|
abortHandler();
|
|
3634
3825
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3635
|
-
const timer = setTimeout(() => {
|
|
3826
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3636
3827
|
timedOut = true;
|
|
3637
3828
|
if (child.pid)
|
|
3638
3829
|
killProcessGroup(child.pid);
|
|
3639
|
-
}, spec.timeoutMs);
|
|
3640
|
-
timer
|
|
3830
|
+
}, spec.timeoutMs) : undefined;
|
|
3831
|
+
timer?.unref();
|
|
3641
3832
|
let idleTimer;
|
|
3642
3833
|
const resetIdleTimer = () => {
|
|
3643
3834
|
if (!spec.idleTimeoutMs)
|
|
@@ -3669,7 +3860,8 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
3669
3860
|
} catch (err) {
|
|
3670
3861
|
error = err instanceof Error ? err.message : String(err);
|
|
3671
3862
|
} finally {
|
|
3672
|
-
|
|
3863
|
+
if (timer)
|
|
3864
|
+
clearTimeout(timer);
|
|
3673
3865
|
if (idleTimer)
|
|
3674
3866
|
clearTimeout(idleTimer);
|
|
3675
3867
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -3811,12 +4003,12 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3811
4003
|
if (opts.signal?.aborted)
|
|
3812
4004
|
abortHandler();
|
|
3813
4005
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3814
|
-
const timer = setTimeout(() => {
|
|
4006
|
+
const timer = typeof spec.timeoutMs === "number" ? setTimeout(() => {
|
|
3815
4007
|
timedOut = true;
|
|
3816
4008
|
if (child.pid)
|
|
3817
4009
|
killProcessGroup(child.pid);
|
|
3818
|
-
}, spec.timeoutMs);
|
|
3819
|
-
timer
|
|
4010
|
+
}, spec.timeoutMs) : undefined;
|
|
4011
|
+
timer?.unref();
|
|
3820
4012
|
let idleTimer;
|
|
3821
4013
|
const resetIdleTimer = () => {
|
|
3822
4014
|
if (!spec.idleTimeoutMs)
|
|
@@ -3848,7 +4040,8 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3848
4040
|
} catch (err) {
|
|
3849
4041
|
error = err instanceof Error ? err.message : String(err);
|
|
3850
4042
|
} finally {
|
|
3851
|
-
|
|
4043
|
+
if (timer)
|
|
4044
|
+
clearTimeout(timer);
|
|
3852
4045
|
if (idleTimer)
|
|
3853
4046
|
clearTimeout(idleTimer);
|
|
3854
4047
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
@@ -4273,7 +4466,7 @@ ${result.stderr}`);
|
|
|
4273
4466
|
// src/lib/workflow-runner.ts
|
|
4274
4467
|
function targetWithStepAccount(step) {
|
|
4275
4468
|
const account = step.account ?? step.target.account;
|
|
4276
|
-
const timeoutMs = step.timeoutMs
|
|
4469
|
+
const timeoutMs = step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs;
|
|
4277
4470
|
if (!account && timeoutMs === step.target.timeoutMs)
|
|
4278
4471
|
return step.target;
|
|
4279
4472
|
return { ...step.target, account, timeoutMs };
|
|
@@ -4569,7 +4762,8 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4569
4762
|
})
|
|
4570
4763
|
});
|
|
4571
4764
|
}
|
|
4572
|
-
const
|
|
4765
|
+
const workflowTimeoutMs = typeof loop.target.timeoutMs === "number" ? loop.target.timeoutMs : undefined;
|
|
4766
|
+
const controller = workflowTimeoutMs !== undefined ? new AbortController : undefined;
|
|
4573
4767
|
let workflowTimedOut = false;
|
|
4574
4768
|
const externalAbort = () => controller?.abort();
|
|
4575
4769
|
if (controller && opts.signal?.aborted)
|
|
@@ -4579,13 +4773,13 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
4579
4773
|
const timer = controller ? setTimeout(() => {
|
|
4580
4774
|
workflowTimedOut = true;
|
|
4581
4775
|
controller.abort();
|
|
4582
|
-
},
|
|
4776
|
+
}, workflowTimeoutMs) : undefined;
|
|
4583
4777
|
timer?.unref();
|
|
4584
4778
|
try {
|
|
4585
4779
|
return await executeWorkflow(store, workflow, {
|
|
4586
4780
|
...opts,
|
|
4587
4781
|
signal: controller?.signal ?? opts.signal,
|
|
4588
|
-
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${
|
|
4782
|
+
signalTimeoutMessage: () => workflowTimedOut && loop.target.type === "workflow" ? `workflow timed out after ${workflowTimeoutMs}ms` : undefined,
|
|
4589
4783
|
loop,
|
|
4590
4784
|
loopRun: run,
|
|
4591
4785
|
scheduledFor: run.scheduledFor,
|
|
@@ -5036,9 +5230,9 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
5036
5230
|
}
|
|
5037
5231
|
// src/lib/templates.ts
|
|
5038
5232
|
import { execFileSync } from "child_process";
|
|
5039
|
-
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
5233
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5040
5234
|
import { homedir as homedir3 } from "os";
|
|
5041
|
-
import { basename as basename2, isAbsolute, join as join5, relative, resolve } from "path";
|
|
5235
|
+
import { basename as basename2, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
|
|
5042
5236
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
5043
5237
|
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
5044
5238
|
var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
|
|
@@ -5076,7 +5270,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5076
5270
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
5077
5271
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5078
5272
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5079
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
|
|
5273
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." },
|
|
5274
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5080
5275
|
]
|
|
5081
5276
|
},
|
|
5082
5277
|
{
|
|
@@ -5106,7 +5301,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5106
5301
|
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
5107
5302
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5108
5303
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5109
|
-
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
|
|
5304
|
+
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." },
|
|
5305
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5110
5306
|
]
|
|
5111
5307
|
},
|
|
5112
5308
|
{
|
|
@@ -5134,7 +5330,7 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5134
5330
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
5135
5331
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
5136
5332
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
5137
|
-
{ name: "timeoutMs", default: "
|
|
5333
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5138
5334
|
]
|
|
5139
5335
|
},
|
|
5140
5336
|
{
|
|
@@ -5153,7 +5349,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
5153
5349
|
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
5154
5350
|
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
5155
5351
|
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
5156
|
-
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
5352
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." },
|
|
5353
|
+
{ name: "timeoutMs", default: "unlimited", description: "Agent step timeout in milliseconds, or unlimited/none/null for no timeout. Deterministic helper steps remain bounded." }
|
|
5157
5354
|
]
|
|
5158
5355
|
},
|
|
5159
5356
|
{
|
|
@@ -5258,6 +5455,30 @@ function taskLabel(input) {
|
|
|
5258
5455
|
const head = input.taskTitle?.trim() || input.taskId;
|
|
5259
5456
|
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
5260
5457
|
}
|
|
5458
|
+
var UNLIMITED_AGENT_TIMEOUT_MS = null;
|
|
5459
|
+
function agentTimeoutMs(input) {
|
|
5460
|
+
return input.timeoutMs === undefined ? UNLIMITED_AGENT_TIMEOUT_MS : input.timeoutMs;
|
|
5461
|
+
}
|
|
5462
|
+
function parseTemplateTimeoutMs(raw) {
|
|
5463
|
+
if (raw === undefined || raw.trim() === "")
|
|
5464
|
+
return;
|
|
5465
|
+
const normalized = raw.trim().toLowerCase();
|
|
5466
|
+
if (["unlimited", "none", "null", "never", "off", "false"].includes(normalized))
|
|
5467
|
+
return null;
|
|
5468
|
+
const value = Number(raw);
|
|
5469
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
5470
|
+
throw new Error("timeoutMs must be a positive integer number of milliseconds, or unlimited/none/null");
|
|
5471
|
+
}
|
|
5472
|
+
return value;
|
|
5473
|
+
}
|
|
5474
|
+
function parseDeterministicTimeoutMs(raw, fallbackMs, label = "timeoutMs") {
|
|
5475
|
+
if (raw === undefined || raw.trim() === "")
|
|
5476
|
+
return fallbackMs;
|
|
5477
|
+
const value = Number(raw);
|
|
5478
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
5479
|
+
throw new Error(`${label} must be a positive integer number of milliseconds`);
|
|
5480
|
+
return value;
|
|
5481
|
+
}
|
|
5261
5482
|
function stableIndex(seed, size) {
|
|
5262
5483
|
let hash = 2166136261;
|
|
5263
5484
|
for (let i = 0;i < seed.length; i += 1) {
|
|
@@ -5320,7 +5541,7 @@ function normalizeWorktreeMode(mode) {
|
|
|
5320
5541
|
function defaultWorktreeRoot(root) {
|
|
5321
5542
|
if (root?.trim()) {
|
|
5322
5543
|
const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
|
|
5323
|
-
return
|
|
5544
|
+
return isAbsolute2(expanded) ? expanded : resolve2(expanded);
|
|
5324
5545
|
}
|
|
5325
5546
|
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
5326
5547
|
}
|
|
@@ -5427,7 +5648,7 @@ function worktreePlan(input, seed) {
|
|
|
5427
5648
|
const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
|
|
5428
5649
|
const worktreePath = join5(root, repoSlug, seedSlug);
|
|
5429
5650
|
const relativeCwd = relative(repoRoot, originalCwd);
|
|
5430
|
-
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !
|
|
5651
|
+
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
|
|
5431
5652
|
const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
|
|
5432
5653
|
const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
|
|
5433
5654
|
const prepareStep = {
|
|
@@ -5529,7 +5750,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
5529
5750
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
5530
5751
|
},
|
|
5531
5752
|
account: accountForRole(input, role, seed),
|
|
5532
|
-
timeoutMs:
|
|
5753
|
+
timeoutMs: agentTimeoutMs(input)
|
|
5533
5754
|
};
|
|
5534
5755
|
}
|
|
5535
5756
|
function workflowStepsWithWorktree(plan, steps) {
|
|
@@ -5662,9 +5883,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
|
|
|
5662
5883
|
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
5663
5884
|
}
|
|
5664
5885
|
}
|
|
5886
|
+
function assertNoCustomTemplatePromptFiles(value, label) {
|
|
5887
|
+
if (!value || typeof value !== "object")
|
|
5888
|
+
return;
|
|
5889
|
+
if (Array.isArray(value)) {
|
|
5890
|
+
value.forEach((entry, index) => assertNoCustomTemplatePromptFiles(entry, `${label}[${index}]`));
|
|
5891
|
+
return;
|
|
5892
|
+
}
|
|
5893
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
5894
|
+
if (key === "promptFile") {
|
|
5895
|
+
throw new Error(`${label}.${key} is not allowed in custom templates; use direct workflow JSON for prompt-file-backed workflows`);
|
|
5896
|
+
}
|
|
5897
|
+
assertNoCustomTemplatePromptFiles(entry, `${label}.${key}`);
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5665
5900
|
function assertCustomTemplateSafety(value, label) {
|
|
5666
5901
|
assertNoDangerousCustomTemplateScalars(value, label);
|
|
5667
5902
|
assertNoImplicitDangerFullAccess(value, label);
|
|
5903
|
+
assertNoCustomTemplatePromptFiles(value, label);
|
|
5668
5904
|
}
|
|
5669
5905
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
5670
5906
|
assertRecord(value, sourcePath);
|
|
@@ -5694,7 +5930,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
5694
5930
|
function readCustomTemplateFile(file) {
|
|
5695
5931
|
let parsed;
|
|
5696
5932
|
try {
|
|
5697
|
-
parsed = JSON.parse(
|
|
5933
|
+
parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
5698
5934
|
} catch (error) {
|
|
5699
5935
|
const message = error instanceof Error ? error.message : String(error);
|
|
5700
5936
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -5833,19 +6069,19 @@ function renderCustomLoopTemplate(entry, values) {
|
|
|
5833
6069
|
return workflow;
|
|
5834
6070
|
}
|
|
5835
6071
|
function validateCustomLoopTemplateFile(file) {
|
|
5836
|
-
const source =
|
|
6072
|
+
const source = resolve2(file);
|
|
5837
6073
|
const entry = readCustomTemplateFile(source);
|
|
5838
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
6074
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
|
|
5839
6075
|
assertNoTemplateCollisions([...existing, entry]);
|
|
5840
6076
|
return structuredClone(entry.summary);
|
|
5841
6077
|
}
|
|
5842
6078
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
5843
|
-
const source =
|
|
6079
|
+
const source = resolve2(file);
|
|
5844
6080
|
const entry = readCustomTemplateFile(source);
|
|
5845
6081
|
const dir = ensureCustomLoopTemplatesDir();
|
|
5846
6082
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
5847
6083
|
const replaced = existsSync3(destination);
|
|
5848
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
6084
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
5849
6085
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
5850
6086
|
if (replaced) {
|
|
5851
6087
|
const stat = lstatSync(destination);
|
|
@@ -5957,18 +6193,15 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
5957
6193
|
name: "Worker",
|
|
5958
6194
|
description: "Implement the todos task and record evidence.",
|
|
5959
6195
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
5960
|
-
timeoutMs:
|
|
6196
|
+
timeoutMs: agentTimeoutMs(input)
|
|
5961
6197
|
},
|
|
5962
6198
|
{
|
|
5963
6199
|
id: "verifier",
|
|
5964
6200
|
name: "Verifier",
|
|
5965
6201
|
description: "Adversarially verify worker output and update todos.",
|
|
5966
6202
|
dependsOn: ["worker"],
|
|
5967
|
-
target:
|
|
5968
|
-
|
|
5969
|
-
idleTimeoutMs: 10 * 60000
|
|
5970
|
-
},
|
|
5971
|
-
timeoutMs: 30 * 60000
|
|
6203
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6204
|
+
timeoutMs: agentTimeoutMs(input)
|
|
5972
6205
|
}
|
|
5973
6206
|
])
|
|
5974
6207
|
};
|
|
@@ -6129,7 +6362,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6129
6362
|
name: "Triage",
|
|
6130
6363
|
description: "Check task eligibility, duplicates, dependencies, and automation gates.",
|
|
6131
6364
|
target: agentTarget(input, triagePrompt, "triage", input.taskId, plan),
|
|
6132
|
-
timeoutMs:
|
|
6365
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6133
6366
|
},
|
|
6134
6367
|
{
|
|
6135
6368
|
id: "triage-gate",
|
|
@@ -6151,7 +6384,7 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6151
6384
|
description: "Create a concise implementation plan and split unsafe scope before work starts.",
|
|
6152
6385
|
dependsOn: ["triage-gate"],
|
|
6153
6386
|
target: agentTarget(input, plannerPrompt, "planner", input.taskId, plan),
|
|
6154
|
-
timeoutMs:
|
|
6387
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6155
6388
|
},
|
|
6156
6389
|
{
|
|
6157
6390
|
id: "planner-gate",
|
|
@@ -6173,18 +6406,15 @@ function renderTaskLifecycleWorkflow(input) {
|
|
|
6173
6406
|
description: "Implement the todos task according to triage and planner evidence.",
|
|
6174
6407
|
dependsOn: ["planner-gate"],
|
|
6175
6408
|
target: agentTarget(input, workerPrompt, "worker", input.taskId, plan),
|
|
6176
|
-
timeoutMs:
|
|
6409
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6177
6410
|
},
|
|
6178
6411
|
{
|
|
6179
6412
|
id: "verifier",
|
|
6180
6413
|
name: "Verifier",
|
|
6181
6414
|
description: "Adversarially verify worker output and update todos.",
|
|
6182
6415
|
dependsOn: ["worker"],
|
|
6183
|
-
target:
|
|
6184
|
-
|
|
6185
|
-
idleTimeoutMs: 10 * 60000
|
|
6186
|
-
},
|
|
6187
|
-
timeoutMs: 30 * 60000
|
|
6416
|
+
target: agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
6417
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6188
6418
|
}
|
|
6189
6419
|
])
|
|
6190
6420
|
};
|
|
@@ -6254,18 +6484,15 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
6254
6484
|
name: "Worker",
|
|
6255
6485
|
description: "Handle the Hasna event and record evidence.",
|
|
6256
6486
|
target: agentTarget(input, workerPrompt, "worker", seed, plan),
|
|
6257
|
-
timeoutMs:
|
|
6487
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6258
6488
|
},
|
|
6259
6489
|
{
|
|
6260
6490
|
id: "verifier",
|
|
6261
6491
|
name: "Verifier",
|
|
6262
6492
|
description: "Adversarially verify event handling.",
|
|
6263
6493
|
dependsOn: ["worker"],
|
|
6264
|
-
target:
|
|
6265
|
-
|
|
6266
|
-
idleTimeoutMs: 10 * 60000
|
|
6267
|
-
},
|
|
6268
|
-
timeoutMs: 30 * 60000
|
|
6494
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6495
|
+
timeoutMs: agentTimeoutMs(input)
|
|
6269
6496
|
}
|
|
6270
6497
|
])
|
|
6271
6498
|
};
|
|
@@ -6277,7 +6504,7 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6277
6504
|
throw new Error("projectPath is required");
|
|
6278
6505
|
const seed = `${input.projectPath}:${input.objective}`;
|
|
6279
6506
|
const plan = worktreePlan(input, seed);
|
|
6280
|
-
const timeoutMs =
|
|
6507
|
+
const timeoutMs = agentTimeoutMs(input);
|
|
6281
6508
|
const workerPrompt = [
|
|
6282
6509
|
`/goal ${input.objective}`,
|
|
6283
6510
|
"",
|
|
@@ -6315,11 +6542,8 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
6315
6542
|
name: "Verifier",
|
|
6316
6543
|
description: "Adversarially verify the bounded objective result.",
|
|
6317
6544
|
dependsOn: ["worker"],
|
|
6318
|
-
target:
|
|
6319
|
-
|
|
6320
|
-
idleTimeoutMs: 10 * 60000
|
|
6321
|
-
},
|
|
6322
|
-
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
6545
|
+
target: agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
6546
|
+
timeoutMs
|
|
6323
6547
|
}
|
|
6324
6548
|
])
|
|
6325
6549
|
};
|
|
@@ -6348,7 +6572,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6348
6572
|
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
6349
6573
|
worktreeRoot: values.worktreeRoot,
|
|
6350
6574
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6351
|
-
timeoutMs:
|
|
6575
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6352
6576
|
};
|
|
6353
6577
|
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
6354
6578
|
const taskId = values.taskId ?? "";
|
|
@@ -6385,6 +6609,7 @@ function renderLifecycleBoundedTemplate(id, values) {
|
|
|
6385
6609
|
worktreeMode: values.worktreeMode ?? "required",
|
|
6386
6610
|
worktreeRoot: values.worktreeRoot,
|
|
6387
6611
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6612
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6388
6613
|
eventId: values.eventId,
|
|
6389
6614
|
eventType: values.eventType
|
|
6390
6615
|
});
|
|
@@ -6451,6 +6676,8 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6451
6676
|
if (!checkCommand.trim())
|
|
6452
6677
|
throw new Error("checkCommand is required");
|
|
6453
6678
|
const seed = `${projectPath}:${checkCommand}`;
|
|
6679
|
+
const timeoutMs = parseDeterministicTimeoutMs(values.timeoutMs, 5 * 60000);
|
|
6680
|
+
const idleTimeoutMs = parseDeterministicTimeoutMs(values.idleTimeoutMs, 60000, "idleTimeoutMs");
|
|
6454
6681
|
return {
|
|
6455
6682
|
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
6456
6683
|
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
@@ -6465,10 +6692,10 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
|
6465
6692
|
command: "bash",
|
|
6466
6693
|
args: ["-lc", checkCommand],
|
|
6467
6694
|
cwd: projectPath,
|
|
6468
|
-
timeoutMs
|
|
6469
|
-
idleTimeoutMs
|
|
6695
|
+
timeoutMs,
|
|
6696
|
+
idleTimeoutMs
|
|
6470
6697
|
},
|
|
6471
|
-
timeoutMs
|
|
6698
|
+
timeoutMs
|
|
6472
6699
|
}
|
|
6473
6700
|
]
|
|
6474
6701
|
};
|
|
@@ -6506,6 +6733,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6506
6733
|
worktreeMode: values.worktreeMode,
|
|
6507
6734
|
worktreeRoot: values.worktreeRoot,
|
|
6508
6735
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6736
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs),
|
|
6509
6737
|
eventId: values.eventId,
|
|
6510
6738
|
eventType: values.eventType
|
|
6511
6739
|
});
|
|
@@ -6537,7 +6765,8 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6537
6765
|
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
6538
6766
|
worktreeMode: values.worktreeMode,
|
|
6539
6767
|
worktreeRoot: values.worktreeRoot,
|
|
6540
|
-
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
6768
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6769
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6541
6770
|
});
|
|
6542
6771
|
}
|
|
6543
6772
|
if (id === BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
@@ -6565,7 +6794,7 @@ function renderBuiltinLoopTemplate(id, values) {
|
|
|
6565
6794
|
worktreeMode: values.worktreeMode,
|
|
6566
6795
|
worktreeRoot: values.worktreeRoot,
|
|
6567
6796
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
6568
|
-
timeoutMs:
|
|
6797
|
+
timeoutMs: parseTemplateTimeoutMs(values.timeoutMs)
|
|
6569
6798
|
});
|
|
6570
6799
|
}
|
|
6571
6800
|
throw new Error(`unknown template: ${id}`);
|
|
@@ -6606,13 +6835,13 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
6606
6835
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
6607
6836
|
|
|
6608
6837
|
// src/daemon/control.ts
|
|
6609
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as
|
|
6838
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
6610
6839
|
import { hostname } from "os";
|
|
6611
6840
|
import { dirname as dirname2 } from "path";
|
|
6612
6841
|
|
|
6613
6842
|
// src/daemon/loop.ts
|
|
6614
6843
|
function realSleep(ms) {
|
|
6615
|
-
return new Promise((
|
|
6844
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
6616
6845
|
}
|
|
6617
6846
|
async function runLoop(opts) {
|
|
6618
6847
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -6637,7 +6866,7 @@ function readPid(path = pidFilePath()) {
|
|
|
6637
6866
|
if (!existsSync4(path))
|
|
6638
6867
|
return;
|
|
6639
6868
|
try {
|
|
6640
|
-
const pid = Number(
|
|
6869
|
+
const pid = Number(readFileSync3(path, "utf8").trim());
|
|
6641
6870
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
6642
6871
|
} catch {
|
|
6643
6872
|
return;
|
|
@@ -6800,7 +7029,11 @@ function runDoctor(store) {
|
|
|
6800
7029
|
if (loop.target.type === "workflow") {
|
|
6801
7030
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
6802
7031
|
for (const step of workflowExecutionOrder(workflow)) {
|
|
6803
|
-
preflightTarget({
|
|
7032
|
+
preflightTarget({
|
|
7033
|
+
...step.target,
|
|
7034
|
+
account: step.account ?? step.target.account,
|
|
7035
|
+
timeoutMs: step.timeoutMs !== undefined ? step.timeoutMs : step.target.timeoutMs
|
|
7036
|
+
}, { loopId: loop.id, loopName: loop.name, workflowId: workflow.id, workflowName: workflow.name, workflowStepId: step.id }, { machine: loop.machine });
|
|
6804
7037
|
}
|
|
6805
7038
|
} else {
|
|
6806
7039
|
preflightTarget(loop.target, { loopId: loop.id, loopName: loop.name }, { machine: loop.machine });
|