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