@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/README.md
CHANGED
|
@@ -101,6 +101,51 @@ accounts tools add codewith --label "Codewith" --env-var CODEWITH_HOME --bin cod
|
|
|
101
101
|
accounts tools add aicopilot --label "AI Copilot" --env-var AICOPILOT_CONFIG_DIR --bin aicopilot
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## Prompt Files
|
|
105
|
+
|
|
106
|
+
Use prompt files for production agent prompts instead of long inline strings.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
mkdir -p ~/.hasna/loops/prompts
|
|
110
|
+
$EDITOR ~/.hasna/loops/prompts/repo-morning-check.md
|
|
111
|
+
|
|
112
|
+
loops create agent morning-check \
|
|
113
|
+
--provider codewith \
|
|
114
|
+
--auth-profile account001 \
|
|
115
|
+
--cron "0 8 * * *" \
|
|
116
|
+
--cwd /path/to/repo \
|
|
117
|
+
--prompt-file ~/.hasna/loops/prompts/repo-morning-check.md
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Workflow JSON also supports `promptFile` on agent targets. Relative paths
|
|
121
|
+
resolve from the workflow JSON file's directory:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"name": "repo-morning",
|
|
126
|
+
"steps": [
|
|
127
|
+
{
|
|
128
|
+
"id": "review",
|
|
129
|
+
"target": {
|
|
130
|
+
"type": "agent",
|
|
131
|
+
"provider": "codewith",
|
|
132
|
+
"cwd": "/path/to/repo",
|
|
133
|
+
"promptFile": "prompts/repo-morning-review.md"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
OpenLoops records `promptSource` metadata and redacts prompt bodies in public
|
|
141
|
+
CLI output by default, including `templates render`. Use
|
|
142
|
+
`~/.hasna/loops/prompts/<stable-name>.md` as the default prompt store for
|
|
143
|
+
production loops.
|
|
144
|
+
|
|
145
|
+
Reusable custom templates cannot contain `promptFile`. Keep prompt files in
|
|
146
|
+
direct workflow JSON or agent loop creation; use template variables only for
|
|
147
|
+
non-secret routing/configuration data.
|
|
148
|
+
|
|
104
149
|
## Goals
|
|
105
150
|
|
|
106
151
|
Add `--goal` to wrap a command, agent, or workflow loop in an AI-SDK orchestration layer. OpenLoops asks the configured model to create a flat DAG plan, executes ready nodes by calling the underlying target, then runs an adversarial achievement audit before marking the goal complete.
|
package/dist/cli/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),
|
|
@@ -2914,9 +2961,9 @@ class Store {
|
|
|
2914
2961
|
|
|
2915
2962
|
// src/cli/index.ts
|
|
2916
2963
|
import { createHash as createHash3, randomUUID } from "crypto";
|
|
2917
|
-
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as
|
|
2964
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync4, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2918
2965
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
2919
|
-
import { join as join6, resolve as
|
|
2966
|
+
import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
|
|
2920
2967
|
import { tmpdir as tmpdir2 } from "os";
|
|
2921
2968
|
import { Database as Database2 } from "bun:sqlite";
|
|
2922
2969
|
import { Command } from "commander";
|
|
@@ -5031,13 +5078,13 @@ async function tick(deps) {
|
|
|
5031
5078
|
}
|
|
5032
5079
|
|
|
5033
5080
|
// src/daemon/control.ts
|
|
5034
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5081
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5035
5082
|
import { hostname } from "os";
|
|
5036
5083
|
import { dirname as dirname2 } from "path";
|
|
5037
5084
|
|
|
5038
5085
|
// src/daemon/loop.ts
|
|
5039
5086
|
function realSleep(ms) {
|
|
5040
|
-
return new Promise((
|
|
5087
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
5041
5088
|
}
|
|
5042
5089
|
async function runLoop(opts) {
|
|
5043
5090
|
const sleep = opts.sleep ?? realSleep;
|
|
@@ -5062,7 +5109,7 @@ function readPid(path = pidFilePath()) {
|
|
|
5062
5109
|
if (!existsSync3(path))
|
|
5063
5110
|
return;
|
|
5064
5111
|
try {
|
|
5065
|
-
const pid = Number(
|
|
5112
|
+
const pid = Number(readFileSync2(path, "utf8").trim());
|
|
5066
5113
|
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
5067
5114
|
} catch {
|
|
5068
5115
|
return;
|
|
@@ -5967,7 +6014,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5967
6014
|
// package.json
|
|
5968
6015
|
var package_default = {
|
|
5969
6016
|
name: "@hasna/loops",
|
|
5970
|
-
version: "0.3.
|
|
6017
|
+
version: "0.3.54",
|
|
5971
6018
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5972
6019
|
type: "module",
|
|
5973
6020
|
main: "dist/index.js",
|
|
@@ -6057,9 +6104,9 @@ function packageVersion() {
|
|
|
6057
6104
|
|
|
6058
6105
|
// src/lib/templates.ts
|
|
6059
6106
|
import { execFileSync } from "child_process";
|
|
6060
|
-
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as
|
|
6107
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6061
6108
|
import { homedir as homedir3 } from "os";
|
|
6062
|
-
import { basename as basename3, isAbsolute, join as join5, relative, resolve } from "path";
|
|
6109
|
+
import { basename as basename3, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve2 } from "path";
|
|
6063
6110
|
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
6064
6111
|
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
6065
6112
|
var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
|
|
@@ -6341,7 +6388,7 @@ function normalizeWorktreeMode(mode) {
|
|
|
6341
6388
|
function defaultWorktreeRoot(root) {
|
|
6342
6389
|
if (root?.trim()) {
|
|
6343
6390
|
const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
|
|
6344
|
-
return
|
|
6391
|
+
return isAbsolute2(expanded) ? expanded : resolve2(expanded);
|
|
6345
6392
|
}
|
|
6346
6393
|
return join5(homedir3(), ".hasna", "loops", "worktrees");
|
|
6347
6394
|
}
|
|
@@ -6448,7 +6495,7 @@ function worktreePlan(input, seed) {
|
|
|
6448
6495
|
const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
|
|
6449
6496
|
const worktreePath = join5(root, repoSlug, seedSlug);
|
|
6450
6497
|
const relativeCwd = relative(repoRoot, originalCwd);
|
|
6451
|
-
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !
|
|
6498
|
+
const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
|
|
6452
6499
|
const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
|
|
6453
6500
|
const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
|
|
6454
6501
|
const prepareStep = {
|
|
@@ -6683,9 +6730,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
|
|
|
6683
6730
|
assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
|
|
6684
6731
|
}
|
|
6685
6732
|
}
|
|
6733
|
+
function assertNoCustomTemplatePromptFiles(value, label) {
|
|
6734
|
+
if (!value || typeof value !== "object")
|
|
6735
|
+
return;
|
|
6736
|
+
if (Array.isArray(value)) {
|
|
6737
|
+
value.forEach((entry, index) => assertNoCustomTemplatePromptFiles(entry, `${label}[${index}]`));
|
|
6738
|
+
return;
|
|
6739
|
+
}
|
|
6740
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
6741
|
+
if (key === "promptFile") {
|
|
6742
|
+
throw new Error(`${label}.${key} is not allowed in custom templates; use direct workflow JSON for prompt-file-backed workflows`);
|
|
6743
|
+
}
|
|
6744
|
+
assertNoCustomTemplatePromptFiles(entry, `${label}.${key}`);
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6686
6747
|
function assertCustomTemplateSafety(value, label) {
|
|
6687
6748
|
assertNoDangerousCustomTemplateScalars(value, label);
|
|
6688
6749
|
assertNoImplicitDangerFullAccess(value, label);
|
|
6750
|
+
assertNoCustomTemplatePromptFiles(value, label);
|
|
6689
6751
|
}
|
|
6690
6752
|
function customTemplateDefinitionFromJson(value, sourcePath) {
|
|
6691
6753
|
assertRecord(value, sourcePath);
|
|
@@ -6715,7 +6777,7 @@ function customTemplateSummary(definition, sourcePath) {
|
|
|
6715
6777
|
function readCustomTemplateFile(file) {
|
|
6716
6778
|
let parsed;
|
|
6717
6779
|
try {
|
|
6718
|
-
parsed = JSON.parse(
|
|
6780
|
+
parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
6719
6781
|
} catch (error) {
|
|
6720
6782
|
const message = error instanceof Error ? error.message : String(error);
|
|
6721
6783
|
throw new Error(`failed to read custom template ${file}: ${message}`);
|
|
@@ -6854,19 +6916,19 @@ function renderCustomLoopTemplate(entry, values) {
|
|
|
6854
6916
|
return workflow;
|
|
6855
6917
|
}
|
|
6856
6918
|
function validateCustomLoopTemplateFile(file) {
|
|
6857
|
-
const source =
|
|
6919
|
+
const source = resolve2(file);
|
|
6858
6920
|
const entry = readCustomTemplateFile(source);
|
|
6859
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
6921
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
|
|
6860
6922
|
assertNoTemplateCollisions([...existing, entry]);
|
|
6861
6923
|
return structuredClone(entry.summary);
|
|
6862
6924
|
}
|
|
6863
6925
|
function importCustomLoopTemplate(file, opts = {}) {
|
|
6864
|
-
const source =
|
|
6926
|
+
const source = resolve2(file);
|
|
6865
6927
|
const entry = readCustomTemplateFile(source);
|
|
6866
6928
|
const dir = ensureCustomLoopTemplatesDir();
|
|
6867
6929
|
const destination = join5(dir, `${entry.definition.id}.json`);
|
|
6868
6930
|
const replaced = existsSync4(destination);
|
|
6869
|
-
const existing = loadCustomLoopTemplatesRaw().filter((template) =>
|
|
6931
|
+
const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
|
|
6870
6932
|
assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
|
|
6871
6933
|
if (replaced) {
|
|
6872
6934
|
const stat = lstatSync(destination);
|
|
@@ -7672,12 +7734,13 @@ function validationFailed(error, context) {
|
|
|
7672
7734
|
});
|
|
7673
7735
|
process.exit(1);
|
|
7674
7736
|
}
|
|
7675
|
-
function
|
|
7737
|
+
function normalizeLoopTargetForStorage(target, context, opts = {}) {
|
|
7676
7738
|
try {
|
|
7677
|
-
workflowBodyFromJson({
|
|
7739
|
+
const workflow = workflowBodyFromJson({
|
|
7678
7740
|
name: "loop-target-validation",
|
|
7679
7741
|
steps: [{ id: "target", target }]
|
|
7680
|
-
});
|
|
7742
|
+
}, undefined, opts);
|
|
7743
|
+
return workflow.steps[0].target;
|
|
7681
7744
|
} catch (error) {
|
|
7682
7745
|
validationFailed(error, context);
|
|
7683
7746
|
}
|
|
@@ -7689,6 +7752,14 @@ function normalizeWorkflowForStorage(body, context) {
|
|
|
7689
7752
|
validationFailed(error, context);
|
|
7690
7753
|
}
|
|
7691
7754
|
}
|
|
7755
|
+
function workflowBodyFromFile(file, fallbackName, context) {
|
|
7756
|
+
try {
|
|
7757
|
+
const resolved = resolve3(file);
|
|
7758
|
+
return workflowBodyFromJson(JSON.parse(readFileSync4(resolved, "utf8")), fallbackName, { baseDir: dirname4(resolved) });
|
|
7759
|
+
} catch (error) {
|
|
7760
|
+
validationFailed(error, context);
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7692
7763
|
function preflightLoopTarget(target, context, metadata, opts) {
|
|
7693
7764
|
try {
|
|
7694
7765
|
return preflightTarget(target, metadata, opts);
|
|
@@ -7717,6 +7788,11 @@ function workflowSpecForPreflight(body, id = "validation") {
|
|
|
7717
7788
|
updatedAt: now
|
|
7718
7789
|
};
|
|
7719
7790
|
}
|
|
7791
|
+
function publicWorkflowBody(body) {
|
|
7792
|
+
const value = publicWorkflow(workflowSpecForPreflight(body, "render"));
|
|
7793
|
+
const { id: _id, status: _status, createdAt: _createdAt, updatedAt: _updatedAt, ...bodyOnly } = value;
|
|
7794
|
+
return bodyOnly;
|
|
7795
|
+
}
|
|
7720
7796
|
function printTextOutput(value) {
|
|
7721
7797
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
7722
7798
|
console.log(line);
|
|
@@ -7913,7 +7989,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
|
|
|
7913
7989
|
return {
|
|
7914
7990
|
ok: result.status === 0,
|
|
7915
7991
|
status: result.status,
|
|
7916
|
-
stdout:
|
|
7992
|
+
stdout: readFileSync4(stdoutPath, "utf8"),
|
|
7917
7993
|
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
|
|
7918
7994
|
error: result.error ? String(result.error.message || result.error) : ""
|
|
7919
7995
|
};
|
|
@@ -7957,7 +8033,7 @@ function readRouteCursors() {
|
|
|
7957
8033
|
if (!existsSync5(path))
|
|
7958
8034
|
return {};
|
|
7959
8035
|
try {
|
|
7960
|
-
const parsed = JSON.parse(
|
|
8036
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
7961
8037
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
7962
8038
|
} catch {
|
|
7963
8039
|
return {};
|
|
@@ -8312,7 +8388,7 @@ function hasThrottleLimits(limits) {
|
|
|
8312
8388
|
function normalizeRoutePath(value) {
|
|
8313
8389
|
if (!value?.trim())
|
|
8314
8390
|
return;
|
|
8315
|
-
const resolved =
|
|
8391
|
+
const resolved = resolve3(value.trim());
|
|
8316
8392
|
let canonical = resolved;
|
|
8317
8393
|
try {
|
|
8318
8394
|
canonical = realpathSync(resolved);
|
|
@@ -8324,7 +8400,7 @@ function normalizeRoutePath(value) {
|
|
|
8324
8400
|
try {
|
|
8325
8401
|
return realpathSync(gitRoot.stdout.trim());
|
|
8326
8402
|
} catch {
|
|
8327
|
-
return
|
|
8403
|
+
return resolve3(gitRoot.stdout.trim());
|
|
8328
8404
|
}
|
|
8329
8405
|
}
|
|
8330
8406
|
return canonical;
|
|
@@ -8333,7 +8409,7 @@ function routeProjectGroup(optsGroup, data, metadata) {
|
|
|
8333
8409
|
return optsGroup?.trim() || taskEventField(data, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]) || taskEventField(metadata, ["project_group", "projectGroup", "repo_group", "repoGroup", "workspace_group", "workspaceGroup"]);
|
|
8334
8410
|
}
|
|
8335
8411
|
function routeThrottleDecision(store, args) {
|
|
8336
|
-
const projectPath = normalizeRoutePath(args.projectPath) ??
|
|
8412
|
+
const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
|
|
8337
8413
|
const projectGroup = args.projectGroup?.trim() || undefined;
|
|
8338
8414
|
const counts = store.countActiveWorkflowWorkItems({ projectKey: projectPath, projectGroup });
|
|
8339
8415
|
const base = {
|
|
@@ -8358,7 +8434,7 @@ function routeThrottleDecision(store, args) {
|
|
|
8358
8434
|
return { ...base, allowed: true };
|
|
8359
8435
|
}
|
|
8360
8436
|
function routeThrottleDryRunPreview(args) {
|
|
8361
|
-
const projectPath = normalizeRoutePath(args.projectPath) ??
|
|
8437
|
+
const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
|
|
8362
8438
|
const projectGroup = args.projectGroup?.trim() || undefined;
|
|
8363
8439
|
return {
|
|
8364
8440
|
evaluated: false,
|
|
@@ -8380,7 +8456,7 @@ function todosTaskRouteTemplateId(opts) {
|
|
|
8380
8456
|
return id;
|
|
8381
8457
|
}
|
|
8382
8458
|
async function readEventEnvelopeInput(opts = {}) {
|
|
8383
|
-
const raw = opts.eventJson ?? (opts.eventFile ?
|
|
8459
|
+
const raw = opts.eventJson ?? (opts.eventFile ? readFileSync4(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
|
|
8384
8460
|
const event = JSON.parse(raw);
|
|
8385
8461
|
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
8386
8462
|
throw new Error("event JSON must be an object");
|
|
@@ -8421,7 +8497,7 @@ function routeTodosTaskEvent(event, opts) {
|
|
|
8421
8497
|
"cwd"
|
|
8422
8498
|
]);
|
|
8423
8499
|
const projectPath = dataProjectPath ?? metadataProjectPath ?? opts.projectPath ?? process.cwd();
|
|
8424
|
-
const routeProjectPath = normalizeRoutePath(projectPath) ??
|
|
8500
|
+
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
|
|
8425
8501
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
8426
8502
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
8427
8503
|
const idempotencyKey = `todos-task:${taskId}`;
|
|
@@ -8671,7 +8747,7 @@ function routeGenericEvent(event, opts) {
|
|
|
8671
8747
|
const data = eventData(event);
|
|
8672
8748
|
const metadata = eventMetadata(event);
|
|
8673
8749
|
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? taskEventField(metadata, ["working_dir", "workingDir", "project_path", "projectPath", "project_canonical_path", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
|
|
8674
|
-
const routeProjectPath = normalizeRoutePath(projectPath) ??
|
|
8750
|
+
const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
|
|
8675
8751
|
const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
|
|
8676
8752
|
const throttleLimits = routeThrottleLimitsFromOpts(opts);
|
|
8677
8753
|
const eventSuffix = event.id.slice(0, 8);
|
|
@@ -9001,8 +9077,8 @@ function taskMatchesDrainFilters(task, filters) {
|
|
|
9001
9077
|
const path = taskProjectPath(task);
|
|
9002
9078
|
if (!path)
|
|
9003
9079
|
return false;
|
|
9004
|
-
const normalizedPath = normalizeRoutePath(path) ??
|
|
9005
|
-
const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ??
|
|
9080
|
+
const normalizedPath = normalizeRoutePath(path) ?? resolve3(path);
|
|
9081
|
+
const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve3(filters.projectPathPrefix);
|
|
9006
9082
|
if (normalizedPath !== normalizedPrefix && !normalizedPath.startsWith(`${normalizedPrefix}/`))
|
|
9007
9083
|
return false;
|
|
9008
9084
|
}
|
|
@@ -9077,7 +9153,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9077
9153
|
store.close();
|
|
9078
9154
|
}
|
|
9079
9155
|
});
|
|
9080
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").
|
|
9156
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").option("--prompt <prompt>", "agent prompt").option("--prompt-file <file>", "read the agent prompt from a markdown/text file").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight-each-run", "check provider/account readiness before every scheduled run").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
9081
9157
|
const provider = opts.provider;
|
|
9082
9158
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
9083
9159
|
throw new Error("unsupported provider");
|
|
@@ -9087,10 +9163,11 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9087
9163
|
}
|
|
9088
9164
|
const store = new Store;
|
|
9089
9165
|
try {
|
|
9090
|
-
const target = {
|
|
9166
|
+
const target = normalizeLoopTargetForStorage({
|
|
9091
9167
|
type: "agent",
|
|
9092
9168
|
provider,
|
|
9093
9169
|
prompt: opts.prompt,
|
|
9170
|
+
promptFile: opts.promptFile,
|
|
9094
9171
|
cwd: opts.cwd,
|
|
9095
9172
|
model: opts.model,
|
|
9096
9173
|
variant: opts.variant,
|
|
@@ -9104,9 +9181,8 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
9104
9181
|
allowlist: allowlistFromOpts(opts),
|
|
9105
9182
|
account: accountFromOpts(opts),
|
|
9106
9183
|
preflight: runtimePreflightFromOpts(opts)
|
|
9107
|
-
};
|
|
9184
|
+
}, { name, type: "agent", provider }, { baseDir: process.cwd() });
|
|
9108
9185
|
const input = baseCreateInput(name, opts, target);
|
|
9109
|
-
validateLoopTargetForStorage(input.target, { name, type: "agent", provider });
|
|
9110
9186
|
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
9111
9187
|
const loop = store.createLoop(input);
|
|
9112
9188
|
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
@@ -9400,7 +9476,8 @@ addTemplateSourceOption(templates.command("show <id>").description("show a templ
|
|
|
9400
9476
|
});
|
|
9401
9477
|
addTemplateSourceOption(templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
|
|
9402
9478
|
const workflow = renderLoopTemplate(id, parseVars(opts.var), { source: templateSource(opts.source) });
|
|
9403
|
-
|
|
9479
|
+
const value = publicWorkflowBody(workflow);
|
|
9480
|
+
print(value, JSON.stringify(value, null, 2));
|
|
9404
9481
|
});
|
|
9405
9482
|
addTemplateSourceOption(templates.command("create-workflow <id>").description("render and store a template as a workflow").option("--var <key=value>", "template variable; may be repeated", collectValues, [])).action((id, opts) => {
|
|
9406
9483
|
const store = new Store;
|
|
@@ -9577,7 +9654,7 @@ machines.command("show <id>").description("resolve a machine assignment").action
|
|
|
9577
9654
|
print(resolveLoopMachine(id));
|
|
9578
9655
|
});
|
|
9579
9656
|
workflows.command("validate <file>").description("validate a workflow JSON file without storing or running it").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables").action((file, opts) => {
|
|
9580
|
-
const body =
|
|
9657
|
+
const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
|
|
9581
9658
|
const workflow = workflowSpecForPreflight(body);
|
|
9582
9659
|
const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
|
|
9583
9660
|
print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
|
|
@@ -9585,7 +9662,7 @@ workflows.command("validate <file>").description("validate a workflow JSON file
|
|
|
9585
9662
|
workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables before storing").action((file, opts) => {
|
|
9586
9663
|
const store = new Store;
|
|
9587
9664
|
try {
|
|
9588
|
-
const body =
|
|
9665
|
+
const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
|
|
9589
9666
|
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
|
|
9590
9667
|
const workflow = store.createWorkflow(body);
|
|
9591
9668
|
if (preflight !== undefined)
|
|
@@ -10394,7 +10471,7 @@ daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) =>
|
|
|
10394
10471
|
console.log("");
|
|
10395
10472
|
return;
|
|
10396
10473
|
}
|
|
10397
|
-
const lines =
|
|
10474
|
+
const lines = readFileSync4(path, "utf8").trimEnd().split(`
|
|
10398
10475
|
`);
|
|
10399
10476
|
console.log(lines.slice(-Number(opts.lines)).join(`
|
|
10400
10477
|
`));
|