@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.
- package/README.md +45 -0
- package/dist/cli/index.js +189 -51
- package/dist/daemon/index.js +118 -12
- package/dist/index.js +141 -20
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +114 -8
- package/dist/lib/workflow-spec.d.ts +8 -2
- package/dist/sdk/index.js +114 -8
- package/dist/types.d.ts +19 -4
- package/docs/USAGE.md +50 -0
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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((
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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("..") && !
|
|
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(
|
|
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 =
|
|
5898
|
+
const source = resolve2(file);
|
|
5778
5899
|
const entry = readCustomTemplateFile(source);
|
|
5779
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
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 =
|
|
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) =>
|
|
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
|
|
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((
|
|
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(
|
|
6702
|
+
const pid = Number(readFileSync3(path, "utf8").trim());
|
|
6582
6703
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
6583
6704
|
} catch {
|
|
6584
6705
|
return;
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -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;
|