@hasna/loops 0.3.53 → 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 +122 -45
- package/dist/daemon/index.js +59 -12
- package/dist/index.js +82 -20
- package/dist/lib/store.js +55 -8
- package/dist/lib/workflow-spec.d.ts +8 -2
- package/dist/sdk/index.js +55 -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),
|
|
@@ -4915,13 +4962,13 @@ async function tick(deps) {
|
|
|
4915
4962
|
}
|
|
4916
4963
|
|
|
4917
4964
|
// src/daemon/control.ts
|
|
4918
|
-
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";
|
|
4919
4966
|
import { hostname } from "os";
|
|
4920
4967
|
import { dirname as dirname2 } from "path";
|
|
4921
4968
|
|
|
4922
4969
|
// src/daemon/loop.ts
|
|
4923
4970
|
function realSleep(ms) {
|
|
4924
|
-
return new Promise((
|
|
4971
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4925
4972
|
}
|
|
4926
4973
|
async function runLoop(opts) {
|
|
4927
4974
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -4946,7 +4993,7 @@ function readPid(path = pidFilePath()) {
|
|
|
4946
4993
|
if (!existsSync3(path))
|
|
4947
4994
|
return;
|
|
4948
4995
|
try {
|
|
4949
|
-
const pid = Number(
|
|
4996
|
+
const pid = Number(readFileSync2(path, "utf8").trim());
|
|
4950
4997
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
4951
4998
|
} catch {
|
|
4952
4999
|
return;
|
|
@@ -5299,7 +5346,7 @@ function enableStartup(result) {
|
|
|
5299
5346
|
// package.json
|
|
5300
5347
|
var package_default = {
|
|
5301
5348
|
name: "@hasna/loops",
|
|
5302
|
-
version: "0.3.
|
|
5349
|
+
version: "0.3.54",
|
|
5303
5350
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5304
5351
|
type: "module",
|
|
5305
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),
|
|
@@ -5036,9 +5083,9 @@ function openAutomationsRuntimeBinding(overrides = {}) {
|
|
|
5036
5083
|
}
|
|
5037
5084
|
// src/lib/templates.ts
|
|
5038
5085
|
import { execFileSync } from "child_process";
|
|
5039
|
-
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";
|
|
5040
5087
|
import { homedir as homedir3 } from "os";
|
|
5041
|
-
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";
|
|
5042
5089
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
5043
5090
|
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
5044
5091
|
var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
|
|
@@ -5320,7 +5367,7 @@ function normalizeWorktreeMode(mode) {
|
|
|
5320
5367
|
function defaultWorktreeRoot(root) {
|
|
5321
5368
|
if (root?.trim()) {
|
|
5322
5369
|
const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
|
|
5323
|
-
return
|
|
5370
|
+
return isAbsolute2(expanded) ? expanded : resolve2(expanded);
|
|
5324
5371
|
}
|
|
5325
5372
|
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
5326
5373
|
}
|
|
@@ -5427,7 +5474,7 @@ function worktreePlan(input, seed) {
|
|
|
5427
5474
|
const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
|
|
5428
5475
|
const worktreePath = join5(root, repoSlug, seedSlug);
|
|
5429
5476
|
const relativeCwd = relative(repoRoot, originalCwd);
|
|
5430
|
-
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !
|
|
5477
|
+
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
|
|
5431
5478
|
const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
|
|
5432
5479
|
const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
|
|
5433
5480
|
const prepareStep = {
|
|
@@ -5662,9 +5709,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
|
|
|
5662
5709
|
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
5663
5710
|
}
|
|
5664
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
|
+
}
|
|
5665
5726
|
function assertCustomTemplateSafety(value, label) {
|
|
5666
5727
|
assertNoDangerousCustomTemplateScalars(value, label);
|
|
5667
5728
|
assertNoImplicitDangerFullAccess(value, label);
|
|
5729
|
+
assertNoCustomTemplatePromptFiles(value, label);
|
|
5668
5730
|
}
|
|
5669
5731
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
5670
5732
|
assertRecord(value, sourcePath);
|
|
@@ -5694,7 +5756,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
5694
5756
|
function readCustomTemplateFile(file) {
|
|
5695
5757
|
let parsed;
|
|
5696
5758
|
try {
|
|
5697
|
-
parsed = JSON.parse(
|
|
5759
|
+
parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
5698
5760
|
} catch (error) {
|
|
5699
5761
|
const message = error instanceof Error ? error.message : String(error);
|
|
5700
5762
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -5833,19 +5895,19 @@ function renderCustomLoopTemplate(entry, values) {
|
|
|
5833
5895
|
return workflow;
|
|
5834
5896
|
}
|
|
5835
5897
|
function validateCustomLoopTemplateFile(file) {
|
|
5836
|
-
const source =
|
|
5898
|
+
const source = resolve2(file);
|
|
5837
5899
|
const entry = readCustomTemplateFile(source);
|
|
5838
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
5900
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
|
|
5839
5901
|
assertNoTemplateCollisions([...existing, entry]);
|
|
5840
5902
|
return structuredClone(entry.summary);
|
|
5841
5903
|
}
|
|
5842
5904
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
5843
|
-
const source =
|
|
5905
|
+
const source = resolve2(file);
|
|
5844
5906
|
const entry = readCustomTemplateFile(source);
|
|
5845
5907
|
const dir = ensureCustomLoopTemplatesDir();
|
|
5846
5908
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
5847
5909
|
const replaced = existsSync3(destination);
|
|
5848
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
5910
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
5849
5911
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
5850
5912
|
if (replaced) {
|
|
5851
5913
|
const stat = lstatSync(destination);
|
|
@@ -6606,13 +6668,13 @@ import { spawnSync as spawnSync3 } from "child_process";
|
|
|
6606
6668
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
6607
6669
|
|
|
6608
6670
|
// src/daemon/control.ts
|
|
6609
|
-
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";
|
|
6610
6672
|
import { hostname } from "os";
|
|
6611
6673
|
import { dirname as dirname2 } from "path";
|
|
6612
6674
|
|
|
6613
6675
|
// src/daemon/loop.ts
|
|
6614
6676
|
function realSleep(ms) {
|
|
6615
|
-
return new Promise((
|
|
6677
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
6616
6678
|
}
|
|
6617
6679
|
async function runLoop(opts) {
|
|
6618
6680
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -6637,7 +6699,7 @@ function readPid(path = pidFilePath()) {
|
|
|
6637
6699
|
if (!existsSync4(path))
|
|
6638
6700
|
return;
|
|
6639
6701
|
try {
|
|
6640
|
-
const pid = Number(
|
|
6702
|
+
const pid = Number(readFileSync3(path, "utf8").trim());
|
|
6641
6703
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
6642
6704
|
} catch {
|
|
6643
6705
|
return;
|
package/dist/lib/store.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),
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { CreateWorkflowInput, GoalSpec, WorkflowSpec, WorkflowStep } from "../types.js";
|
|
2
2
|
export type WorkflowSpecBody = Pick<WorkflowSpec, "name" | "description" | "version" | "steps">;
|
|
3
|
+
export type NormalizedCreateWorkflowInput = Omit<CreateWorkflowInput, "steps"> & {
|
|
4
|
+
steps: WorkflowStep[];
|
|
5
|
+
};
|
|
6
|
+
export interface WorkflowNormalizeOptions {
|
|
7
|
+
baseDir?: string;
|
|
8
|
+
}
|
|
3
9
|
export declare function normalizeGoalSpec(value: unknown, label?: string): GoalSpec | undefined;
|
|
4
|
-
export declare function normalizeCreateWorkflowInput(input: CreateWorkflowInput):
|
|
10
|
+
export declare function normalizeCreateWorkflowInput(input: CreateWorkflowInput, opts?: WorkflowNormalizeOptions): NormalizedCreateWorkflowInput;
|
|
5
11
|
export declare function workflowExecutionOrder(workflow: Pick<WorkflowSpec, "steps">): WorkflowStep[];
|
|
6
|
-
export declare function workflowBodyFromJson(value: unknown, fallbackName?: string):
|
|
12
|
+
export declare function workflowBodyFromJson(value: unknown, fallbackName?: string, opts?: WorkflowNormalizeOptions): NormalizedCreateWorkflowInput;
|