@hasna/loops 0.3.43 → 0.3.45

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
@@ -224,7 +224,9 @@ loops templates render todos-task-worker-verifier \
224
224
  --var projectPath=/path/to/repo \
225
225
  --var provider=codewith \
226
226
  --var authProfilePool=account004,account005,account006 \
227
- --var sandbox=workspace-write
227
+ --var sandbox=workspace-write \
228
+ --var todosProjectPath=$HOME/.hasna/loops \
229
+ --var addDirs=$HOME/.hasna/todos,$HOME/.hasna/loops
228
230
  loops templates create-workflow todos-task-worker-verifier \
229
231
  --var taskId=<task-id> \
230
232
  --var projectPath=/path/to/repo
@@ -260,6 +262,8 @@ cat task-created-event.json | loops events handle todos-task \
260
262
  --auth-profile-pool account004,account005,account006 \
261
263
  --permission-mode bypass \
262
264
  --sandbox workspace-write \
265
+ --todos-project "$HOME/.hasna/loops" \
266
+ --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
263
267
  --worktree-mode required
264
268
  ```
265
269
 
@@ -290,6 +294,13 @@ is requested without `manualBreakGlass=true`. Use `workspace-write` for
290
294
  unattended task/event routes. Full access is an explicit manual emergency path,
291
295
  not a default automation mode.
292
296
 
297
+ When a sandboxed Codewith/Codex worker must update app stores outside the repo
298
+ worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
299
+ For task-created routes, pass `--todos-project` so worker/verifier prompts use
300
+ the actual todos storage project while repo routing and worktree isolation still
301
+ use the repository path. This avoids `danger-full-access` for normal todos
302
+ comments, completion state, and loop evidence writes.
303
+
293
304
  Inspect route state with:
294
305
 
295
306
  ```bash
@@ -484,10 +495,10 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
484
495
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
485
496
 
486
497
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
487
- - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
498
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
488
499
  - AI Copilot and OpenCode use `run --format json --pure`.
489
500
  - Cursor is CLI-first for now via `cursor agent -p`, with `agent -p` as the fallback launcher on machines that expose the standalone Cursor Agent binary; treat output as less stable until a stronger public SDK contract is selected.
490
- - Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
501
+ - Codex uses `codex exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
491
502
  - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
492
503
  - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
493
504
  - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/lib/store.ts
5
5
  import { Database } from "bun:sqlite";
6
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
6
+ import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
7
7
  import { tmpdir } from "os";
8
8
  import { dirname, join as join3 } from "path";
9
9
 
@@ -388,6 +388,7 @@ function validateTarget(value, label) {
388
388
  }
389
389
  if (value.variant !== undefined)
390
390
  assertString(value.variant, `${label}.variant`);
391
+ optionalStringArray(value.addDirs, `${label}.addDirs`);
391
392
  if (value.permissionMode !== undefined) {
392
393
  assertString(value.permissionMode, `${label}.permissionMode`);
393
394
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -824,6 +825,20 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
824
825
  }
825
826
  return;
826
827
  }
828
+ function chmodIfExists(path, mode) {
829
+ try {
830
+ if (existsSync(path))
831
+ chmodSync(path, mode);
832
+ } catch {}
833
+ }
834
+ function ensurePrivateStorePath(file) {
835
+ const dir = dirname(file);
836
+ mkdirSync3(dir, { recursive: true, mode: 448 });
837
+ chmodIfExists(dir, 448);
838
+ chmodIfExists(file, 384);
839
+ chmodIfExists(`${file}-wal`, 384);
840
+ chmodIfExists(`${file}-shm`, 384);
841
+ }
827
842
 
828
843
  class Store {
829
844
  db;
@@ -831,11 +846,13 @@ class Store {
831
846
  constructor(path) {
832
847
  const file = path ?? dbPath();
833
848
  if (file !== ":memory:")
834
- mkdirSync3(dirname(file), { recursive: true, mode: 448 });
849
+ ensurePrivateStorePath(file);
835
850
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
836
851
  this.db = new Database(file);
837
852
  this.db.exec("PRAGMA busy_timeout = 5000;");
838
853
  this.db.exec("PRAGMA journal_mode = WAL;");
854
+ if (file !== ":memory:")
855
+ ensurePrivateStorePath(file);
839
856
  this.migrate();
840
857
  }
841
858
  migrate() {
@@ -2661,7 +2678,7 @@ class Store {
2661
2678
 
2662
2679
  // src/cli/index.ts
2663
2680
  import { createHash as createHash3, randomUUID } from "crypto";
2664
- import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
2681
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync2, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
2665
2682
  import { spawnSync as spawnSync5 } from "child_process";
2666
2683
  import { join as join6, resolve as resolve2 } from "path";
2667
2684
  import { tmpdir as tmpdir2 } from "os";
@@ -2791,7 +2808,7 @@ import { resolveMachineCommand } from "@hasna/machines/consumer";
2791
2808
 
2792
2809
  // src/lib/accounts.ts
2793
2810
  import { spawnSync } from "child_process";
2794
- import { existsSync } from "fs";
2811
+ import { existsSync as existsSync2 } from "fs";
2795
2812
  var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
2796
2813
  function accountToolForProvider(provider) {
2797
2814
  switch (provider) {
@@ -2876,7 +2893,7 @@ function resolveAccountEnv(account, toolHint, env) {
2876
2893
  const profileDir = (accountDirEnvVar(tool) ? accountEnv[accountDirEnvVar(tool)] : undefined) ?? primaryAccountDir(result.stdout);
2877
2894
  if (!profileDir)
2878
2895
  throw new Error(`accounts env returned no profile directory for ${account.profile}/${tool}`);
2879
- if (!existsSync(profileDir))
2896
+ if (!existsSync2(profileDir))
2880
2897
  throw new Error(`account profile directory does not exist for ${account.profile}/${tool}: ${profileDir}`);
2881
2898
  return {
2882
2899
  ...accountEnv,
@@ -3231,6 +3248,8 @@ function agentArgs(target) {
3231
3248
  args.push("--ignore-rules");
3232
3249
  if (target.cwd)
3233
3250
  args.push("--cd", target.cwd);
3251
+ for (const dir of target.addDirs ?? [])
3252
+ args.push("--add-dir", dir);
3234
3253
  if (target.model)
3235
3254
  args.push("--model", target.model);
3236
3255
  if (target.agent)
@@ -3240,11 +3259,13 @@ function agentArgs(target) {
3240
3259
  case "codex":
3241
3260
  if (target.variant)
3242
3261
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3243
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3262
+ args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3244
3263
  if (isolation === "safe")
3245
3264
  args.push("--ignore-rules");
3246
3265
  if (target.cwd)
3247
3266
  args.push("--cd", target.cwd);
3267
+ for (const dir of target.addDirs ?? [])
3268
+ args.push("--add-dir", dir);
3248
3269
  if (target.model)
3249
3270
  args.push("--model", target.model);
3250
3271
  args.push(...target.extraArgs ?? []);
@@ -4762,7 +4783,7 @@ async function tick(deps) {
4762
4783
  }
4763
4784
 
4764
4785
  // src/daemon/control.ts
4765
- import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4786
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4766
4787
  import { hostname } from "os";
4767
4788
  import { dirname as dirname2 } from "path";
4768
4789
 
@@ -4790,7 +4811,7 @@ async function runLoop(opts) {
4790
4811
 
4791
4812
  // src/daemon/control.ts
4792
4813
  function readPid(path = pidFilePath()) {
4793
- if (!existsSync2(path))
4814
+ if (!existsSync3(path))
4794
4815
  return;
4795
4816
  try {
4796
4817
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -5063,7 +5084,7 @@ async function startDaemon(opts) {
5063
5084
  }
5064
5085
 
5065
5086
  // src/daemon/install.ts
5066
- import { chmodSync, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5087
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5067
5088
  import { spawnSync as spawnSync3 } from "child_process";
5068
5089
  import { dirname as dirname3 } from "path";
5069
5090
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
@@ -5122,7 +5143,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
5122
5143
  </dict>
5123
5144
  </plist>
5124
5145
  `);
5125
- chmodSync(path, 384);
5146
+ chmodSync2(path, 384);
5126
5147
  return {
5127
5148
  platform: process.platform,
5128
5149
  path,
@@ -5716,7 +5737,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5716
5737
  // package.json
5717
5738
  var package_default = {
5718
5739
  name: "@hasna/loops",
5719
- version: "0.3.43",
5740
+ version: "0.3.45",
5720
5741
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5721
5742
  type: "module",
5722
5743
  main: "dist/index.js",
@@ -5806,7 +5827,7 @@ function packageVersion() {
5806
5827
 
5807
5828
  // src/lib/templates.ts
5808
5829
  import { execFileSync } from "child_process";
5809
- import { existsSync as existsSync3 } from "fs";
5830
+ import { existsSync as existsSync4 } from "fs";
5810
5831
  import { homedir as homedir3 } from "os";
5811
5832
  import { basename as basename3, isAbsolute, join as join5, relative, resolve } from "path";
5812
5833
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
@@ -5829,6 +5850,7 @@ var TEMPLATE_SUMMARIES = [
5829
5850
  { name: "taskId", required: true, description: "Todos task id to execute." },
5830
5851
  { name: "taskTitle", description: "Human-readable task title." },
5831
5852
  { name: "projectPath", required: true, description: "Repository or project working directory." },
5853
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
5832
5854
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
5833
5855
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
5834
5856
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -5839,6 +5861,7 @@ var TEMPLATE_SUMMARIES = [
5839
5861
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5840
5862
  { name: "model", description: "Provider model." },
5841
5863
  { name: "variant", description: "Provider reasoning/model effort variant." },
5864
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5842
5865
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5843
5866
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5844
5867
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5868,6 +5891,7 @@ var TEMPLATE_SUMMARIES = [
5868
5891
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5869
5892
  { name: "model", description: "Provider model." },
5870
5893
  { name: "variant", description: "Provider reasoning/model effort variant." },
5894
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5871
5895
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5872
5896
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5873
5897
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -6066,7 +6090,7 @@ function defaultWorktreeRoot(root) {
6066
6090
  return join5(homedir3(), ".hasna", "loops", "worktrees");
6067
6091
  }
6068
6092
  function gitRootFor(path) {
6069
- if (!existsSync3(path))
6093
+ if (!existsSync4(path))
6070
6094
  return;
6071
6095
  try {
6072
6096
  return execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], {
@@ -6248,6 +6272,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6248
6272
  model: input.model,
6249
6273
  variant: input.variant,
6250
6274
  agent: input.agent,
6275
+ addDirs: input.addDirs,
6251
6276
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
6252
6277
  configIsolation: "safe",
6253
6278
  permissionMode: input.permissionMode ?? "bypass",
@@ -6291,7 +6316,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6291
6316
  throw new Error("taskId is required");
6292
6317
  if (!input.projectPath?.trim())
6293
6318
  throw new Error("projectPath is required");
6294
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
6319
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
6295
6320
  const plan = worktreePlan(input, input.taskId);
6296
6321
  const taskContext = {
6297
6322
  taskId: input.taskId,
@@ -6525,8 +6550,9 @@ function renderLifecycleBoundedTemplate(id, values) {
6525
6550
  model: values.model,
6526
6551
  variant: values.variant,
6527
6552
  agent: values.agent,
6553
+ addDirs: listVar(values.addDirs ?? values.addDir),
6528
6554
  permissionMode: values.permissionMode,
6529
- sandbox: values.sandbox,
6555
+ sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
6530
6556
  manualBreakGlass: booleanVar(values.manualBreakGlass),
6531
6557
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
6532
6558
  worktreeRoot: values.worktreeRoot,
@@ -6641,6 +6667,7 @@ function renderLoopTemplate(id, values) {
6641
6667
  taskTitle: values.taskTitle,
6642
6668
  taskDescription: values.taskDescription,
6643
6669
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
6670
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
6644
6671
  routeProjectPath: values.routeProjectPath,
6645
6672
  projectGroup: values.projectGroup,
6646
6673
  provider: values.provider,
@@ -6653,6 +6680,7 @@ function renderLoopTemplate(id, values) {
6653
6680
  model: values.model,
6654
6681
  variant: values.variant,
6655
6682
  agent: values.agent,
6683
+ addDirs: listVar(values.addDirs ?? values.addDir),
6656
6684
  permissionMode: values.permissionMode,
6657
6685
  sandbox: values.sandbox,
6658
6686
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6684,6 +6712,7 @@ function renderLoopTemplate(id, values) {
6684
6712
  model: values.model,
6685
6713
  variant: values.variant,
6686
6714
  agent: values.agent,
6715
+ addDirs: listVar(values.addDirs ?? values.addDir),
6687
6716
  permissionMode: values.permissionMode,
6688
6717
  sandbox: values.sandbox,
6689
6718
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6710,6 +6739,7 @@ function renderLoopTemplate(id, values) {
6710
6739
  model: values.model,
6711
6740
  variant: values.variant,
6712
6741
  agent: values.agent,
6742
+ addDirs: listVar(values.addDirs ?? values.addDir),
6713
6743
  permissionMode: values.permissionMode,
6714
6744
  sandbox: values.sandbox,
6715
6745
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6922,6 +6952,10 @@ function allowlistFromOpts(opts) {
6922
6952
  enforcement: "metadata_only"
6923
6953
  };
6924
6954
  }
6955
+ function listFromRepeatedOpts(value) {
6956
+ const values = (value ?? []).flatMap((entry) => splitList(entry) ?? []);
6957
+ return values.length ? values : undefined;
6958
+ }
6925
6959
  function accountPoolFromOpts(opts) {
6926
6960
  return splitList(opts.accountPool)?.map((profile) => ({ profile, tool: opts.accountTool }));
6927
6961
  }
@@ -7026,7 +7060,7 @@ function routeCursorsPath() {
7026
7060
  }
7027
7061
  function readRouteCursors() {
7028
7062
  const path = routeCursorsPath();
7029
- if (!existsSync4(path))
7063
+ if (!existsSync5(path))
7030
7064
  return {};
7031
7065
  try {
7032
7066
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
@@ -7525,6 +7559,7 @@ function routeTodosTaskEvent(event, opts) {
7525
7559
  model: opts.model,
7526
7560
  variant: opts.variant,
7527
7561
  agent: opts.agent,
7562
+ addDirs: listFromRepeatedOpts(opts.addDir),
7528
7563
  permissionMode,
7529
7564
  sandbox,
7530
7565
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -7532,7 +7567,8 @@ function routeTodosTaskEvent(event, opts) {
7532
7567
  worktreeRoot: opts.worktreeRoot,
7533
7568
  worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
7534
7569
  eventId: event.id,
7535
- eventType: event.type
7570
+ eventType: event.type,
7571
+ todosProjectPath: opts.todosProject
7536
7572
  });
7537
7573
  workflowBody.name = workflowName;
7538
7574
  workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
@@ -7737,6 +7773,7 @@ function routeGenericEvent(event, opts) {
7737
7773
  model: opts.model,
7738
7774
  variant: opts.variant,
7739
7775
  agent: opts.agent,
7776
+ addDirs: listFromRepeatedOpts(opts.addDir),
7740
7777
  permissionMode,
7741
7778
  sandbox,
7742
7779
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -8099,7 +8136,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8099
8136
  store.close();
8100
8137
  }
8101
8138
  });
8102
- 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("--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) => {
8139
+ 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) => {
8103
8140
  const provider = opts.provider;
8104
8141
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
8105
8142
  throw new Error("unsupported provider");
@@ -8118,6 +8155,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8118
8155
  variant: opts.variant,
8119
8156
  agent: opts.agent,
8120
8157
  authProfile: providerAuthProfileFromOpts(opts, provider),
8158
+ addDirs: listFromRepeatedOpts(opts.addDir),
8121
8159
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
8122
8160
  configIsolation: opts.configIsolation,
8123
8161
  permissionMode: permissionModeFromOpts(opts, provider),
@@ -8158,10 +8196,10 @@ var events = program.command("events").description("handle Hasna event envelopes
8158
8196
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
8159
8197
  var goal = program.command("goal").description("inspect goal runs");
8160
8198
  function addRouteEventOptions(command) {
8161
- return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
8199
+ return command.option("--event-file <file>", "read event envelope JSON from a file instead of stdin/HASNA_EVENT_JSON").option("--event-json <json>", "read event envelope JSON from this string instead of stdin/HASNA_EVENT_JSON").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix").option("--preflight", "check generated workflow steps before storing the workflow loop");
8162
8200
  }
8163
8201
  function addTodosDrainOptions(command) {
8164
- return command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
8202
+ return command.option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything");
8165
8203
  }
8166
8204
  function routeEventByKind(kind, event, opts) {
8167
8205
  if (kind === "todos-task")
@@ -8203,6 +8241,8 @@ function routeDrainArgs(opts) {
8203
8241
  add("--model", opts.model);
8204
8242
  add("--variant", opts.variant);
8205
8243
  add("--agent", opts.agent);
8244
+ for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
8245
+ add("--add-dir", dir);
8206
8246
  add("--permission-mode", opts.permissionMode);
8207
8247
  add("--sandbox", opts.sandbox);
8208
8248
  addBool("--manual-break-glass", opts.manualBreakGlass);
@@ -8421,13 +8461,13 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
8421
8461
  }
8422
8462
  });
8423
8463
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
8424
- eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
8464
+ eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--todos-project <path>", "todos storage project path for generated task commands", defaultLoopsProject()).option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
8425
8465
  const event = await readEventEnvelopeFromStdin();
8426
8466
  const result = routeTodosTaskEvent(event, opts);
8427
8467
  print(result.value, result.human);
8428
8468
  });
8429
8469
  var eventsDrain = events.command("drain").description("drain durable source queues into bounded OpenLoops workflows");
8430
- eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops").option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything").action((opts) => {
8470
+ eventsDrain.command("todos-task").description("drain ready todos tasks into bounded worker/verifier workflow loops").option("--todos-project <path>", "todos storage project path", defaultLoopsProject()).option("--todos-project-id <id>", "filter todos ready output to one todos project id").option("--task-list <id-or-slug>", "filter ready tasks to one task-list id, slug, or name").option("--project-path-prefix <path>", "filter ready tasks to a project/repo path prefix").option("--tags <tags>", "require all comma-separated tags before routing").option("--tag <tags>", "alias for --tags").option("--limit <n>", "maximum filtered ready-task candidates to consider", "50").option("--scan-limit <n>", "maximum raw todos ready rows to fetch before filters; defaults to 500 when filters are used").option("--max-dispatch <n>", "maximum new workflow loops to create in this drain run", "1").option("--evidence-dir <path>", "write a JSON drain report to this directory").option("--compact", "print compact JSON to stdout while preserving the full evidence file").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated task worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing workflow loops").option("--dry-run", "preview selected tasks and generated workflow loops without storing anything").action((opts) => {
8431
8471
  const maxDispatch = positiveInteger(opts.maxDispatch ?? "1", "--max-dispatch") ?? 1;
8432
8472
  const todosProject = opts.todosProject ?? defaultLoopsProject();
8433
8473
  const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
@@ -8511,7 +8551,7 @@ eventsDrain.command("todos-task").description("drain ready todos tasks into boun
8511
8551
  } : { ...report, evidencePath };
8512
8552
  print(output, `drained todos ready queue: considered=${report.considered} created=${report.created} deduped=${report.deduped} throttled=${report.throttled} skipped=${report.skipped}`);
8513
8553
  });
8514
- eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
8554
+ eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--add-dir <dir>", "additional writable directory for provider sandboxes; may be repeated or comma-separated", collectValues, []).option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--manual-break-glass", "allow danger-full-access in generated worker/verifier workflow metadata; for explicit operator emergency use only").option("--project-path <path>", "fallback project/repo working directory").option("--project-group <name>", "optional project group for concurrency limits").option("--max-active <n>", "skip creating a workflow when this many active routed workflows already exist globally").option("--max-active-per-project <n>", "skip creating a workflow when this many active routed workflows already exist for the project").option("--max-active-per-project-group <n>", "skip creating a workflow when this many active routed workflows already exist for the project group").option("--worktree-mode <mode>", "worktree isolation mode: auto, required, off, or main", "auto").option("--worktree-root <path>", "base directory for OpenLoops-managed git worktrees").option("--worktree-branch-prefix <prefix>", "branch prefix for generated event worktrees", "openloops").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
8515
8555
  const event = await readEventEnvelopeFromStdin();
8516
8556
  const data = eventData(event);
8517
8557
  const metadata = eventMetadata(event);
@@ -9538,7 +9578,7 @@ ${result.instructions.join(`
9538
9578
  });
9539
9579
  daemon.command("logs").option("-n, --lines <n>", "lines", "80").action((opts) => {
9540
9580
  const path = daemonLogPath();
9541
- if (!existsSync4(path)) {
9581
+ if (!existsSync5(path)) {
9542
9582
  console.log("");
9543
9583
  return;
9544
9584
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/lib/store.ts
5
5
  import { Database } from "bun:sqlite";
6
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
6
+ import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
7
7
  import { tmpdir } from "os";
8
8
  import { dirname, join as join3 } from "path";
9
9
 
@@ -388,6 +388,7 @@ function validateTarget(value, label) {
388
388
  }
389
389
  if (value.variant !== undefined)
390
390
  assertString(value.variant, `${label}.variant`);
391
+ optionalStringArray(value.addDirs, `${label}.addDirs`);
391
392
  if (value.permissionMode !== undefined) {
392
393
  assertString(value.permissionMode, `${label}.permissionMode`);
393
394
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -824,6 +825,20 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
824
825
  }
825
826
  return;
826
827
  }
828
+ function chmodIfExists(path, mode) {
829
+ try {
830
+ if (existsSync(path))
831
+ chmodSync(path, mode);
832
+ } catch {}
833
+ }
834
+ function ensurePrivateStorePath(file) {
835
+ const dir = dirname(file);
836
+ mkdirSync3(dir, { recursive: true, mode: 448 });
837
+ chmodIfExists(dir, 448);
838
+ chmodIfExists(file, 384);
839
+ chmodIfExists(`${file}-wal`, 384);
840
+ chmodIfExists(`${file}-shm`, 384);
841
+ }
827
842
 
828
843
  class Store {
829
844
  db;
@@ -831,11 +846,13 @@ class Store {
831
846
  constructor(path) {
832
847
  const file = path ?? dbPath();
833
848
  if (file !== ":memory:")
834
- mkdirSync3(dirname(file), { recursive: true, mode: 448 });
849
+ ensurePrivateStorePath(file);
835
850
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
836
851
  this.db = new Database(file);
837
852
  this.db.exec("PRAGMA busy_timeout = 5000;");
838
853
  this.db.exec("PRAGMA journal_mode = WAL;");
854
+ if (file !== ":memory:")
855
+ ensurePrivateStorePath(file);
839
856
  this.migrate();
840
857
  }
841
858
  migrate() {
@@ -2675,7 +2692,7 @@ import { resolveMachineCommand } from "@hasna/machines/consumer";
2675
2692
 
2676
2693
  // src/lib/accounts.ts
2677
2694
  import { spawnSync } from "child_process";
2678
- import { existsSync } from "fs";
2695
+ import { existsSync as existsSync2 } from "fs";
2679
2696
  var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
2680
2697
  function accountToolForProvider(provider) {
2681
2698
  switch (provider) {
@@ -2760,7 +2777,7 @@ function resolveAccountEnv(account, toolHint, env) {
2760
2777
  const profileDir = (accountDirEnvVar(tool) ? accountEnv[accountDirEnvVar(tool)] : undefined) ?? primaryAccountDir(result.stdout);
2761
2778
  if (!profileDir)
2762
2779
  throw new Error(`accounts env returned no profile directory for ${account.profile}/${tool}`);
2763
- if (!existsSync(profileDir))
2780
+ if (!existsSync2(profileDir))
2764
2781
  throw new Error(`account profile directory does not exist for ${account.profile}/${tool}: ${profileDir}`);
2765
2782
  return {
2766
2783
  ...accountEnv,
@@ -3115,6 +3132,8 @@ function agentArgs(target) {
3115
3132
  args.push("--ignore-rules");
3116
3133
  if (target.cwd)
3117
3134
  args.push("--cd", target.cwd);
3135
+ for (const dir of target.addDirs ?? [])
3136
+ args.push("--add-dir", dir);
3118
3137
  if (target.model)
3119
3138
  args.push("--model", target.model);
3120
3139
  if (target.agent)
@@ -3124,11 +3143,13 @@ function agentArgs(target) {
3124
3143
  case "codex":
3125
3144
  if (target.variant)
3126
3145
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3127
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3146
+ args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3128
3147
  if (isolation === "safe")
3129
3148
  args.push("--ignore-rules");
3130
3149
  if (target.cwd)
3131
3150
  args.push("--cd", target.cwd);
3151
+ for (const dir of target.addDirs ?? [])
3152
+ args.push("--add-dir", dir);
3132
3153
  if (target.model)
3133
3154
  args.push("--model", target.model);
3134
3155
  args.push(...target.extraArgs ?? []);
@@ -4646,7 +4667,7 @@ async function tick(deps) {
4646
4667
  }
4647
4668
 
4648
4669
  // src/daemon/control.ts
4649
- import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4670
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4650
4671
  import { hostname } from "os";
4651
4672
  import { dirname as dirname2 } from "path";
4652
4673
 
@@ -4674,7 +4695,7 @@ async function runLoop(opts) {
4674
4695
 
4675
4696
  // src/daemon/control.ts
4676
4697
  function readPid(path = pidFilePath()) {
4677
- if (!existsSync2(path))
4698
+ if (!existsSync3(path))
4678
4699
  return;
4679
4700
  try {
4680
4701
  const pid = Number(readFileSync(path, "utf8").trim());
@@ -4944,7 +4965,7 @@ async function startDaemon(opts) {
4944
4965
  }
4945
4966
 
4946
4967
  // src/daemon/install.ts
4947
- import { chmodSync, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
4968
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
4948
4969
  import { spawnSync as spawnSync3 } from "child_process";
4949
4970
  import { dirname as dirname3 } from "path";
4950
4971
  function installStartup(cliEntry, execPath = process.execPath, args = ["daemon", "run"]) {
@@ -5003,7 +5024,7 @@ ${args.map((arg) => ` <string>${arg}</string>`).join(`
5003
5024
  </dict>
5004
5025
  </plist>
5005
5026
  `);
5006
- chmodSync(path, 384);
5027
+ chmodSync2(path, 384);
5007
5028
  return {
5008
5029
  platform: process.platform,
5009
5030
  path,
@@ -5030,7 +5051,7 @@ function enableStartup(result) {
5030
5051
  // package.json
5031
5052
  var package_default = {
5032
5053
  name: "@hasna/loops",
5033
- version: "0.3.43",
5054
+ version: "0.3.45",
5034
5055
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5035
5056
  type: "module",
5036
5057
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/lib/store.ts
3
3
  import { Database } from "bun:sqlite";
4
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
4
+ import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
5
5
  import { tmpdir } from "os";
6
6
  import { dirname, join as join3 } from "path";
7
7
 
@@ -386,6 +386,7 @@ function validateTarget(value, label) {
386
386
  }
387
387
  if (value.variant !== undefined)
388
388
  assertString(value.variant, `${label}.variant`);
389
+ optionalStringArray(value.addDirs, `${label}.addDirs`);
389
390
  if (value.permissionMode !== undefined) {
390
391
  assertString(value.permissionMode, `${label}.permissionMode`);
391
392
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -822,6 +823,20 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
822
823
  }
823
824
  return;
824
825
  }
826
+ function chmodIfExists(path, mode) {
827
+ try {
828
+ if (existsSync(path))
829
+ chmodSync(path, mode);
830
+ } catch {}
831
+ }
832
+ function ensurePrivateStorePath(file) {
833
+ const dir = dirname(file);
834
+ mkdirSync3(dir, { recursive: true, mode: 448 });
835
+ chmodIfExists(dir, 448);
836
+ chmodIfExists(file, 384);
837
+ chmodIfExists(`${file}-wal`, 384);
838
+ chmodIfExists(`${file}-shm`, 384);
839
+ }
825
840
 
826
841
  class Store {
827
842
  db;
@@ -829,11 +844,13 @@ class Store {
829
844
  constructor(path) {
830
845
  const file = path ?? dbPath();
831
846
  if (file !== ":memory:")
832
- mkdirSync3(dirname(file), { recursive: true, mode: 448 });
847
+ ensurePrivateStorePath(file);
833
848
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
834
849
  this.db = new Database(file);
835
850
  this.db.exec("PRAGMA busy_timeout = 5000;");
836
851
  this.db.exec("PRAGMA journal_mode = WAL;");
852
+ if (file !== ":memory:")
853
+ ensurePrivateStorePath(file);
837
854
  this.migrate();
838
855
  }
839
856
  migrate() {
@@ -2665,7 +2682,7 @@ import { resolveMachineCommand } from "@hasna/machines/consumer";
2665
2682
 
2666
2683
  // src/lib/accounts.ts
2667
2684
  import { spawnSync } from "child_process";
2668
- import { existsSync } from "fs";
2685
+ import { existsSync as existsSync2 } from "fs";
2669
2686
  var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
2670
2687
  function accountToolForProvider(provider) {
2671
2688
  switch (provider) {
@@ -2750,7 +2767,7 @@ function resolveAccountEnv(account, toolHint, env) {
2750
2767
  const profileDir = (accountDirEnvVar(tool) ? accountEnv[accountDirEnvVar(tool)] : undefined) ?? primaryAccountDir(result.stdout);
2751
2768
  if (!profileDir)
2752
2769
  throw new Error(`accounts env returned no profile directory for ${account.profile}/${tool}`);
2753
- if (!existsSync(profileDir))
2770
+ if (!existsSync2(profileDir))
2754
2771
  throw new Error(`account profile directory does not exist for ${account.profile}/${tool}: ${profileDir}`);
2755
2772
  return {
2756
2773
  ...accountEnv,
@@ -3105,6 +3122,8 @@ function agentArgs(target) {
3105
3122
  args.push("--ignore-rules");
3106
3123
  if (target.cwd)
3107
3124
  args.push("--cd", target.cwd);
3125
+ for (const dir of target.addDirs ?? [])
3126
+ args.push("--add-dir", dir);
3108
3127
  if (target.model)
3109
3128
  args.push("--model", target.model);
3110
3129
  if (target.agent)
@@ -3114,11 +3133,13 @@ function agentArgs(target) {
3114
3133
  case "codex":
3115
3134
  if (target.variant)
3116
3135
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3117
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3136
+ args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3118
3137
  if (isolation === "safe")
3119
3138
  args.push("--ignore-rules");
3120
3139
  if (target.cwd)
3121
3140
  args.push("--cd", target.cwd);
3141
+ for (const dir of target.addDirs ?? [])
3142
+ args.push("--add-dir", dir);
3122
3143
  if (target.model)
3123
3144
  args.push("--model", target.model);
3124
3145
  args.push(...target.extraArgs ?? []);
@@ -4767,7 +4788,7 @@ function openAutomationsRuntimeBinding(overrides = {}) {
4767
4788
  }
4768
4789
  // src/lib/templates.ts
4769
4790
  import { execFileSync } from "child_process";
4770
- import { existsSync as existsSync2 } from "fs";
4791
+ import { existsSync as existsSync3 } from "fs";
4771
4792
  import { homedir as homedir3 } from "os";
4772
4793
  import { basename as basename2, isAbsolute, join as join5, relative, resolve } from "path";
4773
4794
  var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
@@ -4790,6 +4811,7 @@ var TEMPLATE_SUMMARIES = [
4790
4811
  { name: "taskId", required: true, description: "Todos task id to execute." },
4791
4812
  { name: "taskTitle", description: "Human-readable task title." },
4792
4813
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4814
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
4793
4815
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4794
4816
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4795
4817
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -4800,6 +4822,7 @@ var TEMPLATE_SUMMARIES = [
4800
4822
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4801
4823
  { name: "model", description: "Provider model." },
4802
4824
  { name: "variant", description: "Provider reasoning/model effort variant." },
4825
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4803
4826
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4804
4827
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4805
4828
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -4829,6 +4852,7 @@ var TEMPLATE_SUMMARIES = [
4829
4852
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4830
4853
  { name: "model", description: "Provider model." },
4831
4854
  { name: "variant", description: "Provider reasoning/model effort variant." },
4855
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4832
4856
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4833
4857
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4834
4858
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5027,7 +5051,7 @@ function defaultWorktreeRoot(root) {
5027
5051
  return join5(homedir3(), ".hasna", "loops", "worktrees");
5028
5052
  }
5029
5053
  function gitRootFor(path) {
5030
- if (!existsSync2(path))
5054
+ if (!existsSync3(path))
5031
5055
  return;
5032
5056
  try {
5033
5057
  return execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], {
@@ -5209,6 +5233,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5209
5233
  model: input.model,
5210
5234
  variant: input.variant,
5211
5235
  agent: input.agent,
5236
+ addDirs: input.addDirs,
5212
5237
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
5213
5238
  configIsolation: "safe",
5214
5239
  permissionMode: input.permissionMode ?? "bypass",
@@ -5252,7 +5277,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
5252
5277
  throw new Error("taskId is required");
5253
5278
  if (!input.projectPath?.trim())
5254
5279
  throw new Error("projectPath is required");
5255
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
5280
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
5256
5281
  const plan = worktreePlan(input, input.taskId);
5257
5282
  const taskContext = {
5258
5283
  taskId: input.taskId,
@@ -5486,8 +5511,9 @@ function renderLifecycleBoundedTemplate(id, values) {
5486
5511
  model: values.model,
5487
5512
  variant: values.variant,
5488
5513
  agent: values.agent,
5514
+ addDirs: listVar(values.addDirs ?? values.addDir),
5489
5515
  permissionMode: values.permissionMode,
5490
- sandbox: values.sandbox,
5516
+ sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
5491
5517
  manualBreakGlass: booleanVar(values.manualBreakGlass),
5492
5518
  worktreeMode: values.worktreeMode ?? (id === REPORT_ONLY_TEMPLATE_ID ? "main" : "required"),
5493
5519
  worktreeRoot: values.worktreeRoot,
@@ -5602,6 +5628,7 @@ function renderLoopTemplate(id, values) {
5602
5628
  taskTitle: values.taskTitle,
5603
5629
  taskDescription: values.taskDescription,
5604
5630
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
5631
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
5605
5632
  routeProjectPath: values.routeProjectPath,
5606
5633
  projectGroup: values.projectGroup,
5607
5634
  provider: values.provider,
@@ -5614,6 +5641,7 @@ function renderLoopTemplate(id, values) {
5614
5641
  model: values.model,
5615
5642
  variant: values.variant,
5616
5643
  agent: values.agent,
5644
+ addDirs: listVar(values.addDirs ?? values.addDir),
5617
5645
  permissionMode: values.permissionMode,
5618
5646
  sandbox: values.sandbox,
5619
5647
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5645,6 +5673,7 @@ function renderLoopTemplate(id, values) {
5645
5673
  model: values.model,
5646
5674
  variant: values.variant,
5647
5675
  agent: values.agent,
5676
+ addDirs: listVar(values.addDirs ?? values.addDir),
5648
5677
  permissionMode: values.permissionMode,
5649
5678
  sandbox: values.sandbox,
5650
5679
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5671,6 +5700,7 @@ function renderLoopTemplate(id, values) {
5671
5700
  model: values.model,
5672
5701
  variant: values.variant,
5673
5702
  agent: values.agent,
5703
+ addDirs: listVar(values.addDirs ?? values.addDir),
5674
5704
  permissionMode: values.permissionMode,
5675
5705
  sandbox: values.sandbox,
5676
5706
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5704,7 +5734,7 @@ import { spawnSync as spawnSync3 } from "child_process";
5704
5734
  import { accessSync as accessSync2, constants as constants2 } from "fs";
5705
5735
 
5706
5736
  // src/daemon/control.ts
5707
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5737
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5708
5738
  import { hostname } from "os";
5709
5739
  import { dirname as dirname2 } from "path";
5710
5740
 
@@ -5732,7 +5762,7 @@ async function runLoop(opts) {
5732
5762
 
5733
5763
  // src/daemon/control.ts
5734
5764
  function readPid(path = pidFilePath()) {
5735
- if (!existsSync3(path))
5765
+ if (!existsSync4(path))
5736
5766
  return;
5737
5767
  try {
5738
5768
  const pid = Number(readFileSync(path, "utf8").trim());
package/dist/lib/store.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/lib/store.ts
3
3
  import { Database } from "bun:sqlite";
4
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
4
+ import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
5
5
  import { tmpdir } from "os";
6
6
  import { dirname, join as join3 } from "path";
7
7
 
@@ -386,6 +386,7 @@ function validateTarget(value, label) {
386
386
  }
387
387
  if (value.variant !== undefined)
388
388
  assertString(value.variant, `${label}.variant`);
389
+ optionalStringArray(value.addDirs, `${label}.addDirs`);
389
390
  if (value.permissionMode !== undefined) {
390
391
  assertString(value.permissionMode, `${label}.permissionMode`);
391
392
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -822,6 +823,20 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
822
823
  }
823
824
  return;
824
825
  }
826
+ function chmodIfExists(path, mode) {
827
+ try {
828
+ if (existsSync(path))
829
+ chmodSync(path, mode);
830
+ } catch {}
831
+ }
832
+ function ensurePrivateStorePath(file) {
833
+ const dir = dirname(file);
834
+ mkdirSync3(dir, { recursive: true, mode: 448 });
835
+ chmodIfExists(dir, 448);
836
+ chmodIfExists(file, 384);
837
+ chmodIfExists(`${file}-wal`, 384);
838
+ chmodIfExists(`${file}-shm`, 384);
839
+ }
825
840
 
826
841
  class Store {
827
842
  db;
@@ -829,11 +844,13 @@ class Store {
829
844
  constructor(path) {
830
845
  const file = path ?? dbPath();
831
846
  if (file !== ":memory:")
832
- mkdirSync3(dirname(file), { recursive: true, mode: 448 });
847
+ ensurePrivateStorePath(file);
833
848
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
834
849
  this.db = new Database(file);
835
850
  this.db.exec("PRAGMA busy_timeout = 5000;");
836
851
  this.db.exec("PRAGMA journal_mode = WAL;");
852
+ if (file !== ":memory:")
853
+ ensurePrivateStorePath(file);
837
854
  this.migrate();
838
855
  }
839
856
  migrate() {
@@ -14,6 +14,7 @@ export interface TodosTaskWorkflowTemplateInput {
14
14
  taskTitle?: string;
15
15
  taskDescription?: string;
16
16
  projectPath: string;
17
+ todosProjectPath?: string;
17
18
  routeProjectPath?: string;
18
19
  projectGroup?: string;
19
20
  provider?: AgentProvider;
@@ -28,6 +29,7 @@ export interface TodosTaskWorkflowTemplateInput {
28
29
  model?: string;
29
30
  variant?: string;
30
31
  agent?: string;
32
+ addDirs?: string[];
31
33
  permissionMode?: AgentPermissionMode;
32
34
  sandbox?: AgentSandbox;
33
35
  manualBreakGlass?: boolean;
@@ -59,6 +61,7 @@ export interface EventWorkflowTemplateInput {
59
61
  model?: string;
60
62
  variant?: string;
61
63
  agent?: string;
64
+ addDirs?: string[];
62
65
  permissionMode?: AgentPermissionMode;
63
66
  sandbox?: AgentSandbox;
64
67
  manualBreakGlass?: boolean;
@@ -85,6 +88,7 @@ export interface BoundedAgentWorkflowTemplateInput {
85
88
  model?: string;
86
89
  variant?: string;
87
90
  agent?: string;
91
+ addDirs?: string[];
88
92
  permissionMode?: AgentPermissionMode;
89
93
  sandbox?: AgentSandbox;
90
94
  manualBreakGlass?: boolean;
package/dist/sdk/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/lib/store.ts
3
3
  import { Database } from "bun:sqlite";
4
- import { mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
4
+ import { chmodSync, existsSync, mkdirSync as mkdirSync3, mkdtempSync, rmSync } from "fs";
5
5
  import { tmpdir } from "os";
6
6
  import { dirname, join as join3 } from "path";
7
7
 
@@ -386,6 +386,7 @@ function validateTarget(value, label) {
386
386
  }
387
387
  if (value.variant !== undefined)
388
388
  assertString(value.variant, `${label}.variant`);
389
+ optionalStringArray(value.addDirs, `${label}.addDirs`);
389
390
  if (value.permissionMode !== undefined) {
390
391
  assertString(value.permissionMode, `${label}.permissionMode`);
391
392
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -822,6 +823,20 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
822
823
  }
823
824
  return;
824
825
  }
826
+ function chmodIfExists(path, mode) {
827
+ try {
828
+ if (existsSync(path))
829
+ chmodSync(path, mode);
830
+ } catch {}
831
+ }
832
+ function ensurePrivateStorePath(file) {
833
+ const dir = dirname(file);
834
+ mkdirSync3(dir, { recursive: true, mode: 448 });
835
+ chmodIfExists(dir, 448);
836
+ chmodIfExists(file, 384);
837
+ chmodIfExists(`${file}-wal`, 384);
838
+ chmodIfExists(`${file}-shm`, 384);
839
+ }
825
840
 
826
841
  class Store {
827
842
  db;
@@ -829,11 +844,13 @@ class Store {
829
844
  constructor(path) {
830
845
  const file = path ?? dbPath();
831
846
  if (file !== ":memory:")
832
- mkdirSync3(dirname(file), { recursive: true, mode: 448 });
847
+ ensurePrivateStorePath(file);
833
848
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname(file);
834
849
  this.db = new Database(file);
835
850
  this.db.exec("PRAGMA busy_timeout = 5000;");
836
851
  this.db.exec("PRAGMA journal_mode = WAL;");
852
+ if (file !== ":memory:")
853
+ ensurePrivateStorePath(file);
837
854
  this.migrate();
838
855
  }
839
856
  migrate() {
@@ -2665,7 +2682,7 @@ import { resolveMachineCommand } from "@hasna/machines/consumer";
2665
2682
 
2666
2683
  // src/lib/accounts.ts
2667
2684
  import { spawnSync } from "child_process";
2668
- import { existsSync } from "fs";
2685
+ import { existsSync as existsSync2 } from "fs";
2669
2686
  var EXPORT_RE = /^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
2670
2687
  function accountToolForProvider(provider) {
2671
2688
  switch (provider) {
@@ -2750,7 +2767,7 @@ function resolveAccountEnv(account, toolHint, env) {
2750
2767
  const profileDir = (accountDirEnvVar(tool) ? accountEnv[accountDirEnvVar(tool)] : undefined) ?? primaryAccountDir(result.stdout);
2751
2768
  if (!profileDir)
2752
2769
  throw new Error(`accounts env returned no profile directory for ${account.profile}/${tool}`);
2753
- if (!existsSync(profileDir))
2770
+ if (!existsSync2(profileDir))
2754
2771
  throw new Error(`account profile directory does not exist for ${account.profile}/${tool}: ${profileDir}`);
2755
2772
  return {
2756
2773
  ...accountEnv,
@@ -3105,6 +3122,8 @@ function agentArgs(target) {
3105
3122
  args.push("--ignore-rules");
3106
3123
  if (target.cwd)
3107
3124
  args.push("--cd", target.cwd);
3125
+ for (const dir of target.addDirs ?? [])
3126
+ args.push("--add-dir", dir);
3108
3127
  if (target.model)
3109
3128
  args.push("--model", target.model);
3110
3129
  if (target.agent)
@@ -3114,11 +3133,13 @@ function agentArgs(target) {
3114
3133
  case "codex":
3115
3134
  if (target.variant)
3116
3135
  args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
3117
- args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
3136
+ args.push("exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
3118
3137
  if (isolation === "safe")
3119
3138
  args.push("--ignore-rules");
3120
3139
  if (target.cwd)
3121
3140
  args.push("--cd", target.cwd);
3141
+ for (const dir of target.addDirs ?? [])
3142
+ args.push("--add-dir", dir);
3122
3143
  if (target.model)
3123
3144
  args.push("--model", target.model);
3124
3145
  args.push(...target.extraArgs ?? []);
package/dist/types.d.ts CHANGED
@@ -114,6 +114,7 @@ export interface AgentTarget {
114
114
  agent?: string;
115
115
  authProfile?: string;
116
116
  extraArgs?: string[];
117
+ addDirs?: string[];
117
118
  timeoutMs?: number;
118
119
  idleTimeoutMs?: number;
119
120
  configIsolation?: AgentConfigIsolation;
package/docs/USAGE.md CHANGED
@@ -228,7 +228,9 @@ loops templates render todos-task-worker-verifier \
228
228
  --var projectPath=/path/to/repo \
229
229
  --var provider=codewith \
230
230
  --var authProfilePool=account004,account005,account006 \
231
- --var sandbox=workspace-write
231
+ --var sandbox=workspace-write \
232
+ --var todosProjectPath=$HOME/.hasna/loops \
233
+ --var addDirs=$HOME/.hasna/todos,$HOME/.hasna/loops
232
234
  loops templates create-workflow todos-task-worker-verifier \
233
235
  --var taskId=<task-id> \
234
236
  --var projectPath=/path/to/repo
@@ -286,6 +288,8 @@ cat task-created-event.json | loops events handle todos-task \
286
288
  --auth-profile-pool account004,account005,account006 \
287
289
  --permission-mode bypass \
288
290
  --sandbox workspace-write \
291
+ --todos-project "$HOME/.hasna/loops" \
292
+ --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
289
293
  --worktree-mode required
290
294
  ```
291
295
 
@@ -322,6 +326,13 @@ idempotency key before rendering worktree plans or checking route limits. In
322
326
  dry-run mode, throttle counts are not evaluated because opening the live loop
323
327
  store can create or migrate the local database.
324
328
 
329
+ When a sandboxed Codewith/Codex worker must update app stores outside the repo
330
+ worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
331
+ For task-created routes, pass `--todos-project` so worker/verifier prompts use
332
+ the actual todos storage project while route concurrency and worktree isolation
333
+ still use the repository path. This avoids `danger-full-access` for normal
334
+ todos comments, completion state, and loop evidence writes.
335
+
325
336
  Inspect route state with:
326
337
 
327
338
  ```bash
@@ -351,12 +362,13 @@ locks, or non-pending states stay queued in todos and are not routed:
351
362
 
352
363
  ```bash
353
364
  loops events drain todos-task \
354
- --todos-project /home/hasna/.hasna/loops \
365
+ --todos-project "$HOME/.hasna/loops" \
355
366
  --task-list repoops-pr-queue \
356
367
  --tags auto:route \
357
- --project-path-prefix /home/hasna/workspace/hasna/opensource \
368
+ --project-path-prefix "$HOME/workspace/hasna/opensource" \
358
369
  --provider codewith \
359
370
  --auth-profile-pool account004,account005,account006 \
371
+ --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
360
372
  --project-group oss \
361
373
  --max-dispatch 2 \
362
374
  --scan-limit 500 \
@@ -364,7 +376,7 @@ loops events drain todos-task \
364
376
  --max-active-per-project-group 4 \
365
377
  --max-active 12 \
366
378
  --worktree-mode required \
367
- --evidence-dir /home/hasna/.hasna/loops/reports/task-drain
379
+ --evidence-dir "$HOME/.hasna/loops/reports/task-drain"
368
380
  ```
369
381
 
370
382
  `--max-dispatch` caps new workflow-loop creation per drain run. `--limit` caps
@@ -562,10 +574,10 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
562
574
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
563
575
 
564
576
  - Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
565
- - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
577
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
566
578
  - AI Copilot and OpenCode use `run --format json --pure`.
567
579
  - Cursor is CLI-first for now via `cursor agent -p`, with `agent -p` as the fallback launcher on machines that expose the standalone Cursor Agent binary; treat output as less stable until a stronger public SDK contract is selected.
568
- - Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
580
+ - Codex uses `codex exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
569
581
  - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
570
582
  - When `--account` or a step `account` is set, OpenLoops resolves `accounts env <profile> --tool <tool>` before spawning the target, strips inherited tool home/API-key variables, and applies the selected profile only to that process. Missing account profiles fail before the provider binary receives the prompt.
571
583
  - `--auth-profile` and step `authProfile` are provider-native auth selectors. They currently apply to Codewith and are passed to Codewith as `--auth-profile <name>` before `exec`; they do not call OpenAccounts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.43",
3
+ "version": "0.3.45",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",