@hasna/loops 0.3.11 → 0.3.13
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 +64 -2
- package/dist/cli/index.js +683 -15
- package/dist/daemon/index.js +154 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +414 -8
- package/dist/lib/store.js +29 -0
- package/dist/lib/templates.d.ts +41 -0
- package/dist/sdk/index.js +152 -6
- package/dist/types.d.ts +19 -0
- package/docs/USAGE.md +64 -2
- package/package.json +2 -1
package/dist/sdk/index.js
CHANGED
|
@@ -368,6 +368,35 @@ function validateTarget(value, label) {
|
|
|
368
368
|
if (value.provider !== "codewith")
|
|
369
369
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
370
370
|
}
|
|
371
|
+
if (value.variant !== undefined)
|
|
372
|
+
assertString(value.variant, `${label}.variant`);
|
|
373
|
+
if (value.permissionMode !== undefined) {
|
|
374
|
+
assertString(value.permissionMode, `${label}.permissionMode`);
|
|
375
|
+
const permissionModes = ["default", "plan", "auto", "bypass"];
|
|
376
|
+
if (!permissionModes.includes(value.permissionMode)) {
|
|
377
|
+
throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
|
|
378
|
+
}
|
|
379
|
+
if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
|
|
380
|
+
throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
|
|
381
|
+
}
|
|
382
|
+
if (value.permissionMode === "auto" && value.provider !== "claude") {
|
|
383
|
+
throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (value.sandbox !== undefined) {
|
|
387
|
+
assertString(value.sandbox, `${label}.sandbox`);
|
|
388
|
+
const codexLike = ["read-only", "workspace-write", "danger-full-access"];
|
|
389
|
+
const cursorLike = ["enabled", "disabled"];
|
|
390
|
+
if (["codewith", "codex"].includes(value.provider)) {
|
|
391
|
+
if (!codexLike.includes(value.sandbox))
|
|
392
|
+
throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
|
|
393
|
+
} else if (value.provider === "cursor") {
|
|
394
|
+
if (!cursorLike.includes(value.sandbox))
|
|
395
|
+
throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
|
|
396
|
+
} else {
|
|
397
|
+
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
371
400
|
return value;
|
|
372
401
|
}
|
|
373
402
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -2353,7 +2382,7 @@ function providerCommand(provider) {
|
|
|
2353
2382
|
case "claude":
|
|
2354
2383
|
return "claude";
|
|
2355
2384
|
case "cursor":
|
|
2356
|
-
return "
|
|
2385
|
+
return "sh";
|
|
2357
2386
|
case "codewith":
|
|
2358
2387
|
return "codewith";
|
|
2359
2388
|
case "aicopilot":
|
|
@@ -2364,22 +2393,107 @@ function providerCommand(provider) {
|
|
|
2364
2393
|
return "codex";
|
|
2365
2394
|
}
|
|
2366
2395
|
}
|
|
2396
|
+
function codewithLikeSandbox(target) {
|
|
2397
|
+
const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
|
|
2398
|
+
if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
|
|
2399
|
+
throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
|
|
2400
|
+
}
|
|
2401
|
+
return sandbox;
|
|
2402
|
+
}
|
|
2403
|
+
function configStringValue(value) {
|
|
2404
|
+
return JSON.stringify(value);
|
|
2405
|
+
}
|
|
2406
|
+
function assertStringOption(value, label) {
|
|
2407
|
+
if (value !== undefined && typeof value !== "string")
|
|
2408
|
+
throw new Error(`${label} must be a string`);
|
|
2409
|
+
}
|
|
2410
|
+
function assertSupportedAgentOptions(target) {
|
|
2411
|
+
assertStringOption(target.variant, `${target.provider}.variant`);
|
|
2412
|
+
assertStringOption(target.model, `${target.provider}.model`);
|
|
2413
|
+
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
2414
|
+
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
2415
|
+
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
2416
|
+
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
2417
|
+
}
|
|
2418
|
+
if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
|
|
2419
|
+
throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
|
|
2420
|
+
}
|
|
2421
|
+
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
2422
|
+
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
2423
|
+
}
|
|
2424
|
+
if (["codewith", "codex"].includes(target.provider)) {
|
|
2425
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2426
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2427
|
+
}
|
|
2428
|
+
if (target.sandbox)
|
|
2429
|
+
codewithLikeSandbox(target);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (target.provider === "claude") {
|
|
2433
|
+
if (target.sandbox !== undefined)
|
|
2434
|
+
throw new Error("claude.sandbox is not supported");
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (target.provider === "cursor") {
|
|
2438
|
+
if (target.permissionMode === "auto")
|
|
2439
|
+
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
2440
|
+
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
2441
|
+
throw new Error("cursor.sandbox must be enabled or disabled");
|
|
2442
|
+
}
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2446
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2447
|
+
}
|
|
2448
|
+
if (target.sandbox !== undefined)
|
|
2449
|
+
throw new Error(`${target.provider}.sandbox is not supported`);
|
|
2450
|
+
}
|
|
2367
2451
|
function agentArgs(target) {
|
|
2452
|
+
assertSupportedAgentOptions(target);
|
|
2368
2453
|
const isolation = target.configIsolation ?? "safe";
|
|
2454
|
+
const permissionMode = target.permissionMode ?? "default";
|
|
2369
2455
|
const args = [];
|
|
2370
2456
|
switch (target.provider) {
|
|
2371
2457
|
case "claude":
|
|
2372
2458
|
if (isolation === "safe")
|
|
2373
2459
|
args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
|
|
2460
|
+
if (permissionMode !== "default") {
|
|
2461
|
+
const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
|
|
2462
|
+
if (mode)
|
|
2463
|
+
args.push("--permission-mode", mode);
|
|
2464
|
+
}
|
|
2374
2465
|
args.push("-p", "--output-format", "json");
|
|
2375
2466
|
if (target.model)
|
|
2376
2467
|
args.push("--model", target.model);
|
|
2468
|
+
if (target.variant)
|
|
2469
|
+
args.push("--effort", target.variant);
|
|
2377
2470
|
if (target.agent)
|
|
2378
2471
|
args.push("--agent", target.agent);
|
|
2379
2472
|
args.push(...target.extraArgs ?? []);
|
|
2380
2473
|
return args;
|
|
2381
2474
|
case "cursor":
|
|
2382
|
-
args.push("-
|
|
2475
|
+
args.push("-c", [
|
|
2476
|
+
"set -eu",
|
|
2477
|
+
"if command -v cursor >/dev/null 2>&1; then",
|
|
2478
|
+
' exec cursor agent "$@"',
|
|
2479
|
+
"elif command -v agent >/dev/null 2>&1; then",
|
|
2480
|
+
' exec agent "$@"',
|
|
2481
|
+
"else",
|
|
2482
|
+
" echo 'Executable not found in PATH: cursor agent or agent' >&2",
|
|
2483
|
+
" exit 127",
|
|
2484
|
+
"fi"
|
|
2485
|
+
].join(`
|
|
2486
|
+
`), "openloops-cursor", "-p");
|
|
2487
|
+
if (permissionMode === "plan")
|
|
2488
|
+
args.push("--mode", "plan");
|
|
2489
|
+
if (permissionMode === "bypass")
|
|
2490
|
+
args.push("--force");
|
|
2491
|
+
const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
|
|
2492
|
+
if (cursorSandbox) {
|
|
2493
|
+
if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
|
|
2494
|
+
throw new Error("cursor sandbox must be enabled or disabled");
|
|
2495
|
+
args.push("--sandbox", cursorSandbox);
|
|
2496
|
+
}
|
|
2383
2497
|
if (target.model)
|
|
2384
2498
|
args.push("--model", target.model);
|
|
2385
2499
|
if (target.agent)
|
|
@@ -2387,7 +2501,10 @@ function agentArgs(target) {
|
|
|
2387
2501
|
args.push(...target.extraArgs ?? []);
|
|
2388
2502
|
return args;
|
|
2389
2503
|
case "codewith":
|
|
2390
|
-
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []
|
|
2504
|
+
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
2505
|
+
if (target.variant)
|
|
2506
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2507
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
2391
2508
|
if (isolation === "safe")
|
|
2392
2509
|
args.push("--ignore-rules");
|
|
2393
2510
|
if (target.cwd)
|
|
@@ -2399,7 +2516,9 @@ function agentArgs(target) {
|
|
|
2399
2516
|
args.push(...target.extraArgs ?? []);
|
|
2400
2517
|
return args;
|
|
2401
2518
|
case "codex":
|
|
2402
|
-
|
|
2519
|
+
if (target.variant)
|
|
2520
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2521
|
+
args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
|
|
2403
2522
|
if (isolation === "safe")
|
|
2404
2523
|
args.push("--ignore-rules");
|
|
2405
2524
|
if (target.cwd)
|
|
@@ -2412,10 +2531,14 @@ function agentArgs(target) {
|
|
|
2412
2531
|
args.push("run", "--format", "json");
|
|
2413
2532
|
if (isolation === "safe")
|
|
2414
2533
|
args.push("--pure");
|
|
2534
|
+
if (permissionMode === "bypass")
|
|
2535
|
+
args.push("--dangerously-skip-permissions");
|
|
2415
2536
|
if (target.cwd)
|
|
2416
2537
|
args.push("--dir", target.cwd);
|
|
2417
2538
|
if (target.model)
|
|
2418
2539
|
args.push("--model", target.model);
|
|
2540
|
+
if (target.variant)
|
|
2541
|
+
args.push("--variant", target.variant);
|
|
2419
2542
|
if (target.agent)
|
|
2420
2543
|
args.push("--agent", target.agent);
|
|
2421
2544
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2424,10 +2547,14 @@ function agentArgs(target) {
|
|
|
2424
2547
|
args.push("run", "--format", "json");
|
|
2425
2548
|
if (isolation === "safe")
|
|
2426
2549
|
args.push("--pure");
|
|
2550
|
+
if (permissionMode === "bypass")
|
|
2551
|
+
args.push("--dangerously-skip-permissions");
|
|
2427
2552
|
if (target.cwd)
|
|
2428
2553
|
args.push("--dir", target.cwd);
|
|
2429
2554
|
if (target.model)
|
|
2430
2555
|
args.push("--model", target.model);
|
|
2556
|
+
if (target.variant)
|
|
2557
|
+
args.push("--variant", target.variant);
|
|
2431
2558
|
if (target.agent)
|
|
2432
2559
|
args.push("--agent", target.agent);
|
|
2433
2560
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2456,6 +2583,7 @@ function commandSpec(target) {
|
|
|
2456
2583
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2457
2584
|
account: agentTarget.account,
|
|
2458
2585
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2586
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2459
2587
|
stdin: agentTarget.prompt
|
|
2460
2588
|
};
|
|
2461
2589
|
}
|
|
@@ -2523,11 +2651,15 @@ function remoteScript(spec, metadata) {
|
|
|
2523
2651
|
`;
|
|
2524
2652
|
}
|
|
2525
2653
|
function remotePreflightScript(spec, metadata) {
|
|
2526
|
-
|
|
2654
|
+
const lines = [
|
|
2527
2655
|
...remoteBootstrapLines(spec, metadata),
|
|
2528
2656
|
"command -v bash >/dev/null 2>&1",
|
|
2529
2657
|
`command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
|
|
2530
|
-
]
|
|
2658
|
+
];
|
|
2659
|
+
if (spec.preflightAnyOf?.length) {
|
|
2660
|
+
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2661
|
+
}
|
|
2662
|
+
return lines.join(`
|
|
2531
2663
|
`);
|
|
2532
2664
|
}
|
|
2533
2665
|
function transportEnv(opts) {
|
|
@@ -2680,6 +2812,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2680
2812
|
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
2681
2813
|
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
2682
2814
|
}
|
|
2815
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2816
|
+
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2817
|
+
}
|
|
2683
2818
|
return {
|
|
2684
2819
|
command: spec.command,
|
|
2685
2820
|
accountProfile: spec.account?.profile,
|
|
@@ -2710,6 +2845,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2710
2845
|
durationMs: 0
|
|
2711
2846
|
};
|
|
2712
2847
|
}
|
|
2848
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2849
|
+
return {
|
|
2850
|
+
status: "failed",
|
|
2851
|
+
stdout,
|
|
2852
|
+
stderr,
|
|
2853
|
+
error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
|
|
2854
|
+
startedAt,
|
|
2855
|
+
finishedAt: nowIso(),
|
|
2856
|
+
durationMs: 0
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2713
2859
|
const child = spawn(spec.command, spec.args, {
|
|
2714
2860
|
cwd: spec.cwd,
|
|
2715
2861
|
env,
|
package/dist/types.d.ts
CHANGED
|
@@ -52,17 +52,22 @@ export interface CommandTarget {
|
|
|
52
52
|
}
|
|
53
53
|
export type AgentProvider = "claude" | "cursor" | "codewith" | "aicopilot" | "opencode" | "codex";
|
|
54
54
|
export type AgentConfigIsolation = "safe" | "none";
|
|
55
|
+
export type AgentPermissionMode = "default" | "plan" | "auto" | "bypass";
|
|
56
|
+
export type AgentSandbox = "read-only" | "workspace-write" | "danger-full-access" | "enabled" | "disabled";
|
|
55
57
|
export interface AgentTarget {
|
|
56
58
|
type: "agent";
|
|
57
59
|
provider: AgentProvider;
|
|
58
60
|
prompt: string;
|
|
59
61
|
cwd?: string;
|
|
60
62
|
model?: string;
|
|
63
|
+
variant?: string;
|
|
61
64
|
agent?: string;
|
|
62
65
|
authProfile?: string;
|
|
63
66
|
extraArgs?: string[];
|
|
64
67
|
timeoutMs?: number;
|
|
65
68
|
configIsolation?: AgentConfigIsolation;
|
|
69
|
+
permissionMode?: AgentPermissionMode;
|
|
70
|
+
sandbox?: AgentSandbox;
|
|
66
71
|
account?: AccountRef;
|
|
67
72
|
}
|
|
68
73
|
export interface WorkflowTarget {
|
|
@@ -105,6 +110,20 @@ export interface CreateWorkflowInput {
|
|
|
105
110
|
steps: WorkflowStep[];
|
|
106
111
|
version?: number;
|
|
107
112
|
}
|
|
113
|
+
export type LoopTemplateKind = "workflow" | "loop";
|
|
114
|
+
export interface LoopTemplateVariable {
|
|
115
|
+
name: string;
|
|
116
|
+
description?: string;
|
|
117
|
+
required?: boolean;
|
|
118
|
+
default?: string;
|
|
119
|
+
}
|
|
120
|
+
export interface LoopTemplateSummary {
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
kind: LoopTemplateKind;
|
|
125
|
+
variables: LoopTemplateVariable[];
|
|
126
|
+
}
|
|
108
127
|
export interface WorkflowRun {
|
|
109
128
|
id: string;
|
|
110
129
|
workflowId: string;
|
package/docs/USAGE.md
CHANGED
|
@@ -5,7 +5,7 @@ OpenLoops is a local CLI and daemon for persistent loops and workflows: schedule
|
|
|
5
5
|
It supports deterministic command loops, JSON-defined workflows, and guarded CLI adapters for headless coding agents:
|
|
6
6
|
|
|
7
7
|
- `claude`
|
|
8
|
-
- `cursor
|
|
8
|
+
- `cursor agent` or `agent`
|
|
9
9
|
- `codewith exec`
|
|
10
10
|
- `aicopilot run`
|
|
11
11
|
- `opencode run`
|
|
@@ -78,6 +78,7 @@ loops create agent supply-chain-watch \
|
|
|
78
78
|
--provider codewith \
|
|
79
79
|
--every 15m \
|
|
80
80
|
--cwd /path/to/repo \
|
|
81
|
+
--sandbox danger-full-access \
|
|
81
82
|
--prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -89,6 +90,7 @@ loops create agent supply-chain-watch \
|
|
|
89
90
|
--auth-profile account001 \
|
|
90
91
|
--every 15m \
|
|
91
92
|
--cwd /path/to/repo \
|
|
93
|
+
--sandbox danger-full-access \
|
|
92
94
|
--prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -160,6 +162,63 @@ Use `shell: true` only when you intentionally want shell parsing:
|
|
|
160
162
|
{ "type": "command", "command": "git status --short", "shell": true }
|
|
161
163
|
```
|
|
162
164
|
|
|
165
|
+
## Templates And Task Events
|
|
166
|
+
|
|
167
|
+
Built-in templates turn common orchestration flows into reusable workflow JSON.
|
|
168
|
+
`todos-task-worker-verifier` performs one todos task and then verifies it.
|
|
169
|
+
`event-worker-verifier` handles any Hasna event envelope and then verifies the
|
|
170
|
+
handling.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
loops templates list
|
|
174
|
+
loops templates render todos-task-worker-verifier \
|
|
175
|
+
--var taskId=<task-id> \
|
|
176
|
+
--var taskTitle="Fix parser" \
|
|
177
|
+
--var projectPath=/path/to/repo \
|
|
178
|
+
--var provider=codewith \
|
|
179
|
+
--var authProfile=account005 \
|
|
180
|
+
--var sandbox=danger-full-access
|
|
181
|
+
loops templates create-workflow todos-task-worker-verifier \
|
|
182
|
+
--var taskId=<task-id> \
|
|
183
|
+
--var projectPath=/path/to/repo
|
|
184
|
+
loops templates render event-worker-verifier \
|
|
185
|
+
--var eventId=<event-id> \
|
|
186
|
+
--var eventType=knowledge.record.created \
|
|
187
|
+
--var eventSource=knowledge \
|
|
188
|
+
--var eventJson='{"id":"<event-id>"}' \
|
|
189
|
+
--var projectPath=/path/to/repo
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For event-driven task automation, `loops events handle todos-task` reads a
|
|
193
|
+
Hasna event envelope from stdin or `HASNA_EVENT_JSON`, renders the template, and
|
|
194
|
+
schedules a deduped one-shot workflow loop:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
cat task-created-event.json | loops events handle todos-task \
|
|
198
|
+
--provider codewith \
|
|
199
|
+
--auth-profile account005 \
|
|
200
|
+
--permission-mode bypass \
|
|
201
|
+
--sandbox danger-full-access
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
For other Hasna apps that expose `@hasna/events` webhooks, use the generic
|
|
205
|
+
handler:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cat event.json | loops events handle generic \
|
|
209
|
+
--provider codewith \
|
|
210
|
+
--auth-profile account005 \
|
|
211
|
+
--permission-mode bypass \
|
|
212
|
+
--sandbox danger-full-access \
|
|
213
|
+
--project-path /path/to/repo
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
This is the intended deterministic-to-agentic path: a producer creates a todos
|
|
217
|
+
task, `@hasna/events` delivers `task.created`, OpenLoops creates a worker and a
|
|
218
|
+
verifier workflow, and the workflow updates todos with evidence. Use
|
|
219
|
+
`--dry-run` to inspect the rendered workflow and loop input without storing
|
|
220
|
+
anything.
|
|
221
|
+
|
|
163
222
|
## Transcript-Driven Loops
|
|
164
223
|
|
|
165
224
|
OpenLoops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
|
|
@@ -240,11 +299,14 @@ The adapters intentionally use provider command surfaces instead of pretending e
|
|
|
240
299
|
- Claude uses `claude -p --output-format json` and safe-mode/local setting sources by default.
|
|
241
300
|
- Codewith uses `codewith --ask-for-approval never exec --json --ephemeral --skip-git-repo-check`.
|
|
242
301
|
- AI Copilot and OpenCode use `run --format json --pure`.
|
|
243
|
-
- Cursor is CLI-first for now via `cursor-agent -p
|
|
302
|
+
- 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.
|
|
244
303
|
- Codex uses `codex exec --json --ephemeral --ask-for-approval never`.
|
|
245
304
|
- Agent prompts are sent through child stdin instead of argv so prompt bodies do not appear in process listings.
|
|
246
305
|
- 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.
|
|
247
306
|
- `--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.
|
|
307
|
+
- `--sandbox` maps to provider-native sandbox flags. Codewith/Codex accept `read-only`, `workspace-write`, or `danger-full-access`; Cursor accepts `enabled` or `disabled`.
|
|
308
|
+
- `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
|
|
309
|
+
- `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
|
|
248
310
|
- Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
|
|
249
311
|
|
|
250
312
|
For production loops that can mutate repos, prefer disposable worktrees and explicit prompts that name allowed write scope.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"bun": ">=1.0.0"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
+
"@hasna/events": "^0.1.8",
|
|
67
68
|
"@hasna/machines": "0.0.49",
|
|
68
69
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
69
70
|
"ai": "6.0.204",
|