@hasna/loops 0.3.52 → 0.3.54

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.
@@ -314,6 +314,8 @@ function updateReadyFlags(nodes, planStatus) {
314
314
  }
315
315
 
316
316
  // src/lib/workflow-spec.ts
317
+ import { readFileSync } from "fs";
318
+ import { isAbsolute, resolve } from "path";
317
319
  function assertObject(value, label) {
318
320
  if (!value || typeof value !== "object" || Array.isArray(value))
319
321
  throw new Error(`${label} must be an object`);
@@ -359,6 +361,38 @@ function optionalAccountRef(value, label) {
359
361
  tool: typeof value.tool === "string" ? value.tool.trim() : undefined
360
362
  };
361
363
  }
364
+ function promptFilePath(rawPath, opts) {
365
+ return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
366
+ }
367
+ function readPromptFile(rawPath, label, opts) {
368
+ assertString(rawPath, `${label}.promptFile`);
369
+ const path = promptFilePath(rawPath.trim(), opts);
370
+ let prompt;
371
+ try {
372
+ prompt = readFileSync(path, "utf8");
373
+ } catch (error) {
374
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
375
+ throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
376
+ }
377
+ if (prompt.trim() === "")
378
+ throw new Error(`${label}.promptFile must contain a non-empty prompt`);
379
+ return { prompt, promptSource: { type: "file", path } };
380
+ }
381
+ function matchingPromptSource(value, prompt, opts) {
382
+ if (!value || typeof value !== "object" || Array.isArray(value))
383
+ return;
384
+ if (value.type !== "file")
385
+ return;
386
+ const rawPath = value.path;
387
+ if (typeof rawPath !== "string" || rawPath.trim() === "")
388
+ return;
389
+ const path = promptFilePath(rawPath.trim(), opts);
390
+ try {
391
+ return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
392
+ } catch {
393
+ return;
394
+ }
395
+ }
362
396
  function normalizeGoalSpec(value, label = "goal") {
363
397
  if (value === undefined)
364
398
  return;
@@ -381,7 +415,7 @@ function normalizeGoalSpec(value, label = "goal") {
381
415
  autoExecute
382
416
  };
383
417
  }
384
- function validateTarget(value, label) {
418
+ function validateTarget(value, label, opts) {
385
419
  assertObject(value, label);
386
420
  if (value.type === "command") {
387
421
  assertString(value.command, `${label}.command`);
@@ -394,7 +428,13 @@ function validateTarget(value, label) {
394
428
  }
395
429
  if (value.type === "agent") {
396
430
  assertString(value.provider, `${label}.provider`);
397
- assertString(value.prompt, `${label}.prompt`);
431
+ const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
432
+ const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
433
+ if (hasPrompt && hasPromptFile)
434
+ throw new Error(`${label} must use either prompt or promptFile, not both`);
435
+ if (!hasPrompt && !hasPromptFile)
436
+ throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
437
+ const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
398
438
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
399
439
  if (!providers.includes(value.provider))
400
440
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
@@ -495,11 +535,14 @@ function validateTarget(value, label) {
495
535
  if (value.routing.eventSource !== undefined)
496
536
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
497
537
  }
498
- return value;
538
+ const target = { ...value };
539
+ delete target.promptFile;
540
+ delete target.promptSource;
541
+ return { ...target, ...promptFields };
499
542
  }
500
543
  throw new Error(`${label}.type must be command or agent`);
501
544
  }
502
- function normalizeCreateWorkflowInput(input) {
545
+ function normalizeCreateWorkflowInput(input, opts = {}) {
503
546
  assertString(input.name, "workflow.name");
504
547
  const goal = normalizeGoalSpec(input.goal, "goal");
505
548
  if (!Array.isArray(input.steps) || input.steps.length === 0)
@@ -515,7 +558,7 @@ function normalizeCreateWorkflowInput(input) {
515
558
  ...step,
516
559
  id: step.id,
517
560
  goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
518
- target: validateTarget(step.target, `workflow.steps[${index}].target`),
561
+ target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
519
562
  dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
520
563
  continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
521
564
  timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
@@ -558,7 +601,7 @@ function workflowExecutionOrder(workflow) {
558
601
  visit(step);
559
602
  return order;
560
603
  }
561
- function workflowBodyFromJson(value, fallbackName) {
604
+ function workflowBodyFromJson(value, fallbackName, opts = {}) {
562
605
  assertObject(value, "workflow file");
563
606
  const rawName = fallbackName ?? value.name;
564
607
  assertString(rawName, "workflow.name");
@@ -570,7 +613,7 @@ function workflowBodyFromJson(value, fallbackName) {
570
613
  goal: normalizeGoalSpec(value.goal, "goal"),
571
614
  version: typeof value.version === "number" ? value.version : undefined,
572
615
  steps: value.steps
573
- });
616
+ }, opts);
574
617
  }
575
618
 
576
619
  // src/lib/run-artifacts.ts
@@ -1207,13 +1250,17 @@ class Store {
1207
1250
  }
1208
1251
  createLoop(input, from = new Date) {
1209
1252
  const now = nowIso();
1253
+ const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1254
+ name: "loop-target-validation",
1255
+ steps: [{ id: "target", target: input.target }]
1256
+ }).steps[0].target;
1210
1257
  const loop = {
1211
1258
  id: genId(),
1212
1259
  name: input.name,
1213
1260
  description: input.description,
1214
1261
  status: "active",
1215
1262
  schedule: input.schedule,
1216
- target: input.target,
1263
+ target,
1217
1264
  goal: input.goal,
1218
1265
  machine: input.machine,
1219
1266
  nextRunAt: initialNextRun(input.schedule, from),
@@ -1567,6 +1614,65 @@ class Store {
1567
1614
  throw new Error(`workflow invocation not found after create: ${id}`);
1568
1615
  return rowToWorkflowInvocation(row);
1569
1616
  }
1617
+ refreshWorkflowInvocationForWorkItem(workItemId, input) {
1618
+ const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
1619
+ if (!sourceDedupeKey)
1620
+ throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1621
+ const now = nowIso();
1622
+ const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1623
+ const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1624
+ const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1625
+ const result = this.db.query(`UPDATE workflow_invocations
1626
+ SET workflow_id=COALESCE($workflowId, workflow_id),
1627
+ template_id=COALESCE($templateId, template_id),
1628
+ source_id=COALESCE($sourceId, source_id),
1629
+ source_json=$sourceJson,
1630
+ subject_kind=$subjectKind,
1631
+ subject_id=COALESCE($subjectId, subject_id),
1632
+ subject_path=COALESCE($subjectPath, subject_path),
1633
+ subject_url=COALESCE($subjectUrl, subject_url),
1634
+ subject_json=$subjectJson,
1635
+ intent=$intent,
1636
+ scope_json=COALESCE($scopeJson, scope_json),
1637
+ output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
1638
+ updated_at=$updated
1639
+ WHERE source_kind=$sourceKind
1640
+ AND source_dedupe_key=$sourceDedupeKey
1641
+ AND EXISTS (
1642
+ SELECT 1
1643
+ FROM workflow_work_items
1644
+ WHERE id=$workItemId
1645
+ AND invocation_id=workflow_invocations.id
1646
+ AND status IN (${placeholders})
1647
+ )`).run({
1648
+ $workItemId: workItemId,
1649
+ $sourceKind: input.sourceRef.kind,
1650
+ $sourceDedupeKey: sourceDedupeKey,
1651
+ $workflowId: input.workflowId ?? null,
1652
+ $templateId: input.templateId ?? null,
1653
+ $sourceId: input.sourceRef.id ?? null,
1654
+ $sourceJson: JSON.stringify(input.sourceRef),
1655
+ $subjectKind: input.subjectRef.kind,
1656
+ $subjectId: input.subjectRef.id ?? null,
1657
+ $subjectPath: input.subjectRef.path ?? null,
1658
+ $subjectUrl: input.subjectRef.url ?? null,
1659
+ $subjectJson: JSON.stringify(input.subjectRef),
1660
+ $intent: input.intent,
1661
+ $scopeJson: input.scope ? JSON.stringify(input.scope) : null,
1662
+ $outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
1663
+ $updated: now,
1664
+ ...statusBindings
1665
+ });
1666
+ if (result.changes !== 1)
1667
+ throw new Error(`workflow work item is not refreshable: ${workItemId}`);
1668
+ const updated = this.db.query(`SELECT workflow_invocations.*
1669
+ FROM workflow_invocations
1670
+ JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
1671
+ WHERE workflow_work_items.id = ?`).get(workItemId);
1672
+ if (!updated)
1673
+ throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
1674
+ return rowToWorkflowInvocation(updated);
1675
+ }
1570
1676
  getWorkflowInvocation(id) {
1571
1677
  const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1572
1678
  return row ? rowToWorkflowInvocation(row) : undefined;
@@ -4856,13 +4962,13 @@ async function tick(deps) {
4856
4962
  }
4857
4963
 
4858
4964
  // src/daemon/control.ts
4859
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4965
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4860
4966
  import { hostname } from "os";
4861
4967
  import { dirname as dirname2 } from "path";
4862
4968
 
4863
4969
  // src/daemon/loop.ts
4864
4970
  function realSleep(ms) {
4865
- return new Promise((resolve) => setTimeout(resolve, ms));
4971
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4866
4972
  }
4867
4973
  async function runLoop(opts) {
4868
4974
  const sleep = opts.sleep ?? realSleep;
@@ -4887,7 +4993,7 @@ function readPid(path = pidFilePath()) {
4887
4993
  if (!existsSync3(path))
4888
4994
  return;
4889
4995
  try {
4890
- const pid = Number(readFileSync(path, "utf8").trim());
4996
+ const pid = Number(readFileSync2(path, "utf8").trim());
4891
4997
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
4892
4998
  } catch {
4893
4999
  return;
@@ -5240,7 +5346,7 @@ function enableStartup(result) {
5240
5346
  // package.json
5241
5347
  var package_default = {
5242
5348
  name: "@hasna/loops",
5243
- version: "0.3.52",
5349
+ version: "0.3.54",
5244
5350
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5245
5351
  type: "module",
5246
5352
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -312,6 +312,8 @@ function updateReadyFlags(nodes, planStatus) {
312
312
  }
313
313
 
314
314
  // src/lib/workflow-spec.ts
315
+ import { readFileSync } from "fs";
316
+ import { isAbsolute, resolve } from "path";
315
317
  function assertObject(value, label) {
316
318
  if (!value || typeof value !== "object" || Array.isArray(value))
317
319
  throw new Error(`${label} must be an object`);
@@ -357,6 +359,38 @@ function optionalAccountRef(value, label) {
357
359
  tool: typeof value.tool === "string" ? value.tool.trim() : undefined
358
360
  };
359
361
  }
362
+ function promptFilePath(rawPath, opts) {
363
+ return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
364
+ }
365
+ function readPromptFile(rawPath, label, opts) {
366
+ assertString(rawPath, `${label}.promptFile`);
367
+ const path = promptFilePath(rawPath.trim(), opts);
368
+ let prompt;
369
+ try {
370
+ prompt = readFileSync(path, "utf8");
371
+ } catch (error) {
372
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
373
+ throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
374
+ }
375
+ if (prompt.trim() === "")
376
+ throw new Error(`${label}.promptFile must contain a non-empty prompt`);
377
+ return { prompt, promptSource: { type: "file", path } };
378
+ }
379
+ function matchingPromptSource(value, prompt, opts) {
380
+ if (!value || typeof value !== "object" || Array.isArray(value))
381
+ return;
382
+ if (value.type !== "file")
383
+ return;
384
+ const rawPath = value.path;
385
+ if (typeof rawPath !== "string" || rawPath.trim() === "")
386
+ return;
387
+ const path = promptFilePath(rawPath.trim(), opts);
388
+ try {
389
+ return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
390
+ } catch {
391
+ return;
392
+ }
393
+ }
360
394
  function normalizeGoalSpec(value, label = "goal") {
361
395
  if (value === undefined)
362
396
  return;
@@ -379,7 +413,7 @@ function normalizeGoalSpec(value, label = "goal") {
379
413
  autoExecute
380
414
  };
381
415
  }
382
- function validateTarget(value, label) {
416
+ function validateTarget(value, label, opts) {
383
417
  assertObject(value, label);
384
418
  if (value.type === "command") {
385
419
  assertString(value.command, `${label}.command`);
@@ -392,7 +426,13 @@ function validateTarget(value, label) {
392
426
  }
393
427
  if (value.type === "agent") {
394
428
  assertString(value.provider, `${label}.provider`);
395
- assertString(value.prompt, `${label}.prompt`);
429
+ const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
430
+ const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
431
+ if (hasPrompt && hasPromptFile)
432
+ throw new Error(`${label} must use either prompt or promptFile, not both`);
433
+ if (!hasPrompt && !hasPromptFile)
434
+ throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
435
+ const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
396
436
  const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
397
437
  if (!providers.includes(value.provider))
398
438
  throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
@@ -493,11 +533,14 @@ function validateTarget(value, label) {
493
533
  if (value.routing.eventSource !== undefined)
494
534
  assertString(value.routing.eventSource, `${label}.routing.eventSource`);
495
535
  }
496
- return value;
536
+ const target = { ...value };
537
+ delete target.promptFile;
538
+ delete target.promptSource;
539
+ return { ...target, ...promptFields };
497
540
  }
498
541
  throw new Error(`${label}.type must be command or agent`);
499
542
  }
500
- function normalizeCreateWorkflowInput(input) {
543
+ function normalizeCreateWorkflowInput(input, opts = {}) {
501
544
  assertString(input.name, "workflow.name");
502
545
  const goal = normalizeGoalSpec(input.goal, "goal");
503
546
  if (!Array.isArray(input.steps) || input.steps.length === 0)
@@ -513,7 +556,7 @@ function normalizeCreateWorkflowInput(input) {
513
556
  ...step,
514
557
  id: step.id,
515
558
  goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
516
- target: validateTarget(step.target, `workflow.steps[${index}].target`),
559
+ target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
517
560
  dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
518
561
  continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
519
562
  timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
@@ -556,7 +599,7 @@ function workflowExecutionOrder(workflow) {
556
599
  visit(step);
557
600
  return order;
558
601
  }
559
- function workflowBodyFromJson(value, fallbackName) {
602
+ function workflowBodyFromJson(value, fallbackName, opts = {}) {
560
603
  assertObject(value, "workflow file");
561
604
  const rawName = fallbackName ?? value.name;
562
605
  assertString(rawName, "workflow.name");
@@ -568,7 +611,7 @@ function workflowBodyFromJson(value, fallbackName) {
568
611
  goal: normalizeGoalSpec(value.goal, "goal"),
569
612
  version: typeof value.version === "number" ? value.version : undefined,
570
613
  steps: value.steps
571
- });
614
+ }, opts);
572
615
  }
573
616
 
574
617
  // src/lib/run-artifacts.ts
@@ -1205,13 +1248,17 @@ class Store {
1205
1248
  }
1206
1249
  createLoop(input, from = new Date) {
1207
1250
  const now = nowIso();
1251
+ const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1252
+ name: "loop-target-validation",
1253
+ steps: [{ id: "target", target: input.target }]
1254
+ }).steps[0].target;
1208
1255
  const loop = {
1209
1256
  id: genId(),
1210
1257
  name: input.name,
1211
1258
  description: input.description,
1212
1259
  status: "active",
1213
1260
  schedule: input.schedule,
1214
- target: input.target,
1261
+ target,
1215
1262
  goal: input.goal,
1216
1263
  machine: input.machine,
1217
1264
  nextRunAt: initialNextRun(input.schedule, from),
@@ -1565,6 +1612,65 @@ class Store {
1565
1612
  throw new Error(`workflow invocation not found after create: ${id}`);
1566
1613
  return rowToWorkflowInvocation(row);
1567
1614
  }
1615
+ refreshWorkflowInvocationForWorkItem(workItemId, input) {
1616
+ const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
1617
+ if (!sourceDedupeKey)
1618
+ throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1619
+ const now = nowIso();
1620
+ const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1621
+ const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1622
+ const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1623
+ const result = this.db.query(`UPDATE workflow_invocations
1624
+ SET workflow_id=COALESCE($workflowId, workflow_id),
1625
+ template_id=COALESCE($templateId, template_id),
1626
+ source_id=COALESCE($sourceId, source_id),
1627
+ source_json=$sourceJson,
1628
+ subject_kind=$subjectKind,
1629
+ subject_id=COALESCE($subjectId, subject_id),
1630
+ subject_path=COALESCE($subjectPath, subject_path),
1631
+ subject_url=COALESCE($subjectUrl, subject_url),
1632
+ subject_json=$subjectJson,
1633
+ intent=$intent,
1634
+ scope_json=COALESCE($scopeJson, scope_json),
1635
+ output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
1636
+ updated_at=$updated
1637
+ WHERE source_kind=$sourceKind
1638
+ AND source_dedupe_key=$sourceDedupeKey
1639
+ AND EXISTS (
1640
+ SELECT 1
1641
+ FROM workflow_work_items
1642
+ WHERE id=$workItemId
1643
+ AND invocation_id=workflow_invocations.id
1644
+ AND status IN (${placeholders})
1645
+ )`).run({
1646
+ $workItemId: workItemId,
1647
+ $sourceKind: input.sourceRef.kind,
1648
+ $sourceDedupeKey: sourceDedupeKey,
1649
+ $workflowId: input.workflowId ?? null,
1650
+ $templateId: input.templateId ?? null,
1651
+ $sourceId: input.sourceRef.id ?? null,
1652
+ $sourceJson: JSON.stringify(input.sourceRef),
1653
+ $subjectKind: input.subjectRef.kind,
1654
+ $subjectId: input.subjectRef.id ?? null,
1655
+ $subjectPath: input.subjectRef.path ?? null,
1656
+ $subjectUrl: input.subjectRef.url ?? null,
1657
+ $subjectJson: JSON.stringify(input.subjectRef),
1658
+ $intent: input.intent,
1659
+ $scopeJson: input.scope ? JSON.stringify(input.scope) : null,
1660
+ $outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
1661
+ $updated: now,
1662
+ ...statusBindings
1663
+ });
1664
+ if (result.changes !== 1)
1665
+ throw new Error(`workflow work item is not refreshable: ${workItemId}`);
1666
+ const updated = this.db.query(`SELECT workflow_invocations.*
1667
+ FROM workflow_invocations
1668
+ JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
1669
+ WHERE workflow_work_items.id = ?`).get(workItemId);
1670
+ if (!updated)
1671
+ throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
1672
+ return rowToWorkflowInvocation(updated);
1673
+ }
1568
1674
  getWorkflowInvocation(id) {
1569
1675
  const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1570
1676
  return row ? rowToWorkflowInvocation(row) : undefined;
@@ -4977,9 +5083,9 @@ function openAutomationsRuntimeBinding(overrides = {}) {
4977
5083
  }
4978
5084
  // src/lib/templates.ts
4979
5085
  import { execFileSync } from "child_process";
4980
- import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
5086
+ import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4981
5087
  import { homedir as homedir3 } from "os";
4982
- import { basename as basename2, isAbsolute, join as join5, relative, resolve } from "path";
5088
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
4983
5089
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
4984
5090
  var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
4985
5091
  var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
@@ -5261,7 +5367,7 @@ function normalizeWorktreeMode(mode) {
5261
5367
  function defaultWorktreeRoot(root) {
5262
5368
  if (root?.trim()) {
5263
5369
  const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
5264
- return isAbsolute(expanded) ? expanded : resolve(expanded);
5370
+ return isAbsolute2(expanded) ? expanded : resolve2(expanded);
5265
5371
  }
5266
5372
  return join5(homedir3(), ".hasna", "loops", "worktrees");
5267
5373
  }
@@ -5368,7 +5474,7 @@ function worktreePlan(input, seed) {
5368
5474
  const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
5369
5475
  const worktreePath = join5(root, repoSlug, seedSlug);
5370
5476
  const relativeCwd = relative(repoRoot, originalCwd);
5371
- const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
5477
+ const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
5372
5478
  const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
5373
5479
  const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
5374
5480
  const prepareStep = {
@@ -5603,9 +5709,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
5603
5709
  assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
5604
5710
  }
5605
5711
  }
5712
+ function assertNoCustomTemplatePromptFiles(value, label) {
5713
+ if (!value || typeof value !== "object")
5714
+ return;
5715
+ if (Array.isArray(value)) {
5716
+ value.forEach((entry, index) => assertNoCustomTemplatePromptFiles(entry, `${label}[${index}]`));
5717
+ return;
5718
+ }
5719
+ for (const [key, entry] of Object.entries(value)) {
5720
+ if (key === "promptFile") {
5721
+ throw new Error(`${label}.${key} is not allowed in custom templates; use direct workflow JSON for prompt-file-backed workflows`);
5722
+ }
5723
+ assertNoCustomTemplatePromptFiles(entry, `${label}.${key}`);
5724
+ }
5725
+ }
5606
5726
  function assertCustomTemplateSafety(value, label) {
5607
5727
  assertNoDangerousCustomTemplateScalars(value, label);
5608
5728
  assertNoImplicitDangerFullAccess(value, label);
5729
+ assertNoCustomTemplatePromptFiles(value, label);
5609
5730
  }
5610
5731
  function customTemplateDefinitionFromJson(value, sourcePath) {
5611
5732
  assertRecord(value, sourcePath);
@@ -5635,7 +5756,7 @@ function customTemplateSummary(definition, sourcePath) {
5635
5756
  function readCustomTemplateFile(file) {
5636
5757
  let parsed;
5637
5758
  try {
5638
- parsed = JSON.parse(readFileSync(file, "utf8"));
5759
+ parsed = JSON.parse(readFileSync2(file, "utf8"));
5639
5760
  } catch (error) {
5640
5761
  const message = error instanceof Error ? error.message : String(error);
5641
5762
  throw new Error(`failed to read custom template ${file}: ${message}`);
@@ -5774,19 +5895,19 @@ function renderCustomLoopTemplate(entry, values) {
5774
5895
  return workflow;
5775
5896
  }
5776
5897
  function validateCustomLoopTemplateFile(file) {
5777
- const source = resolve(file);
5898
+ const source = resolve2(file);
5778
5899
  const entry = readCustomTemplateFile(source);
5779
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
5900
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
5780
5901
  assertNoTemplateCollisions([...existing, entry]);
5781
5902
  return structuredClone(entry.summary);
5782
5903
  }
5783
5904
  function importCustomLoopTemplate(file, opts = {}) {
5784
- const source = resolve(file);
5905
+ const source = resolve2(file);
5785
5906
  const entry = readCustomTemplateFile(source);
5786
5907
  const dir = ensureCustomLoopTemplatesDir();
5787
5908
  const destination = join5(dir, `${entry.definition.id}.json`);
5788
5909
  const replaced = existsSync3(destination);
5789
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
5910
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
5790
5911
  assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
5791
5912
  if (replaced) {
5792
5913
  const stat = lstatSync(destination);
@@ -6547,13 +6668,13 @@ import { spawnSync as spawnSync3 } from "child_process";
6547
6668
  import { accessSync as accessSync2, constants as constants2 } from "fs";
6548
6669
 
6549
6670
  // src/daemon/control.ts
6550
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6671
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6551
6672
  import { hostname } from "os";
6552
6673
  import { dirname as dirname2 } from "path";
6553
6674
 
6554
6675
  // src/daemon/loop.ts
6555
6676
  function realSleep(ms) {
6556
- return new Promise((resolve2) => setTimeout(resolve2, ms));
6677
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
6557
6678
  }
6558
6679
  async function runLoop(opts) {
6559
6680
  const sleep = opts.sleep ?? realSleep;
@@ -6578,7 +6699,7 @@ function readPid(path = pidFilePath()) {
6578
6699
  if (!existsSync4(path))
6579
6700
  return;
6580
6701
  try {
6581
- const pid = Number(readFileSync2(path, "utf8").trim());
6702
+ const pid = Number(readFileSync3(path, "utf8").trim());
6582
6703
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
6583
6704
  } catch {
6584
6705
  return;
@@ -104,6 +104,7 @@ export declare class Store {
104
104
  private maybeArchiveGeneratedRouteWorkflow;
105
105
  private maybeArchiveTerminalGeneratedRouteWorkflow;
106
106
  createWorkflowInvocation(input: CreateWorkflowInvocationInput): WorkflowInvocation;
107
+ refreshWorkflowInvocationForWorkItem(workItemId: string, input: CreateWorkflowInvocationInput): WorkflowInvocation;
107
108
  getWorkflowInvocation(id: string): WorkflowInvocation | undefined;
108
109
  listWorkflowInvocations(opts?: {
109
110
  limit?: number;