@hasna/loops 0.3.44 → 0.3.46

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,15 @@ 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. `addDirs` is intentionally
303
+ accepted only for Codewith/Codex until other providers expose equivalent
304
+ directory-scoped write controls.
305
+
293
306
  Inspect route state with:
294
307
 
295
308
  ```bash
@@ -484,10 +497,10 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
484
497
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
485
498
 
486
499
  - 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`.
500
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
488
501
  - AI Copilot and OpenCode use `run --format json --pure`.
489
- - 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`.
502
+ - Cursor is CLI-first for now via standalone `agent -p` when available, with `cursor agent -p` as a compatibility fallback; treat output as less stable until a stronger public SDK contract is selected.
503
+ - Codex uses `codex exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
491
504
  - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
492
505
  - 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
506
  - `--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
@@ -388,6 +388,10 @@ 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`);
392
+ if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
393
+ throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
394
+ }
391
395
  if (value.permissionMode !== undefined) {
392
396
  assertString(value.permissionMode, `${label}.permissionMode`);
393
397
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -3153,6 +3157,9 @@ function assertSupportedAgentOptions(target) {
3153
3157
  if (target.authProfile !== undefined && target.provider !== "codewith") {
3154
3158
  throw new Error(`${target.provider}.authProfile is supported only for codewith`);
3155
3159
  }
3160
+ if (target.addDirs?.length && !["codewith", "codex"].includes(target.provider)) {
3161
+ throw new Error(`${target.provider}.addDirs is currently supported only for codewith or codex`);
3162
+ }
3156
3163
  if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
3157
3164
  throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
3158
3165
  }
@@ -3212,10 +3219,10 @@ function agentArgs(target) {
3212
3219
  case "cursor":
3213
3220
  args.push("-c", [
3214
3221
  "set -eu",
3215
- "if command -v cursor >/dev/null 2>&1; then",
3216
- ' exec cursor agent "$@"',
3217
- "elif command -v agent >/dev/null 2>&1; then",
3222
+ "if command -v agent >/dev/null 2>&1; then",
3218
3223
  ' exec agent "$@"',
3224
+ "elif command -v cursor >/dev/null 2>&1; then",
3225
+ ' exec cursor agent "$@"',
3219
3226
  "else",
3220
3227
  " echo 'Executable not found in PATH: cursor agent or agent' >&2",
3221
3228
  " exit 127",
@@ -3247,6 +3254,8 @@ function agentArgs(target) {
3247
3254
  args.push("--ignore-rules");
3248
3255
  if (target.cwd)
3249
3256
  args.push("--cd", target.cwd);
3257
+ for (const dir of target.addDirs ?? [])
3258
+ args.push("--add-dir", dir);
3250
3259
  if (target.model)
3251
3260
  args.push("--model", target.model);
3252
3261
  if (target.agent)
@@ -3261,6 +3270,8 @@ function agentArgs(target) {
3261
3270
  args.push("--ignore-rules");
3262
3271
  if (target.cwd)
3263
3272
  args.push("--cd", target.cwd);
3273
+ for (const dir of target.addDirs ?? [])
3274
+ args.push("--add-dir", dir);
3264
3275
  if (target.model)
3265
3276
  args.push("--model", target.model);
3266
3277
  args.push(...target.extraArgs ?? []);
@@ -5732,7 +5743,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5732
5743
  // package.json
5733
5744
  var package_default = {
5734
5745
  name: "@hasna/loops",
5735
- version: "0.3.44",
5746
+ version: "0.3.46",
5736
5747
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5737
5748
  type: "module",
5738
5749
  main: "dist/index.js",
@@ -5845,6 +5856,7 @@ var TEMPLATE_SUMMARIES = [
5845
5856
  { name: "taskId", required: true, description: "Todos task id to execute." },
5846
5857
  { name: "taskTitle", description: "Human-readable task title." },
5847
5858
  { name: "projectPath", required: true, description: "Repository or project working directory." },
5859
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
5848
5860
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
5849
5861
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
5850
5862
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -5855,6 +5867,7 @@ var TEMPLATE_SUMMARIES = [
5855
5867
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5856
5868
  { name: "model", description: "Provider model." },
5857
5869
  { name: "variant", description: "Provider reasoning/model effort variant." },
5870
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5858
5871
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5859
5872
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5860
5873
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5884,6 +5897,7 @@ var TEMPLATE_SUMMARIES = [
5884
5897
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5885
5898
  { name: "model", description: "Provider model." },
5886
5899
  { name: "variant", description: "Provider reasoning/model effort variant." },
5900
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5887
5901
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5888
5902
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5889
5903
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -6264,6 +6278,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6264
6278
  model: input.model,
6265
6279
  variant: input.variant,
6266
6280
  agent: input.agent,
6281
+ addDirs: input.addDirs,
6267
6282
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
6268
6283
  configIsolation: "safe",
6269
6284
  permissionMode: input.permissionMode ?? "bypass",
@@ -6307,7 +6322,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6307
6322
  throw new Error("taskId is required");
6308
6323
  if (!input.projectPath?.trim())
6309
6324
  throw new Error("projectPath is required");
6310
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
6325
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
6311
6326
  const plan = worktreePlan(input, input.taskId);
6312
6327
  const taskContext = {
6313
6328
  taskId: input.taskId,
@@ -6541,6 +6556,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6541
6556
  model: values.model,
6542
6557
  variant: values.variant,
6543
6558
  agent: values.agent,
6559
+ addDirs: listVar(values.addDirs ?? values.addDir),
6544
6560
  permissionMode: values.permissionMode,
6545
6561
  sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
6546
6562
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6657,6 +6673,7 @@ function renderLoopTemplate(id, values) {
6657
6673
  taskTitle: values.taskTitle,
6658
6674
  taskDescription: values.taskDescription,
6659
6675
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
6676
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
6660
6677
  routeProjectPath: values.routeProjectPath,
6661
6678
  projectGroup: values.projectGroup,
6662
6679
  provider: values.provider,
@@ -6669,6 +6686,7 @@ function renderLoopTemplate(id, values) {
6669
6686
  model: values.model,
6670
6687
  variant: values.variant,
6671
6688
  agent: values.agent,
6689
+ addDirs: listVar(values.addDirs ?? values.addDir),
6672
6690
  permissionMode: values.permissionMode,
6673
6691
  sandbox: values.sandbox,
6674
6692
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6700,6 +6718,7 @@ function renderLoopTemplate(id, values) {
6700
6718
  model: values.model,
6701
6719
  variant: values.variant,
6702
6720
  agent: values.agent,
6721
+ addDirs: listVar(values.addDirs ?? values.addDir),
6703
6722
  permissionMode: values.permissionMode,
6704
6723
  sandbox: values.sandbox,
6705
6724
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6726,6 +6745,7 @@ function renderLoopTemplate(id, values) {
6726
6745
  model: values.model,
6727
6746
  variant: values.variant,
6728
6747
  agent: values.agent,
6748
+ addDirs: listVar(values.addDirs ?? values.addDir),
6729
6749
  permissionMode: values.permissionMode,
6730
6750
  sandbox: values.sandbox,
6731
6751
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6938,6 +6958,10 @@ function allowlistFromOpts(opts) {
6938
6958
  enforcement: "metadata_only"
6939
6959
  };
6940
6960
  }
6961
+ function listFromRepeatedOpts(value) {
6962
+ const values = (value ?? []).flatMap((entry) => splitList(entry) ?? []);
6963
+ return values.length ? values : undefined;
6964
+ }
6941
6965
  function accountPoolFromOpts(opts) {
6942
6966
  return splitList(opts.accountPool)?.map((profile) => ({ profile, tool: opts.accountTool }));
6943
6967
  }
@@ -7541,6 +7565,7 @@ function routeTodosTaskEvent(event, opts) {
7541
7565
  model: opts.model,
7542
7566
  variant: opts.variant,
7543
7567
  agent: opts.agent,
7568
+ addDirs: listFromRepeatedOpts(opts.addDir),
7544
7569
  permissionMode,
7545
7570
  sandbox,
7546
7571
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -7548,7 +7573,8 @@ function routeTodosTaskEvent(event, opts) {
7548
7573
  worktreeRoot: opts.worktreeRoot,
7549
7574
  worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
7550
7575
  eventId: event.id,
7551
- eventType: event.type
7576
+ eventType: event.type,
7577
+ todosProjectPath: opts.todosProject
7552
7578
  });
7553
7579
  workflowBody.name = workflowName;
7554
7580
  workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}; ` + `idempotency=${idempotencyKey}; event=${event.id}; project=${projectPath}; projectGroup=${projectGroup ?? "-"}`;
@@ -7753,6 +7779,7 @@ function routeGenericEvent(event, opts) {
7753
7779
  model: opts.model,
7754
7780
  variant: opts.variant,
7755
7781
  agent: opts.agent,
7782
+ addDirs: listFromRepeatedOpts(opts.addDir),
7756
7783
  permissionMode,
7757
7784
  sandbox,
7758
7785
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -8115,7 +8142,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8115
8142
  store.close();
8116
8143
  }
8117
8144
  });
8118
- 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) => {
8145
+ 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) => {
8119
8146
  const provider = opts.provider;
8120
8147
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
8121
8148
  throw new Error("unsupported provider");
@@ -8134,6 +8161,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8134
8161
  variant: opts.variant,
8135
8162
  agent: opts.agent,
8136
8163
  authProfile: providerAuthProfileFromOpts(opts, provider),
8164
+ addDirs: listFromRepeatedOpts(opts.addDir),
8137
8165
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
8138
8166
  configIsolation: opts.configIsolation,
8139
8167
  permissionMode: permissionModeFromOpts(opts, provider),
@@ -8174,10 +8202,10 @@ var events = program.command("events").description("handle Hasna event envelopes
8174
8202
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
8175
8203
  var goal = program.command("goal").description("inspect goal runs");
8176
8204
  function addRouteEventOptions(command) {
8177
- 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");
8205
+ 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");
8178
8206
  }
8179
8207
  function addTodosDrainOptions(command) {
8180
- 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");
8208
+ 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");
8181
8209
  }
8182
8210
  function routeEventByKind(kind, event, opts) {
8183
8211
  if (kind === "todos-task")
@@ -8219,6 +8247,8 @@ function routeDrainArgs(opts) {
8219
8247
  add("--model", opts.model);
8220
8248
  add("--variant", opts.variant);
8221
8249
  add("--agent", opts.agent);
8250
+ for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
8251
+ add("--add-dir", dir);
8222
8252
  add("--permission-mode", opts.permissionMode);
8223
8253
  add("--sandbox", opts.sandbox);
8224
8254
  addBool("--manual-break-glass", opts.manualBreakGlass);
@@ -8437,13 +8467,13 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
8437
8467
  }
8438
8468
  });
8439
8469
  var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
8440
- 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) => {
8470
+ 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) => {
8441
8471
  const event = await readEventEnvelopeFromStdin();
8442
8472
  const result = routeTodosTaskEvent(event, opts);
8443
8473
  print(result.value, result.human);
8444
8474
  });
8445
8475
  var eventsDrain = events.command("drain").description("drain durable source queues into bounded OpenLoops workflows");
8446
- 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) => {
8476
+ 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) => {
8447
8477
  const maxDispatch = positiveInteger(opts.maxDispatch ?? "1", "--max-dispatch") ?? 1;
8448
8478
  const todosProject = opts.todosProject ?? defaultLoopsProject();
8449
8479
  const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
@@ -8527,7 +8557,7 @@ eventsDrain.command("todos-task").description("drain ready todos tasks into boun
8527
8557
  } : { ...report, evidencePath };
8528
8558
  print(output, `drained todos ready queue: considered=${report.considered} created=${report.created} deduped=${report.deduped} throttled=${report.throttled} skipped=${report.skipped}`);
8529
8559
  });
8530
- 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) => {
8560
+ 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) => {
8531
8561
  const event = await readEventEnvelopeFromStdin();
8532
8562
  const data = eventData(event);
8533
8563
  const metadata = eventMetadata(event);
@@ -8569,6 +8599,7 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
8569
8599
  model: opts.model,
8570
8600
  variant: opts.variant,
8571
8601
  agent: opts.agent,
8602
+ addDirs: listFromRepeatedOpts(opts.addDir),
8572
8603
  permissionMode,
8573
8604
  sandbox,
8574
8605
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -388,6 +388,10 @@ 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`);
392
+ if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
393
+ throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
394
+ }
391
395
  if (value.permissionMode !== undefined) {
392
396
  assertString(value.permissionMode, `${label}.permissionMode`);
393
397
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -3037,6 +3041,9 @@ function assertSupportedAgentOptions(target) {
3037
3041
  if (target.authProfile !== undefined && target.provider !== "codewith") {
3038
3042
  throw new Error(`${target.provider}.authProfile is supported only for codewith`);
3039
3043
  }
3044
+ if (target.addDirs?.length && !["codewith", "codex"].includes(target.provider)) {
3045
+ throw new Error(`${target.provider}.addDirs is currently supported only for codewith or codex`);
3046
+ }
3040
3047
  if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
3041
3048
  throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
3042
3049
  }
@@ -3096,10 +3103,10 @@ function agentArgs(target) {
3096
3103
  case "cursor":
3097
3104
  args.push("-c", [
3098
3105
  "set -eu",
3099
- "if command -v cursor >/dev/null 2>&1; then",
3100
- ' exec cursor agent "$@"',
3101
- "elif command -v agent >/dev/null 2>&1; then",
3106
+ "if command -v agent >/dev/null 2>&1; then",
3102
3107
  ' exec agent "$@"',
3108
+ "elif command -v cursor >/dev/null 2>&1; then",
3109
+ ' exec cursor agent "$@"',
3103
3110
  "else",
3104
3111
  " echo 'Executable not found in PATH: cursor agent or agent' >&2",
3105
3112
  " exit 127",
@@ -3131,6 +3138,8 @@ function agentArgs(target) {
3131
3138
  args.push("--ignore-rules");
3132
3139
  if (target.cwd)
3133
3140
  args.push("--cd", target.cwd);
3141
+ for (const dir of target.addDirs ?? [])
3142
+ args.push("--add-dir", dir);
3134
3143
  if (target.model)
3135
3144
  args.push("--model", target.model);
3136
3145
  if (target.agent)
@@ -3145,6 +3154,8 @@ function agentArgs(target) {
3145
3154
  args.push("--ignore-rules");
3146
3155
  if (target.cwd)
3147
3156
  args.push("--cd", target.cwd);
3157
+ for (const dir of target.addDirs ?? [])
3158
+ args.push("--add-dir", dir);
3148
3159
  if (target.model)
3149
3160
  args.push("--model", target.model);
3150
3161
  args.push(...target.extraArgs ?? []);
@@ -5046,7 +5057,7 @@ function enableStartup(result) {
5046
5057
  // package.json
5047
5058
  var package_default = {
5048
5059
  name: "@hasna/loops",
5049
- version: "0.3.44",
5060
+ version: "0.3.46",
5050
5061
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5051
5062
  type: "module",
5052
5063
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -386,6 +386,10 @@ 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`);
390
+ if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
391
+ throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
392
+ }
389
393
  if (value.permissionMode !== undefined) {
390
394
  assertString(value.permissionMode, `${label}.permissionMode`);
391
395
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -3027,6 +3031,9 @@ function assertSupportedAgentOptions(target) {
3027
3031
  if (target.authProfile !== undefined && target.provider !== "codewith") {
3028
3032
  throw new Error(`${target.provider}.authProfile is supported only for codewith`);
3029
3033
  }
3034
+ if (target.addDirs?.length && !["codewith", "codex"].includes(target.provider)) {
3035
+ throw new Error(`${target.provider}.addDirs is currently supported only for codewith or codex`);
3036
+ }
3030
3037
  if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
3031
3038
  throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
3032
3039
  }
@@ -3086,10 +3093,10 @@ function agentArgs(target) {
3086
3093
  case "cursor":
3087
3094
  args.push("-c", [
3088
3095
  "set -eu",
3089
- "if command -v cursor >/dev/null 2>&1; then",
3090
- ' exec cursor agent "$@"',
3091
- "elif command -v agent >/dev/null 2>&1; then",
3096
+ "if command -v agent >/dev/null 2>&1; then",
3092
3097
  ' exec agent "$@"',
3098
+ "elif command -v cursor >/dev/null 2>&1; then",
3099
+ ' exec cursor agent "$@"',
3093
3100
  "else",
3094
3101
  " echo 'Executable not found in PATH: cursor agent or agent' >&2",
3095
3102
  " exit 127",
@@ -3121,6 +3128,8 @@ function agentArgs(target) {
3121
3128
  args.push("--ignore-rules");
3122
3129
  if (target.cwd)
3123
3130
  args.push("--cd", target.cwd);
3131
+ for (const dir of target.addDirs ?? [])
3132
+ args.push("--add-dir", dir);
3124
3133
  if (target.model)
3125
3134
  args.push("--model", target.model);
3126
3135
  if (target.agent)
@@ -3135,6 +3144,8 @@ function agentArgs(target) {
3135
3144
  args.push("--ignore-rules");
3136
3145
  if (target.cwd)
3137
3146
  args.push("--cd", target.cwd);
3147
+ for (const dir of target.addDirs ?? [])
3148
+ args.push("--add-dir", dir);
3138
3149
  if (target.model)
3139
3150
  args.push("--model", target.model);
3140
3151
  args.push(...target.extraArgs ?? []);
@@ -4806,6 +4817,7 @@ var TEMPLATE_SUMMARIES = [
4806
4817
  { name: "taskId", required: true, description: "Todos task id to execute." },
4807
4818
  { name: "taskTitle", description: "Human-readable task title." },
4808
4819
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4820
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
4809
4821
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4810
4822
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4811
4823
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -4816,6 +4828,7 @@ var TEMPLATE_SUMMARIES = [
4816
4828
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4817
4829
  { name: "model", description: "Provider model." },
4818
4830
  { name: "variant", description: "Provider reasoning/model effort variant." },
4831
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4819
4832
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4820
4833
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4821
4834
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -4845,6 +4858,7 @@ var TEMPLATE_SUMMARIES = [
4845
4858
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4846
4859
  { name: "model", description: "Provider model." },
4847
4860
  { name: "variant", description: "Provider reasoning/model effort variant." },
4861
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4848
4862
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4849
4863
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4850
4864
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5225,6 +5239,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5225
5239
  model: input.model,
5226
5240
  variant: input.variant,
5227
5241
  agent: input.agent,
5242
+ addDirs: input.addDirs,
5228
5243
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
5229
5244
  configIsolation: "safe",
5230
5245
  permissionMode: input.permissionMode ?? "bypass",
@@ -5268,7 +5283,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
5268
5283
  throw new Error("taskId is required");
5269
5284
  if (!input.projectPath?.trim())
5270
5285
  throw new Error("projectPath is required");
5271
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
5286
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
5272
5287
  const plan = worktreePlan(input, input.taskId);
5273
5288
  const taskContext = {
5274
5289
  taskId: input.taskId,
@@ -5502,6 +5517,7 @@ function renderLifecycleBoundedTemplate(id, values) {
5502
5517
  model: values.model,
5503
5518
  variant: values.variant,
5504
5519
  agent: values.agent,
5520
+ addDirs: listVar(values.addDirs ?? values.addDir),
5505
5521
  permissionMode: values.permissionMode,
5506
5522
  sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
5507
5523
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5618,6 +5634,7 @@ function renderLoopTemplate(id, values) {
5618
5634
  taskTitle: values.taskTitle,
5619
5635
  taskDescription: values.taskDescription,
5620
5636
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
5637
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
5621
5638
  routeProjectPath: values.routeProjectPath,
5622
5639
  projectGroup: values.projectGroup,
5623
5640
  provider: values.provider,
@@ -5630,6 +5647,7 @@ function renderLoopTemplate(id, values) {
5630
5647
  model: values.model,
5631
5648
  variant: values.variant,
5632
5649
  agent: values.agent,
5650
+ addDirs: listVar(values.addDirs ?? values.addDir),
5633
5651
  permissionMode: values.permissionMode,
5634
5652
  sandbox: values.sandbox,
5635
5653
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5661,6 +5679,7 @@ function renderLoopTemplate(id, values) {
5661
5679
  model: values.model,
5662
5680
  variant: values.variant,
5663
5681
  agent: values.agent,
5682
+ addDirs: listVar(values.addDirs ?? values.addDir),
5664
5683
  permissionMode: values.permissionMode,
5665
5684
  sandbox: values.sandbox,
5666
5685
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5687,6 +5706,7 @@ function renderLoopTemplate(id, values) {
5687
5706
  model: values.model,
5688
5707
  variant: values.variant,
5689
5708
  agent: values.agent,
5709
+ addDirs: listVar(values.addDirs ?? values.addDir),
5690
5710
  permissionMode: values.permissionMode,
5691
5711
  sandbox: values.sandbox,
5692
5712
  manualBreakGlass: booleanVar(values.manualBreakGlass),
package/dist/lib/store.js CHANGED
@@ -386,6 +386,10 @@ 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`);
390
+ if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
391
+ throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
392
+ }
389
393
  if (value.permissionMode !== undefined) {
390
394
  assertString(value.permissionMode, `${label}.permissionMode`);
391
395
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -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
@@ -386,6 +386,10 @@ 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`);
390
+ if (Array.isArray(value.addDirs) && value.addDirs.length > 0 && !["codewith", "codex"].includes(value.provider)) {
391
+ throw new Error(`${label}.addDirs is currently supported only for provider codewith or codex`);
392
+ }
389
393
  if (value.permissionMode !== undefined) {
390
394
  assertString(value.permissionMode, `${label}.permissionMode`);
391
395
  const permissionModes = ["default", "plan", "auto", "bypass"];
@@ -3027,6 +3031,9 @@ function assertSupportedAgentOptions(target) {
3027
3031
  if (target.authProfile !== undefined && target.provider !== "codewith") {
3028
3032
  throw new Error(`${target.provider}.authProfile is supported only for codewith`);
3029
3033
  }
3034
+ if (target.addDirs?.length && !["codewith", "codex"].includes(target.provider)) {
3035
+ throw new Error(`${target.provider}.addDirs is currently supported only for codewith or codex`);
3036
+ }
3030
3037
  if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
3031
3038
  throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
3032
3039
  }
@@ -3086,10 +3093,10 @@ function agentArgs(target) {
3086
3093
  case "cursor":
3087
3094
  args.push("-c", [
3088
3095
  "set -eu",
3089
- "if command -v cursor >/dev/null 2>&1; then",
3090
- ' exec cursor agent "$@"',
3091
- "elif command -v agent >/dev/null 2>&1; then",
3096
+ "if command -v agent >/dev/null 2>&1; then",
3092
3097
  ' exec agent "$@"',
3098
+ "elif command -v cursor >/dev/null 2>&1; then",
3099
+ ' exec cursor agent "$@"',
3093
3100
  "else",
3094
3101
  " echo 'Executable not found in PATH: cursor agent or agent' >&2",
3095
3102
  " exit 127",
@@ -3121,6 +3128,8 @@ function agentArgs(target) {
3121
3128
  args.push("--ignore-rules");
3122
3129
  if (target.cwd)
3123
3130
  args.push("--cd", target.cwd);
3131
+ for (const dir of target.addDirs ?? [])
3132
+ args.push("--add-dir", dir);
3124
3133
  if (target.model)
3125
3134
  args.push("--model", target.model);
3126
3135
  if (target.agent)
@@ -3135,6 +3144,8 @@ function agentArgs(target) {
3135
3144
  args.push("--ignore-rules");
3136
3145
  if (target.cwd)
3137
3146
  args.push("--cd", target.cwd);
3147
+ for (const dir of target.addDirs ?? [])
3148
+ args.push("--add-dir", dir);
3138
3149
  if (target.model)
3139
3150
  args.push("--model", target.model);
3140
3151
  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,15 @@ 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. `addDirs` is
335
+ intentionally accepted only for Codewith/Codex until other providers expose
336
+ equivalent directory-scoped write controls.
337
+
325
338
  Inspect route state with:
326
339
 
327
340
  ```bash
@@ -351,12 +364,13 @@ locks, or non-pending states stay queued in todos and are not routed:
351
364
 
352
365
  ```bash
353
366
  loops events drain todos-task \
354
- --todos-project /home/hasna/.hasna/loops \
367
+ --todos-project "$HOME/.hasna/loops" \
355
368
  --task-list repoops-pr-queue \
356
369
  --tags auto:route \
357
- --project-path-prefix /home/hasna/workspace/hasna/opensource \
370
+ --project-path-prefix "$HOME/workspace/hasna/opensource" \
358
371
  --provider codewith \
359
372
  --auth-profile-pool account004,account005,account006 \
373
+ --add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
360
374
  --project-group oss \
361
375
  --max-dispatch 2 \
362
376
  --scan-limit 500 \
@@ -364,7 +378,7 @@ loops events drain todos-task \
364
378
  --max-active-per-project-group 4 \
365
379
  --max-active 12 \
366
380
  --worktree-mode required \
367
- --evidence-dir /home/hasna/.hasna/loops/reports/task-drain
381
+ --evidence-dir "$HOME/.hasna/loops/reports/task-drain"
368
382
  ```
369
383
 
370
384
  `--max-dispatch` caps new workflow-loop creation per drain run. `--limit` caps
@@ -562,10 +576,10 @@ On Linux this writes a user systemd service. On macOS it writes a LaunchAgent pl
562
576
  The adapters intentionally use provider command surfaces instead of pretending every agent has one SDK:
563
577
 
564
578
  - 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`.
579
+ - Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories.
566
580
  - AI Copilot and OpenCode use `run --format json --pure`.
567
- - 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`.
581
+ - Cursor is CLI-first for now via standalone `agent -p` when available, with `cursor agent -p` as a compatibility fallback; treat output as less stable until a stronger public SDK contract is selected.
582
+ - Codex uses `codex exec --json --ephemeral --skip-git-repo-check`, with `--add-dir` for explicit extra writable directories where supported.
569
583
  - Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
570
584
  - 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
585
  - `--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.44",
3
+ "version": "0.3.46",
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",