@hasna/loops 0.3.58 → 0.3.59
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 +175 -46
- package/dist/daemon/index.js +65 -1
- package/dist/index.js +65 -1
- package/dist/lib/store.d.ts +10 -0
- package/dist/lib/store.js +64 -0
- package/dist/mcp/index.js +65 -1
- package/dist/sdk/index.js +64 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1543,6 +1543,70 @@ class Store {
|
|
|
1543
1543
|
throw error;
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1546
1610
|
renameLoop(id, name, opts = {}) {
|
|
1547
1611
|
const current = this.getLoop(id);
|
|
1548
1612
|
if (!current)
|
|
@@ -6497,7 +6561,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
6497
6561
|
// package.json
|
|
6498
6562
|
var package_default = {
|
|
6499
6563
|
name: "@hasna/loops",
|
|
6500
|
-
version: "0.3.
|
|
6564
|
+
version: "0.3.59",
|
|
6501
6565
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
6502
6566
|
type: "module",
|
|
6503
6567
|
main: "dist/index.js",
|
|
@@ -8637,21 +8701,47 @@ function workflowTimeoutMigrationName(workflow, timeoutMs) {
|
|
|
8637
8701
|
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8638
8702
|
return `${workflow.name}-${policy}-${suffix}`;
|
|
8639
8703
|
}
|
|
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
8704
|
function workflowGoalWrapperMigrationName(workflow) {
|
|
8652
8705
|
const suffix = randomUUID().replace(/-/g, "").slice(0, 8);
|
|
8653
8706
|
return `${workflow.name}-no-workflow-goal-${suffix}`;
|
|
8654
8707
|
}
|
|
8708
|
+
function publicMigrationGoalSummary(goal) {
|
|
8709
|
+
if (!goal)
|
|
8710
|
+
return;
|
|
8711
|
+
return {
|
|
8712
|
+
objective: redact(goal.objective),
|
|
8713
|
+
model: goal.model,
|
|
8714
|
+
tokenBudget: goal.tokenBudget,
|
|
8715
|
+
maxTurns: goal.maxTurns
|
|
8716
|
+
};
|
|
8717
|
+
}
|
|
8718
|
+
function publicMigrationWorkflowSummary(workflow) {
|
|
8719
|
+
return {
|
|
8720
|
+
id: workflow.id,
|
|
8721
|
+
name: workflow.name,
|
|
8722
|
+
version: workflow.version,
|
|
8723
|
+
status: workflow.status,
|
|
8724
|
+
stepCount: workflow.steps.length,
|
|
8725
|
+
hasGoal: Boolean(workflow.goal),
|
|
8726
|
+
goal: publicMigrationGoalSummary(workflow.goal)
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
function publicMigrationLoopSummary(loop) {
|
|
8730
|
+
return {
|
|
8731
|
+
id: loop.id,
|
|
8732
|
+
name: loop.name,
|
|
8733
|
+
status: loop.status,
|
|
8734
|
+
archivedAt: loop.archivedAt,
|
|
8735
|
+
nextRunAt: loop.nextRunAt,
|
|
8736
|
+
hasGoal: Boolean(loop.goal),
|
|
8737
|
+
goal: publicMigrationGoalSummary(loop.goal),
|
|
8738
|
+
target: loop.target.type === "workflow" ? { type: "workflow", workflowId: loop.target.workflowId, timeoutMs: loop.target.timeoutMs } : { type: loop.target.type }
|
|
8739
|
+
};
|
|
8740
|
+
}
|
|
8741
|
+
function migrationErrorReason(error) {
|
|
8742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8743
|
+
return redact(message, 240);
|
|
8744
|
+
}
|
|
8655
8745
|
function printTextOutput(value) {
|
|
8656
8746
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
8657
8747
|
console.log(line);
|
|
@@ -11275,19 +11365,30 @@ workflows.command("migrate-agent-timeouts").description("append-only migrate act
|
|
|
11275
11365
|
});
|
|
11276
11366
|
continue;
|
|
11277
11367
|
}
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11368
|
+
try {
|
|
11369
|
+
const migrated = store.createAndRetargetWorkflowLoop(loop.id, migration.body, {
|
|
11370
|
+
workflowTimeoutMs: timeoutMs,
|
|
11371
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
11372
|
+
});
|
|
11373
|
+
rows.push({
|
|
11374
|
+
loop: publicLoop(migrated.loop),
|
|
11375
|
+
previousWorkflow: publicWorkflow(migrated.previousWorkflow),
|
|
11376
|
+
workflow: publicWorkflow(migrated.workflow),
|
|
11377
|
+
archivedOld: migrated.archivedOld ? publicWorkflow(migrated.archivedOld) : undefined,
|
|
11378
|
+
status: "migrated",
|
|
11379
|
+
agentStepIds: migration.agentStepIds,
|
|
11380
|
+
timeoutMs
|
|
11381
|
+
});
|
|
11382
|
+
} catch (error) {
|
|
11383
|
+
rows.push({
|
|
11384
|
+
loop: publicLoop(loop),
|
|
11385
|
+
workflow: publicWorkflow(workflow),
|
|
11386
|
+
status: "blocked",
|
|
11387
|
+
reason: migrationErrorReason(error),
|
|
11388
|
+
agentStepIds: migration.agentStepIds,
|
|
11389
|
+
timeoutMs
|
|
11390
|
+
});
|
|
11391
|
+
}
|
|
11291
11392
|
}
|
|
11292
11393
|
const summary = {
|
|
11293
11394
|
apply: Boolean(opts.apply),
|
|
@@ -11310,45 +11411,73 @@ workflows.command("migrate-goal-wrappers").description("append-only migrate acti
|
|
|
11310
11411
|
const rows = [];
|
|
11311
11412
|
for (const loop of candidateLoops) {
|
|
11312
11413
|
if (loop.archivedAt) {
|
|
11313
|
-
rows.push({ loop:
|
|
11414
|
+
rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is archived" });
|
|
11314
11415
|
continue;
|
|
11315
11416
|
}
|
|
11316
11417
|
if (loop.target.type !== "workflow") {
|
|
11317
|
-
rows.push({ loop:
|
|
11418
|
+
rows.push({ loop: publicMigrationLoopSummary(loop), status: "skipped", reason: "loop is not a workflow loop" });
|
|
11318
11419
|
continue;
|
|
11319
11420
|
}
|
|
11320
11421
|
const workflow = store.requireWorkflow(loop.target.workflowId);
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11422
|
+
if (!loop.goal) {
|
|
11423
|
+
rows.push({
|
|
11424
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11425
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11426
|
+
status: "skipped",
|
|
11427
|
+
reason: "loop has no loop-level goal wrapper"
|
|
11428
|
+
});
|
|
11325
11429
|
continue;
|
|
11326
11430
|
}
|
|
11431
|
+
if (!workflow.goal) {
|
|
11432
|
+
rows.push({
|
|
11433
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11434
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11435
|
+
status: "skipped",
|
|
11436
|
+
reason: "workflow has no top-level goal wrapper"
|
|
11437
|
+
});
|
|
11438
|
+
continue;
|
|
11439
|
+
}
|
|
11440
|
+
const nextWorkflowName = workflowGoalWrapperMigrationName(workflow);
|
|
11327
11441
|
if (store.hasRunningRun(loop.id)) {
|
|
11328
|
-
rows.push({
|
|
11442
|
+
rows.push({
|
|
11443
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11444
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11445
|
+
status: "blocked",
|
|
11446
|
+
reason: "loop has a running run; retry after it finishes"
|
|
11447
|
+
});
|
|
11329
11448
|
continue;
|
|
11330
11449
|
}
|
|
11331
11450
|
if (!opts.apply) {
|
|
11332
11451
|
rows.push({
|
|
11333
|
-
loop:
|
|
11334
|
-
workflow:
|
|
11452
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11453
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11335
11454
|
status: "would_migrate",
|
|
11336
11455
|
nextWorkflowName,
|
|
11337
|
-
removedGoal: workflow.goal
|
|
11456
|
+
removedGoal: publicMigrationGoalSummary(workflow.goal)
|
|
11338
11457
|
});
|
|
11339
11458
|
continue;
|
|
11340
11459
|
}
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11460
|
+
try {
|
|
11461
|
+
const migrated = store.cloneWorkflowWithoutGoalAndRetargetLoop(loop.id, {
|
|
11462
|
+
workflowName: nextWorkflowName,
|
|
11463
|
+
workflowTimeoutMs: loop.target.timeoutMs,
|
|
11464
|
+
archiveOld: Boolean(opts.archiveOld)
|
|
11465
|
+
});
|
|
11466
|
+
rows.push({
|
|
11467
|
+
loop: publicMigrationLoopSummary(migrated.loop),
|
|
11468
|
+
previousWorkflow: publicMigrationWorkflowSummary(migrated.previousWorkflow),
|
|
11469
|
+
workflow: publicMigrationWorkflowSummary(migrated.workflow),
|
|
11470
|
+
archivedOld: migrated.archivedOld ? publicMigrationWorkflowSummary(migrated.archivedOld) : undefined,
|
|
11471
|
+
status: "migrated"
|
|
11472
|
+
});
|
|
11473
|
+
} catch (error) {
|
|
11474
|
+
rows.push({
|
|
11475
|
+
loop: publicMigrationLoopSummary(loop),
|
|
11476
|
+
workflow: publicMigrationWorkflowSummary(workflow),
|
|
11477
|
+
status: "blocked",
|
|
11478
|
+
reason: migrationErrorReason(error)
|
|
11479
|
+
});
|
|
11480
|
+
}
|
|
11352
11481
|
}
|
|
11353
11482
|
const summary = {
|
|
11354
11483
|
apply: Boolean(opts.apply),
|
package/dist/daemon/index.js
CHANGED
|
@@ -1543,6 +1543,70 @@ class Store {
|
|
|
1543
1543
|
throw error;
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1546
1610
|
renameLoop(id, name, opts = {}) {
|
|
1547
1611
|
const current = this.getLoop(id);
|
|
1548
1612
|
if (!current)
|
|
@@ -5631,7 +5695,7 @@ function enableStartup(result) {
|
|
|
5631
5695
|
// package.json
|
|
5632
5696
|
var package_default = {
|
|
5633
5697
|
name: "@hasna/loops",
|
|
5634
|
-
version: "0.3.
|
|
5698
|
+
version: "0.3.59",
|
|
5635
5699
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5636
5700
|
type: "module",
|
|
5637
5701
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -1541,6 +1541,70 @@ class Store {
|
|
|
1541
1541
|
throw error;
|
|
1542
1542
|
}
|
|
1543
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1544
1608
|
renameLoop(id, name, opts = {}) {
|
|
1545
1609
|
const current = this.getLoop(id);
|
|
1546
1610
|
if (!current)
|
|
@@ -4521,7 +4585,7 @@ function publicGoalRun(run) {
|
|
|
4521
4585
|
// package.json
|
|
4522
4586
|
var package_default = {
|
|
4523
4587
|
name: "@hasna/loops",
|
|
4524
|
-
version: "0.3.
|
|
4588
|
+
version: "0.3.59",
|
|
4525
4589
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4526
4590
|
type: "module",
|
|
4527
4591
|
main: "dist/index.js",
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -99,6 +99,16 @@ export declare class Store {
|
|
|
99
99
|
previousWorkflow: WorkflowSpec;
|
|
100
100
|
archivedOld?: WorkflowSpec;
|
|
101
101
|
};
|
|
102
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName: string, opts: DaemonLeaseFence & {
|
|
103
|
+
workflowName: string;
|
|
104
|
+
workflowTimeoutMs?: TimeoutMs;
|
|
105
|
+
archiveOld?: boolean;
|
|
106
|
+
}): {
|
|
107
|
+
loop: Loop;
|
|
108
|
+
workflow: WorkflowSpec;
|
|
109
|
+
previousWorkflow: WorkflowSpec;
|
|
110
|
+
archivedOld?: WorkflowSpec;
|
|
111
|
+
};
|
|
102
112
|
renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop;
|
|
103
113
|
archiveLoop(idOrName: string): Loop;
|
|
104
114
|
unarchiveLoop(idOrName: string): Loop;
|
package/dist/lib/store.js
CHANGED
|
@@ -1541,6 +1541,70 @@ class Store {
|
|
|
1541
1541
|
throw error;
|
|
1542
1542
|
}
|
|
1543
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1544
1608
|
renameLoop(id, name, opts = {}) {
|
|
1545
1609
|
const current = this.getLoop(id);
|
|
1546
1610
|
if (!current)
|
package/dist/mcp/index.js
CHANGED
|
@@ -1543,6 +1543,70 @@ class Store {
|
|
|
1543
1543
|
throw error;
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1547
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1548
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1549
|
+
try {
|
|
1550
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1551
|
+
if (current.target.type !== "workflow")
|
|
1552
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1553
|
+
if (this.hasRunningRun(current.id))
|
|
1554
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1555
|
+
if (!current.goal)
|
|
1556
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1557
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1558
|
+
if (!previousWorkflow.goal)
|
|
1559
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1560
|
+
const workflow = {
|
|
1561
|
+
id: genId(),
|
|
1562
|
+
name: opts.workflowName,
|
|
1563
|
+
description: previousWorkflow.description,
|
|
1564
|
+
version: previousWorkflow.version,
|
|
1565
|
+
status: "active",
|
|
1566
|
+
steps: previousWorkflow.steps,
|
|
1567
|
+
createdAt: updated,
|
|
1568
|
+
updatedAt: updated
|
|
1569
|
+
};
|
|
1570
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1571
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1572
|
+
$id: workflow.id,
|
|
1573
|
+
$name: workflow.name,
|
|
1574
|
+
$description: workflow.description ?? null,
|
|
1575
|
+
$version: workflow.version,
|
|
1576
|
+
$status: workflow.status,
|
|
1577
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1578
|
+
$created: workflow.createdAt,
|
|
1579
|
+
$updated: workflow.updatedAt
|
|
1580
|
+
});
|
|
1581
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1582
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1583
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1584
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1585
|
+
WHERE id=$id
|
|
1586
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1587
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1588
|
+
))`).run({
|
|
1589
|
+
$id: current.id,
|
|
1590
|
+
$target: JSON.stringify(target),
|
|
1591
|
+
$updated: updated,
|
|
1592
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1593
|
+
$now: updated
|
|
1594
|
+
});
|
|
1595
|
+
if (res.changes !== 1)
|
|
1596
|
+
throw new Error("daemon lease lost");
|
|
1597
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1598
|
+
this.db.exec("COMMIT");
|
|
1599
|
+
const loop = this.getLoop(current.id);
|
|
1600
|
+
if (!loop)
|
|
1601
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1602
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.db.exec("ROLLBACK");
|
|
1606
|
+
} catch {}
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1546
1610
|
renameLoop(id, name, opts = {}) {
|
|
1547
1611
|
const current = this.getLoop(id);
|
|
1548
1612
|
if (!current)
|
|
@@ -4523,7 +4587,7 @@ function publicGoalRun(run) {
|
|
|
4523
4587
|
// package.json
|
|
4524
4588
|
var package_default = {
|
|
4525
4589
|
name: "@hasna/loops",
|
|
4526
|
-
version: "0.3.
|
|
4590
|
+
version: "0.3.59",
|
|
4527
4591
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4528
4592
|
type: "module",
|
|
4529
4593
|
main: "dist/index.js",
|
package/dist/sdk/index.js
CHANGED
|
@@ -1541,6 +1541,70 @@ class Store {
|
|
|
1541
1541
|
throw error;
|
|
1542
1542
|
}
|
|
1543
1543
|
}
|
|
1544
|
+
cloneWorkflowWithoutGoalAndRetargetLoop(idOrName, opts) {
|
|
1545
|
+
const updated = (opts.now ?? new Date).toISOString();
|
|
1546
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
1547
|
+
try {
|
|
1548
|
+
const current = this.requireUniqueLoop(idOrName);
|
|
1549
|
+
if (current.target.type !== "workflow")
|
|
1550
|
+
throw new Error(`loop is not a workflow loop: ${idOrName}`);
|
|
1551
|
+
if (this.hasRunningRun(current.id))
|
|
1552
|
+
throw new Error(`refusing to retarget running loop: ${current.id}`);
|
|
1553
|
+
if (!current.goal)
|
|
1554
|
+
throw new Error(`workflow loop ${current.name} has no loop-level goal wrapper`);
|
|
1555
|
+
const previousWorkflow = this.requireWorkflow(current.target.workflowId);
|
|
1556
|
+
if (!previousWorkflow.goal)
|
|
1557
|
+
throw new Error(`workflow ${previousWorkflow.name} has no top-level goal wrapper`);
|
|
1558
|
+
const workflow = {
|
|
1559
|
+
id: genId(),
|
|
1560
|
+
name: opts.workflowName,
|
|
1561
|
+
description: previousWorkflow.description,
|
|
1562
|
+
version: previousWorkflow.version,
|
|
1563
|
+
status: "active",
|
|
1564
|
+
steps: previousWorkflow.steps,
|
|
1565
|
+
createdAt: updated,
|
|
1566
|
+
updatedAt: updated
|
|
1567
|
+
};
|
|
1568
|
+
this.db.query(`INSERT INTO workflow_specs (id, name, description, version, status, goal_json, steps_json, created_at, updated_at)
|
|
1569
|
+
VALUES ($id, $name, $description, $version, $status, NULL, $steps, $created, $updated)`).run({
|
|
1570
|
+
$id: workflow.id,
|
|
1571
|
+
$name: workflow.name,
|
|
1572
|
+
$description: workflow.description ?? null,
|
|
1573
|
+
$version: workflow.version,
|
|
1574
|
+
$status: workflow.status,
|
|
1575
|
+
$steps: JSON.stringify(workflow.steps),
|
|
1576
|
+
$created: workflow.createdAt,
|
|
1577
|
+
$updated: workflow.updatedAt
|
|
1578
|
+
});
|
|
1579
|
+
const target = { ...current.target, workflowId: workflow.id };
|
|
1580
|
+
if (opts.workflowTimeoutMs !== undefined)
|
|
1581
|
+
target.timeoutMs = opts.workflowTimeoutMs;
|
|
1582
|
+
const res = this.db.query(`UPDATE loops SET target_json=$target, updated_at=$updated
|
|
1583
|
+
WHERE id=$id
|
|
1584
|
+
AND ($daemonLeaseId IS NULL OR EXISTS (
|
|
1585
|
+
SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
|
|
1586
|
+
))`).run({
|
|
1587
|
+
$id: current.id,
|
|
1588
|
+
$target: JSON.stringify(target),
|
|
1589
|
+
$updated: updated,
|
|
1590
|
+
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
1591
|
+
$now: updated
|
|
1592
|
+
});
|
|
1593
|
+
if (res.changes !== 1)
|
|
1594
|
+
throw new Error("daemon lease lost");
|
|
1595
|
+
const archivedOld = opts.archiveOld ? this.archiveWorkflowIfUnreferenced(previousWorkflow.id, updated) : undefined;
|
|
1596
|
+
this.db.exec("COMMIT");
|
|
1597
|
+
const loop = this.getLoop(current.id);
|
|
1598
|
+
if (!loop)
|
|
1599
|
+
throw new Error(`loop not found after retarget: ${current.id}`);
|
|
1600
|
+
return { loop, workflow, previousWorkflow, archivedOld };
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
try {
|
|
1603
|
+
this.db.exec("ROLLBACK");
|
|
1604
|
+
} catch {}
|
|
1605
|
+
throw error;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1544
1608
|
renameLoop(id, name, opts = {}) {
|
|
1545
1609
|
const current = this.getLoop(id);
|
|
1546
1610
|
if (!current)
|
package/package.json
CHANGED