@hasna/loops 0.3.44 → 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
@@ -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"];
@@ -3247,6 +3248,8 @@ function agentArgs(target) {
3247
3248
  args.push("--ignore-rules");
3248
3249
  if (target.cwd)
3249
3250
  args.push("--cd", target.cwd);
3251
+ for (const dir of target.addDirs ?? [])
3252
+ args.push("--add-dir", dir);
3250
3253
  if (target.model)
3251
3254
  args.push("--model", target.model);
3252
3255
  if (target.agent)
@@ -3261,6 +3264,8 @@ function agentArgs(target) {
3261
3264
  args.push("--ignore-rules");
3262
3265
  if (target.cwd)
3263
3266
  args.push("--cd", target.cwd);
3267
+ for (const dir of target.addDirs ?? [])
3268
+ args.push("--add-dir", dir);
3264
3269
  if (target.model)
3265
3270
  args.push("--model", target.model);
3266
3271
  args.push(...target.extraArgs ?? []);
@@ -5732,7 +5737,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5732
5737
  // package.json
5733
5738
  var package_default = {
5734
5739
  name: "@hasna/loops",
5735
- version: "0.3.44",
5740
+ version: "0.3.45",
5736
5741
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5737
5742
  type: "module",
5738
5743
  main: "dist/index.js",
@@ -5845,6 +5850,7 @@ var TEMPLATE_SUMMARIES = [
5845
5850
  { name: "taskId", required: true, description: "Todos task id to execute." },
5846
5851
  { name: "taskTitle", description: "Human-readable task title." },
5847
5852
  { name: "projectPath", required: true, description: "Repository or project working directory." },
5853
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
5848
5854
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
5849
5855
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
5850
5856
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -5855,6 +5861,7 @@ var TEMPLATE_SUMMARIES = [
5855
5861
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5856
5862
  { name: "model", description: "Provider model." },
5857
5863
  { name: "variant", description: "Provider reasoning/model effort variant." },
5864
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5858
5865
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5859
5866
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5860
5867
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5884,6 +5891,7 @@ var TEMPLATE_SUMMARIES = [
5884
5891
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
5885
5892
  { name: "model", description: "Provider model." },
5886
5893
  { name: "variant", description: "Provider reasoning/model effort variant." },
5894
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
5887
5895
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
5888
5896
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
5889
5897
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -6264,6 +6272,7 @@ function agentTarget(input, prompt, role, seed, plan) {
6264
6272
  model: input.model,
6265
6273
  variant: input.variant,
6266
6274
  agent: input.agent,
6275
+ addDirs: input.addDirs,
6267
6276
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
6268
6277
  configIsolation: "safe",
6269
6278
  permissionMode: input.permissionMode ?? "bypass",
@@ -6307,7 +6316,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
6307
6316
  throw new Error("taskId is required");
6308
6317
  if (!input.projectPath?.trim())
6309
6318
  throw new Error("projectPath is required");
6310
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
6319
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
6311
6320
  const plan = worktreePlan(input, input.taskId);
6312
6321
  const taskContext = {
6313
6322
  taskId: input.taskId,
@@ -6541,6 +6550,7 @@ function renderLifecycleBoundedTemplate(id, values) {
6541
6550
  model: values.model,
6542
6551
  variant: values.variant,
6543
6552
  agent: values.agent,
6553
+ addDirs: listVar(values.addDirs ?? values.addDir),
6544
6554
  permissionMode: values.permissionMode,
6545
6555
  sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
6546
6556
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6657,6 +6667,7 @@ function renderLoopTemplate(id, values) {
6657
6667
  taskTitle: values.taskTitle,
6658
6668
  taskDescription: values.taskDescription,
6659
6669
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
6670
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
6660
6671
  routeProjectPath: values.routeProjectPath,
6661
6672
  projectGroup: values.projectGroup,
6662
6673
  provider: values.provider,
@@ -6669,6 +6680,7 @@ function renderLoopTemplate(id, values) {
6669
6680
  model: values.model,
6670
6681
  variant: values.variant,
6671
6682
  agent: values.agent,
6683
+ addDirs: listVar(values.addDirs ?? values.addDir),
6672
6684
  permissionMode: values.permissionMode,
6673
6685
  sandbox: values.sandbox,
6674
6686
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6700,6 +6712,7 @@ function renderLoopTemplate(id, values) {
6700
6712
  model: values.model,
6701
6713
  variant: values.variant,
6702
6714
  agent: values.agent,
6715
+ addDirs: listVar(values.addDirs ?? values.addDir),
6703
6716
  permissionMode: values.permissionMode,
6704
6717
  sandbox: values.sandbox,
6705
6718
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6726,6 +6739,7 @@ function renderLoopTemplate(id, values) {
6726
6739
  model: values.model,
6727
6740
  variant: values.variant,
6728
6741
  agent: values.agent,
6742
+ addDirs: listVar(values.addDirs ?? values.addDir),
6729
6743
  permissionMode: values.permissionMode,
6730
6744
  sandbox: values.sandbox,
6731
6745
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -6938,6 +6952,10 @@ function allowlistFromOpts(opts) {
6938
6952
  enforcement: "metadata_only"
6939
6953
  };
6940
6954
  }
6955
+ function listFromRepeatedOpts(value) {
6956
+ const values = (value ?? []).flatMap((entry) => splitList(entry) ?? []);
6957
+ return values.length ? values : undefined;
6958
+ }
6941
6959
  function accountPoolFromOpts(opts) {
6942
6960
  return splitList(opts.accountPool)?.map((profile) => ({ profile, tool: opts.accountTool }));
6943
6961
  }
@@ -7541,6 +7559,7 @@ function routeTodosTaskEvent(event, opts) {
7541
7559
  model: opts.model,
7542
7560
  variant: opts.variant,
7543
7561
  agent: opts.agent,
7562
+ addDirs: listFromRepeatedOpts(opts.addDir),
7544
7563
  permissionMode,
7545
7564
  sandbox,
7546
7565
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -7548,7 +7567,8 @@ function routeTodosTaskEvent(event, opts) {
7548
7567
  worktreeRoot: opts.worktreeRoot,
7549
7568
  worktreeBranchPrefix: opts.worktreeBranchPrefix ?? "openloops",
7550
7569
  eventId: event.id,
7551
- eventType: event.type
7570
+ eventType: event.type,
7571
+ todosProjectPath: opts.todosProject
7552
7572
  });
7553
7573
  workflowBody.name = workflowName;
7554
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 ?? "-"}`;
@@ -7753,6 +7773,7 @@ function routeGenericEvent(event, opts) {
7753
7773
  model: opts.model,
7754
7774
  variant: opts.variant,
7755
7775
  agent: opts.agent,
7776
+ addDirs: listFromRepeatedOpts(opts.addDir),
7756
7777
  permissionMode,
7757
7778
  sandbox,
7758
7779
  manualBreakGlass: Boolean(opts.manualBreakGlass),
@@ -8115,7 +8136,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8115
8136
  store.close();
8116
8137
  }
8117
8138
  });
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) => {
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) => {
8119
8140
  const provider = opts.provider;
8120
8141
  if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
8121
8142
  throw new Error("unsupported provider");
@@ -8134,6 +8155,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
8134
8155
  variant: opts.variant,
8135
8156
  agent: opts.agent,
8136
8157
  authProfile: providerAuthProfileFromOpts(opts, provider),
8158
+ addDirs: listFromRepeatedOpts(opts.addDir),
8137
8159
  timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
8138
8160
  configIsolation: opts.configIsolation,
8139
8161
  permissionMode: permissionModeFromOpts(opts, provider),
@@ -8174,10 +8196,10 @@ var events = program.command("events").description("handle Hasna event envelopes
8174
8196
  var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
8175
8197
  var goal = program.command("goal").description("inspect goal runs");
8176
8198
  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");
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");
8178
8200
  }
8179
8201
  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");
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");
8181
8203
  }
8182
8204
  function routeEventByKind(kind, event, opts) {
8183
8205
  if (kind === "todos-task")
@@ -8219,6 +8241,8 @@ function routeDrainArgs(opts) {
8219
8241
  add("--model", opts.model);
8220
8242
  add("--variant", opts.variant);
8221
8243
  add("--agent", opts.agent);
8244
+ for (const dir of listFromRepeatedOpts(opts.addDir) ?? [])
8245
+ add("--add-dir", dir);
8222
8246
  add("--permission-mode", opts.permissionMode);
8223
8247
  add("--sandbox", opts.sandbox);
8224
8248
  addBool("--manual-break-glass", opts.manualBreakGlass);
@@ -8437,13 +8461,13 @@ addScheduleOptions(addTodosDrainOptions(routes.command("schedule <kind> <name>")
8437
8461
  }
8438
8462
  });
8439
8463
  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) => {
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) => {
8441
8465
  const event = await readEventEnvelopeFromStdin();
8442
8466
  const result = routeTodosTaskEvent(event, opts);
8443
8467
  print(result.value, result.human);
8444
8468
  });
8445
8469
  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) => {
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) => {
8447
8471
  const maxDispatch = positiveInteger(opts.maxDispatch ?? "1", "--max-dispatch") ?? 1;
8448
8472
  const todosProject = opts.todosProject ?? defaultLoopsProject();
8449
8473
  const requiredTags = splitList(opts.tags ?? opts.tag) ?? [];
@@ -8527,7 +8551,7 @@ eventsDrain.command("todos-task").description("drain ready todos tasks into boun
8527
8551
  } : { ...report, evidencePath };
8528
8552
  print(output, `drained todos ready queue: considered=${report.considered} created=${report.created} deduped=${report.deduped} throttled=${report.throttled} skipped=${report.skipped}`);
8529
8553
  });
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) => {
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) => {
8531
8555
  const event = await readEventEnvelopeFromStdin();
8532
8556
  const data = eventData(event);
8533
8557
  const metadata = eventMetadata(event);
@@ -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"];
@@ -3131,6 +3132,8 @@ function agentArgs(target) {
3131
3132
  args.push("--ignore-rules");
3132
3133
  if (target.cwd)
3133
3134
  args.push("--cd", target.cwd);
3135
+ for (const dir of target.addDirs ?? [])
3136
+ args.push("--add-dir", dir);
3134
3137
  if (target.model)
3135
3138
  args.push("--model", target.model);
3136
3139
  if (target.agent)
@@ -3145,6 +3148,8 @@ function agentArgs(target) {
3145
3148
  args.push("--ignore-rules");
3146
3149
  if (target.cwd)
3147
3150
  args.push("--cd", target.cwd);
3151
+ for (const dir of target.addDirs ?? [])
3152
+ args.push("--add-dir", dir);
3148
3153
  if (target.model)
3149
3154
  args.push("--model", target.model);
3150
3155
  args.push(...target.extraArgs ?? []);
@@ -5046,7 +5051,7 @@ function enableStartup(result) {
5046
5051
  // package.json
5047
5052
  var package_default = {
5048
5053
  name: "@hasna/loops",
5049
- version: "0.3.44",
5054
+ version: "0.3.45",
5050
5055
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5051
5056
  type: "module",
5052
5057
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -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"];
@@ -3121,6 +3122,8 @@ function agentArgs(target) {
3121
3122
  args.push("--ignore-rules");
3122
3123
  if (target.cwd)
3123
3124
  args.push("--cd", target.cwd);
3125
+ for (const dir of target.addDirs ?? [])
3126
+ args.push("--add-dir", dir);
3124
3127
  if (target.model)
3125
3128
  args.push("--model", target.model);
3126
3129
  if (target.agent)
@@ -3135,6 +3138,8 @@ function agentArgs(target) {
3135
3138
  args.push("--ignore-rules");
3136
3139
  if (target.cwd)
3137
3140
  args.push("--cd", target.cwd);
3141
+ for (const dir of target.addDirs ?? [])
3142
+ args.push("--add-dir", dir);
3138
3143
  if (target.model)
3139
3144
  args.push("--model", target.model);
3140
3145
  args.push(...target.extraArgs ?? []);
@@ -4806,6 +4811,7 @@ var TEMPLATE_SUMMARIES = [
4806
4811
  { name: "taskId", required: true, description: "Todos task id to execute." },
4807
4812
  { name: "taskTitle", description: "Human-readable task title." },
4808
4813
  { name: "projectPath", required: true, description: "Repository or project working directory." },
4814
+ { name: "todosProjectPath", description: "Todos storage project path used in worker/verifier commands." },
4809
4815
  { name: "routeProjectPath", description: "Canonical project path used for scheduler concurrency limits." },
4810
4816
  { name: "projectGroup", description: "Optional project group used for scheduler concurrency limits." },
4811
4817
  { name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
@@ -4816,6 +4822,7 @@ var TEMPLATE_SUMMARIES = [
4816
4822
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4817
4823
  { name: "model", description: "Provider model." },
4818
4824
  { name: "variant", description: "Provider reasoning/model effort variant." },
4825
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4819
4826
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4820
4827
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4821
4828
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -4845,6 +4852,7 @@ var TEMPLATE_SUMMARIES = [
4845
4852
  { name: "accountPool", description: "Comma-separated OpenAccounts profiles; worker/verifier are selected deterministically." },
4846
4853
  { name: "model", description: "Provider model." },
4847
4854
  { name: "variant", description: "Provider reasoning/model effort variant." },
4855
+ { name: "addDirs", description: "Comma-separated additional writable directories for provider sandboxes." },
4848
4856
  { name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
4849
4857
  { name: "sandbox", default: "workspace-write", description: "Provider sandbox mode." },
4850
4858
  { name: "manualBreakGlass", default: "false", description: "Allow explicit danger-full-access in a generated workflow. Intended for manual emergency use only." },
@@ -5225,6 +5233,7 @@ function agentTarget(input, prompt, role, seed, plan) {
5225
5233
  model: input.model,
5226
5234
  variant: input.variant,
5227
5235
  agent: input.agent,
5236
+ addDirs: input.addDirs,
5228
5237
  authProfile: provider === "codewith" ? authProfileForRole(input, role, seed) : undefined,
5229
5238
  configIsolation: "safe",
5230
5239
  permissionMode: input.permissionMode ?? "bypass",
@@ -5268,7 +5277,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
5268
5277
  throw new Error("taskId is required");
5269
5278
  if (!input.projectPath?.trim())
5270
5279
  throw new Error("projectPath is required");
5271
- const todosProjectPath = input.routeProjectPath ?? input.projectPath;
5280
+ const todosProjectPath = input.todosProjectPath ?? input.routeProjectPath ?? input.projectPath;
5272
5281
  const plan = worktreePlan(input, input.taskId);
5273
5282
  const taskContext = {
5274
5283
  taskId: input.taskId,
@@ -5502,6 +5511,7 @@ function renderLifecycleBoundedTemplate(id, values) {
5502
5511
  model: values.model,
5503
5512
  variant: values.variant,
5504
5513
  agent: values.agent,
5514
+ addDirs: listVar(values.addDirs ?? values.addDir),
5505
5515
  permissionMode: values.permissionMode,
5506
5516
  sandbox: values.sandbox ?? (id === REPORT_ONLY_TEMPLATE_ID ? "read-only" : undefined),
5507
5517
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5618,6 +5628,7 @@ function renderLoopTemplate(id, values) {
5618
5628
  taskTitle: values.taskTitle,
5619
5629
  taskDescription: values.taskDescription,
5620
5630
  projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
5631
+ todosProjectPath: values.todosProjectPath ?? values.todosProject,
5621
5632
  routeProjectPath: values.routeProjectPath,
5622
5633
  projectGroup: values.projectGroup,
5623
5634
  provider: values.provider,
@@ -5630,6 +5641,7 @@ function renderLoopTemplate(id, values) {
5630
5641
  model: values.model,
5631
5642
  variant: values.variant,
5632
5643
  agent: values.agent,
5644
+ addDirs: listVar(values.addDirs ?? values.addDir),
5633
5645
  permissionMode: values.permissionMode,
5634
5646
  sandbox: values.sandbox,
5635
5647
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5661,6 +5673,7 @@ function renderLoopTemplate(id, values) {
5661
5673
  model: values.model,
5662
5674
  variant: values.variant,
5663
5675
  agent: values.agent,
5676
+ addDirs: listVar(values.addDirs ?? values.addDir),
5664
5677
  permissionMode: values.permissionMode,
5665
5678
  sandbox: values.sandbox,
5666
5679
  manualBreakGlass: booleanVar(values.manualBreakGlass),
@@ -5687,6 +5700,7 @@ function renderLoopTemplate(id, values) {
5687
5700
  model: values.model,
5688
5701
  variant: values.variant,
5689
5702
  agent: values.agent,
5703
+ addDirs: listVar(values.addDirs ?? values.addDir),
5690
5704
  permissionMode: values.permissionMode,
5691
5705
  sandbox: values.sandbox,
5692
5706
  manualBreakGlass: booleanVar(values.manualBreakGlass),
package/dist/lib/store.js CHANGED
@@ -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"];
@@ -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,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"];
@@ -3121,6 +3122,8 @@ function agentArgs(target) {
3121
3122
  args.push("--ignore-rules");
3122
3123
  if (target.cwd)
3123
3124
  args.push("--cd", target.cwd);
3125
+ for (const dir of target.addDirs ?? [])
3126
+ args.push("--add-dir", dir);
3124
3127
  if (target.model)
3125
3128
  args.push("--model", target.model);
3126
3129
  if (target.agent)
@@ -3135,6 +3138,8 @@ function agentArgs(target) {
3135
3138
  args.push("--ignore-rules");
3136
3139
  if (target.cwd)
3137
3140
  args.push("--cd", target.cwd);
3141
+ for (const dir of target.addDirs ?? [])
3142
+ args.push("--add-dir", dir);
3138
3143
  if (target.model)
3139
3144
  args.push("--model", target.model);
3140
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.44",
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",