@hasna/loops 0.3.48 → 0.3.50

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/dist/index.js CHANGED
@@ -621,6 +621,8 @@ function writeWorkflowRunManifest(args) {
621
621
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
622
622
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
623
623
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
624
+ var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
625
+ var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
624
626
  function rowToLoop(row) {
625
627
  return {
626
628
  id: row.id,
@@ -1441,10 +1443,23 @@ class Store {
1441
1443
  })();
1442
1444
  }
1443
1445
  listWorkflows(opts = {}) {
1444
- const limit = opts.limit ?? 200;
1445
- const rows = opts.status ? this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ?").all(opts.status, limit) : this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ?").all(limit);
1446
+ const offset = Math.max(0, opts.offset ?? 0);
1447
+ let rows;
1448
+ if (opts.status && opts.limit !== undefined) {
1449
+ rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT ? OFFSET ?").all(opts.status, opts.limit, offset);
1450
+ } else if (opts.status) {
1451
+ rows = this.db.query("SELECT * FROM workflow_specs WHERE status = ? ORDER BY updated_at DESC LIMIT -1 OFFSET ?").all(opts.status, offset);
1452
+ } else if (opts.limit !== undefined) {
1453
+ rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT ? OFFSET ?").all(opts.limit, offset);
1454
+ } else {
1455
+ rows = this.db.query("SELECT * FROM workflow_specs ORDER BY status ASC, updated_at DESC LIMIT -1 OFFSET ?").all(offset);
1456
+ }
1446
1457
  return rows.map(rowToWorkflow);
1447
1458
  }
1459
+ countWorkflows(opts = {}) {
1460
+ const row = opts.status ? this.db.query("SELECT COUNT(*) AS count FROM workflow_specs WHERE status = ?").get(opts.status) : this.db.query("SELECT COUNT(*) AS count FROM workflow_specs").get();
1461
+ return row?.count ?? 0;
1462
+ }
1448
1463
  archiveWorkflow(idOrName) {
1449
1464
  const workflow = this.requireWorkflow(idOrName);
1450
1465
  const updated = nowIso();
@@ -1454,6 +1469,64 @@ class Store {
1454
1469
  throw new Error(`workflow not found after archive: ${workflow.id}`);
1455
1470
  return archived;
1456
1471
  }
1472
+ generatedRouteArchiveContext(args) {
1473
+ if (!args.loopId || !args.workItemId)
1474
+ return;
1475
+ const workItem = this.getWorkflowWorkItem(args.workItemId);
1476
+ if (!workItem || !GENERATED_ROUTE_KEYS.has(workItem.routeKey))
1477
+ return;
1478
+ const invocation = this.getWorkflowInvocation(workItem.invocationId);
1479
+ if (!invocation?.templateId || !GENERATED_ROUTE_TEMPLATE_IDS.has(invocation.templateId))
1480
+ return;
1481
+ const loop = this.getLoop(args.loopId);
1482
+ if (!loop || loop.schedule.type !== "once" || loop.target.type !== "workflow" || loop.target.workflowId !== args.workflowId)
1483
+ return;
1484
+ const input = loop.target.input ?? {};
1485
+ if (input.workflowWorkItemId && input.workflowWorkItemId !== workItem.id)
1486
+ return;
1487
+ if (input.workflowInvocationId && input.workflowInvocationId !== invocation.id)
1488
+ return;
1489
+ if (workItem.workflowId && workItem.workflowId !== args.workflowId)
1490
+ return;
1491
+ const workflow = this.getWorkflow(args.workflowId);
1492
+ if (!workflow)
1493
+ return;
1494
+ return { workflow, loop, workItem, invocation };
1495
+ }
1496
+ maybeArchiveGeneratedRouteWorkflow(args) {
1497
+ const context = this.generatedRouteArchiveContext(args);
1498
+ if (!context)
1499
+ return;
1500
+ const { workflow, loop, workItem } = context;
1501
+ if (!workflow || workflow.status !== "active")
1502
+ return;
1503
+ const nonTerminal = this.db.query(`SELECT COUNT(*) AS count FROM workflow_runs
1504
+ WHERE workflow_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).get(args.workflowId)?.count ?? 0;
1505
+ if (nonTerminal > 0)
1506
+ return;
1507
+ const res = this.db.query("UPDATE workflow_specs SET status='archived', updated_at=? WHERE id=? AND status='active'").run(args.updated, args.workflowId);
1508
+ if (res.changes === 1 && args.workflowRunId) {
1509
+ this.appendWorkflowEvent(args.workflowRunId, "workflow_archived", undefined, {
1510
+ workflowId: args.workflowId,
1511
+ loopId: loop.id,
1512
+ workItemId: workItem.id,
1513
+ routeKey: workItem.routeKey,
1514
+ reason: "terminal generated one-shot route workflow"
1515
+ });
1516
+ }
1517
+ }
1518
+ maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, updated) {
1519
+ const run = this.getWorkflowRun(workflowRunId);
1520
+ if (!run)
1521
+ return;
1522
+ this.maybeArchiveGeneratedRouteWorkflow({
1523
+ workflowId: run.workflowId,
1524
+ loopId: run.loopId,
1525
+ workItemId: run.workItemId,
1526
+ workflowRunId,
1527
+ updated
1528
+ });
1529
+ }
1457
1530
  createWorkflowInvocation(input) {
1458
1531
  const now = nowIso();
1459
1532
  const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
@@ -2239,6 +2312,7 @@ class Store {
2239
2312
  if (changed) {
2240
2313
  const itemStatus = status === "succeeded" ? "succeeded" : status === "cancelled" ? "cancelled" : "failed";
2241
2314
  this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, itemStatus, patch.error, finishedAt);
2315
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, finishedAt);
2242
2316
  }
2243
2317
  this.db.exec("COMMIT");
2244
2318
  } catch (error) {
@@ -2266,6 +2340,7 @@ class Store {
2266
2340
  WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
2267
2341
  this.setWorkflowWorkItemsForWorkflowRun(workflowRunId, "cancelled", reason, now);
2268
2342
  this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
2343
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRunId, now);
2269
2344
  }
2270
2345
  this.db.exec("COMMIT");
2271
2346
  return this.requireWorkflowRun(workflowRunId);
@@ -2518,8 +2593,20 @@ class Store {
2518
2593
  throw new Error(`run not found after finalize: ${id}`);
2519
2594
  if (opts.claimedBy && res.changes !== 1)
2520
2595
  return run;
2521
- if (res.changes === 1)
2596
+ if (res.changes === 1) {
2522
2597
  this.setWorkflowWorkItemsForLoopRun(run, patch.error, finishedAt);
2598
+ const loop = this.getLoop(run.loopId);
2599
+ const itemStatus = workItemStatusForLoopRun(run.status, run.attempt, loop?.maxAttempts);
2600
+ if (loop?.target.type === "workflow" && itemStatus && itemStatus !== "admitted") {
2601
+ const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
2602
+ this.maybeArchiveGeneratedRouteWorkflow({
2603
+ workflowId: loop.target.workflowId,
2604
+ loopId: loop.id,
2605
+ workItemId,
2606
+ updated: finishedAt
2607
+ });
2608
+ }
2609
+ }
2523
2610
  return run;
2524
2611
  }
2525
2612
  heartbeatRunLease(id, claimedBy, leaseMs, now = new Date, opts = {}) {
@@ -2640,6 +2727,7 @@ class Store {
2640
2727
  loopRunId: row.id
2641
2728
  });
2642
2729
  this.setWorkflowWorkItemsForWorkflowRun(workflowRow.id, "failed", "parent loop run lease expired before completion", finished);
2730
+ this.maybeArchiveTerminalGeneratedRouteWorkflow(workflowRow.id, finished);
2643
2731
  }
2644
2732
  const loop = this.getLoop(row.loop_id);
2645
2733
  const itemStatus = workItemStatusForLoopRun("abandoned", row.attempt, loop?.maxAttempts);
@@ -2647,6 +2735,15 @@ class Store {
2647
2735
  const statuses = itemStatus === "admitted" ? ["admitted", "running", "failed"] : ["admitted", "running"];
2648
2736
  const reason = itemStatus === "admitted" ? "run lease expired before completion; retry pending" : "run lease expired before completion";
2649
2737
  this.setWorkflowWorkItemsForLoop(row.loop_id, itemStatus, reason, finished, statuses);
2738
+ if (loop?.target.type === "workflow" && itemStatus !== "admitted") {
2739
+ const workItemId = loop.target.input?.workflowWorkItemId ?? loop.target.input?.workItemId;
2740
+ this.maybeArchiveGeneratedRouteWorkflow({
2741
+ workflowId: loop.target.workflowId,
2742
+ loopId: loop.id,
2743
+ workItemId,
2744
+ updated: finished
2745
+ });
2746
+ }
2650
2747
  }
2651
2748
  this.db.exec("COMMIT");
2652
2749
  } catch (error) {
@@ -4880,7 +4977,7 @@ function openAutomationsRuntimeBinding(overrides = {}) {
4880
4977
  }
4881
4978
  // src/lib/templates.ts
4882
4979
  import { execFileSync } from "child_process";
4883
- import { existsSync as existsSync3 } from "fs";
4980
+ import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
4884
4981
  import { homedir as homedir3 } from "os";
4885
4982
  import { basename as basename2, isAbsolute, join as join5, relative, resolve } from "path";
4886
4983
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
@@ -5081,6 +5178,11 @@ var TEMPLATE_SUMMARIES = [
5081
5178
  ]
5082
5179
  }
5083
5180
  ];
5181
+ var CUSTOM_TEMPLATE_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
5182
+ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
5183
+ var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
5184
+ var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
5185
+ var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
5084
5186
  function compactJson(value) {
5085
5187
  return JSON.stringify(value);
5086
5188
  }
@@ -5358,11 +5460,310 @@ function workflowStepsWithWorktree(plan, steps) {
5358
5460
  ...steps.map((step) => step.id === "worker" ? { ...step, dependsOn: [...new Set([...step.dependsOn ?? [], plan.prepareStep.id])] } : step)
5359
5461
  ];
5360
5462
  }
5361
- function listLoopTemplates() {
5362
- return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
5463
+ function assertRecord(value, label) {
5464
+ if (!value || typeof value !== "object" || Array.isArray(value))
5465
+ throw new Error(`${label} must be an object`);
5466
+ }
5467
+ function assertTemplateString(value, label) {
5468
+ if (typeof value !== "string" || value.trim() === "")
5469
+ throw new Error(`${label} must be a non-empty string`);
5470
+ return value.trim();
5471
+ }
5472
+ function assertTemplateKind(value, label) {
5473
+ const kind = assertTemplateString(value, label);
5474
+ if (kind !== "workflow")
5475
+ throw new Error(`${label} must be workflow; custom loop templates are not supported yet`);
5476
+ return kind;
5477
+ }
5478
+ function customLoopTemplatesDir() {
5479
+ return join5(dataDir(), "templates");
5480
+ }
5481
+ function ensureCustomLoopTemplatesDir() {
5482
+ const dir = customLoopTemplatesDir();
5483
+ mkdirSync4(dir, { recursive: true, mode: 448 });
5484
+ return dir;
5485
+ }
5486
+ function loopTemplatesDir() {
5487
+ return customLoopTemplatesDir();
5488
+ }
5489
+ function builtinLoopTemplates() {
5490
+ return TEMPLATE_SUMMARIES.map((template) => ({ ...structuredClone(template), source: "builtin" }));
5491
+ }
5492
+ function getBuiltinLoopTemplate(id) {
5493
+ return builtinLoopTemplates().find((template) => template.id === id || template.name === id);
5494
+ }
5495
+ function builtinTemplateKeys() {
5496
+ const keys = new Set;
5497
+ for (const template of TEMPLATE_SUMMARIES) {
5498
+ keys.add(template.id);
5499
+ keys.add(template.name);
5500
+ }
5501
+ return keys;
5502
+ }
5503
+ function validateCustomTemplateId(id, label) {
5504
+ if (!CUSTOM_TEMPLATE_ID_PATTERN.test(id)) {
5505
+ throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
5506
+ }
5507
+ }
5508
+ function validateCustomTemplateVariables(value, label) {
5509
+ if (value === undefined)
5510
+ return [];
5511
+ if (!Array.isArray(value))
5512
+ throw new Error(`${label} must be an array`);
5513
+ const seen = new Set;
5514
+ return value.map((entry, index) => {
5515
+ const entryLabel = `${label}[${index}]`;
5516
+ assertRecord(entry, entryLabel);
5517
+ const name = assertTemplateString(entry.name, `${entryLabel}.name`);
5518
+ if (!CUSTOM_TEMPLATE_VARIABLE_PATTERN.test(name)) {
5519
+ throw new Error(`${entryLabel}.name must match ${CUSTOM_TEMPLATE_VARIABLE_PATTERN.source}`);
5520
+ }
5521
+ if (seen.has(name))
5522
+ throw new Error(`duplicate custom template variable: ${name}`);
5523
+ seen.add(name);
5524
+ const description = entry.description === undefined ? undefined : assertTemplateString(entry.description, `${entryLabel}.description`);
5525
+ const defaultValue = entry.default === undefined ? undefined : assertTemplateString(entry.default, `${entryLabel}.default`);
5526
+ const type = entry.type === undefined ? undefined : assertTemplateString(entry.type, `${entryLabel}.type`);
5527
+ if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
5528
+ throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
5529
+ }
5530
+ if (defaultValue === "danger-full-access") {
5531
+ throw new Error(`${entryLabel}.default cannot be danger-full-access in a custom template`);
5532
+ }
5533
+ return {
5534
+ name,
5535
+ description,
5536
+ required: entry.required === undefined ? undefined : Boolean(entry.required),
5537
+ default: defaultValue,
5538
+ type
5539
+ };
5540
+ });
5541
+ }
5542
+ function assertNoDangerFullAccess(value, label) {
5543
+ if (!value || typeof value !== "object")
5544
+ return;
5545
+ if (Array.isArray(value)) {
5546
+ value.forEach((entry, index) => assertNoDangerFullAccess(entry, `${label}[${index}]`));
5547
+ return;
5548
+ }
5549
+ for (const [key, entry] of Object.entries(value)) {
5550
+ if (key === "sandbox" && entry === "danger-full-access") {
5551
+ throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
5552
+ }
5553
+ assertNoDangerFullAccess(entry, `${label}.${key}`);
5554
+ }
5555
+ }
5556
+ function customTemplateDefinitionFromJson(value, sourcePath) {
5557
+ assertRecord(value, sourcePath);
5558
+ const id = assertTemplateString(value.id, `${sourcePath}.id`);
5559
+ validateCustomTemplateId(id, `${sourcePath}.id`);
5560
+ const name = assertTemplateString(value.name, `${sourcePath}.name`);
5561
+ const description = assertTemplateString(value.description, `${sourcePath}.description`);
5562
+ const kind = assertTemplateKind(value.kind ?? "workflow", `${sourcePath}.kind`);
5563
+ const variables = validateCustomTemplateVariables(value.variables, `${sourcePath}.variables`);
5564
+ if (value.workflow === undefined)
5565
+ throw new Error(`${sourcePath}.workflow is required`);
5566
+ assertRecord(value.workflow, `${sourcePath}.workflow`);
5567
+ assertNoDangerFullAccess(value.workflow, `${sourcePath}.workflow`);
5568
+ return { id, name, description, kind, variables, workflow: value.workflow };
5569
+ }
5570
+ function customTemplateSummary(definition, sourcePath) {
5571
+ return {
5572
+ id: definition.id,
5573
+ name: definition.name,
5574
+ description: definition.description,
5575
+ kind: definition.kind,
5576
+ variables: structuredClone(definition.variables),
5577
+ source: "custom",
5578
+ sourcePath
5579
+ };
5363
5580
  }
5364
- function getLoopTemplate(id) {
5365
- return listLoopTemplates().find((template) => template.id === id || template.name === id);
5581
+ function readCustomTemplateFile(file) {
5582
+ let parsed;
5583
+ try {
5584
+ parsed = JSON.parse(readFileSync(file, "utf8"));
5585
+ } catch (error) {
5586
+ const message = error instanceof Error ? error.message : String(error);
5587
+ throw new Error(`failed to read custom template ${file}: ${message}`);
5588
+ }
5589
+ const definition = customTemplateDefinitionFromJson(parsed, file);
5590
+ return { definition, summary: customTemplateSummary(definition, file), path: file };
5591
+ }
5592
+ function assertNoTemplateCollisions(entries) {
5593
+ const builtinKeys = builtinTemplateKeys();
5594
+ const seen = new Map;
5595
+ for (const entry of entries) {
5596
+ for (const key of [entry.definition.id, entry.definition.name]) {
5597
+ if (builtinKeys.has(key)) {
5598
+ throw new Error(`custom template ${entry.definition.id} collides with built-in template key ${key}; choose a different id or name`);
5599
+ }
5600
+ const existing = seen.get(key);
5601
+ if (existing) {
5602
+ throw new Error(`custom template ${entry.definition.id} collides with ${existing} on key ${key}`);
5603
+ }
5604
+ seen.set(key, entry.definition.id);
5605
+ }
5606
+ }
5607
+ }
5608
+ function loadCustomLoopTemplates() {
5609
+ const dir = customLoopTemplatesDir();
5610
+ if (!existsSync3(dir))
5611
+ return [];
5612
+ const entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
5613
+ const file = join5(dir, entry.name);
5614
+ if (entry.isSymbolicLink())
5615
+ throw new Error(`refusing symlinked custom template file: ${file}`);
5616
+ if (!entry.isFile())
5617
+ throw new Error(`custom template registry entry is not a regular file: ${file}`);
5618
+ return readCustomTemplateFile(file);
5619
+ });
5620
+ assertNoTemplateCollisions(entries);
5621
+ return entries;
5622
+ }
5623
+ function getCustomLoopTemplate(id) {
5624
+ return loadCustomLoopTemplates().find((template) => template.definition.id === id || template.definition.name === id);
5625
+ }
5626
+ function coerceCustomTemplateValue(raw, type, label) {
5627
+ const normalizedType = type ?? "string";
5628
+ if (normalizedType === "string")
5629
+ return String(raw);
5630
+ if (normalizedType === "number") {
5631
+ const value = typeof raw === "number" ? raw : Number(String(raw));
5632
+ if (!Number.isFinite(value))
5633
+ throw new Error(`${label} must be a finite number`);
5634
+ return value;
5635
+ }
5636
+ if (normalizedType === "boolean") {
5637
+ if (typeof raw === "boolean")
5638
+ return raw;
5639
+ const normalized = String(raw).trim().toLowerCase();
5640
+ if (["1", "true", "yes", "on"].includes(normalized))
5641
+ return true;
5642
+ if (["0", "false", "no", "off"].includes(normalized))
5643
+ return false;
5644
+ throw new Error(`${label} must be a boolean`);
5645
+ }
5646
+ if (normalizedType === "json") {
5647
+ if (typeof raw !== "string")
5648
+ return raw;
5649
+ try {
5650
+ return JSON.parse(raw);
5651
+ } catch (error) {
5652
+ const message = error instanceof Error ? error.message : String(error);
5653
+ throw new Error(`${label} must be valid JSON: ${message}`);
5654
+ }
5655
+ }
5656
+ if (normalizedType === "string[]") {
5657
+ if (Array.isArray(raw))
5658
+ return raw.map((entry) => String(entry));
5659
+ return String(raw).split(",").map((entry) => entry.trim()).filter(Boolean);
5660
+ }
5661
+ return String(raw);
5662
+ }
5663
+ function customTemplateValues(definition, values) {
5664
+ const variablesByName = new Map(definition.variables.map((variable) => [variable.name, variable]));
5665
+ for (const [name, value] of Object.entries(values)) {
5666
+ if (value !== undefined && !variablesByName.has(name)) {
5667
+ throw new Error(`unknown variable for custom template ${definition.id}: ${name}`);
5668
+ }
5669
+ }
5670
+ const rendered = {};
5671
+ for (const variable of definition.variables) {
5672
+ const raw = values[variable.name] ?? variable.default;
5673
+ if (raw === undefined || raw === "") {
5674
+ if (variable.required)
5675
+ throw new Error(`${variable.name} is required`);
5676
+ continue;
5677
+ }
5678
+ rendered[variable.name] = coerceCustomTemplateValue(raw, variable.type, variable.name);
5679
+ }
5680
+ return rendered;
5681
+ }
5682
+ function customTemplateValueForPlaceholder(values, name, templateId) {
5683
+ if (!(name in values))
5684
+ throw new Error(`custom template ${templateId} requires variable ${name}`);
5685
+ return values[name];
5686
+ }
5687
+ function stringifyCustomTemplateValue(value, name) {
5688
+ if (typeof value === "string")
5689
+ return value;
5690
+ if (typeof value === "number" || typeof value === "boolean")
5691
+ return String(value);
5692
+ if (value === null)
5693
+ return "null";
5694
+ if (Array.isArray(value) || typeof value === "object")
5695
+ return JSON.stringify(value);
5696
+ throw new Error(`custom template variable ${name} cannot be rendered as a string`);
5697
+ }
5698
+ function renderCustomTemplateNode(value, values, templateId) {
5699
+ if (typeof value === "string") {
5700
+ const exact = CUSTOM_TEMPLATE_EXACT_PLACEHOLDER.exec(value);
5701
+ if (exact)
5702
+ return customTemplateValueForPlaceholder(values, exact[1], templateId);
5703
+ return value.replace(CUSTOM_TEMPLATE_PLACEHOLDER, (_match, name) => stringifyCustomTemplateValue(customTemplateValueForPlaceholder(values, name, templateId), name));
5704
+ }
5705
+ if (Array.isArray(value))
5706
+ return value.map((entry) => renderCustomTemplateNode(entry, values, templateId));
5707
+ if (!value || typeof value !== "object")
5708
+ return value;
5709
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, renderCustomTemplateNode(entry, values, templateId)]));
5710
+ }
5711
+ function renderCustomLoopTemplate(entry, values) {
5712
+ const renderedValues = customTemplateValues(entry.definition, values);
5713
+ const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
5714
+ assertNoDangerFullAccess(rendered, `custom template ${entry.definition.id}.workflow`);
5715
+ return workflowBodyFromJson(rendered);
5716
+ }
5717
+ function validateCustomLoopTemplateFile(file) {
5718
+ const entry = readCustomTemplateFile(resolve(file));
5719
+ assertNoTemplateCollisions([entry]);
5720
+ return structuredClone(entry.summary);
5721
+ }
5722
+ function importCustomLoopTemplate(file, opts = {}) {
5723
+ const source = resolve(file);
5724
+ const entry = readCustomTemplateFile(source);
5725
+ assertNoTemplateCollisions([entry]);
5726
+ const dir = ensureCustomLoopTemplatesDir();
5727
+ const destination = join5(dir, `${entry.definition.id}.json`);
5728
+ const replaced = existsSync3(destination);
5729
+ if (replaced) {
5730
+ const stat = lstatSync(destination);
5731
+ if (!stat.isFile() || stat.isSymbolicLink())
5732
+ throw new Error(`refusing to replace non-regular custom template file: ${destination}`);
5733
+ if (!opts.replace)
5734
+ throw new Error(`custom template already exists: ${entry.definition.id}; use --replace to overwrite it`);
5735
+ }
5736
+ writeFileSync2(destination, `${JSON.stringify(entry.definition, null, 2)}
5737
+ `, { mode: 384 });
5738
+ const imported = readCustomTemplateFile(destination);
5739
+ return { template: structuredClone(imported.summary), path: destination, replaced };
5740
+ }
5741
+ function validateLoopTemplateRegistry(opts = {}) {
5742
+ return {
5743
+ ok: true,
5744
+ templates: listLoopTemplates(opts),
5745
+ customDir: customLoopTemplatesDir()
5746
+ };
5747
+ }
5748
+ function listLoopTemplates(opts = {}) {
5749
+ const source = opts.source ?? "all";
5750
+ const templates = [];
5751
+ if (source !== "custom")
5752
+ templates.push(...builtinLoopTemplates());
5753
+ if (source !== "builtin")
5754
+ templates.push(...loadCustomLoopTemplates().map((entry) => structuredClone(entry.summary)));
5755
+ return templates;
5756
+ }
5757
+ function getLoopTemplate(id, opts = {}) {
5758
+ const source = opts.source ?? "all";
5759
+ if (source !== "custom") {
5760
+ const builtin = getBuiltinLoopTemplate(id);
5761
+ if (builtin)
5762
+ return builtin;
5763
+ if (source === "builtin")
5764
+ return;
5765
+ }
5766
+ return getCustomLoopTemplate(id)?.summary;
5366
5767
  }
5367
5768
  function renderTodosTaskWorkerVerifierWorkflow(input) {
5368
5769
  if (!input.taskId?.trim())
@@ -5707,7 +6108,7 @@ function renderDeterministicCheckCreateTaskWorkflow(values) {
5707
6108
  ]
5708
6109
  };
5709
6110
  }
5710
- function renderLoopTemplate(id, values) {
6111
+ function renderBuiltinLoopTemplate(id, values) {
5711
6112
  if (id === DETERMINISTIC_CHECK_CREATE_TASK_TEMPLATE_ID) {
5712
6113
  return renderDeterministicCheckCreateTaskWorkflow(values);
5713
6114
  }
@@ -5821,12 +6222,26 @@ function booleanVar(value) {
5821
6222
  function accountPoolVar(value, tool) {
5822
6223
  return listVar(value)?.map((profile) => ({ profile, tool }));
5823
6224
  }
6225
+ function renderLoopTemplate(id, values, opts = {}) {
6226
+ const source = opts.source ?? "all";
6227
+ if (source !== "custom") {
6228
+ const builtin = getBuiltinLoopTemplate(id);
6229
+ if (builtin)
6230
+ return renderBuiltinLoopTemplate(builtin.id, values);
6231
+ if (source === "builtin")
6232
+ throw new Error(`unknown built-in template: ${id}`);
6233
+ }
6234
+ const custom = getCustomLoopTemplate(id);
6235
+ if (custom)
6236
+ return renderCustomLoopTemplate(custom, values);
6237
+ throw new Error(`unknown template: ${id}`);
6238
+ }
5824
6239
  // src/lib/doctor.ts
5825
6240
  import { spawnSync as spawnSync3 } from "child_process";
5826
6241
  import { accessSync as accessSync2, constants as constants2 } from "fs";
5827
6242
 
5828
6243
  // src/daemon/control.ts
5829
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
6244
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5830
6245
  import { hostname } from "os";
5831
6246
  import { dirname as dirname2 } from "path";
5832
6247
 
@@ -5857,15 +6272,15 @@ function readPid(path = pidFilePath()) {
5857
6272
  if (!existsSync4(path))
5858
6273
  return;
5859
6274
  try {
5860
- const pid = Number(readFileSync(path, "utf8").trim());
6275
+ const pid = Number(readFileSync2(path, "utf8").trim());
5861
6276
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
5862
6277
  } catch {
5863
6278
  return;
5864
6279
  }
5865
6280
  }
5866
6281
  function writePid(pid = process.pid, path = pidFilePath()) {
5867
- mkdirSync4(dirname2(path), { recursive: true, mode: 448 });
5868
- writeFileSync2(path, String(pid));
6282
+ mkdirSync5(dirname2(path), { recursive: true, mode: 448 });
6283
+ writeFileSync3(path, String(pid));
5869
6284
  }
5870
6285
  function removePid(path = pidFilePath()) {
5871
6286
  rmSync2(path, { force: true });
@@ -94,8 +94,15 @@ export declare class Store {
94
94
  listWorkflows(opts?: {
95
95
  status?: WorkflowSpec["status"];
96
96
  limit?: number;
97
+ offset?: number;
97
98
  }): WorkflowSpec[];
99
+ countWorkflows(opts?: {
100
+ status?: WorkflowSpec["status"];
101
+ }): number;
98
102
  archiveWorkflow(idOrName: string): WorkflowSpec;
103
+ private generatedRouteArchiveContext;
104
+ private maybeArchiveGeneratedRouteWorkflow;
105
+ private maybeArchiveTerminalGeneratedRouteWorkflow;
99
106
  createWorkflowInvocation(input: CreateWorkflowInvocationInput): WorkflowInvocation;
100
107
  getWorkflowInvocation(id: string): WorkflowInvocation | undefined;
101
108
  listWorkflowInvocations(opts?: {