@hasna/loops 0.3.38 → 0.3.40
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 +95 -14
- package/dist/cli/index.js +1491 -176
- package/dist/daemon/index.js +499 -43
- package/dist/index.d.ts +1 -1
- package/dist/index.js +823 -53
- package/dist/lib/format.d.ts +3 -1
- package/dist/lib/run-artifacts.d.ts +15 -0
- package/dist/lib/store.d.ts +34 -1
- package/dist/lib/store.js +424 -8
- package/dist/lib/templates.d.ts +10 -0
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.js +529 -33
- package/dist/types.d.ts +109 -0
- package/docs/USAGE.md +57 -20
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/lib/store.ts
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
|
-
import { mkdirSync as
|
|
5
|
-
import {
|
|
4
|
+
import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { dirname, join as join3 } from "path";
|
|
6
7
|
|
|
7
8
|
// src/lib/ids.ts
|
|
8
9
|
import { randomBytes } from "crypto";
|
|
@@ -363,6 +364,8 @@ function validateTarget(value, label) {
|
|
|
363
364
|
assertObject(value, label);
|
|
364
365
|
if (value.type === "command") {
|
|
365
366
|
assertString(value.command, `${label}.command`);
|
|
367
|
+
optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
|
|
368
|
+
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
366
369
|
if (value.shell !== true && /\s/.test(value.command.trim())) {
|
|
367
370
|
throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
|
|
368
371
|
}
|
|
@@ -374,6 +377,8 @@ function validateTarget(value, label) {
|
|
|
374
377
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
375
378
|
if (!providers.includes(value.provider))
|
|
376
379
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
380
|
+
optionalPositiveInteger(value.timeoutMs, `${label}.timeoutMs`);
|
|
381
|
+
optionalPositiveInteger(value.idleTimeoutMs, `${label}.idleTimeoutMs`);
|
|
377
382
|
if (value.authProfile !== undefined) {
|
|
378
383
|
assertString(value.authProfile, `${label}.authProfile`);
|
|
379
384
|
if (value.provider !== "codewith")
|
|
@@ -528,6 +533,52 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
528
533
|
});
|
|
529
534
|
}
|
|
530
535
|
|
|
536
|
+
// src/lib/run-artifacts.ts
|
|
537
|
+
import { createHash } from "crypto";
|
|
538
|
+
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
539
|
+
import { basename, join as join2 } from "path";
|
|
540
|
+
function shortHash(value) {
|
|
541
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
542
|
+
}
|
|
543
|
+
function safeRunPathSlug(value, fallback) {
|
|
544
|
+
const raw = value?.trim() || fallback;
|
|
545
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
|
|
546
|
+
return slug || fallback;
|
|
547
|
+
}
|
|
548
|
+
function workflowRunSubjectKey(kind, rawSubjectRef) {
|
|
549
|
+
const raw = rawSubjectRef?.trim() || "subject";
|
|
550
|
+
const kindSlug = safeRunPathSlug(kind, "subject").slice(0, 24);
|
|
551
|
+
const subjectSlug = safeRunPathSlug(raw, "subject").slice(0, 48);
|
|
552
|
+
return `${kindSlug}-${subjectSlug}-${shortHash(`${kindSlug}
|
|
553
|
+
${raw}`)}`;
|
|
554
|
+
}
|
|
555
|
+
function workflowRunProjectSlug(projectKey) {
|
|
556
|
+
if (!projectKey?.trim())
|
|
557
|
+
return "global";
|
|
558
|
+
return safeRunPathSlug(projectKey.startsWith("/") ? basename(projectKey) : projectKey, "project");
|
|
559
|
+
}
|
|
560
|
+
function writeWorkflowRunManifest(args) {
|
|
561
|
+
const projectSlug = workflowRunProjectSlug(args.projectKey);
|
|
562
|
+
const subjectKey = workflowRunSubjectKey(args.subjectKind, args.rawSubjectRef);
|
|
563
|
+
const dir = join2(args.loopsDataDir, "runs", projectSlug, subjectKey, args.workflowRunId);
|
|
564
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
565
|
+
const manifestPath = join2(dir, "manifest.json");
|
|
566
|
+
writeFileSync(manifestPath, JSON.stringify({
|
|
567
|
+
version: 1,
|
|
568
|
+
workflowRunId: args.workflowRunId,
|
|
569
|
+
workflowId: args.workflowId,
|
|
570
|
+
workflowName: args.workflowName,
|
|
571
|
+
invocationId: args.invocationId,
|
|
572
|
+
workItemId: args.workItemId,
|
|
573
|
+
projectSlug,
|
|
574
|
+
subjectKey,
|
|
575
|
+
requiredReading: [],
|
|
576
|
+
createdAt: new Date().toISOString(),
|
|
577
|
+
...args.payload
|
|
578
|
+
}, null, 2), { mode: 384 });
|
|
579
|
+
return manifestPath;
|
|
580
|
+
}
|
|
581
|
+
|
|
531
582
|
// src/lib/store.ts
|
|
532
583
|
function rowToLoop(row) {
|
|
533
584
|
return {
|
|
@@ -597,8 +648,11 @@ function rowToWorkflowRun(row) {
|
|
|
597
648
|
workflowName: row.workflow_name,
|
|
598
649
|
loopId: row.loop_id ?? undefined,
|
|
599
650
|
loopRunId: row.loop_run_id ?? undefined,
|
|
651
|
+
invocationId: row.invocation_id ?? undefined,
|
|
652
|
+
workItemId: row.work_item_id ?? undefined,
|
|
600
653
|
scheduledFor: row.scheduled_for ?? undefined,
|
|
601
654
|
idempotencyKey: row.idempotency_key ?? undefined,
|
|
655
|
+
manifestPath: row.manifest_path ?? undefined,
|
|
602
656
|
status: row.status,
|
|
603
657
|
startedAt: row.started_at ?? undefined,
|
|
604
658
|
finishedAt: row.finished_at ?? undefined,
|
|
@@ -609,6 +663,44 @@ function rowToWorkflowRun(row) {
|
|
|
609
663
|
updatedAt: row.updated_at
|
|
610
664
|
};
|
|
611
665
|
}
|
|
666
|
+
function rowToWorkflowInvocation(row) {
|
|
667
|
+
return {
|
|
668
|
+
id: row.id,
|
|
669
|
+
workflowId: row.workflow_id ?? undefined,
|
|
670
|
+
templateId: row.template_id ?? undefined,
|
|
671
|
+
sourceRef: JSON.parse(row.source_json),
|
|
672
|
+
subjectRef: JSON.parse(row.subject_json),
|
|
673
|
+
intent: row.intent,
|
|
674
|
+
scope: row.scope_json ? JSON.parse(row.scope_json) : undefined,
|
|
675
|
+
outputPolicy: row.output_policy_json ? JSON.parse(row.output_policy_json) : undefined,
|
|
676
|
+
createdAt: row.created_at,
|
|
677
|
+
updatedAt: row.updated_at
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function rowToWorkflowWorkItem(row) {
|
|
681
|
+
return {
|
|
682
|
+
id: row.id,
|
|
683
|
+
routeKey: row.route_key,
|
|
684
|
+
idempotencyKey: row.idempotency_key,
|
|
685
|
+
invocationId: row.invocation_id,
|
|
686
|
+
sourceType: row.source_type,
|
|
687
|
+
sourceRef: row.source_ref,
|
|
688
|
+
subjectRef: row.subject_ref,
|
|
689
|
+
projectKey: row.project_key ?? undefined,
|
|
690
|
+
projectGroup: row.project_group ?? undefined,
|
|
691
|
+
priority: row.priority,
|
|
692
|
+
status: row.status,
|
|
693
|
+
attempts: row.attempts,
|
|
694
|
+
nextAttemptAt: row.next_attempt_at ?? undefined,
|
|
695
|
+
leaseExpiresAt: row.lease_expires_at ?? undefined,
|
|
696
|
+
workflowId: row.workflow_id ?? undefined,
|
|
697
|
+
loopId: row.loop_id ?? undefined,
|
|
698
|
+
workflowRunId: row.workflow_run_id ?? undefined,
|
|
699
|
+
lastReason: row.last_reason ?? undefined,
|
|
700
|
+
createdAt: row.created_at,
|
|
701
|
+
updatedAt: row.updated_at
|
|
702
|
+
};
|
|
703
|
+
}
|
|
612
704
|
function rowToWorkflowStepRun(row) {
|
|
613
705
|
return {
|
|
614
706
|
id: row.id,
|
|
@@ -722,13 +814,23 @@ function rowToLease(row) {
|
|
|
722
814
|
updatedAt: row.updated_at
|
|
723
815
|
};
|
|
724
816
|
}
|
|
817
|
+
function workItemStatusForLoopRun(status, attempt, maxAttempts) {
|
|
818
|
+
if (status === "succeeded")
|
|
819
|
+
return "succeeded";
|
|
820
|
+
if (["failed", "timed_out", "abandoned"].includes(status)) {
|
|
821
|
+
return maxAttempts !== undefined && attempt < maxAttempts ? "admitted" : "failed";
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
725
825
|
|
|
726
826
|
class Store {
|
|
727
827
|
db;
|
|
828
|
+
rootDir;
|
|
728
829
|
constructor(path) {
|
|
729
830
|
const file = path ?? dbPath();
|
|
730
831
|
if (file !== ":memory:")
|
|
731
|
-
|
|
832
|
+
mkdirSync3(dirname(file), { recursive: true, mode: 448 });
|
|
833
|
+
this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
|
|
732
834
|
this.db = new Database(file);
|
|
733
835
|
this.db.exec("PRAGMA busy_timeout = 5000;");
|
|
734
836
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
@@ -823,8 +925,11 @@ class Store {
|
|
|
823
925
|
workflow_name TEXT NOT NULL,
|
|
824
926
|
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
825
927
|
loop_run_id TEXT REFERENCES loop_runs(id) ON DELETE SET NULL,
|
|
928
|
+
invocation_id TEXT,
|
|
929
|
+
work_item_id TEXT,
|
|
826
930
|
scheduled_for TEXT,
|
|
827
931
|
idempotency_key TEXT,
|
|
932
|
+
manifest_path TEXT,
|
|
828
933
|
status TEXT NOT NULL,
|
|
829
934
|
started_at TEXT,
|
|
830
935
|
finished_at TEXT,
|
|
@@ -841,6 +946,59 @@ class Store {
|
|
|
841
946
|
CREATE INDEX IF NOT EXISTS idx_workflow_runs_loop_run ON workflow_runs(loop_run_id);
|
|
842
947
|
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
843
948
|
|
|
949
|
+
CREATE TABLE IF NOT EXISTS workflow_invocations (
|
|
950
|
+
id TEXT PRIMARY KEY,
|
|
951
|
+
workflow_id TEXT,
|
|
952
|
+
template_id TEXT,
|
|
953
|
+
source_kind TEXT NOT NULL,
|
|
954
|
+
source_id TEXT,
|
|
955
|
+
source_dedupe_key TEXT,
|
|
956
|
+
source_json TEXT NOT NULL,
|
|
957
|
+
subject_kind TEXT NOT NULL,
|
|
958
|
+
subject_id TEXT,
|
|
959
|
+
subject_path TEXT,
|
|
960
|
+
subject_url TEXT,
|
|
961
|
+
subject_json TEXT NOT NULL,
|
|
962
|
+
intent TEXT NOT NULL,
|
|
963
|
+
scope_json TEXT,
|
|
964
|
+
output_policy_json TEXT,
|
|
965
|
+
created_at TEXT NOT NULL,
|
|
966
|
+
updated_at TEXT NOT NULL
|
|
967
|
+
);
|
|
968
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_source ON workflow_invocations(source_kind, source_id);
|
|
969
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_invocations_subject ON workflow_invocations(subject_kind, subject_id, subject_path);
|
|
970
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_invocations_dedupe
|
|
971
|
+
ON workflow_invocations(source_kind, source_dedupe_key)
|
|
972
|
+
WHERE source_dedupe_key IS NOT NULL;
|
|
973
|
+
|
|
974
|
+
CREATE TABLE IF NOT EXISTS workflow_work_items (
|
|
975
|
+
id TEXT PRIMARY KEY,
|
|
976
|
+
route_key TEXT NOT NULL,
|
|
977
|
+
idempotency_key TEXT NOT NULL,
|
|
978
|
+
invocation_id TEXT NOT NULL REFERENCES workflow_invocations(id) ON DELETE CASCADE,
|
|
979
|
+
source_type TEXT NOT NULL,
|
|
980
|
+
source_ref TEXT NOT NULL,
|
|
981
|
+
subject_ref TEXT NOT NULL,
|
|
982
|
+
project_key TEXT,
|
|
983
|
+
project_group TEXT,
|
|
984
|
+
priority INTEGER NOT NULL,
|
|
985
|
+
status TEXT NOT NULL,
|
|
986
|
+
attempts INTEGER NOT NULL,
|
|
987
|
+
next_attempt_at TEXT,
|
|
988
|
+
lease_expires_at TEXT,
|
|
989
|
+
workflow_id TEXT REFERENCES workflow_specs(id) ON DELETE SET NULL,
|
|
990
|
+
loop_id TEXT REFERENCES loops(id) ON DELETE SET NULL,
|
|
991
|
+
workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE SET NULL,
|
|
992
|
+
last_reason TEXT,
|
|
993
|
+
created_at TEXT NOT NULL,
|
|
994
|
+
updated_at TEXT NOT NULL,
|
|
995
|
+
UNIQUE(route_key, idempotency_key)
|
|
996
|
+
);
|
|
997
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_status_next ON workflow_work_items(status, next_attempt_at, priority DESC, created_at ASC);
|
|
998
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
|
|
999
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
|
|
1000
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
|
|
1001
|
+
|
|
844
1002
|
CREATE TABLE IF NOT EXISTS workflow_step_runs (
|
|
845
1003
|
id TEXT PRIMARY KEY,
|
|
846
1004
|
workflow_run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
@@ -953,12 +1111,17 @@ class Store {
|
|
|
953
1111
|
this.addColumnIfMissing("loop_runs", "goal_run_id", "TEXT");
|
|
954
1112
|
this.addColumnIfMissing("workflow_specs", "goal_json", "TEXT");
|
|
955
1113
|
this.addColumnIfMissing("workflow_runs", "goal_run_id", "TEXT");
|
|
1114
|
+
this.addColumnIfMissing("workflow_runs", "invocation_id", "TEXT");
|
|
1115
|
+
this.addColumnIfMissing("workflow_runs", "work_item_id", "TEXT");
|
|
1116
|
+
this.addColumnIfMissing("workflow_runs", "manifest_path", "TEXT");
|
|
956
1117
|
this.addColumnIfMissing("workflow_step_runs", "pid", "INTEGER");
|
|
957
1118
|
this.addColumnIfMissing("workflow_step_runs", "goal_run_id", "TEXT");
|
|
1119
|
+
this.createWorkflowRunBackfillIndexes();
|
|
958
1120
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
|
|
959
1121
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0002_loop_machines", nowIso());
|
|
960
1122
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0003_goals", nowIso());
|
|
961
1123
|
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0004_loop_archive_metadata", nowIso());
|
|
1124
|
+
this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0005_workflow_invocations_and_admission", nowIso());
|
|
962
1125
|
}
|
|
963
1126
|
addColumnIfMissing(table, column, definition) {
|
|
964
1127
|
const columns = this.db.query(`PRAGMA table_info(${table})`).all();
|
|
@@ -966,6 +1129,12 @@ class Store {
|
|
|
966
1129
|
return;
|
|
967
1130
|
this.db.query(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).run();
|
|
968
1131
|
}
|
|
1132
|
+
createWorkflowRunBackfillIndexes() {
|
|
1133
|
+
this.db.exec(`
|
|
1134
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_invocation ON workflow_runs(invocation_id);
|
|
1135
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_work_item ON workflow_runs(work_item_id);
|
|
1136
|
+
`);
|
|
1137
|
+
}
|
|
969
1138
|
assertDaemonLeaseFence(opts = {}, now = nowIso()) {
|
|
970
1139
|
if (!opts.daemonLeaseId)
|
|
971
1140
|
return;
|
|
@@ -1081,6 +1250,10 @@ class Store {
|
|
|
1081
1250
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1082
1251
|
$now: updated
|
|
1083
1252
|
});
|
|
1253
|
+
if (patch.status && patch.status !== "active") {
|
|
1254
|
+
const status = patch.status === "paused" ? "deferred" : "cancelled";
|
|
1255
|
+
this.setWorkflowWorkItemsForLoop(id, status, `loop ${patch.status}`, updated);
|
|
1256
|
+
}
|
|
1084
1257
|
const after = this.getLoop(id);
|
|
1085
1258
|
if (!after)
|
|
1086
1259
|
throw new Error(`loop not found after update: ${id}`);
|
|
@@ -1125,6 +1298,7 @@ class Store {
|
|
|
1125
1298
|
$archivedFromStatus: loop.status,
|
|
1126
1299
|
$updated: updated
|
|
1127
1300
|
});
|
|
1301
|
+
this.setWorkflowWorkItemsForLoop(loop.id, "deferred", "loop archived", updated);
|
|
1128
1302
|
const archived = this.getLoop(loop.id);
|
|
1129
1303
|
if (!archived)
|
|
1130
1304
|
throw new Error(`loop not found after archive: ${loop.id}`);
|
|
@@ -1150,6 +1324,7 @@ class Store {
|
|
|
1150
1324
|
}
|
|
1151
1325
|
deleteLoop(idOrName) {
|
|
1152
1326
|
const loop = this.requireLoop(idOrName);
|
|
1327
|
+
this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
|
|
1153
1328
|
const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
|
|
1154
1329
|
return res.changes > 0;
|
|
1155
1330
|
}
|
|
@@ -1208,6 +1383,185 @@ class Store {
|
|
|
1208
1383
|
throw new Error(`workflow not found after archive: ${workflow.id}`);
|
|
1209
1384
|
return archived;
|
|
1210
1385
|
}
|
|
1386
|
+
createWorkflowInvocation(input) {
|
|
1387
|
+
const now = nowIso();
|
|
1388
|
+
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
1389
|
+
if (sourceDedupeKey) {
|
|
1390
|
+
const existing = this.db.query("SELECT * FROM workflow_invocations WHERE source_kind = ? AND source_dedupe_key = ? LIMIT 1").get(input.sourceRef.kind, sourceDedupeKey);
|
|
1391
|
+
if (existing)
|
|
1392
|
+
return rowToWorkflowInvocation(existing);
|
|
1393
|
+
}
|
|
1394
|
+
const id = input.id ?? genId();
|
|
1395
|
+
this.db.query(`INSERT INTO workflow_invocations (id, workflow_id, template_id, source_kind, source_id, source_dedupe_key,
|
|
1396
|
+
source_json, subject_kind, subject_id, subject_path, subject_url, subject_json, intent, scope_json,
|
|
1397
|
+
output_policy_json, created_at, updated_at)
|
|
1398
|
+
VALUES ($id, $workflowId, $templateId, $sourceKind, $sourceId, $sourceDedupeKey, $sourceJson,
|
|
1399
|
+
$subjectKind, $subjectId, $subjectPath, $subjectUrl, $subjectJson, $intent, $scopeJson,
|
|
1400
|
+
$outputPolicyJson, $created, $updated)`).run({
|
|
1401
|
+
$id: id,
|
|
1402
|
+
$workflowId: input.workflowId ?? null,
|
|
1403
|
+
$templateId: input.templateId ?? null,
|
|
1404
|
+
$sourceKind: input.sourceRef.kind,
|
|
1405
|
+
$sourceId: input.sourceRef.id ?? null,
|
|
1406
|
+
$sourceDedupeKey: sourceDedupeKey ?? null,
|
|
1407
|
+
$sourceJson: JSON.stringify(input.sourceRef),
|
|
1408
|
+
$subjectKind: input.subjectRef.kind,
|
|
1409
|
+
$subjectId: input.subjectRef.id ?? null,
|
|
1410
|
+
$subjectPath: input.subjectRef.path ?? null,
|
|
1411
|
+
$subjectUrl: input.subjectRef.url ?? null,
|
|
1412
|
+
$subjectJson: JSON.stringify(input.subjectRef),
|
|
1413
|
+
$intent: input.intent,
|
|
1414
|
+
$scopeJson: input.scope ? JSON.stringify(input.scope) : null,
|
|
1415
|
+
$outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
|
|
1416
|
+
$created: now,
|
|
1417
|
+
$updated: now
|
|
1418
|
+
});
|
|
1419
|
+
const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
|
|
1420
|
+
if (!row)
|
|
1421
|
+
throw new Error(`workflow invocation not found after create: ${id}`);
|
|
1422
|
+
return rowToWorkflowInvocation(row);
|
|
1423
|
+
}
|
|
1424
|
+
getWorkflowInvocation(id) {
|
|
1425
|
+
const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
|
|
1426
|
+
return row ? rowToWorkflowInvocation(row) : undefined;
|
|
1427
|
+
}
|
|
1428
|
+
listWorkflowInvocations(opts = {}) {
|
|
1429
|
+
const rows = this.db.query("SELECT * FROM workflow_invocations ORDER BY created_at DESC LIMIT ?").all(opts.limit ?? 100);
|
|
1430
|
+
return rows.map(rowToWorkflowInvocation);
|
|
1431
|
+
}
|
|
1432
|
+
upsertWorkflowWorkItem(input) {
|
|
1433
|
+
const now = nowIso();
|
|
1434
|
+
const id = genId();
|
|
1435
|
+
const status = input.status ?? "queued";
|
|
1436
|
+
this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
|
|
1437
|
+
subject_ref, project_key, project_group, priority, status, attempts, next_attempt_at, lease_expires_at,
|
|
1438
|
+
workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
|
|
1439
|
+
VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
|
|
1440
|
+
$projectKey, $projectGroup, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
|
|
1441
|
+
$lastReason, $created, $updated)
|
|
1442
|
+
ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
|
|
1443
|
+
invocation_id=excluded.invocation_id,
|
|
1444
|
+
source_type=excluded.source_type,
|
|
1445
|
+
source_ref=excluded.source_ref,
|
|
1446
|
+
subject_ref=excluded.subject_ref,
|
|
1447
|
+
project_key=excluded.project_key,
|
|
1448
|
+
project_group=excluded.project_group,
|
|
1449
|
+
priority=excluded.priority,
|
|
1450
|
+
status=CASE
|
|
1451
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1452
|
+
THEN workflow_work_items.status
|
|
1453
|
+
ELSE excluded.status
|
|
1454
|
+
END,
|
|
1455
|
+
workflow_id=CASE
|
|
1456
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1457
|
+
ELSE NULL
|
|
1458
|
+
END,
|
|
1459
|
+
loop_id=CASE
|
|
1460
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1461
|
+
ELSE NULL
|
|
1462
|
+
END,
|
|
1463
|
+
workflow_run_id=CASE
|
|
1464
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1465
|
+
ELSE NULL
|
|
1466
|
+
END,
|
|
1467
|
+
lease_expires_at=CASE
|
|
1468
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1469
|
+
ELSE NULL
|
|
1470
|
+
END,
|
|
1471
|
+
next_attempt_at=excluded.next_attempt_at,
|
|
1472
|
+
last_reason=COALESCE(excluded.last_reason, workflow_work_items.last_reason),
|
|
1473
|
+
updated_at=excluded.updated_at`).run({
|
|
1474
|
+
$id: id,
|
|
1475
|
+
$routeKey: input.routeKey,
|
|
1476
|
+
$idempotencyKey: input.idempotencyKey,
|
|
1477
|
+
$invocationId: input.invocationId,
|
|
1478
|
+
$sourceType: input.sourceType,
|
|
1479
|
+
$sourceRef: input.sourceRef,
|
|
1480
|
+
$subjectRef: input.subjectRef,
|
|
1481
|
+
$projectKey: input.projectKey ?? null,
|
|
1482
|
+
$projectGroup: input.projectGroup ?? null,
|
|
1483
|
+
$priority: input.priority ?? 0,
|
|
1484
|
+
$status: status,
|
|
1485
|
+
$nextAttemptAt: input.nextAttemptAt ?? null,
|
|
1486
|
+
$lastReason: input.lastReason ?? null,
|
|
1487
|
+
$created: now,
|
|
1488
|
+
$updated: now
|
|
1489
|
+
});
|
|
1490
|
+
const row = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND idempotency_key = ? LIMIT 1").get(input.routeKey, input.idempotencyKey);
|
|
1491
|
+
if (!row)
|
|
1492
|
+
throw new Error(`workflow work item not found after upsert: ${input.routeKey}/${input.idempotencyKey}`);
|
|
1493
|
+
return rowToWorkflowWorkItem(row);
|
|
1494
|
+
}
|
|
1495
|
+
getWorkflowWorkItem(id) {
|
|
1496
|
+
const row = this.db.query("SELECT * FROM workflow_work_items WHERE id = ?").get(id);
|
|
1497
|
+
return row ? rowToWorkflowWorkItem(row) : undefined;
|
|
1498
|
+
}
|
|
1499
|
+
findWorkflowWorkItem(routeKey, idempotencyKey) {
|
|
1500
|
+
const row = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND idempotency_key = ? LIMIT 1").get(routeKey, idempotencyKey);
|
|
1501
|
+
return row ? rowToWorkflowWorkItem(row) : undefined;
|
|
1502
|
+
}
|
|
1503
|
+
listWorkflowWorkItems(opts = {}) {
|
|
1504
|
+
const limit = opts.limit ?? 100;
|
|
1505
|
+
let rows;
|
|
1506
|
+
if (opts.status && opts.routeKey) {
|
|
1507
|
+
rows = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? AND status = ? ORDER BY priority DESC, created_at ASC LIMIT ?").all(opts.routeKey, opts.status, limit);
|
|
1508
|
+
} else if (opts.status) {
|
|
1509
|
+
rows = this.db.query("SELECT * FROM workflow_work_items WHERE status = ? ORDER BY priority DESC, created_at ASC LIMIT ?").all(opts.status, limit);
|
|
1510
|
+
} else if (opts.routeKey) {
|
|
1511
|
+
rows = this.db.query("SELECT * FROM workflow_work_items WHERE route_key = ? ORDER BY created_at DESC LIMIT ?").all(opts.routeKey, limit);
|
|
1512
|
+
} else {
|
|
1513
|
+
rows = this.db.query("SELECT * FROM workflow_work_items ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
1514
|
+
}
|
|
1515
|
+
return rows.map(rowToWorkflowWorkItem);
|
|
1516
|
+
}
|
|
1517
|
+
countActiveWorkflowWorkItems(args = {}) {
|
|
1518
|
+
const active = ["admitted", "running"];
|
|
1519
|
+
const placeholders = active.map(() => "?").join(",");
|
|
1520
|
+
const global = this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders})`).get(...active)?.count ?? 0;
|
|
1521
|
+
const project = args.projectKey ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_key = ?`).get(...active, args.projectKey)?.count ?? 0 : 0;
|
|
1522
|
+
const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
|
|
1523
|
+
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1524
|
+
}
|
|
1525
|
+
admitWorkflowWorkItem(id, patch) {
|
|
1526
|
+
const now = nowIso();
|
|
1527
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1528
|
+
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1529
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=$reason, updated_at=$updated
|
|
1530
|
+
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1531
|
+
$id: id,
|
|
1532
|
+
$workflowId: patch.workflowId,
|
|
1533
|
+
$loopId: patch.loopId,
|
|
1534
|
+
$reason: patch.reason ?? null,
|
|
1535
|
+
$updated: now
|
|
1536
|
+
});
|
|
1537
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1538
|
+
if (!item)
|
|
1539
|
+
throw new Error(`workflow work item not found after admit: ${id}`);
|
|
1540
|
+
if (res.changes !== 1)
|
|
1541
|
+
throw new Error(`workflow work item is not claimable: ${id} status=${item.status}`);
|
|
1542
|
+
return item;
|
|
1543
|
+
}
|
|
1544
|
+
setWorkflowWorkItemsForLoop(loopId, status, reason, updated, statuses = ["admitted", "running"]) {
|
|
1545
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
1546
|
+
this.db.query(`UPDATE workflow_work_items
|
|
1547
|
+
SET status=?, lease_expires_at=NULL, last_reason=COALESCE(?, last_reason), updated_at=?
|
|
1548
|
+
WHERE loop_id = ? AND status IN (${placeholders})`).run(status, reason ?? null, updated, loopId, ...statuses);
|
|
1549
|
+
}
|
|
1550
|
+
setWorkflowWorkItemsForWorkflowRun(workflowRunId, status, reason, updated, statuses = ["admitted", "running"]) {
|
|
1551
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
1552
|
+
this.db.query(`UPDATE workflow_work_items
|
|
1553
|
+
SET status=?, lease_expires_at=NULL, last_reason=COALESCE(?, last_reason), updated_at=?
|
|
1554
|
+
WHERE workflow_run_id = ? AND status IN (${placeholders})`).run(status, reason ?? null, updated, workflowRunId, ...statuses);
|
|
1555
|
+
}
|
|
1556
|
+
setWorkflowWorkItemsForLoopRun(run, reason, updated) {
|
|
1557
|
+
const loop = this.getLoop(run.loopId);
|
|
1558
|
+
const status = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
|
|
1559
|
+
if (!status)
|
|
1560
|
+
return;
|
|
1561
|
+
const statuses = status === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
1562
|
+
const nextReason = status === "admitted" ? reason ? `attempt failed; retry pending: ${reason}` : "attempt failed; retry pending" : reason;
|
|
1563
|
+
this.setWorkflowWorkItemsForLoop(run.loopId, status, nextReason, updated, statuses);
|
|
1564
|
+
}
|
|
1211
1565
|
createGoal(input, opts = {}) {
|
|
1212
1566
|
const now = nowIso();
|
|
1213
1567
|
this.db.exec("BEGIN IMMEDIATE");
|
|
@@ -1481,6 +1835,10 @@ class Store {
|
|
|
1481
1835
|
}
|
|
1482
1836
|
createWorkflowRun(input) {
|
|
1483
1837
|
const now = nowIso();
|
|
1838
|
+
const targetInput = input.loop?.target.type === "workflow" ? input.loop.target.input : undefined;
|
|
1839
|
+
const invocationId = input.invocationId ?? targetInput?.workflowInvocationId ?? targetInput?.invocationId;
|
|
1840
|
+
const workItemId = input.workItemId ?? targetInput?.workflowWorkItemId ?? targetInput?.workItemId;
|
|
1841
|
+
let manifestPath;
|
|
1484
1842
|
if (input.idempotencyKey) {
|
|
1485
1843
|
const existing = this.db.query("SELECT * FROM workflow_runs WHERE workflow_id = ? AND idempotency_key = ? LIMIT 1").get(input.workflow.id, input.idempotencyKey);
|
|
1486
1844
|
if (existing) {
|
|
@@ -1499,21 +1857,59 @@ class Store {
|
|
|
1499
1857
|
}
|
|
1500
1858
|
}
|
|
1501
1859
|
const runId = genId();
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1860
|
+
const workItem = workItemId ? this.getWorkflowWorkItem(workItemId) : undefined;
|
|
1861
|
+
const invocation = invocationId ? this.getWorkflowInvocation(invocationId) : undefined;
|
|
1862
|
+
manifestPath = invocation || workItem ? writeWorkflowRunManifest({
|
|
1863
|
+
loopsDataDir: this.rootDir,
|
|
1864
|
+
workflowRunId: runId,
|
|
1865
|
+
workflowId: input.workflow.id,
|
|
1866
|
+
workflowName: input.workflow.name,
|
|
1867
|
+
invocationId,
|
|
1868
|
+
workItemId,
|
|
1869
|
+
projectKey: workItem?.projectKey ?? invocation?.scope?.projectPath,
|
|
1870
|
+
subjectKind: invocation?.subjectRef.kind,
|
|
1871
|
+
rawSubjectRef: workItem?.subjectRef ?? invocation?.subjectRef.path ?? invocation?.subjectRef.id ?? invocation?.subjectRef.url,
|
|
1872
|
+
payload: {
|
|
1873
|
+
workflowInvocation: invocation,
|
|
1874
|
+
workflowWorkItem: workItem,
|
|
1875
|
+
loopId: input.loop?.id,
|
|
1876
|
+
loopRunId: input.loopRun?.id,
|
|
1877
|
+
scheduledFor: input.scheduledFor ?? input.loopRun?.scheduledFor
|
|
1878
|
+
}
|
|
1879
|
+
}) : undefined;
|
|
1880
|
+
this.db.query(`INSERT INTO workflow_runs (id, workflow_id, workflow_name, loop_id, loop_run_id, invocation_id, work_item_id,
|
|
1881
|
+
scheduled_for, idempotency_key, manifest_path, status, started_at, finished_at, duration_ms, error,
|
|
1882
|
+
created_at, updated_at)
|
|
1883
|
+
VALUES ($id, $workflowId, $workflowName, $loopId, $loopRunId, $invocationId, $workItemId, $scheduledFor,
|
|
1884
|
+
$idempotencyKey, $manifestPath, 'running', $started, NULL, NULL, NULL, $created, $updated)`).run({
|
|
1506
1885
|
$id: runId,
|
|
1507
1886
|
$workflowId: input.workflow.id,
|
|
1508
1887
|
$workflowName: input.workflow.name,
|
|
1509
1888
|
$loopId: input.loop?.id ?? null,
|
|
1510
1889
|
$loopRunId: input.loopRun?.id ?? null,
|
|
1890
|
+
$invocationId: invocationId ?? null,
|
|
1891
|
+
$workItemId: workItemId ?? null,
|
|
1511
1892
|
$scheduledFor: input.scheduledFor ?? input.loopRun?.scheduledFor ?? null,
|
|
1512
1893
|
$idempotencyKey: input.idempotencyKey ?? null,
|
|
1894
|
+
$manifestPath: manifestPath ?? null,
|
|
1513
1895
|
$started: now,
|
|
1514
1896
|
$created: now,
|
|
1515
1897
|
$updated: now
|
|
1516
1898
|
});
|
|
1899
|
+
if (workItemId) {
|
|
1900
|
+
const workItemRes = this.db.query(`UPDATE workflow_work_items
|
|
1901
|
+
SET status='running', workflow_run_id=$workflowRunId, lease_expires_at=$leaseExpiresAt, updated_at=$updated
|
|
1902
|
+
WHERE id=$id AND status IN ('admitted', 'queued', 'deferred', 'running')`).run({
|
|
1903
|
+
$id: workItemId,
|
|
1904
|
+
$workflowRunId: runId,
|
|
1905
|
+
$leaseExpiresAt: input.loop ? new Date(Date.now() + input.loop.leaseMs).toISOString() : null,
|
|
1906
|
+
$updated: now
|
|
1907
|
+
});
|
|
1908
|
+
if (workItemRes.changes !== 1) {
|
|
1909
|
+
const current = this.getWorkflowWorkItem(workItemId);
|
|
1910
|
+
throw new Error(`workflow work item is not runnable: ${workItemId}${current ? ` status=${current.status}` : ""}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1517
1913
|
input.workflow.steps.forEach((step, sequence) => {
|
|
1518
1914
|
const account = step.account ?? step.target.account;
|
|
1519
1915
|
this.db.query(`INSERT INTO workflow_step_runs (id, workflow_run_id, step_id, sequence, status, started_at, finished_at,
|
|
@@ -1539,7 +1935,10 @@ class Store {
|
|
|
1539
1935
|
workflowName: input.workflow.name,
|
|
1540
1936
|
stepCount: input.workflow.steps.length,
|
|
1541
1937
|
loopId: input.loop?.id,
|
|
1542
|
-
loopRunId: input.loopRun?.id
|
|
1938
|
+
loopRunId: input.loopRun?.id,
|
|
1939
|
+
invocationId,
|
|
1940
|
+
workItemId,
|
|
1941
|
+
manifestPath
|
|
1543
1942
|
}),
|
|
1544
1943
|
$created: now
|
|
1545
1944
|
});
|
|
@@ -1552,6 +1951,8 @@ class Store {
|
|
|
1552
1951
|
try {
|
|
1553
1952
|
this.db.exec("ROLLBACK");
|
|
1554
1953
|
} catch {}
|
|
1954
|
+
if (manifestPath)
|
|
1955
|
+
rmSync(manifestPath, { force: true });
|
|
1555
1956
|
throw error;
|
|
1556
1957
|
}
|
|
1557
1958
|
}
|
|
@@ -1764,6 +2165,10 @@ class Store {
|
|
|
1764
2165
|
changed = res.changes === 1;
|
|
1765
2166
|
if (changed)
|
|
1766
2167
|
this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
|
|
2168
|
+
if (changed) {
|
|
2169
|
+
const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
|
|
2170
|
+
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
|
|
2171
|
+
}
|
|
1767
2172
|
this.db.exec("COMMIT");
|
|
1768
2173
|
} catch (error) {
|
|
1769
2174
|
try {
|
|
@@ -1788,6 +2193,7 @@ class Store {
|
|
|
1788
2193
|
this.db.query(`UPDATE workflow_step_runs
|
|
1789
2194
|
SET status='cancelled', finished_at=$finished, pid=NULL, error=$reason, updated_at=$updated
|
|
1790
2195
|
WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
|
|
2196
|
+
this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
|
|
1791
2197
|
this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
|
|
1792
2198
|
}
|
|
1793
2199
|
this.db.exec("COMMIT");
|
|
@@ -2041,6 +2447,8 @@ class Store {
|
|
|
2041
2447
|
throw new Error(`run not found after finalize: ${id}`);
|
|
2042
2448
|
if (opts.claimedBy && res.changes !== 1)
|
|
2043
2449
|
return run;
|
|
2450
|
+
if (res.changes === 1)
|
|
2451
|
+
this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
|
|
2044
2452
|
return run;
|
|
2045
2453
|
}
|
|
2046
2454
|
heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
|
|
@@ -2134,6 +2542,14 @@ class Store {
|
|
|
2134
2542
|
error: "parent loop run lease expired before completion",
|
|
2135
2543
|
loopRunId: row.id
|
|
2136
2544
|
});
|
|
2545
|
+
this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
|
|
2546
|
+
}
|
|
2547
|
+
const loop = this.getLoop(row.loop_id);
|
|
2548
|
+
const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
|
|
2549
|
+
if (itemStatus) {
|
|
2550
|
+
const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
|
|
2551
|
+
const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
|
|
2552
|
+
this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
|
|
2137
2553
|
}
|
|
2138
2554
|
this.db.exec("COMMIT");
|
|
2139
2555
|
} catch (error) {
|
|
@@ -2346,7 +2762,7 @@ function resolveAccountEnv(account, toolHint, env) {
|
|
|
2346
2762
|
// src/lib/env.ts
|
|
2347
2763
|
import { accessSync, constants } from "fs";
|
|
2348
2764
|
import { homedir as homedir2 } from "os";
|
|
2349
|
-
import { delimiter, join as
|
|
2765
|
+
import { delimiter, join as join4 } from "path";
|
|
2350
2766
|
function compactPathParts(parts) {
|
|
2351
2767
|
const seen = new Set;
|
|
2352
2768
|
const result = [];
|
|
@@ -2362,14 +2778,14 @@ function compactPathParts(parts) {
|
|
|
2362
2778
|
function commonExecutableDirs(env = process.env) {
|
|
2363
2779
|
const home = env.HOME || homedir2();
|
|
2364
2780
|
return compactPathParts([
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
env.BUN_INSTALL ?
|
|
2781
|
+
join4(home, ".local", "bin"),
|
|
2782
|
+
join4(home, ".bun", "bin"),
|
|
2783
|
+
join4(home, ".cargo", "bin"),
|
|
2784
|
+
join4(home, ".npm-global", "bin"),
|
|
2785
|
+
join4(home, "bin"),
|
|
2786
|
+
env.BUN_INSTALL ? join4(env.BUN_INSTALL, "bin") : undefined,
|
|
2371
2787
|
env.PNPM_HOME,
|
|
2372
|
-
env.NPM_CONFIG_PREFIX ?
|
|
2788
|
+
env.NPM_CONFIG_PREFIX ? join4(env.NPM_CONFIG_PREFIX, "bin") : undefined,
|
|
2373
2789
|
"/opt/homebrew/bin",
|
|
2374
2790
|
"/usr/local/bin",
|
|
2375
2791
|
"/usr/bin",
|
|
@@ -2393,7 +2809,7 @@ function executableExists(command, env = process.env) {
|
|
|
2393
2809
|
if (command.includes("/"))
|
|
2394
2810
|
return isExecutable(command);
|
|
2395
2811
|
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
2396
|
-
if (dir && isExecutable(
|
|
2812
|
+
if (dir && isExecutable(join4(dir, command)))
|
|
2397
2813
|
return true;
|
|
2398
2814
|
}
|
|
2399
2815
|
return false;
|
|
@@ -2751,6 +3167,7 @@ function commandSpec(target) {
|
|
|
2751
3167
|
shell: commandTarget.shell,
|
|
2752
3168
|
env: commandTarget.env,
|
|
2753
3169
|
timeoutMs: commandTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
3170
|
+
idleTimeoutMs: commandTarget.idleTimeoutMs,
|
|
2754
3171
|
account: commandTarget.account,
|
|
2755
3172
|
accountTool: commandTarget.account?.tool
|
|
2756
3173
|
};
|
|
@@ -2761,6 +3178,7 @@ function commandSpec(target) {
|
|
|
2761
3178
|
args: agentArgs(agentTarget),
|
|
2762
3179
|
cwd: agentTarget.cwd,
|
|
2763
3180
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
3181
|
+
idleTimeoutMs: agentTarget.idleTimeoutMs,
|
|
2764
3182
|
account: agentTarget.account,
|
|
2765
3183
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2766
3184
|
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
@@ -2908,6 +3326,7 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
2908
3326
|
let stdout = "";
|
|
2909
3327
|
let stderr = "";
|
|
2910
3328
|
let timedOut = false;
|
|
3329
|
+
let idleTimedOut = false;
|
|
2911
3330
|
let exitCode;
|
|
2912
3331
|
let error;
|
|
2913
3332
|
let plan;
|
|
@@ -2946,18 +3365,34 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
2946
3365
|
if (opts.signal?.aborted)
|
|
2947
3366
|
abortHandler();
|
|
2948
3367
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
2949
|
-
child.stdout?.on("data", (chunk) => {
|
|
2950
|
-
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
2951
|
-
});
|
|
2952
|
-
child.stderr?.on("data", (chunk) => {
|
|
2953
|
-
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
2954
|
-
});
|
|
2955
3368
|
const timer = setTimeout(() => {
|
|
2956
3369
|
timedOut = true;
|
|
2957
3370
|
if (child.pid)
|
|
2958
3371
|
killProcessGroup(child.pid);
|
|
2959
3372
|
}, spec.timeoutMs);
|
|
2960
3373
|
timer.unref();
|
|
3374
|
+
let idleTimer;
|
|
3375
|
+
const resetIdleTimer = () => {
|
|
3376
|
+
if (!spec.idleTimeoutMs)
|
|
3377
|
+
return;
|
|
3378
|
+
if (idleTimer)
|
|
3379
|
+
clearTimeout(idleTimer);
|
|
3380
|
+
idleTimer = setTimeout(() => {
|
|
3381
|
+
idleTimedOut = true;
|
|
3382
|
+
if (child.pid)
|
|
3383
|
+
killProcessGroup(child.pid);
|
|
3384
|
+
}, spec.idleTimeoutMs);
|
|
3385
|
+
idleTimer.unref();
|
|
3386
|
+
};
|
|
3387
|
+
resetIdleTimer();
|
|
3388
|
+
child.stdout?.on("data", (chunk) => {
|
|
3389
|
+
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
3390
|
+
resetIdleTimer();
|
|
3391
|
+
});
|
|
3392
|
+
child.stderr?.on("data", (chunk) => {
|
|
3393
|
+
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
3394
|
+
resetIdleTimer();
|
|
3395
|
+
});
|
|
2961
3396
|
try {
|
|
2962
3397
|
const [code, signal] = await once(child, "exit");
|
|
2963
3398
|
if (typeof code === "number")
|
|
@@ -2968,17 +3403,19 @@ async function executeRemoteSpec(spec, machine, metadata, opts) {
|
|
|
2968
3403
|
error = err instanceof Error ? err.message : String(err);
|
|
2969
3404
|
} finally {
|
|
2970
3405
|
clearTimeout(timer);
|
|
3406
|
+
if (idleTimer)
|
|
3407
|
+
clearTimeout(idleTimer);
|
|
2971
3408
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
2972
3409
|
}
|
|
2973
3410
|
const finishedAt = nowIso();
|
|
2974
3411
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
2975
|
-
if (timedOut) {
|
|
3412
|
+
if (timedOut || idleTimedOut) {
|
|
2976
3413
|
return {
|
|
2977
3414
|
status: "timed_out",
|
|
2978
3415
|
exitCode,
|
|
2979
3416
|
stdout,
|
|
2980
3417
|
stderr,
|
|
2981
|
-
error: `timed out after ${spec.timeoutMs}ms`,
|
|
3418
|
+
error: idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`,
|
|
2982
3419
|
pid: child.pid,
|
|
2983
3420
|
startedAt,
|
|
2984
3421
|
finishedAt,
|
|
@@ -3044,6 +3481,7 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3044
3481
|
let stdout = "";
|
|
3045
3482
|
let stderr = "";
|
|
3046
3483
|
let timedOut = false;
|
|
3484
|
+
let idleTimedOut = false;
|
|
3047
3485
|
let exitCode;
|
|
3048
3486
|
let error;
|
|
3049
3487
|
const env = executionEnv(spec, metadata, opts);
|
|
@@ -3106,18 +3544,34 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3106
3544
|
if (opts.signal?.aborted)
|
|
3107
3545
|
abortHandler();
|
|
3108
3546
|
opts.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
3109
|
-
child.stdout?.on("data", (chunk) => {
|
|
3110
|
-
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
3111
|
-
});
|
|
3112
|
-
child.stderr?.on("data", (chunk) => {
|
|
3113
|
-
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
3114
|
-
});
|
|
3115
3547
|
const timer = setTimeout(() => {
|
|
3116
3548
|
timedOut = true;
|
|
3117
3549
|
if (child.pid)
|
|
3118
3550
|
killProcessGroup(child.pid);
|
|
3119
3551
|
}, spec.timeoutMs);
|
|
3120
3552
|
timer.unref();
|
|
3553
|
+
let idleTimer;
|
|
3554
|
+
const resetIdleTimer = () => {
|
|
3555
|
+
if (!spec.idleTimeoutMs)
|
|
3556
|
+
return;
|
|
3557
|
+
if (idleTimer)
|
|
3558
|
+
clearTimeout(idleTimer);
|
|
3559
|
+
idleTimer = setTimeout(() => {
|
|
3560
|
+
idleTimedOut = true;
|
|
3561
|
+
if (child.pid)
|
|
3562
|
+
killProcessGroup(child.pid);
|
|
3563
|
+
}, spec.idleTimeoutMs);
|
|
3564
|
+
idleTimer.unref();
|
|
3565
|
+
};
|
|
3566
|
+
resetIdleTimer();
|
|
3567
|
+
child.stdout?.on("data", (chunk) => {
|
|
3568
|
+
stdout = appendBounded(stdout, chunk, maxOutputBytes);
|
|
3569
|
+
resetIdleTimer();
|
|
3570
|
+
});
|
|
3571
|
+
child.stderr?.on("data", (chunk) => {
|
|
3572
|
+
stderr = appendBounded(stderr, chunk, maxOutputBytes);
|
|
3573
|
+
resetIdleTimer();
|
|
3574
|
+
});
|
|
3121
3575
|
try {
|
|
3122
3576
|
const [code, signal] = await once(child, "exit");
|
|
3123
3577
|
if (typeof code === "number")
|
|
@@ -3128,17 +3582,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3128
3582
|
error = err instanceof Error ? err.message : String(err);
|
|
3129
3583
|
} finally {
|
|
3130
3584
|
clearTimeout(timer);
|
|
3585
|
+
if (idleTimer)
|
|
3586
|
+
clearTimeout(idleTimer);
|
|
3131
3587
|
opts.signal?.removeEventListener("abort", abortHandler);
|
|
3132
3588
|
}
|
|
3133
3589
|
const finishedAt = nowIso();
|
|
3134
3590
|
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
3135
|
-
if (timedOut) {
|
|
3591
|
+
if (timedOut || idleTimedOut) {
|
|
3136
3592
|
return {
|
|
3137
3593
|
status: "timed_out",
|
|
3138
3594
|
exitCode,
|
|
3139
3595
|
stdout,
|
|
3140
3596
|
stderr,
|
|
3141
|
-
error: `timed out after ${spec.timeoutMs}ms`,
|
|
3597
|
+
error: idleTimedOut ? `idle timed out after ${spec.idleTimeoutMs}ms without stdout/stderr` : `timed out after ${spec.timeoutMs}ms`,
|
|
3142
3598
|
pid: child.pid,
|
|
3143
3599
|
startedAt,
|
|
3144
3600
|
finishedAt,
|
|
@@ -4270,14 +4726,60 @@ class LoopsClient {
|
|
|
4270
4726
|
function loops(opts = {}) {
|
|
4271
4727
|
return new LoopsClient(opts);
|
|
4272
4728
|
}
|
|
4729
|
+
function openAutomationsRuntimeBinding(overrides = {}) {
|
|
4730
|
+
const defaults = {
|
|
4731
|
+
integration: "open-automations",
|
|
4732
|
+
role: "runtime",
|
|
4733
|
+
handoff: "claim-queue",
|
|
4734
|
+
queueOwner: "open-automations",
|
|
4735
|
+
runtimeOwner: "open-loops",
|
|
4736
|
+
statusCommand: "automations status",
|
|
4737
|
+
claimCommand: "automations queue claim",
|
|
4738
|
+
completeCommand: "automations queue complete",
|
|
4739
|
+
failCommand: "automations queue fail",
|
|
4740
|
+
eventHandoff: {
|
|
4741
|
+
envelopeCommand: "automations webhooks event",
|
|
4742
|
+
handlerCommand: "loops events handle generic",
|
|
4743
|
+
pipeExample: "automations --json webhooks event <route> --body-json '<json>' | loops --json events handle generic",
|
|
4744
|
+
boundary: "Use only for explicit event-envelope workflow handoff. OpenAutomations still owns deterministic automation materialization and queue state; OpenLoops owns workflow invocation."
|
|
4745
|
+
},
|
|
4746
|
+
requiredEnvironment: ["HASNA_AUTOMATIONS_DIR"],
|
|
4747
|
+
guarantees: [
|
|
4748
|
+
"OpenAutomations owns automation specs, run materialization, queue state, DLQ, replay, idempotency, and approvals.",
|
|
4749
|
+
"OpenLoops may execute claimed actions through explicit command or SDK handoff only.",
|
|
4750
|
+
"OpenLoops may consume exported event envelopes only through explicit events handle commands.",
|
|
4751
|
+
"Workers must complete or fail actions by action id and runner id so OpenAutomations can enforce queue leases."
|
|
4752
|
+
],
|
|
4753
|
+
nonGoals: [
|
|
4754
|
+
"OpenLoops must not become the OpenAutomations product surface.",
|
|
4755
|
+
"OpenLoops must not store automation specs or replace the OpenAutomations queue.",
|
|
4756
|
+
"OpenLoops must not infer automation trigger semantics from event transport alone."
|
|
4757
|
+
]
|
|
4758
|
+
};
|
|
4759
|
+
return {
|
|
4760
|
+
...defaults,
|
|
4761
|
+
...overrides,
|
|
4762
|
+
eventHandoff: overrides.eventHandoff ?? defaults.eventHandoff,
|
|
4763
|
+
requiredEnvironment: overrides.requiredEnvironment ?? defaults.requiredEnvironment,
|
|
4764
|
+
guarantees: overrides.guarantees ?? defaults.guarantees,
|
|
4765
|
+
nonGoals: overrides.nonGoals ?? defaults.nonGoals
|
|
4766
|
+
};
|
|
4767
|
+
}
|
|
4273
4768
|
// src/lib/templates.ts
|
|
4274
4769
|
import { execFileSync } from "child_process";
|
|
4275
4770
|
import { existsSync as existsSync2 } from "fs";
|
|
4276
4771
|
import { homedir as homedir3 } from "os";
|
|
4277
|
-
import { basename, isAbsolute, join as
|
|
4772
|
+
import { basename as basename2, isAbsolute, join as join5, relative, resolve } from "path";
|
|
4278
4773
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
4279
4774
|
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
4280
4775
|
var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
|
|
4776
|
+
var TASK_LIFECYCLE_TEMPLATE_ID = "task-lifecycle";
|
|
4777
|
+
var PR_REVIEW_TEMPLATE_ID = "pr-review";
|
|
4778
|
+
var SCHEDULED_AUDIT_TEMPLATE_ID = "scheduled-audit";
|
|
4779
|
+
var KNOWLEDGE_REFRESH_TEMPLATE_ID = "knowledge-refresh";
|
|
4780
|
+
var REPORT_ONLY_TEMPLATE_ID = "report-only";
|
|
4781
|
+
var INCIDENT_RESPONSE_TEMPLATE_ID = "incident-response";
|
|
4782
|
+
var DETERMINISTIC_CHECK_CREATE_TASK_TEMPLATE_ID = "deterministic-check-create-task";
|
|
4281
4783
|
var TEMPLATE_SUMMARIES = [
|
|
4282
4784
|
{
|
|
4283
4785
|
id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
@@ -4299,7 +4801,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
4299
4801
|
{ name: "model", description: "Provider model." },
|
|
4300
4802
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4301
4803
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
4302
|
-
{ name: "sandbox", default: "
|
|
4804
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4805
|
+
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
4303
4806
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
4304
4807
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
4305
4808
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated task/event worktree branches." }
|
|
@@ -4327,7 +4830,8 @@ var TEMPLATE_SUMMARIES = [
|
|
|
4327
4830
|
{ name: "model", description: "Provider model." },
|
|
4328
4831
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4329
4832
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
4330
|
-
{ name: "sandbox", default: "
|
|
4833
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4834
|
+
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
4331
4835
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
4332
4836
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
4333
4837
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated event worktree branches." }
|
|
@@ -4353,12 +4857,112 @@ var TEMPLATE_SUMMARIES = [
|
|
|
4353
4857
|
{ name: "model", description: "Provider model." },
|
|
4354
4858
|
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4355
4859
|
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
4356
|
-
{ name: "sandbox", default: "
|
|
4860
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4861
|
+
{ name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
|
|
4357
4862
|
{ name: "worktreeMode", default: "auto", description: "Worktree isolation mode: auto, required, off, or main." },
|
|
4358
4863
|
{ name: "worktreeRoot", default: "~/.hasna/loops/worktrees", description: "Base directory for OpenLoops-managed git worktrees." },
|
|
4359
4864
|
{ name: "worktreeBranchPrefix", default: "openloops", description: "Branch prefix for generated bounded-agent worktree branches." },
|
|
4360
4865
|
{ name: "timeoutMs", default: "2700000", description: "Step timeout in milliseconds." }
|
|
4361
4866
|
]
|
|
4867
|
+
},
|
|
4868
|
+
{
|
|
4869
|
+
id: TASK_LIFECYCLE_TEMPLATE_ID,
|
|
4870
|
+
name: "Task Lifecycle",
|
|
4871
|
+
description: "Run the standard task-created lifecycle: triage/dedupe, plan, worker execution, independent verification, and todos closure/follow-up evidence.",
|
|
4872
|
+
kind: "workflow",
|
|
4873
|
+
variables: [
|
|
4874
|
+
{ name: "taskId", required: true, description: "Todos task id." },
|
|
4875
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4876
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4877
|
+
{ name: "accountPool", description: "Comma-separated OpenAccounts profiles for non-Codewith providers." },
|
|
4878
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4879
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4880
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
4881
|
+
]
|
|
4882
|
+
},
|
|
4883
|
+
{
|
|
4884
|
+
id: PR_REVIEW_TEMPLATE_ID,
|
|
4885
|
+
name: "PR Review",
|
|
4886
|
+
description: "Review and drive a pull request toward merge-ready state with a worker and fresh adversarial verifier.",
|
|
4887
|
+
kind: "workflow",
|
|
4888
|
+
variables: [
|
|
4889
|
+
{ name: "prUrl", description: "Pull request URL." },
|
|
4890
|
+
{ name: "prNumber", description: "Pull request number." },
|
|
4891
|
+
{ name: "projectPath", required: true, description: "Repository working directory." },
|
|
4892
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4893
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4894
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4895
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
4896
|
+
]
|
|
4897
|
+
},
|
|
4898
|
+
{
|
|
4899
|
+
id: SCHEDULED_AUDIT_TEMPLATE_ID,
|
|
4900
|
+
name: "Scheduled Audit",
|
|
4901
|
+
description: "Run a bounded scheduled audit, record evidence, create follow-up tasks for actionable findings, then verify the audit result.",
|
|
4902
|
+
kind: "workflow",
|
|
4903
|
+
variables: [
|
|
4904
|
+
{ name: "objective", required: true, description: "Audit objective." },
|
|
4905
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4906
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4907
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4908
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4909
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
4910
|
+
]
|
|
4911
|
+
},
|
|
4912
|
+
{
|
|
4913
|
+
id: KNOWLEDGE_REFRESH_TEMPLATE_ID,
|
|
4914
|
+
name: "Knowledge Refresh",
|
|
4915
|
+
description: "Review recent knowledge, improve structure/schema where needed, create deduped tasks for code changes, and verify the knowledge update.",
|
|
4916
|
+
kind: "workflow",
|
|
4917
|
+
variables: [
|
|
4918
|
+
{ name: "scope", description: "Knowledge scope or label to refresh." },
|
|
4919
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4920
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4921
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4922
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4923
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
4924
|
+
]
|
|
4925
|
+
},
|
|
4926
|
+
{
|
|
4927
|
+
id: REPORT_ONLY_TEMPLATE_ID,
|
|
4928
|
+
name: "Report Only",
|
|
4929
|
+
description: "Produce a bounded report without mutating repositories; verifier checks evidence, scope, and absence of unauthorized changes.",
|
|
4930
|
+
kind: "workflow",
|
|
4931
|
+
variables: [
|
|
4932
|
+
{ name: "objective", required: true, description: "Report objective." },
|
|
4933
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4934
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4935
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4936
|
+
{ name: "sandbox", default: "read-only", description: "Provider sandbox mode." },
|
|
4937
|
+
{ name: "worktreeMode", default: "main", description: "Report-only workflows normally inspect the main checkout read-only." }
|
|
4938
|
+
]
|
|
4939
|
+
},
|
|
4940
|
+
{
|
|
4941
|
+
id: INCIDENT_RESPONSE_TEMPLATE_ID,
|
|
4942
|
+
name: "Incident Response",
|
|
4943
|
+
description: "Triage an incident, gather bounded evidence, apply only allowed narrow mitigation, create follow-up tasks, and verify the response.",
|
|
4944
|
+
kind: "workflow",
|
|
4945
|
+
variables: [
|
|
4946
|
+
{ name: "incidentId", description: "Incident or task id." },
|
|
4947
|
+
{ name: "objective", required: true, description: "Incident response objective." },
|
|
4948
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4949
|
+
{ name: "authProfilePool", description: "Comma-separated Codewith profiles for worker/verifier rotation." },
|
|
4950
|
+
{ name: "provider", default: "codewith", description: "Agent provider." },
|
|
4951
|
+
{ name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
|
|
4952
|
+
{ name: "worktreeMode", default: "required", description: "Worktree isolation mode." }
|
|
4953
|
+
]
|
|
4954
|
+
},
|
|
4955
|
+
{
|
|
4956
|
+
id: DETERMINISTIC_CHECK_CREATE_TASK_TEMPLATE_ID,
|
|
4957
|
+
name: "Deterministic Check Create Task",
|
|
4958
|
+
description: "Run a deterministic check command that writes compact evidence and upserts one deduped todos task when its expectation is not met.",
|
|
4959
|
+
kind: "workflow",
|
|
4960
|
+
variables: [
|
|
4961
|
+
{ name: "checkCommand", required: true, description: "Shell command that performs the check and task upsert." },
|
|
4962
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4963
|
+
{ name: "name", description: "Workflow name." },
|
|
4964
|
+
{ name: "timeoutMs", default: "300000", description: "Check timeout in milliseconds." }
|
|
4965
|
+
]
|
|
4362
4966
|
}
|
|
4363
4967
|
];
|
|
4364
4968
|
function compactJson(value) {
|
|
@@ -4420,7 +5024,7 @@ function defaultWorktreeRoot(root) {
|
|
|
4420
5024
|
const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
|
|
4421
5025
|
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
4422
5026
|
}
|
|
4423
|
-
return
|
|
5027
|
+
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
4424
5028
|
}
|
|
4425
5029
|
function gitRootFor(path) {
|
|
4426
5030
|
if (!existsSync2(path))
|
|
@@ -4521,11 +5125,11 @@ function worktreePlan(input, seed) {
|
|
|
4521
5125
|
};
|
|
4522
5126
|
}
|
|
4523
5127
|
const root = defaultWorktreeRoot(input.worktreeRoot);
|
|
4524
|
-
const repoSlug = slugSegment(
|
|
5128
|
+
const repoSlug = slugSegment(basename2(repoRoot), "repo");
|
|
4525
5129
|
const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
|
|
4526
|
-
const worktreePath =
|
|
5130
|
+
const worktreePath = join5(root, repoSlug, seedSlug);
|
|
4527
5131
|
const relativeCwd = relative(repoRoot, originalCwd);
|
|
4528
|
-
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ?
|
|
5132
|
+
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
|
|
4529
5133
|
const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
|
|
4530
5134
|
const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
|
|
4531
5135
|
const prepareStep = {
|
|
@@ -4583,10 +5187,20 @@ function assertNativeAuthProfileSupport(input, provider) {
|
|
|
4583
5187
|
return;
|
|
4584
5188
|
throw new Error(`authProfile, authProfilePool, workerAuthProfile, and verifierAuthProfile are supported only for provider codewith; use account/accountPool for ${provider} profile isolation`);
|
|
4585
5189
|
}
|
|
5190
|
+
function failClosedSandbox(input, provider, sandbox) {
|
|
5191
|
+
if (!["codewith", "codex"].includes(provider))
|
|
5192
|
+
return;
|
|
5193
|
+
if (sandbox !== "danger-full-access")
|
|
5194
|
+
return;
|
|
5195
|
+
if (input.manualBreakGlass)
|
|
5196
|
+
return;
|
|
5197
|
+
throw new Error("danger-full-access is manual break-glass only for generated worker/verifier workflows; use sandbox=workspace-write or set manualBreakGlass=true with explicit operator approval");
|
|
5198
|
+
}
|
|
4586
5199
|
function agentTarget(input, prompt, role, seed, plan) {
|
|
4587
5200
|
const provider = input.provider ?? "codewith";
|
|
4588
5201
|
assertNativeAuthProfileSupport(input, provider);
|
|
4589
|
-
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "
|
|
5202
|
+
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "workspace-write" : provider === "cursor" ? "enabled" : undefined);
|
|
5203
|
+
failClosedSandbox(input, provider, sandbox);
|
|
4590
5204
|
return {
|
|
4591
5205
|
type: "agent",
|
|
4592
5206
|
provider,
|
|
@@ -4610,6 +5224,7 @@ function agentTarget(input, prompt, role, seed, plan) {
|
|
|
4610
5224
|
branch: plan.branch,
|
|
4611
5225
|
reason: plan.reason
|
|
4612
5226
|
},
|
|
5227
|
+
allowlist: input.manualBreakGlass ? { enforcement: "metadata_only", commands: ["manual-break-glass"] } : undefined,
|
|
4613
5228
|
routing: {
|
|
4614
5229
|
projectPath: input.routeProjectPath ?? input.projectPath,
|
|
4615
5230
|
...input.projectGroup ? { projectGroup: input.projectGroup } : {}
|
|
@@ -4699,7 +5314,10 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4699
5314
|
name: "Verifier",
|
|
4700
5315
|
description: "Adversarially verify worker output and update todos.",
|
|
4701
5316
|
dependsOn: ["worker"],
|
|
4702
|
-
target:
|
|
5317
|
+
target: {
|
|
5318
|
+
...agentTarget(input, verifierPrompt, "verifier", input.taskId, plan),
|
|
5319
|
+
idleTimeoutMs: 10 * 60000
|
|
5320
|
+
},
|
|
4703
5321
|
timeoutMs: 30 * 60000
|
|
4704
5322
|
}
|
|
4705
5323
|
])
|
|
@@ -4777,7 +5395,10 @@ function renderEventWorkerVerifierWorkflow(input) {
|
|
|
4777
5395
|
name: "Verifier",
|
|
4778
5396
|
description: "Adversarially verify event handling.",
|
|
4779
5397
|
dependsOn: ["worker"],
|
|
4780
|
-
target:
|
|
5398
|
+
target: {
|
|
5399
|
+
...agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
5400
|
+
idleTimeoutMs: 10 * 60000
|
|
5401
|
+
},
|
|
4781
5402
|
timeoutMs: 30 * 60000
|
|
4782
5403
|
}
|
|
4783
5404
|
])
|
|
@@ -4828,13 +5449,142 @@ function renderBoundedAgentWorkerVerifierWorkflow(input) {
|
|
|
4828
5449
|
name: "Verifier",
|
|
4829
5450
|
description: "Adversarially verify the bounded objective result.",
|
|
4830
5451
|
dependsOn: ["worker"],
|
|
4831
|
-
target:
|
|
5452
|
+
target: {
|
|
5453
|
+
...agentTarget(input, verifierPrompt, "verifier", seed, plan),
|
|
5454
|
+
idleTimeoutMs: 10 * 60000
|
|
5455
|
+
},
|
|
4832
5456
|
timeoutMs: Math.min(timeoutMs, 30 * 60000)
|
|
4833
5457
|
}
|
|
4834
5458
|
])
|
|
4835
5459
|
};
|
|
4836
5460
|
}
|
|
5461
|
+
function renderLifecycleBoundedTemplate(id, values) {
|
|
5462
|
+
const projectPath = values.projectPath ?? values.cwd ?? process.cwd();
|
|
5463
|
+
const common = {
|
|
5464
|
+
name: values.name,
|
|
5465
|
+
projectPath,
|
|
5466
|
+
routeProjectPath: values.routeProjectPath,
|
|
5467
|
+
projectGroup: values.projectGroup,
|
|
5468
|
+
provider: values.provider,
|
|
5469
|
+
authProfile: values.authProfile,
|
|
5470
|
+
authProfilePool: listVar(values.authProfilePool),
|
|
5471
|
+
workerAuthProfile: values.workerAuthProfile,
|
|
5472
|
+
verifierAuthProfile: values.verifierAuthProfile,
|
|
5473
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
5474
|
+
accountPool: accountPoolVar(values.accountPool, values.accountTool),
|
|
5475
|
+
model: values.model,
|
|
5476
|
+
variant: values.variant,
|
|
5477
|
+
agent: values.agent,
|
|
5478
|
+
permissionMode: values.permissionMode,
|
|
5479
|
+
sandbox: values.sandbox,
|
|
5480
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
5481
|
+
worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
|
|
5482
|
+
worktreeRoot: values.worktreeRoot,
|
|
5483
|
+
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
5484
|
+
timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : undefined
|
|
5485
|
+
};
|
|
5486
|
+
if (id === TASK_LIFECYCLE_TEMPLATE_ID) {
|
|
5487
|
+
const taskId = values.taskId ?? "";
|
|
5488
|
+
if (!taskId.trim())
|
|
5489
|
+
throw new Error("taskId is required");
|
|
5490
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5491
|
+
...common,
|
|
5492
|
+
name: values.name ?? `task-lifecycle-${slugSegment(taskId)}-worker-verifier`,
|
|
5493
|
+
objective: values.objective ?? `Run the full task lifecycle for todos task ${taskId}.`,
|
|
5494
|
+
prompt: values.prompt ?? "Triage and dedupe the task, verify it is eligible for loop execution, create or update a concise plan artifact/comment, execute only the allowed scope, validate, record evidence, and let the verifier decide final task state. Add follow-up tasks instead of broadening scope."
|
|
5495
|
+
});
|
|
5496
|
+
}
|
|
5497
|
+
if (id === PR_REVIEW_TEMPLATE_ID) {
|
|
5498
|
+
const pr = values.prUrl ?? values.prNumber ?? "";
|
|
5499
|
+
if (!pr.trim())
|
|
5500
|
+
throw new Error("prUrl or prNumber is required");
|
|
5501
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5502
|
+
...common,
|
|
5503
|
+
name: values.name ?? `pr-review-${slugSegment(pr)}-worker-verifier`,
|
|
5504
|
+
objective: values.objective ?? `Review and drive PR ${pr} toward merge-ready state.`,
|
|
5505
|
+
prompt: values.prompt ?? "Inspect PR state, checks, conflicts, branch freshness, review requirements, and repo policy. Apply only owned logical fixes in the isolated worktree, validate, update the PR/task with evidence, and do not merge unless policy/checks make it clearly safe."
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
if (id === SCHEDULED_AUDIT_TEMPLATE_ID) {
|
|
5509
|
+
const objective = values.objective ?? "";
|
|
5510
|
+
if (!objective.trim())
|
|
5511
|
+
throw new Error("objective is required");
|
|
5512
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5513
|
+
...common,
|
|
5514
|
+
name: values.name ?? `scheduled-audit-${stableIndex(`${projectPath}:${objective}`, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
|
|
5515
|
+
objective,
|
|
5516
|
+
prompt: values.prompt ?? "Run the bounded audit, write compact evidence, create deduped todos tasks for actionable issues, and avoid implementation unless the task explicitly allows it."
|
|
5517
|
+
});
|
|
5518
|
+
}
|
|
5519
|
+
if (id === KNOWLEDGE_REFRESH_TEMPLATE_ID) {
|
|
5520
|
+
const scope = values.scope ?? values.label ?? "recent knowledge";
|
|
5521
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5522
|
+
...common,
|
|
5523
|
+
name: values.name ?? `knowledge-refresh-${slugSegment(scope)}-worker-verifier`,
|
|
5524
|
+
objective: values.objective ?? `Refresh and verify ${scope}.`,
|
|
5525
|
+
prompt: values.prompt ?? "Inspect recent knowledge records, improve structure/schema where appropriate, avoid duplicates, create tasks for code changes instead of doing unrelated implementation, and record verification evidence."
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
if (id === REPORT_ONLY_TEMPLATE_ID) {
|
|
5529
|
+
const objective = values.objective ?? "";
|
|
5530
|
+
if (!objective.trim())
|
|
5531
|
+
throw new Error("objective is required");
|
|
5532
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5533
|
+
...common,
|
|
5534
|
+
name: values.name ?? `report-only-${stableIndex(`${projectPath}:${objective}`, 4294967295).toString(16).padStart(8, "0")}-worker-verifier`,
|
|
5535
|
+
objective,
|
|
5536
|
+
prompt: values.prompt ?? "Produce a report only. Do not mutate repositories, tasks, secrets, databases, or external systems except for writing the requested report/evidence artifact."
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
if (id === INCIDENT_RESPONSE_TEMPLATE_ID) {
|
|
5540
|
+
const objective = values.objective ?? "";
|
|
5541
|
+
if (!objective.trim())
|
|
5542
|
+
throw new Error("objective is required");
|
|
5543
|
+
const incident = values.incidentId ?? values.taskId ?? "incident";
|
|
5544
|
+
return renderBoundedAgentWorkerVerifierWorkflow({
|
|
5545
|
+
...common,
|
|
5546
|
+
name: values.name ?? `incident-response-${slugSegment(incident)}-worker-verifier`,
|
|
5547
|
+
objective,
|
|
5548
|
+
prompt: values.prompt ?? "Triage first, gather bounded evidence, mitigate only narrow allowed issues, preserve data/history/secrets, create follow-up tasks for larger fixes, and require verifier confirmation before closure."
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
return;
|
|
5552
|
+
}
|
|
5553
|
+
function renderDeterministicCheckCreateTaskWorkflow(values) {
|
|
5554
|
+
const projectPath = values.projectPath ?? values.cwd ?? process.cwd();
|
|
5555
|
+
const checkCommand = values.checkCommand ?? "";
|
|
5556
|
+
if (!checkCommand.trim())
|
|
5557
|
+
throw new Error("checkCommand is required");
|
|
5558
|
+
const seed = `${projectPath}:${checkCommand}`;
|
|
5559
|
+
return {
|
|
5560
|
+
name: values.name ?? `deterministic-check-${stableIndex(seed, 4294967295).toString(16).padStart(8, "0")}`,
|
|
5561
|
+
description: values.description ?? "Deterministic check that writes compact evidence and upserts one deduped todos task when the expectation is not met.",
|
|
5562
|
+
version: 1,
|
|
5563
|
+
steps: [
|
|
5564
|
+
{
|
|
5565
|
+
id: "check",
|
|
5566
|
+
name: "Check",
|
|
5567
|
+
description: "Run the deterministic check/task-upsert command.",
|
|
5568
|
+
target: {
|
|
5569
|
+
type: "command",
|
|
5570
|
+
command: "bash",
|
|
5571
|
+
args: ["-lc", checkCommand],
|
|
5572
|
+
cwd: projectPath,
|
|
5573
|
+
timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000,
|
|
5574
|
+
idleTimeoutMs: values.idleTimeoutMs ? Number(values.idleTimeoutMs) : 60000
|
|
5575
|
+
},
|
|
5576
|
+
timeoutMs: values.timeoutMs ? Number(values.timeoutMs) : 5 * 60000
|
|
5577
|
+
}
|
|
5578
|
+
]
|
|
5579
|
+
};
|
|
5580
|
+
}
|
|
4837
5581
|
function renderLoopTemplate(id, values) {
|
|
5582
|
+
if (id === DETERMINISTIC_CHECK_CREATE_TASK_TEMPLATE_ID) {
|
|
5583
|
+
return renderDeterministicCheckCreateTaskWorkflow(values);
|
|
5584
|
+
}
|
|
5585
|
+
const lifecycle = renderLifecycleBoundedTemplate(id, values);
|
|
5586
|
+
if (lifecycle)
|
|
5587
|
+
return lifecycle;
|
|
4838
5588
|
if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
4839
5589
|
return renderTodosTaskWorkerVerifierWorkflow({
|
|
4840
5590
|
taskId: values.taskId ?? "",
|
|
@@ -4855,6 +5605,7 @@ function renderLoopTemplate(id, values) {
|
|
|
4855
5605
|
agent: values.agent,
|
|
4856
5606
|
permissionMode: values.permissionMode,
|
|
4857
5607
|
sandbox: values.sandbox,
|
|
5608
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
4858
5609
|
worktreeMode: values.worktreeMode,
|
|
4859
5610
|
worktreeRoot: values.worktreeRoot,
|
|
4860
5611
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
@@ -4885,6 +5636,7 @@ function renderLoopTemplate(id, values) {
|
|
|
4885
5636
|
agent: values.agent,
|
|
4886
5637
|
permissionMode: values.permissionMode,
|
|
4887
5638
|
sandbox: values.sandbox,
|
|
5639
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
4888
5640
|
worktreeMode: values.worktreeMode,
|
|
4889
5641
|
worktreeRoot: values.worktreeRoot,
|
|
4890
5642
|
worktreeBranchPrefix: values.worktreeBranchPrefix
|
|
@@ -4910,6 +5662,7 @@ function renderLoopTemplate(id, values) {
|
|
|
4910
5662
|
agent: values.agent,
|
|
4911
5663
|
permissionMode: values.permissionMode,
|
|
4912
5664
|
sandbox: values.sandbox,
|
|
5665
|
+
manualBreakGlass: booleanVar(values.manualBreakGlass),
|
|
4913
5666
|
worktreeMode: values.worktreeMode,
|
|
4914
5667
|
worktreeRoot: values.worktreeRoot,
|
|
4915
5668
|
worktreeBranchPrefix: values.worktreeBranchPrefix,
|
|
@@ -4922,6 +5675,16 @@ function listVar(value) {
|
|
|
4922
5675
|
const values = value?.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
4923
5676
|
return values?.length ? values : undefined;
|
|
4924
5677
|
}
|
|
5678
|
+
function booleanVar(value) {
|
|
5679
|
+
if (value === undefined)
|
|
5680
|
+
return;
|
|
5681
|
+
const normalized = value.trim().toLowerCase();
|
|
5682
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
5683
|
+
return true;
|
|
5684
|
+
if (["0", "false", "no", "off", ""].includes(normalized))
|
|
5685
|
+
return false;
|
|
5686
|
+
throw new Error(`expected boolean value, got ${value}`);
|
|
5687
|
+
}
|
|
4925
5688
|
function accountPoolVar(value, tool) {
|
|
4926
5689
|
return listVar(value)?.map((profile) => ({ profile, tool }));
|
|
4927
5690
|
}
|
|
@@ -4930,7 +5693,7 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
4930
5693
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
4931
5694
|
|
|
4932
5695
|
// src/daemon/control.ts
|
|
4933
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
5696
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4934
5697
|
import { hostname } from "os";
|
|
4935
5698
|
import { dirname as dirname2 } from "path";
|
|
4936
5699
|
|
|
@@ -4968,11 +5731,11 @@ function readPid(path = pidFilePath()) {
|
|
|
4968
5731
|
}
|
|
4969
5732
|
}
|
|
4970
5733
|
function writePid(pid = process.pid, path = pidFilePath()) {
|
|
4971
|
-
|
|
4972
|
-
|
|
5734
|
+
mkdirSync4(dirname2(path), { recursive: true, mode: 448 });
|
|
5735
|
+
writeFileSync2(path, String(pid));
|
|
4973
5736
|
}
|
|
4974
5737
|
function removePid(path = pidFilePath()) {
|
|
4975
|
-
|
|
5738
|
+
rmSync2(path, { force: true });
|
|
4976
5739
|
}
|
|
4977
5740
|
function isAlive(pid) {
|
|
4978
5741
|
try {
|
|
@@ -5163,7 +5926,7 @@ function runDoctor(store) {
|
|
|
5163
5926
|
};
|
|
5164
5927
|
}
|
|
5165
5928
|
// src/lib/health.ts
|
|
5166
|
-
import { createHash } from "crypto";
|
|
5929
|
+
import { createHash as createHash2 } from "crypto";
|
|
5167
5930
|
|
|
5168
5931
|
// src/lib/format.ts
|
|
5169
5932
|
var TEXT_OUTPUT_LIMIT = 32 * 1024;
|
|
@@ -5249,6 +6012,12 @@ function publicWorkflow(workflow) {
|
|
|
5249
6012
|
function publicWorkflowRun(run) {
|
|
5250
6013
|
return { ...run, error: redact(run.error) };
|
|
5251
6014
|
}
|
|
6015
|
+
function publicWorkflowInvocation(invocation) {
|
|
6016
|
+
return redactSensitivePayload(invocation);
|
|
6017
|
+
}
|
|
6018
|
+
function publicWorkflowWorkItem(item) {
|
|
6019
|
+
return { ...item, lastReason: redact(item.lastReason, 240) };
|
|
6020
|
+
}
|
|
5252
6021
|
function publicWorkflowStepRun(run, showOutput = false) {
|
|
5253
6022
|
return {
|
|
5254
6023
|
...run,
|
|
@@ -5306,7 +6075,7 @@ function searchableText(run) {
|
|
|
5306
6075
|
`).toLowerCase();
|
|
5307
6076
|
}
|
|
5308
6077
|
function stableFingerprint(parts) {
|
|
5309
|
-
return
|
|
6078
|
+
return createHash2("sha256").update(parts.join(`
|
|
5310
6079
|
`)).digest("hex").slice(0, 16);
|
|
5311
6080
|
}
|
|
5312
6081
|
function stableFailureFingerprint(run, classification) {
|
|
@@ -5493,7 +6262,7 @@ function buildHealthReport(store, opts = {}) {
|
|
|
5493
6262
|
};
|
|
5494
6263
|
}
|
|
5495
6264
|
// src/lib/hygiene.ts
|
|
5496
|
-
import { basename as
|
|
6265
|
+
import { basename as basename3 } from "path";
|
|
5497
6266
|
var PROVIDER_TOKENS = new Set([
|
|
5498
6267
|
"codewith",
|
|
5499
6268
|
"claude",
|
|
@@ -5514,7 +6283,7 @@ function repoSlugFromCwd(cwd) {
|
|
|
5514
6283
|
return "";
|
|
5515
6284
|
if (cwd.includes("/.hasna/loops/"))
|
|
5516
6285
|
return "";
|
|
5517
|
-
return slugify(
|
|
6286
|
+
return slugify(basename3(cwd));
|
|
5518
6287
|
}
|
|
5519
6288
|
function scopeForLoop(loop) {
|
|
5520
6289
|
const cwd = loop.target.type === "command" || loop.target.type === "agent" ? loop.target.cwd : undefined;
|
|
@@ -5747,6 +6516,7 @@ export {
|
|
|
5747
6516
|
preflightTarget,
|
|
5748
6517
|
parseDuration,
|
|
5749
6518
|
parseCron,
|
|
6519
|
+
openAutomationsRuntimeBinding,
|
|
5750
6520
|
nextCronRun,
|
|
5751
6521
|
loops,
|
|
5752
6522
|
listOpenMachines,
|