@hasna/loops 0.3.52 → 0.3.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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),
@@ -1567,6 +1614,65 @@ class Store {
1567
1614
  throw new Error(`workflow invocation not found after create: ${id}`);
1568
1615
  return rowToWorkflowInvocation(row);
1569
1616
  }
1617
+ refreshWorkflowInvocationForWorkItem(workItemId, input) {
1618
+ const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
1619
+ if (!sourceDedupeKey)
1620
+ throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1621
+ const now = nowIso();
1622
+ const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1623
+ const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1624
+ const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1625
+ const result = this.db.query(`UPDATE workflow_invocations
1626
+ SET workflow_id=COALESCE($workflowId, workflow_id),
1627
+ template_id=COALESCE($templateId, template_id),
1628
+ source_id=COALESCE($sourceId, source_id),
1629
+ source_json=$sourceJson,
1630
+ subject_kind=$subjectKind,
1631
+ subject_id=COALESCE($subjectId, subject_id),
1632
+ subject_path=COALESCE($subjectPath, subject_path),
1633
+ subject_url=COALESCE($subjectUrl, subject_url),
1634
+ subject_json=$subjectJson,
1635
+ intent=$intent,
1636
+ scope_json=COALESCE($scopeJson, scope_json),
1637
+ output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
1638
+ updated_at=$updated
1639
+ WHERE source_kind=$sourceKind
1640
+ AND source_dedupe_key=$sourceDedupeKey
1641
+ AND EXISTS (
1642
+ SELECT 1
1643
+ FROM workflow_work_items
1644
+ WHERE id=$workItemId
1645
+ AND invocation_id=workflow_invocations.id
1646
+ AND status IN (${placeholders})
1647
+ )`).run({
1648
+ $workItemId: workItemId,
1649
+ $sourceKind: input.sourceRef.kind,
1650
+ $sourceDedupeKey: sourceDedupeKey,
1651
+ $workflowId: input.workflowId ?? null,
1652
+ $templateId: input.templateId ?? null,
1653
+ $sourceId: input.sourceRef.id ?? null,
1654
+ $sourceJson: JSON.stringify(input.sourceRef),
1655
+ $subjectKind: input.subjectRef.kind,
1656
+ $subjectId: input.subjectRef.id ?? null,
1657
+ $subjectPath: input.subjectRef.path ?? null,
1658
+ $subjectUrl: input.subjectRef.url ?? null,
1659
+ $subjectJson: JSON.stringify(input.subjectRef),
1660
+ $intent: input.intent,
1661
+ $scopeJson: input.scope ? JSON.stringify(input.scope) : null,
1662
+ $outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
1663
+ $updated: now,
1664
+ ...statusBindings
1665
+ });
1666
+ if (result.changes !== 1)
1667
+ throw new Error(`workflow work item is not refreshable: ${workItemId}`);
1668
+ const updated = this.db.query(`SELECT workflow_invocations.*
1669
+ FROM workflow_invocations
1670
+ JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
1671
+ WHERE workflow_work_items.id = ?`).get(workItemId);
1672
+ if (!updated)
1673
+ throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
1674
+ return rowToWorkflowInvocation(updated);
1675
+ }
1570
1676
  getWorkflowInvocation(id) {
1571
1677
  const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1572
1678
  return row ? rowToWorkflowInvocation(row) : undefined;
@@ -2855,9 +2961,9 @@ class Store {
2855
2961
 
2856
2962
  // src/cli/index.ts
2857
2963
  import { createHash as createHash3, randomUUID } from "crypto";
2858
- 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";
2859
2965
  import { spawnSync as spawnSync5 } from "child_process";
2860
- import { join as join6, resolve as resolve2 } from "path";
2966
+ import { dirname as dirname4, join as join6, resolve as resolve3 } from "path";
2861
2967
  import { tmpdir as tmpdir2 } from "os";
2862
2968
  import { Database as Database2 } from "bun:sqlite";
2863
2969
  import { Command } from "commander";
@@ -4972,13 +5078,13 @@ async function tick(deps) {
4972
5078
  }
4973
5079
 
4974
5080
  // src/daemon/control.ts
4975
- 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";
4976
5082
  import { hostname } from "os";
4977
5083
  import { dirname as dirname2 } from "path";
4978
5084
 
4979
5085
  // src/daemon/loop.ts
4980
5086
  function realSleep(ms) {
4981
- return new Promise((resolve) => setTimeout(resolve, ms));
5087
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4982
5088
  }
4983
5089
  async function runLoop(opts) {
4984
5090
  const sleep = opts.sleep ?? realSleep;
@@ -5003,7 +5109,7 @@ function readPid(path = pidFilePath()) {
5003
5109
  if (!existsSync3(path))
5004
5110
  return;
5005
5111
  try {
5006
- const pid = Number(readFileSync(path, "utf8").trim());
5112
+ const pid = Number(readFileSync2(path, "utf8").trim());
5007
5113
  return Number.isInteger(pid) && pid > 0 ? pid : undefined;
5008
5114
  } catch {
5009
5115
  return;
@@ -5908,7 +6014,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5908
6014
  // package.json
5909
6015
  var package_default = {
5910
6016
  name: "@hasna/loops",
5911
- version: "0.3.52",
6017
+ version: "0.3.54",
5912
6018
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5913
6019
  type: "module",
5914
6020
  main: "dist/index.js",
@@ -5998,9 +6104,9 @@ function packageVersion() {
5998
6104
 
5999
6105
  // src/lib/templates.ts
6000
6106
  import { execFileSync } from "child_process";
6001
- 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";
6002
6108
  import { homedir as homedir3 } from "os";
6003
- 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";
6004
6110
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
6005
6111
  var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
6006
6112
  var BOUNDED_AGENT_WORKER_VERIFIER_TEMPLATE_ID = "bounded-agent-worker-verifier";
@@ -6282,7 +6388,7 @@ function normalizeWorktreeMode(mode) {
6282
6388
  function defaultWorktreeRoot(root) {
6283
6389
  if (root?.trim()) {
6284
6390
  const expanded = root.trim().replace(/^~(?=$|\/)/, homedir3());
6285
- return isAbsolute(expanded) ? expanded : resolve(expanded);
6391
+ return isAbsolute2(expanded) ? expanded : resolve2(expanded);
6286
6392
  }
6287
6393
  return join5(homedir3(), ".hasna", "loops", "worktrees");
6288
6394
  }
@@ -6389,7 +6495,7 @@ function worktreePlan(input, seed) {
6389
6495
  const seedSlug = `${slugSegment(seed, "run").slice(0, 48)}-${stableHex(`${repoRoot}:${seed}`)}`;
6390
6496
  const worktreePath = join5(root, repoSlug, seedSlug);
6391
6497
  const relativeCwd = relative(repoRoot, originalCwd);
6392
- const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
6498
+ const cwd = relativeCwd && !relativeCwd.startsWith("..") && !isAbsolute2(relativeCwd) ? join5(worktreePath, relativeCwd) : worktreePath;
6393
6499
  const branchPrefix = (input.worktreeBranchPrefix?.trim() || "openloops").replace(/^\/+|\/+$/g, "") || "openloops";
6394
6500
  const branch = `${branchPrefix}/${repoSlug}/${seedSlug}`;
6395
6501
  const prepareStep = {
@@ -6624,9 +6730,24 @@ function assertNoImplicitDangerFullAccess(value, label) {
6624
6730
  assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
6625
6731
  }
6626
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
+ }
6627
6747
  function assertCustomTemplateSafety(value, label) {
6628
6748
  assertNoDangerousCustomTemplateScalars(value, label);
6629
6749
  assertNoImplicitDangerFullAccess(value, label);
6750
+ assertNoCustomTemplatePromptFiles(value, label);
6630
6751
  }
6631
6752
  function customTemplateDefinitionFromJson(value, sourcePath) {
6632
6753
  assertRecord(value, sourcePath);
@@ -6656,7 +6777,7 @@ function customTemplateSummary(definition, sourcePath) {
6656
6777
  function readCustomTemplateFile(file) {
6657
6778
  let parsed;
6658
6779
  try {
6659
- parsed = JSON.parse(readFileSync2(file, "utf8"));
6780
+ parsed = JSON.parse(readFileSync3(file, "utf8"));
6660
6781
  } catch (error) {
6661
6782
  const message = error instanceof Error ? error.message : String(error);
6662
6783
  throw new Error(`failed to read custom template ${file}: ${message}`);
@@ -6795,19 +6916,19 @@ function renderCustomLoopTemplate(entry, values) {
6795
6916
  return workflow;
6796
6917
  }
6797
6918
  function validateCustomLoopTemplateFile(file) {
6798
- const source = resolve(file);
6919
+ const source = resolve2(file);
6799
6920
  const entry = readCustomTemplateFile(source);
6800
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
6921
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== source);
6801
6922
  assertNoTemplateCollisions([...existing, entry]);
6802
6923
  return structuredClone(entry.summary);
6803
6924
  }
6804
6925
  function importCustomLoopTemplate(file, opts = {}) {
6805
- const source = resolve(file);
6926
+ const source = resolve2(file);
6806
6927
  const entry = readCustomTemplateFile(source);
6807
6928
  const dir = ensureCustomLoopTemplatesDir();
6808
6929
  const destination = join5(dir, `${entry.definition.id}.json`);
6809
6930
  const replaced = existsSync4(destination);
6810
- const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
6931
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve2(template.path) !== resolve2(destination));
6811
6932
  assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
6812
6933
  if (replaced) {
6813
6934
  const stat = lstatSync(destination);
@@ -7613,12 +7734,13 @@ function validationFailed(error, context) {
7613
7734
  });
7614
7735
  process.exit(1);
7615
7736
  }
7616
- function validateLoopTargetForStorage(target, context) {
7737
+ function normalizeLoopTargetForStorage(target, context, opts = {}) {
7617
7738
  try {
7618
- workflowBodyFromJson({
7739
+ const workflow = workflowBodyFromJson({
7619
7740
  name: "loop-target-validation",
7620
7741
  steps: [{ id: "target", target }]
7621
- });
7742
+ }, undefined, opts);
7743
+ return workflow.steps[0].target;
7622
7744
  } catch (error) {
7623
7745
  validationFailed(error, context);
7624
7746
  }
@@ -7630,6 +7752,14 @@ function normalizeWorkflowForStorage(body, context) {
7630
7752
  validationFailed(error, context);
7631
7753
  }
7632
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
+ }
7633
7763
  function preflightLoopTarget(target, context, metadata, opts) {
7634
7764
  try {
7635
7765
  return preflightTarget(target, metadata, opts);
@@ -7658,6 +7788,11 @@ function workflowSpecForPreflight(body, id = "validation") {
7658
7788
  updatedAt: now
7659
7789
  };
7660
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
+ }
7661
7796
  function printTextOutput(value) {
7662
7797
  for (const line of textOutputBlocks(value, { indent: " " }))
7663
7798
  console.log(line);
@@ -7854,7 +7989,7 @@ function runLocalCommandWithStdoutFile(command, args, opts = {}) {
7854
7989
  return {
7855
7990
  ok: result.status === 0,
7856
7991
  status: result.status,
7857
- stdout: readFileSync3(stdoutPath, "utf8"),
7992
+ stdout: readFileSync4(stdoutPath, "utf8"),
7858
7993
  stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString() || "",
7859
7994
  error: result.error ? String(result.error.message || result.error) : ""
7860
7995
  };
@@ -7898,7 +8033,7 @@ function readRouteCursors() {
7898
8033
  if (!existsSync5(path))
7899
8034
  return {};
7900
8035
  try {
7901
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
8036
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
7902
8037
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
7903
8038
  } catch {
7904
8039
  return {};
@@ -8253,7 +8388,7 @@ function hasThrottleLimits(limits) {
8253
8388
  function normalizeRoutePath(value) {
8254
8389
  if (!value?.trim())
8255
8390
  return;
8256
- const resolved = resolve2(value.trim());
8391
+ const resolved = resolve3(value.trim());
8257
8392
  let canonical = resolved;
8258
8393
  try {
8259
8394
  canonical = realpathSync(resolved);
@@ -8265,7 +8400,7 @@ function normalizeRoutePath(value) {
8265
8400
  try {
8266
8401
  return realpathSync(gitRoot.stdout.trim());
8267
8402
  } catch {
8268
- return resolve2(gitRoot.stdout.trim());
8403
+ return resolve3(gitRoot.stdout.trim());
8269
8404
  }
8270
8405
  }
8271
8406
  return canonical;
@@ -8274,7 +8409,7 @@ function routeProjectGroup(optsGroup, data, metadata) {
8274
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"]);
8275
8410
  }
8276
8411
  function routeThrottleDecision(store, args) {
8277
- const projectPath = normalizeRoutePath(args.projectPath) ?? resolve2(args.projectPath);
8412
+ const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
8278
8413
  const projectGroup = args.projectGroup?.trim() || undefined;
8279
8414
  const counts = store.countActiveWorkflowWorkItems({ projectKey: projectPath, projectGroup });
8280
8415
  const base = {
@@ -8299,7 +8434,7 @@ function routeThrottleDecision(store, args) {
8299
8434
  return { ...base, allowed: true };
8300
8435
  }
8301
8436
  function routeThrottleDryRunPreview(args) {
8302
- const projectPath = normalizeRoutePath(args.projectPath) ?? resolve2(args.projectPath);
8437
+ const projectPath = normalizeRoutePath(args.projectPath) ?? resolve3(args.projectPath);
8303
8438
  const projectGroup = args.projectGroup?.trim() || undefined;
8304
8439
  return {
8305
8440
  evaluated: false,
@@ -8321,7 +8456,7 @@ function todosTaskRouteTemplateId(opts) {
8321
8456
  return id;
8322
8457
  }
8323
8458
  async function readEventEnvelopeInput(opts = {}) {
8324
- 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());
8325
8460
  const event = JSON.parse(raw);
8326
8461
  if (!event || typeof event !== "object" || Array.isArray(event))
8327
8462
  throw new Error("event JSON must be an object");
@@ -8362,7 +8497,7 @@ function routeTodosTaskEvent(event, opts) {
8362
8497
  "cwd"
8363
8498
  ]);
8364
8499
  const projectPath = dataProjectPath ?? metadataProjectPath ?? opts.projectPath ?? process.cwd();
8365
- const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
8500
+ const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
8366
8501
  const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
8367
8502
  const throttleLimits = routeThrottleLimitsFromOpts(opts);
8368
8503
  const idempotencyKey = `todos-task:${taskId}`;
@@ -8536,8 +8671,9 @@ function routeTodosTaskEvent(event, opts) {
8536
8671
  status: throttle && !throttle.allowed ? "deferred" : "queued",
8537
8672
  lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
8538
8673
  });
8674
+ const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
8539
8675
  if (throttle && !throttle.allowed)
8540
- return { kind: "throttled", invocation, workItem, throttle };
8676
+ return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
8541
8677
  const workflow = routeWorkflowForStorage(store, workflowBody);
8542
8678
  const loop = store.createLoop({
8543
8679
  ...loopInput,
@@ -8545,13 +8681,13 @@ function routeTodosTaskEvent(event, opts) {
8545
8681
  type: "workflow",
8546
8682
  workflowId: workflow.id,
8547
8683
  input: {
8548
- workflowInvocationId: invocation.id,
8684
+ workflowInvocationId: refreshedInvocation.id,
8549
8685
  workflowWorkItemId: workItem.id
8550
8686
  }
8551
8687
  }
8552
8688
  });
8553
8689
  const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by todos-task route" });
8554
- return { kind: "created", invocation, workItem: admitted, workflow, loop, throttle };
8690
+ return { kind: "created", invocation: refreshedInvocation, workItem: admitted, workflow, loop, throttle };
8555
8691
  });
8556
8692
  if (outcome.kind === "deduped") {
8557
8693
  return {
@@ -8611,7 +8747,7 @@ function routeGenericEvent(event, opts) {
8611
8747
  const data = eventData(event);
8612
8748
  const metadata = eventMetadata(event);
8613
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();
8614
- const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve2(projectPath);
8750
+ const routeProjectPath = normalizeRoutePath(projectPath) ?? resolve3(projectPath);
8615
8751
  const projectGroup = routeProjectGroup(opts.projectGroup, data, metadata);
8616
8752
  const throttleLimits = routeThrottleLimitsFromOpts(opts);
8617
8753
  const eventSuffix = event.id.slice(0, 8);
@@ -8752,8 +8888,9 @@ function routeGenericEvent(event, opts) {
8752
8888
  status: throttle && !throttle.allowed ? "deferred" : "queued",
8753
8889
  lastReason: throttle && !throttle.allowed ? throttle.reason : undefined
8754
8890
  });
8891
+ const refreshedInvocation = store.refreshWorkflowInvocationForWorkItem(workItem.id, invocationInput);
8755
8892
  if (throttle && !throttle.allowed)
8756
- return { kind: "throttled", invocation, workItem, throttle };
8893
+ return { kind: "throttled", invocation: refreshedInvocation, workItem, throttle };
8757
8894
  const workflow = routeWorkflowForStorage(store, workflowBody);
8758
8895
  const loop = store.createLoop({
8759
8896
  ...loopInput,
@@ -8761,13 +8898,13 @@ function routeGenericEvent(event, opts) {
8761
8898
  type: "workflow",
8762
8899
  workflowId: workflow.id,
8763
8900
  input: {
8764
- workflowInvocationId: invocation.id,
8901
+ workflowInvocationId: refreshedInvocation.id,
8765
8902
  workflowWorkItemId: workItem.id
8766
8903
  }
8767
8904
  }
8768
8905
  });
8769
8906
  const admitted = store.admitWorkflowWorkItem(workItem.id, { workflowId: workflow.id, loopId: loop.id, reason: "admitted by generic-event route" });
8770
- return { kind: "created", invocation, workItem: admitted, workflow, loop, throttle };
8907
+ return { kind: "created", invocation: refreshedInvocation, workItem: admitted, workflow, loop, throttle };
8771
8908
  });
8772
8909
  if (outcome.kind === "deduped") {
8773
8910
  return {
@@ -8940,8 +9077,8 @@ function taskMatchesDrainFilters(task, filters) {
8940
9077
  const path = taskProjectPath(task);
8941
9078
  if (!path)
8942
9079
  return false;
8943
- const normalizedPath = normalizeRoutePath(path) ?? resolve2(path);
8944
- const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve2(filters.projectPathPrefix);
9080
+ const normalizedPath = normalizeRoutePath(path) ?? resolve3(path);
9081
+ const normalizedPrefix = normalizeRoutePath(filters.projectPathPrefix) ?? resolve3(filters.projectPathPrefix);
8945
9082
  if (normalizedPath !== normalizedPrefix && !normalizedPath.startsWith(`${normalizedPrefix}/`))
8946
9083
  return false;
8947
9084
  }
@@ -9016,7 +9153,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9016
9153
  store.close();
9017
9154
  }
9018
9155
  });
9019
- 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) => {
9020
9157
  const provider = opts.provider;
9021
9158
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
9022
9159
  throw new Error("unsupported provider");
@@ -9026,10 +9163,11 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9026
9163
  }
9027
9164
  const store = new Store;
9028
9165
  try {
9029
- const target = {
9166
+ const target = normalizeLoopTargetForStorage({
9030
9167
  type: "agent",
9031
9168
  provider,
9032
9169
  prompt: opts.prompt,
9170
+ promptFile: opts.promptFile,
9033
9171
  cwd: opts.cwd,
9034
9172
  model: opts.model,
9035
9173
  variant: opts.variant,
@@ -9043,9 +9181,8 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
9043
9181
  allowlist: allowlistFromOpts(opts),
9044
9182
  account: accountFromOpts(opts),
9045
9183
  preflight: runtimePreflightFromOpts(opts)
9046
- };
9184
+ }, { name, type: "agent", provider }, { baseDir: process.cwd() });
9047
9185
  const input = baseCreateInput(name, opts, target);
9048
- validateLoopTargetForStorage(input.target, { name, type: "agent", provider });
9049
9186
  const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
9050
9187
  const loop = store.createLoop(input);
9051
9188
  printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
@@ -9339,7 +9476,8 @@ addTemplateSourceOption(templates.command("show <id>").description("show a templ
9339
9476
  });
9340
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) => {
9341
9478
  const workflow = renderLoopTemplate(id, parseVars(opts.var), { source: templateSource(opts.source) });
9342
- print(workflow, JSON.stringify(workflow, null, 2));
9479
+ const value = publicWorkflowBody(workflow);
9480
+ print(value, JSON.stringify(value, null, 2));
9343
9481
  });
9344
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) => {
9345
9483
  const store = new Store;
@@ -9516,7 +9654,7 @@ machines.command("show <id>").description("resolve a machine assignment").action
9516
9654
  print(resolveLoopMachine(id));
9517
9655
  });
9518
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) => {
9519
- const body = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
9657
+ const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
9520
9658
  const workflow = workflowSpecForPreflight(body);
9521
9659
  const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
9522
9660
  print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
@@ -9524,7 +9662,7 @@ workflows.command("validate <file>").description("validate a workflow JSON file
9524
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) => {
9525
9663
  const store = new Store;
9526
9664
  try {
9527
- const body = workflowBodyFromJson(JSON.parse(readFileSync3(file, "utf8")), opts.name);
9665
+ const body = workflowBodyFromFile(file, opts.name, { file, type: "workflow" });
9528
9666
  const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
9529
9667
  const workflow = store.createWorkflow(body);
9530
9668
  if (preflight !== undefined)
@@ -10333,7 +10471,7 @@ daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) =>
10333
10471
  console.log("");
10334
10472
  return;
10335
10473
  }
10336
- const lines = readFileSync3(path, "utf8").trimEnd().split(`
10474
+ const lines = readFileSync4(path, "utf8").trimEnd().split(`
10337
10475
  `);
10338
10476
  console.log(lines.slice(-Number(opts.lines)).join(`
10339
10477
  `));