@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 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
- assertString(value.prompt, `${label}.prompt`);
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
- return value;
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: input.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 readFileSync3, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
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 resolve2 } from "path";
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((resolve) => setTimeout(resolve, ms));
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(readFileSync(path, "utf8").trim());
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.53",
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 readFileSync2, writeFileSync as writeFileSync4 } from "fs";
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 isAbsolute(expanded) ? expanded : resolve(expanded);
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("..") && !isAbsolute(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
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(readFileSync2(file, "utf8"));
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 = resolve(file);
6919
+ const source = resolve2(file);
6858
6920
  const entry = readCustomTemplateFile(source);
6859
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
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 = resolve(file);
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) => resolve(template.path) !== resolve(destination));
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 validateLoopTargetForStorage(target, context) {
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: readFileSync3(stdoutPath, "utf8"),
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(readFileSync3(path, "utf8"));
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 = resolve2(value.trim());
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 resolve2(gitRoot.stdout.trim());
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) ?? resolve2(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) ?? resolve2(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 ? readFileSync3(opts.eventFile, "utf8") : process.env.HASNA_EVENT_JSON || await Bun.stdin.text());
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) ?? resolve2(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) ?? resolve2(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) ?? resolve2(path);
9005
- const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve2(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").requiredOption("--prompt <prompt>", "agent prompt").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) => {
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
- print(workflow, JSON.stringify(workflow, null, 2));
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 = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
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 = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
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 = readFileSync3(path, "utf8").trimEnd().split(`
10474
+ const lines = readFileSync4(path, "utf8").trimEnd().split(`
10398
10475
  `);
10399
10476
  console.log(lines.slice(-Number(opts.lines)).join(`
10400
10477
  `));