@hasna/loops 0.3.57 → 0.3.58

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/cli/index.js CHANGED
@@ -1260,12 +1260,21 @@ class Store {
1260
1260
  if (!row)
1261
1261
  throw new Error("daemon lease lost");
1262
1262
  }
1263
+ assertNoNestedWorkflowGoal(target, goal) {
1264
+ if (!goal || target.type !== "workflow")
1265
+ return;
1266
+ const workflow = this.getWorkflow(target.workflowId);
1267
+ if (workflow?.goal) {
1268
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1269
+ }
1270
+ }
1263
1271
  createLoop(input, from = new Date) {
1264
1272
  const now = nowIso();
1265
1273
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1266
1274
  name: "loop-target-validation",
1267
1275
  steps: [{ id: "target", target: input.target }]
1268
1276
  }).steps[0].target;
1277
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1269
1278
  const loop = {
1270
1279
  id: genId(),
1271
1280
  name: input.name,
@@ -1437,6 +1446,9 @@ class Store {
1437
1446
  if (this.hasRunningRun(current.id))
1438
1447
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1439
1448
  const workflow = this.requireWorkflow(workflowId);
1449
+ if (current.goal && workflow.goal) {
1450
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1451
+ }
1440
1452
  const target = { ...current.target, workflowId: workflow.id };
1441
1453
  if (opts.workflowTimeoutMs !== undefined)
1442
1454
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1475,6 +1487,9 @@ class Store {
1475
1487
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1476
1488
  if (this.hasRunningRun(current.id))
1477
1489
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1490
+ if (current.goal && normalized.goal) {
1491
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1492
+ }
1478
1493
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1479
1494
  const workflow = {
1480
1495
  id: genId(),
@@ -4733,6 +4748,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4733
4748
  const workflowWithoutGoal = { ...workflow, goal: undefined };
4734
4749
  return runGoal(store, workflow.goal, {
4735
4750
  ...opts,
4751
+ model: opts.goalModel,
4736
4752
  context: {
4737
4753
  loopId: opts.loop?.id,
4738
4754
  loopName: opts.loop?.name,
@@ -4825,6 +4841,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4825
4841
  if (step.goal) {
4826
4842
  result = await runGoal(store, step.goal, {
4827
4843
  ...opts,
4844
+ model: opts.goalModel,
4828
4845
  target: targetWithStepAccount(step),
4829
4846
  signal: controller.signal,
4830
4847
  context: {
@@ -4966,6 +4983,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4966
4983
  if (loop.goal) {
4967
4984
  return runGoal(store, loop.goal, {
4968
4985
  ...opts,
4986
+ model: opts.goalModel,
4969
4987
  target: loop.target,
4970
4988
  context: {
4971
4989
  loopId: loop.id,
@@ -4987,8 +5005,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4987
5005
  }
4988
5006
  }
4989
5007
  if (loop.goal) {
5008
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
4990
5009
  return runGoal(store, loop.goal, {
4991
5010
  ...opts,
5011
+ model: opts.goalModel,
4992
5012
  context: {
4993
5013
  loopId: loop.id,
4994
5014
  loopName: loop.name,
@@ -4997,7 +5017,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4997
5017
  workflowId: workflow.id,
4998
5018
  workflowName: workflow.name
4999
5019
  },
5000
- executeNode: async (node) => executeWorkflow(store, workflow, {
5020
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
5001
5021
  ...opts,
5002
5022
  loop,
5003
5023
  loopRun: run,
@@ -6477,7 +6497,7 @@ function buildScriptInventoryReport(store, opts = {}) {
6477
6497
  // package.json
6478
6498
  var package_default = {
6479
6499
  name: "@hasna/loops",
6480
- version: "0.3.57",
6500
+ version: "0.3.58",
6481
6501
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
6482
6502
  type: "module",
6483
6503
  main: "dist/index.js",
@@ -8617,6 +8637,21 @@ function workflowTimeoutMigrationName(workflow, timeoutMs) {
8617
8637
  const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8618
8638
  return `${workflow.name}-${policy}-${suffix}`;
8619
8639
  }
8640
+ function workflowWithoutGoalWrapper(workflow, opts = {}) {
8641
+ return {
8642
+ body: {
8643
+ name: opts.name ?? workflow.name,
8644
+ description: workflow.description,
8645
+ version: workflow.version,
8646
+ steps: workflow.steps
8647
+ },
8648
+ changed: workflow.goal !== undefined
8649
+ };
8650
+ }
8651
+ function workflowGoalWrapperMigrationName(workflow) {
8652
+ const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
8653
+ return `${workflow.name}-no-workflow-goal-${suffix}`;
8654
+ }
8620
8655
  function printTextOutput(value) {
8621
8656
  for (const line of textOutputBlocks(value, { indent: " " }))
8622
8657
  console.log(line);
@@ -11268,6 +11303,66 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
11268
11303
  store.close();
11269
11304
  }
11270
11305
  });
11306
+ workflows.command("migrate-goal-wrappers").description("append-only migrate active workflow loops away from workflow-level goal wrappers").option("--loop <idOrName>", "migrate only one loop instead of all active workflow loops").option("--apply", "create new workflow specs and retarget eligible loops").option("--archive-old", "archive old workflow specs after retargeting when no active loops still reference them").action((opts) => {
11307
+ const store = new Store;
11308
+ try {
11309
+ const candidateLoops = opts.loop ? [store.requireUniqueLoop(opts.loop)] : store.listLoops({ status: "active", limit: 1e4 }).filter((loop) => loop.target.type === "workflow");
11310
+ const rows = [];
11311
+ for (const loop of candidateLoops) {
11312
+ if (loop.archivedAt) {
11313
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is archived" });
11314
+ continue;
11315
+ }
11316
+ if (loop.target.type !== "workflow") {
11317
+ rows.push({ loop: publicLoop(loop), status: "skipped", reason: "loop is not a workflow loop" });
11318
+ continue;
11319
+ }
11320
+ const workflow = store.requireWorkflow(loop.target.workflowId);
11321
+ const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
11322
+ const migration = workflowWithoutGoalWrapper(workflow, { name: nextWorkflowName });
11323
+ if (!migration.changed) {
11324
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "skipped", reason: "workflow has no top-level goal wrapper" });
11325
+ continue;
11326
+ }
11327
+ if (store.hasRunningRun(loop.id)) {
11328
+ rows.push({ loop: publicLoop(loop), workflow: publicWorkflow(workflow), status: "blocked", reason: "loop has a running run; retry after it finishes" });
11329
+ continue;
11330
+ }
11331
+ if (!opts.apply) {
11332
+ rows.push({
11333
+ loop: publicLoop(loop),
11334
+ workflow: publicWorkflow(workflow),
11335
+ status: "would_migrate",
11336
+ nextWorkflowName,
11337
+ removedGoal: workflow.goal
11338
+ });
11339
+ continue;
11340
+ }
11341
+ const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
11342
+ workflowTimeoutMs: loop.target.timeoutMs,
11343
+ archiveOld: Boolean(opts.archiveOld)
11344
+ });
11345
+ rows.push({
11346
+ loop: publicLoop(migrated.loop),
11347
+ previousWorkflow: publicWorkflow(migrated.previousWorkflow),
11348
+ workflow: publicWorkflow(migrated.workflow),
11349
+ archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
11350
+ status: "migrated"
11351
+ });
11352
+ }
11353
+ const summary = {
11354
+ apply: Boolean(opts.apply),
11355
+ total: rows.length,
11356
+ migrated: rows.filter((row) => row.status === "migrated").length,
11357
+ wouldMigrate: rows.filter((row) => row.status === "would_migrate").length,
11358
+ blocked: rows.filter((row) => row.status === "blocked").length,
11359
+ skipped: rows.filter((row) => row.status === "skipped").length
11360
+ };
11361
+ print({ summary, rows }, opts.apply ? `migrated=${summary.migrated} blocked=${summary.blocked} skipped=${summary.skipped}` : `would_migrate=${summary.wouldMigrate} blocked=${summary.blocked} skipped=${summary.skipped}`);
11362
+ } finally {
11363
+ store.close();
11364
+ }
11365
+ });
11271
11366
  workflows.command("archive <idOrName>").action((idOrName) => {
11272
11367
  const store = new Store;
11273
11368
  try {
@@ -1260,12 +1260,21 @@ class Store {
1260
1260
  if (!row)
1261
1261
  throw new Error("daemon lease lost");
1262
1262
  }
1263
+ assertNoNestedWorkflowGoal(target, goal) {
1264
+ if (!goal || target.type !== "workflow")
1265
+ return;
1266
+ const workflow = this.getWorkflow(target.workflowId);
1267
+ if (workflow?.goal) {
1268
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1269
+ }
1270
+ }
1263
1271
  createLoop(input, from = new Date) {
1264
1272
  const now = nowIso();
1265
1273
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1266
1274
  name: "loop-target-validation",
1267
1275
  steps: [{ id: "target", target: input.target }]
1268
1276
  }).steps[0].target;
1277
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1269
1278
  const loop = {
1270
1279
  id: genId(),
1271
1280
  name: input.name,
@@ -1437,6 +1446,9 @@ class Store {
1437
1446
  if (this.hasRunningRun(current.id))
1438
1447
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1439
1448
  const workflow = this.requireWorkflow(workflowId);
1449
+ if (current.goal && workflow.goal) {
1450
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1451
+ }
1440
1452
  const target = { ...current.target, workflowId: workflow.id };
1441
1453
  if (opts.workflowTimeoutMs !== undefined)
1442
1454
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1475,6 +1487,9 @@ class Store {
1475
1487
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1476
1488
  if (this.hasRunningRun(current.id))
1477
1489
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1490
+ if (current.goal && normalized.goal) {
1491
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1492
+ }
1478
1493
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1479
1494
  const workflow = {
1480
1495
  id: genId(),
@@ -4617,6 +4632,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4617
4632
  const workflowWithoutGoal = { ...workflow, goal: undefined };
4618
4633
  return runGoal(store, workflow.goal, {
4619
4634
  ...opts,
4635
+ model: opts.goalModel,
4620
4636
  context: {
4621
4637
  loopId: opts.loop?.id,
4622
4638
  loopName: opts.loop?.name,
@@ -4709,6 +4725,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4709
4725
  if (step.goal) {
4710
4726
  result = await runGoal(store, step.goal, {
4711
4727
  ...opts,
4728
+ model: opts.goalModel,
4712
4729
  target: targetWithStepAccount(step),
4713
4730
  signal: controller.signal,
4714
4731
  context: {
@@ -4850,6 +4867,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4850
4867
  if (loop.goal) {
4851
4868
  return runGoal(store, loop.goal, {
4852
4869
  ...opts,
4870
+ model: opts.goalModel,
4853
4871
  target: loop.target,
4854
4872
  context: {
4855
4873
  loopId: loop.id,
@@ -4871,8 +4889,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4871
4889
  }
4872
4890
  }
4873
4891
  if (loop.goal) {
4892
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
4874
4893
  return runGoal(store, loop.goal, {
4875
4894
  ...opts,
4895
+ model: opts.goalModel,
4876
4896
  context: {
4877
4897
  loopId: loop.id,
4878
4898
  loopName: loop.name,
@@ -4881,7 +4901,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4881
4901
  workflowId: workflow.id,
4882
4902
  workflowName: workflow.name
4883
4903
  },
4884
- executeNode: async (node) => executeWorkflow(store, workflow, {
4904
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
4885
4905
  ...opts,
4886
4906
  loop,
4887
4907
  loopRun: run,
@@ -5611,7 +5631,7 @@ function enableStartup(result) {
5611
5631
  // package.json
5612
5632
  var package_default = {
5613
5633
  name: "@hasna/loops",
5614
- version: "0.3.57",
5634
+ version: "0.3.58",
5615
5635
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5616
5636
  type: "module",
5617
5637
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -1258,12 +1258,21 @@ class Store {
1258
1258
  if (!row)
1259
1259
  throw new Error("daemon lease lost");
1260
1260
  }
1261
+ assertNoNestedWorkflowGoal(target, goal) {
1262
+ if (!goal || target.type !== "workflow")
1263
+ return;
1264
+ const workflow = this.getWorkflow(target.workflowId);
1265
+ if (workflow?.goal) {
1266
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1267
+ }
1268
+ }
1261
1269
  createLoop(input, from = new Date) {
1262
1270
  const now = nowIso();
1263
1271
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1264
1272
  name: "loop-target-validation",
1265
1273
  steps: [{ id: "target", target: input.target }]
1266
1274
  }).steps[0].target;
1275
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1267
1276
  const loop = {
1268
1277
  id: genId(),
1269
1278
  name: input.name,
@@ -1435,6 +1444,9 @@ class Store {
1435
1444
  if (this.hasRunningRun(current.id))
1436
1445
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1437
1446
  const workflow = this.requireWorkflow(workflowId);
1447
+ if (current.goal && workflow.goal) {
1448
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1449
+ }
1438
1450
  const target = { ...current.target, workflowId: workflow.id };
1439
1451
  if (opts.workflowTimeoutMs !== undefined)
1440
1452
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1473,6 +1485,9 @@ class Store {
1473
1485
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1474
1486
  if (this.hasRunningRun(current.id))
1475
1487
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1488
+ if (current.goal && normalized.goal) {
1489
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1490
+ }
1476
1491
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1477
1492
  const workflow = {
1478
1493
  id: genId(),
@@ -4506,7 +4521,7 @@ function publicGoalRun(run) {
4506
4521
  // package.json
4507
4522
  var package_default = {
4508
4523
  name: "@hasna/loops",
4509
- version: "0.3.57",
4524
+ version: "0.3.58",
4510
4525
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4511
4526
  type: "module",
4512
4527
  main: "dist/index.js",
@@ -5047,6 +5062,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
5047
5062
  const workflowWithoutGoal = { ...workflow, goal: undefined };
5048
5063
  return runGoal(store, workflow.goal, {
5049
5064
  ...opts,
5065
+ model: opts.goalModel,
5050
5066
  context: {
5051
5067
  loopId: opts.loop?.id,
5052
5068
  loopName: opts.loop?.name,
@@ -5139,6 +5155,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
5139
5155
  if (step.goal) {
5140
5156
  result = await runGoal(store, step.goal, {
5141
5157
  ...opts,
5158
+ model: opts.goalModel,
5142
5159
  target: targetWithStepAccount(step),
5143
5160
  signal: controller.signal,
5144
5161
  context: {
@@ -5280,6 +5297,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5280
5297
  if (loop.goal) {
5281
5298
  return runGoal(store, loop.goal, {
5282
5299
  ...opts,
5300
+ model: opts.goalModel,
5283
5301
  target: loop.target,
5284
5302
  context: {
5285
5303
  loopId: loop.id,
@@ -5301,8 +5319,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5301
5319
  }
5302
5320
  }
5303
5321
  if (loop.goal) {
5322
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
5304
5323
  return runGoal(store, loop.goal, {
5305
5324
  ...opts,
5325
+ model: opts.goalModel,
5306
5326
  context: {
5307
5327
  loopId: loop.id,
5308
5328
  loopName: loop.name,
@@ -5311,7 +5331,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5311
5331
  workflowId: workflow.id,
5312
5332
  workflowName: workflow.name
5313
5333
  },
5314
- executeNode: async (node) => executeWorkflow(store, workflow, {
5334
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
5315
5335
  ...opts,
5316
5336
  loop,
5317
5337
  loopRun: run,
@@ -1,7 +1,9 @@
1
+ import type { LanguageModel } from "ai";
1
2
  import type { ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
2
3
  export interface ExecuteOptions extends PersistGuardOptions {
3
4
  maxOutputBytes?: number;
4
5
  env?: NodeJS.ProcessEnv;
6
+ goalModel?: LanguageModel;
5
7
  log?: (message: string) => void;
6
8
  signal?: AbortSignal;
7
9
  onSpawn?: (pid: number) => void;
@@ -71,6 +71,7 @@ export declare class Store {
71
71
  private addColumnIfMissing;
72
72
  private createWorkflowRunBackfillIndexes;
73
73
  private assertDaemonLeaseFence;
74
+ private assertNoNestedWorkflowGoal;
74
75
  createLoop(input: CreateLoopInput, from?: Date): Loop;
75
76
  getLoop(id: string): Loop | undefined;
76
77
  findLoopByName(name: string): Loop | undefined;
package/dist/lib/store.js CHANGED
@@ -1258,12 +1258,21 @@ class Store {
1258
1258
  if (!row)
1259
1259
  throw new Error("daemon lease lost");
1260
1260
  }
1261
+ assertNoNestedWorkflowGoal(target, goal) {
1262
+ if (!goal || target.type !== "workflow")
1263
+ return;
1264
+ const workflow = this.getWorkflow(target.workflowId);
1265
+ if (workflow?.goal) {
1266
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1267
+ }
1268
+ }
1261
1269
  createLoop(input, from = new Date) {
1262
1270
  const now = nowIso();
1263
1271
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1264
1272
  name: "loop-target-validation",
1265
1273
  steps: [{ id: "target", target: input.target }]
1266
1274
  }).steps[0].target;
1275
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1267
1276
  const loop = {
1268
1277
  id: genId(),
1269
1278
  name: input.name,
@@ -1435,6 +1444,9 @@ class Store {
1435
1444
  if (this.hasRunningRun(current.id))
1436
1445
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1437
1446
  const workflow = this.requireWorkflow(workflowId);
1447
+ if (current.goal && workflow.goal) {
1448
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1449
+ }
1438
1450
  const target = { ...current.target, workflowId: workflow.id };
1439
1451
  if (opts.workflowTimeoutMs !== undefined)
1440
1452
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1473,6 +1485,9 @@ class Store {
1473
1485
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1474
1486
  if (this.hasRunningRun(current.id))
1475
1487
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1488
+ if (current.goal && normalized.goal) {
1489
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1490
+ }
1476
1491
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1477
1492
  const workflow = {
1478
1493
  id: genId(),
package/dist/mcp/index.js CHANGED
@@ -1260,12 +1260,21 @@ class Store {
1260
1260
  if (!row)
1261
1261
  throw new Error("daemon lease lost");
1262
1262
  }
1263
+ assertNoNestedWorkflowGoal(target, goal) {
1264
+ if (!goal || target.type !== "workflow")
1265
+ return;
1266
+ const workflow = this.getWorkflow(target.workflowId);
1267
+ if (workflow?.goal) {
1268
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1269
+ }
1270
+ }
1263
1271
  createLoop(input, from = new Date) {
1264
1272
  const now = nowIso();
1265
1273
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1266
1274
  name: "loop-target-validation",
1267
1275
  steps: [{ id: "target", target: input.target }]
1268
1276
  }).steps[0].target;
1277
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1269
1278
  const loop = {
1270
1279
  id: genId(),
1271
1280
  name: input.name,
@@ -1437,6 +1446,9 @@ class Store {
1437
1446
  if (this.hasRunningRun(current.id))
1438
1447
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1439
1448
  const workflow = this.requireWorkflow(workflowId);
1449
+ if (current.goal && workflow.goal) {
1450
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1451
+ }
1440
1452
  const target = { ...current.target, workflowId: workflow.id };
1441
1453
  if (opts.workflowTimeoutMs !== undefined)
1442
1454
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1475,6 +1487,9 @@ class Store {
1475
1487
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1476
1488
  if (this.hasRunningRun(current.id))
1477
1489
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1490
+ if (current.goal && normalized.goal) {
1491
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1492
+ }
1478
1493
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1479
1494
  const workflow = {
1480
1495
  id: genId(),
@@ -4508,7 +4523,7 @@ function publicGoalRun(run) {
4508
4523
  // package.json
4509
4524
  var package_default = {
4510
4525
  name: "@hasna/loops",
4511
- version: "0.3.57",
4526
+ version: "0.3.58",
4512
4527
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
4513
4528
  type: "module",
4514
4529
  main: "dist/index.js",
@@ -5049,6 +5064,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
5049
5064
  const workflowWithoutGoal = { ...workflow, goal: undefined };
5050
5065
  return runGoal(store, workflow.goal, {
5051
5066
  ...opts,
5067
+ model: opts.goalModel,
5052
5068
  context: {
5053
5069
  loopId: opts.loop?.id,
5054
5070
  loopName: opts.loop?.name,
@@ -5141,6 +5157,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
5141
5157
  if (step.goal) {
5142
5158
  result = await runGoal(store, step.goal, {
5143
5159
  ...opts,
5160
+ model: opts.goalModel,
5144
5161
  target: targetWithStepAccount(step),
5145
5162
  signal: controller.signal,
5146
5163
  context: {
@@ -5282,6 +5299,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5282
5299
  if (loop.goal) {
5283
5300
  return runGoal(store, loop.goal, {
5284
5301
  ...opts,
5302
+ model: opts.goalModel,
5285
5303
  target: loop.target,
5286
5304
  context: {
5287
5305
  loopId: loop.id,
@@ -5303,8 +5321,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5303
5321
  }
5304
5322
  }
5305
5323
  if (loop.goal) {
5324
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
5306
5325
  return runGoal(store, loop.goal, {
5307
5326
  ...opts,
5327
+ model: opts.goalModel,
5308
5328
  context: {
5309
5329
  loopId: loop.id,
5310
5330
  loopName: loop.name,
@@ -5313,7 +5333,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
5313
5333
  workflowId: workflow.id,
5314
5334
  workflowName: workflow.name
5315
5335
  },
5316
- executeNode: async (node) => executeWorkflow(store, workflow, {
5336
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
5317
5337
  ...opts,
5318
5338
  loop,
5319
5339
  loopRun: run,
package/dist/sdk/index.js CHANGED
@@ -1258,12 +1258,21 @@ class Store {
1258
1258
  if (!row)
1259
1259
  throw new Error("daemon lease lost");
1260
1260
  }
1261
+ assertNoNestedWorkflowGoal(target, goal) {
1262
+ if (!goal || target.type !== "workflow")
1263
+ return;
1264
+ const workflow = this.getWorkflow(target.workflowId);
1265
+ if (workflow?.goal) {
1266
+ throw new Error(`workflow loop cannot define a loop-level goal when workflow ${workflow.name} already has a top-level goal; remove one goal wrapper`);
1267
+ }
1268
+ }
1261
1269
  createLoop(input, from = new Date) {
1262
1270
  const now = nowIso();
1263
1271
  const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
1264
1272
  name: "loop-target-validation",
1265
1273
  steps: [{ id: "target", target: input.target }]
1266
1274
  }).steps[0].target;
1275
+ this.assertNoNestedWorkflowGoal(target, input.goal);
1267
1276
  const loop = {
1268
1277
  id: genId(),
1269
1278
  name: input.name,
@@ -1435,6 +1444,9 @@ class Store {
1435
1444
  if (this.hasRunningRun(current.id))
1436
1445
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1437
1446
  const workflow = this.requireWorkflow(workflowId);
1447
+ if (current.goal && workflow.goal) {
1448
+ throw new Error(`workflow loop cannot retarget ${current.name} to workflow ${workflow.name} because both define top-level goals`);
1449
+ }
1438
1450
  const target = { ...current.target, workflowId: workflow.id };
1439
1451
  if (opts.workflowTimeoutMs !== undefined)
1440
1452
  target.timeoutMs = opts.workflowTimeoutMs;
@@ -1473,6 +1485,9 @@ class Store {
1473
1485
  throw new Error(`loop is not a workflow loop: ${idOrName}`);
1474
1486
  if (this.hasRunningRun(current.id))
1475
1487
  throw new Error(`refusing to retarget running loop: ${current.id}`);
1488
+ if (current.goal && normalized.goal) {
1489
+ throw new Error(`workflow loop cannot retarget ${current.name} to a workflow that also defines a top-level goal`);
1490
+ }
1476
1491
  const previousWorkflow = this.requireWorkflow(current.target.workflowId);
1477
1492
  const workflow = {
1478
1493
  id: genId(),
@@ -4607,6 +4622,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4607
4622
  const workflowWithoutGoal = { ...workflow, goal: undefined };
4608
4623
  return runGoal(store, workflow.goal, {
4609
4624
  ...opts,
4625
+ model: opts.goalModel,
4610
4626
  context: {
4611
4627
  loopId: opts.loop?.id,
4612
4628
  loopName: opts.loop?.name,
@@ -4699,6 +4715,7 @@ async function executeWorkflow(store, workflow, opts = {}) {
4699
4715
  if (step.goal) {
4700
4716
  result = await runGoal(store, step.goal, {
4701
4717
  ...opts,
4718
+ model: opts.goalModel,
4702
4719
  target: targetWithStepAccount(step),
4703
4720
  signal: controller.signal,
4704
4721
  context: {
@@ -4840,6 +4857,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4840
4857
  if (loop.goal) {
4841
4858
  return runGoal(store, loop.goal, {
4842
4859
  ...opts,
4860
+ model: opts.goalModel,
4843
4861
  target: loop.target,
4844
4862
  context: {
4845
4863
  loopId: loop.id,
@@ -4861,8 +4879,10 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4861
4879
  }
4862
4880
  }
4863
4881
  if (loop.goal) {
4882
+ const workflowForLoopGoal = workflow.goal ? { ...workflow, goal: undefined } : workflow;
4864
4883
  return runGoal(store, loop.goal, {
4865
4884
  ...opts,
4885
+ model: opts.goalModel,
4866
4886
  context: {
4867
4887
  loopId: loop.id,
4868
4888
  loopName: loop.name,
@@ -4871,7 +4891,7 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
4871
4891
  workflowId: workflow.id,
4872
4892
  workflowName: workflow.name
4873
4893
  },
4874
- executeNode: async (node) => executeWorkflow(store, workflow, {
4894
+ executeNode: async (node) => executeWorkflow(store, workflowForLoopGoal, {
4875
4895
  ...opts,
4876
4896
  loop,
4877
4897
  loopRun: run,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.57",
3
+ "version": "0.3.58",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",