@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/cli/index.js
CHANGED
|
@@ -370,6 +370,35 @@ function validateTarget(value, label) {
|
|
|
370
370
|
if (value.provider !== "codewith")
|
|
371
371
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
372
372
|
}
|
|
373
|
+
if (value.variant !== undefined)
|
|
374
|
+
assertString(value.variant, `${label}.variant`);
|
|
375
|
+
if (value.permissionMode !== undefined) {
|
|
376
|
+
assertString(value.permissionMode, `${label}.permissionMode`);
|
|
377
|
+
const permissionModes = ["default", "plan", "auto", "bypass"];
|
|
378
|
+
if (!permissionModes.includes(value.permissionMode)) {
|
|
379
|
+
throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
|
|
380
|
+
}
|
|
381
|
+
if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
|
|
382
|
+
throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
|
|
383
|
+
}
|
|
384
|
+
if (value.permissionMode === "auto" && value.provider !== "claude") {
|
|
385
|
+
throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (value.sandbox !== undefined) {
|
|
389
|
+
assertString(value.sandbox, `${label}.sandbox`);
|
|
390
|
+
const codexLike = ["read-only", "workspace-write", "danger-full-access"];
|
|
391
|
+
const cursorLike = ["enabled", "disabled"];
|
|
392
|
+
if (["codewith", "codex"].includes(value.provider)) {
|
|
393
|
+
if (!codexLike.includes(value.sandbox))
|
|
394
|
+
throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
|
|
395
|
+
} else if (value.provider === "cursor") {
|
|
396
|
+
if (!cursorLike.includes(value.sandbox))
|
|
397
|
+
throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
373
402
|
return value;
|
|
374
403
|
}
|
|
375
404
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -2468,7 +2497,7 @@ function providerCommand(provider) {
|
|
|
2468
2497
|
case "claude":
|
|
2469
2498
|
return "claude";
|
|
2470
2499
|
case "cursor":
|
|
2471
|
-
return "
|
|
2500
|
+
return "sh";
|
|
2472
2501
|
case "codewith":
|
|
2473
2502
|
return "codewith";
|
|
2474
2503
|
case "aicopilot":
|
|
@@ -2479,22 +2508,107 @@ function providerCommand(provider) {
|
|
|
2479
2508
|
return "codex";
|
|
2480
2509
|
}
|
|
2481
2510
|
}
|
|
2511
|
+
function codewithLikeSandbox(target) {
|
|
2512
|
+
const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
|
|
2513
|
+
if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
|
|
2514
|
+
throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
|
|
2515
|
+
}
|
|
2516
|
+
return sandbox;
|
|
2517
|
+
}
|
|
2518
|
+
function configStringValue(value) {
|
|
2519
|
+
return JSON.stringify(value);
|
|
2520
|
+
}
|
|
2521
|
+
function assertStringOption(value, label) {
|
|
2522
|
+
if (value !== undefined && typeof value !== "string")
|
|
2523
|
+
throw new Error(`${label} must be a string`);
|
|
2524
|
+
}
|
|
2525
|
+
function assertSupportedAgentOptions(target) {
|
|
2526
|
+
assertStringOption(target.variant, `${target.provider}.variant`);
|
|
2527
|
+
assertStringOption(target.model, `${target.provider}.model`);
|
|
2528
|
+
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
2529
|
+
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
2530
|
+
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
2531
|
+
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
2532
|
+
}
|
|
2533
|
+
if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
|
|
2534
|
+
throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
|
|
2535
|
+
}
|
|
2536
|
+
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
2537
|
+
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
2538
|
+
}
|
|
2539
|
+
if (["codewith", "codex"].includes(target.provider)) {
|
|
2540
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2541
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2542
|
+
}
|
|
2543
|
+
if (target.sandbox)
|
|
2544
|
+
codewithLikeSandbox(target);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (target.provider === "claude") {
|
|
2548
|
+
if (target.sandbox !== undefined)
|
|
2549
|
+
throw new Error("claude.sandbox is not supported");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (target.provider === "cursor") {
|
|
2553
|
+
if (target.permissionMode === "auto")
|
|
2554
|
+
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
2555
|
+
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
2556
|
+
throw new Error("cursor.sandbox must be enabled or disabled");
|
|
2557
|
+
}
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2561
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2562
|
+
}
|
|
2563
|
+
if (target.sandbox !== undefined)
|
|
2564
|
+
throw new Error(`${target.provider}.sandbox is not supported`);
|
|
2565
|
+
}
|
|
2482
2566
|
function agentArgs(target) {
|
|
2567
|
+
assertSupportedAgentOptions(target);
|
|
2483
2568
|
const isolation = target.configIsolation ?? "safe";
|
|
2569
|
+
const permissionMode = target.permissionMode ?? "default";
|
|
2484
2570
|
const args = [];
|
|
2485
2571
|
switch (target.provider) {
|
|
2486
2572
|
case "claude":
|
|
2487
2573
|
if (isolation === "safe")
|
|
2488
2574
|
args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
|
|
2575
|
+
if (permissionMode !== "default") {
|
|
2576
|
+
const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
|
|
2577
|
+
if (mode)
|
|
2578
|
+
args.push("--permission-mode", mode);
|
|
2579
|
+
}
|
|
2489
2580
|
args.push("-p", "--output-format", "json");
|
|
2490
2581
|
if (target.model)
|
|
2491
2582
|
args.push("--model", target.model);
|
|
2583
|
+
if (target.variant)
|
|
2584
|
+
args.push("--effort", target.variant);
|
|
2492
2585
|
if (target.agent)
|
|
2493
2586
|
args.push("--agent", target.agent);
|
|
2494
2587
|
args.push(...target.extraArgs ?? []);
|
|
2495
2588
|
return args;
|
|
2496
2589
|
case "cursor":
|
|
2497
|
-
args.push("-
|
|
2590
|
+
args.push("-c", [
|
|
2591
|
+
"set -eu",
|
|
2592
|
+
"if command -v cursor >/dev/null 2>&1; then",
|
|
2593
|
+
' exec cursor agent "$@"',
|
|
2594
|
+
"elif command -v agent >/dev/null 2>&1; then",
|
|
2595
|
+
' exec agent "$@"',
|
|
2596
|
+
"else",
|
|
2597
|
+
" echo 'Executable not found in PATH: cursor agent or agent' >&2",
|
|
2598
|
+
" exit 127",
|
|
2599
|
+
"fi"
|
|
2600
|
+
].join(`
|
|
2601
|
+
`), "openloops-cursor", "-p");
|
|
2602
|
+
if (permissionMode === "plan")
|
|
2603
|
+
args.push("--mode", "plan");
|
|
2604
|
+
if (permissionMode === "bypass")
|
|
2605
|
+
args.push("--force");
|
|
2606
|
+
const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
|
|
2607
|
+
if (cursorSandbox) {
|
|
2608
|
+
if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
|
|
2609
|
+
throw new Error("cursor sandbox must be enabled or disabled");
|
|
2610
|
+
args.push("--sandbox", cursorSandbox);
|
|
2611
|
+
}
|
|
2498
2612
|
if (target.model)
|
|
2499
2613
|
args.push("--model", target.model);
|
|
2500
2614
|
if (target.agent)
|
|
@@ -2502,7 +2616,10 @@ function agentArgs(target) {
|
|
|
2502
2616
|
args.push(...target.extraArgs ?? []);
|
|
2503
2617
|
return args;
|
|
2504
2618
|
case "codewith":
|
|
2505
|
-
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []
|
|
2619
|
+
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
2620
|
+
if (target.variant)
|
|
2621
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2622
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
2506
2623
|
if (isolation === "safe")
|
|
2507
2624
|
args.push("--ignore-rules");
|
|
2508
2625
|
if (target.cwd)
|
|
@@ -2514,7 +2631,9 @@ function agentArgs(target) {
|
|
|
2514
2631
|
args.push(...target.extraArgs ?? []);
|
|
2515
2632
|
return args;
|
|
2516
2633
|
case "codex":
|
|
2517
|
-
|
|
2634
|
+
if (target.variant)
|
|
2635
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2636
|
+
args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
|
|
2518
2637
|
if (isolation === "safe")
|
|
2519
2638
|
args.push("--ignore-rules");
|
|
2520
2639
|
if (target.cwd)
|
|
@@ -2527,10 +2646,14 @@ function agentArgs(target) {
|
|
|
2527
2646
|
args.push("run", "--format", "json");
|
|
2528
2647
|
if (isolation === "safe")
|
|
2529
2648
|
args.push("--pure");
|
|
2649
|
+
if (permissionMode === "bypass")
|
|
2650
|
+
args.push("--dangerously-skip-permissions");
|
|
2530
2651
|
if (target.cwd)
|
|
2531
2652
|
args.push("--dir", target.cwd);
|
|
2532
2653
|
if (target.model)
|
|
2533
2654
|
args.push("--model", target.model);
|
|
2655
|
+
if (target.variant)
|
|
2656
|
+
args.push("--variant", target.variant);
|
|
2534
2657
|
if (target.agent)
|
|
2535
2658
|
args.push("--agent", target.agent);
|
|
2536
2659
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2539,10 +2662,14 @@ function agentArgs(target) {
|
|
|
2539
2662
|
args.push("run", "--format", "json");
|
|
2540
2663
|
if (isolation === "safe")
|
|
2541
2664
|
args.push("--pure");
|
|
2665
|
+
if (permissionMode === "bypass")
|
|
2666
|
+
args.push("--dangerously-skip-permissions");
|
|
2542
2667
|
if (target.cwd)
|
|
2543
2668
|
args.push("--dir", target.cwd);
|
|
2544
2669
|
if (target.model)
|
|
2545
2670
|
args.push("--model", target.model);
|
|
2671
|
+
if (target.variant)
|
|
2672
|
+
args.push("--variant", target.variant);
|
|
2546
2673
|
if (target.agent)
|
|
2547
2674
|
args.push("--agent", target.agent);
|
|
2548
2675
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2571,6 +2698,7 @@ function commandSpec(target) {
|
|
|
2571
2698
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2572
2699
|
account: agentTarget.account,
|
|
2573
2700
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2701
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2574
2702
|
stdin: agentTarget.prompt
|
|
2575
2703
|
};
|
|
2576
2704
|
}
|
|
@@ -2638,11 +2766,15 @@ function remoteScript(spec, metadata) {
|
|
|
2638
2766
|
`;
|
|
2639
2767
|
}
|
|
2640
2768
|
function remotePreflightScript(spec, metadata) {
|
|
2641
|
-
|
|
2769
|
+
const lines = [
|
|
2642
2770
|
...remoteBootstrapLines(spec, metadata),
|
|
2643
2771
|
"command -v bash >/dev/null 2>&1",
|
|
2644
2772
|
`command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
|
|
2645
|
-
]
|
|
2773
|
+
];
|
|
2774
|
+
if (spec.preflightAnyOf?.length) {
|
|
2775
|
+
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");
|
|
2776
|
+
}
|
|
2777
|
+
return lines.join(`
|
|
2646
2778
|
`);
|
|
2647
2779
|
}
|
|
2648
2780
|
function transportEnv(opts) {
|
|
@@ -2795,6 +2927,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2795
2927
|
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
2796
2928
|
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
2797
2929
|
}
|
|
2930
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2931
|
+
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2932
|
+
}
|
|
2798
2933
|
return {
|
|
2799
2934
|
command: spec.command,
|
|
2800
2935
|
accountProfile: spec.account?.profile,
|
|
@@ -2825,6 +2960,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2825
2960
|
durationMs: 0
|
|
2826
2961
|
};
|
|
2827
2962
|
}
|
|
2963
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2964
|
+
return {
|
|
2965
|
+
status: "failed",
|
|
2966
|
+
stdout,
|
|
2967
|
+
stderr,
|
|
2968
|
+
error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
|
|
2969
|
+
startedAt,
|
|
2970
|
+
finishedAt: nowIso(),
|
|
2971
|
+
durationMs: 0
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2828
2974
|
const child = spawn(spec.command, spec.args, {
|
|
2829
2975
|
cwd: spec.cwd,
|
|
2830
2976
|
env,
|
|
@@ -4241,17 +4387,35 @@ import { spawnSync as spawnSync4 } from "child_process";
|
|
|
4241
4387
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
4242
4388
|
var PROVIDER_COMMANDS = [
|
|
4243
4389
|
"claude",
|
|
4244
|
-
"cursor
|
|
4390
|
+
"cursor agent",
|
|
4245
4391
|
"codewith",
|
|
4246
4392
|
"aicopilot",
|
|
4247
4393
|
"opencode",
|
|
4248
4394
|
"codex"
|
|
4249
4395
|
];
|
|
4250
4396
|
function hasCommand(command) {
|
|
4397
|
+
if (command === "cursor agent") {
|
|
4398
|
+
return hasCommand("cursor") || hasCommand("agent");
|
|
4399
|
+
}
|
|
4251
4400
|
const result = spawnSync4("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
4252
4401
|
return (result.status ?? 1) === 0;
|
|
4253
4402
|
}
|
|
4254
4403
|
function commandVersion(command) {
|
|
4404
|
+
if (command === "cursor agent") {
|
|
4405
|
+
const cursorResult = spawnSync4("cursor", ["agent", "--version"], {
|
|
4406
|
+
encoding: "utf8",
|
|
4407
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4408
|
+
});
|
|
4409
|
+
if ((cursorResult.status ?? 1) === 0)
|
|
4410
|
+
return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
|
|
4411
|
+
const agentResult = spawnSync4("agent", ["--version"], {
|
|
4412
|
+
encoding: "utf8",
|
|
4413
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4414
|
+
});
|
|
4415
|
+
if ((agentResult.status ?? 1) === 0)
|
|
4416
|
+
return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4255
4419
|
const result = spawnSync4(command, ["--version"], {
|
|
4256
4420
|
encoding: "utf8",
|
|
4257
4421
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4330,7 +4494,7 @@ function runDoctor(store) {
|
|
|
4330
4494
|
// package.json
|
|
4331
4495
|
var package_default = {
|
|
4332
4496
|
name: "@hasna/loops",
|
|
4333
|
-
version: "0.3.
|
|
4497
|
+
version: "0.3.13",
|
|
4334
4498
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4335
4499
|
type: "module",
|
|
4336
4500
|
main: "dist/index.js",
|
|
@@ -4394,6 +4558,7 @@ var package_default = {
|
|
|
4394
4558
|
bun: ">=1.0.0"
|
|
4395
4559
|
},
|
|
4396
4560
|
dependencies: {
|
|
4561
|
+
"@hasna/events": "^0.1.8",
|
|
4397
4562
|
"@hasna/machines": "0.0.49",
|
|
4398
4563
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4399
4564
|
ai: "6.0.204",
|
|
@@ -4417,6 +4582,242 @@ function packageVersion() {
|
|
|
4417
4582
|
return package_default.version;
|
|
4418
4583
|
}
|
|
4419
4584
|
|
|
4585
|
+
// src/lib/templates.ts
|
|
4586
|
+
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
4587
|
+
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
4588
|
+
var TEMPLATE_SUMMARIES = [
|
|
4589
|
+
{
|
|
4590
|
+
id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
4591
|
+
name: "Todos Task Worker + Verifier",
|
|
4592
|
+
description: "Create a one-shot workflow for a todos task: one agent performs the task, then a fresh verifier agent audits the result and records follow-up tasks or completion evidence.",
|
|
4593
|
+
kind: "workflow",
|
|
4594
|
+
variables: [
|
|
4595
|
+
{ name: "taskId", required: true, description: "Todos task id to execute." },
|
|
4596
|
+
{ name: "taskTitle", description: "Human-readable task title." },
|
|
4597
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4598
|
+
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
4599
|
+
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
4600
|
+
{ name: "model", description: "Provider model." },
|
|
4601
|
+
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4602
|
+
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
4603
|
+
{ name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
|
|
4604
|
+
]
|
|
4605
|
+
},
|
|
4606
|
+
{
|
|
4607
|
+
id: EVENT_WORKER_VERIFIER_TEMPLATE_ID,
|
|
4608
|
+
name: "Hasna Event Worker + Verifier",
|
|
4609
|
+
description: "Create a one-shot workflow for a generic Hasna event: one agent handles the event, then a fresh verifier agent audits the result and records evidence or follow-up tasks.",
|
|
4610
|
+
kind: "workflow",
|
|
4611
|
+
variables: [
|
|
4612
|
+
{ name: "eventId", required: true, description: "Hasna event id." },
|
|
4613
|
+
{ name: "eventType", required: true, description: "Hasna event type." },
|
|
4614
|
+
{ name: "eventSource", required: true, description: "Hasna event source." },
|
|
4615
|
+
{ name: "eventJson", required: true, description: "Full event envelope JSON." },
|
|
4616
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
4617
|
+
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
4618
|
+
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
4619
|
+
{ name: "model", description: "Provider model." },
|
|
4620
|
+
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
4621
|
+
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
4622
|
+
{ name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
|
|
4623
|
+
]
|
|
4624
|
+
}
|
|
4625
|
+
];
|
|
4626
|
+
function compactJson(value) {
|
|
4627
|
+
return JSON.stringify(value);
|
|
4628
|
+
}
|
|
4629
|
+
function taskLabel(input) {
|
|
4630
|
+
const head = input.taskTitle?.trim() || input.taskId;
|
|
4631
|
+
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
4632
|
+
}
|
|
4633
|
+
function agentTarget(input, prompt) {
|
|
4634
|
+
const provider = input.provider ?? "codewith";
|
|
4635
|
+
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
|
|
4636
|
+
return {
|
|
4637
|
+
type: "agent",
|
|
4638
|
+
provider,
|
|
4639
|
+
prompt,
|
|
4640
|
+
cwd: input.projectPath,
|
|
4641
|
+
model: input.model,
|
|
4642
|
+
variant: input.variant,
|
|
4643
|
+
agent: input.agent,
|
|
4644
|
+
authProfile: provider === "codewith" ? input.authProfile : undefined,
|
|
4645
|
+
configIsolation: "safe",
|
|
4646
|
+
permissionMode: input.permissionMode ?? "bypass",
|
|
4647
|
+
sandbox,
|
|
4648
|
+
account: input.account,
|
|
4649
|
+
timeoutMs: 45 * 60000
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
function listLoopTemplates() {
|
|
4653
|
+
return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
|
|
4654
|
+
}
|
|
4655
|
+
function getLoopTemplate(id) {
|
|
4656
|
+
return listLoopTemplates().find((template) => template.id === id || template.name === id);
|
|
4657
|
+
}
|
|
4658
|
+
function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
4659
|
+
if (!input.taskId?.trim())
|
|
4660
|
+
throw new Error("taskId is required");
|
|
4661
|
+
if (!input.projectPath?.trim())
|
|
4662
|
+
throw new Error("projectPath is required");
|
|
4663
|
+
const taskContext = {
|
|
4664
|
+
taskId: input.taskId,
|
|
4665
|
+
taskTitle: input.taskTitle,
|
|
4666
|
+
taskDescription: input.taskDescription,
|
|
4667
|
+
eventId: input.eventId,
|
|
4668
|
+
eventType: input.eventType,
|
|
4669
|
+
projectPath: input.projectPath
|
|
4670
|
+
};
|
|
4671
|
+
const workerPrompt = [
|
|
4672
|
+
`/goal Complete todos task ${input.taskId} in ${input.projectPath}.`,
|
|
4673
|
+
"",
|
|
4674
|
+
"You are the worker agent for a task-triggered OpenLoops workflow.",
|
|
4675
|
+
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
4676
|
+
"Claim/start the task if appropriate, inspect the repository/project state, implement only the task scope, run focused validation, preserve unrelated user changes, and update the task with comments, evidence, changed files, commits, and blockers.",
|
|
4677
|
+
"Do not mark the task complete unless the work is genuinely done and validated.",
|
|
4678
|
+
"",
|
|
4679
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
4680
|
+
].join(`
|
|
4681
|
+
`);
|
|
4682
|
+
const verifierPrompt = [
|
|
4683
|
+
`/goal Verify todos task ${input.taskId} after the worker step.`,
|
|
4684
|
+
"",
|
|
4685
|
+
"You are the verifier agent for a task-triggered OpenLoops workflow.",
|
|
4686
|
+
"Use fresh context. Inspect the task, repository state, commits, tests, and worker evidence. Act as an adversarial reviewer focused on correctness, regressions, missing tests, security, and incomplete requirements.",
|
|
4687
|
+
"If the work is valid, record verification evidence in todos and mark/leave the task in the correct completed state according to the todos CLI. If it is not valid, add precise follow-up tasks or comments and leave the original task open or blocked with clear evidence.",
|
|
4688
|
+
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
4689
|
+
"",
|
|
4690
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
4691
|
+
].join(`
|
|
4692
|
+
`);
|
|
4693
|
+
return {
|
|
4694
|
+
name: `todos-task-${input.taskId.slice(0, 8)}-worker-verifier`,
|
|
4695
|
+
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
4696
|
+
version: 1,
|
|
4697
|
+
steps: [
|
|
4698
|
+
{
|
|
4699
|
+
id: "worker",
|
|
4700
|
+
name: "Worker",
|
|
4701
|
+
description: "Implement the todos task and record evidence.",
|
|
4702
|
+
target: agentTarget(input, workerPrompt),
|
|
4703
|
+
timeoutMs: 45 * 60000
|
|
4704
|
+
},
|
|
4705
|
+
{
|
|
4706
|
+
id: "verifier",
|
|
4707
|
+
name: "Verifier",
|
|
4708
|
+
description: "Adversarially verify worker output and update todos.",
|
|
4709
|
+
dependsOn: ["worker"],
|
|
4710
|
+
target: agentTarget(input, verifierPrompt),
|
|
4711
|
+
timeoutMs: 30 * 60000
|
|
4712
|
+
}
|
|
4713
|
+
]
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
function renderEventWorkerVerifierWorkflow(input) {
|
|
4717
|
+
if (!input.eventId?.trim())
|
|
4718
|
+
throw new Error("eventId is required");
|
|
4719
|
+
if (!input.eventType?.trim())
|
|
4720
|
+
throw new Error("eventType is required");
|
|
4721
|
+
if (!input.eventSource?.trim())
|
|
4722
|
+
throw new Error("eventSource is required");
|
|
4723
|
+
if (!input.eventJson?.trim())
|
|
4724
|
+
throw new Error("eventJson is required");
|
|
4725
|
+
if (!input.projectPath?.trim())
|
|
4726
|
+
throw new Error("projectPath is required");
|
|
4727
|
+
const eventContext = {
|
|
4728
|
+
eventId: input.eventId,
|
|
4729
|
+
eventType: input.eventType,
|
|
4730
|
+
eventSource: input.eventSource,
|
|
4731
|
+
eventSubject: input.eventSubject,
|
|
4732
|
+
eventMessage: input.eventMessage,
|
|
4733
|
+
projectPath: input.projectPath
|
|
4734
|
+
};
|
|
4735
|
+
const workerPrompt = [
|
|
4736
|
+
`/goal Handle Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}) in ${input.projectPath}.`,
|
|
4737
|
+
"",
|
|
4738
|
+
"You are the worker agent for an event-triggered OpenLoops workflow.",
|
|
4739
|
+
"Investigate first before changing files. Read the full event envelope and decide the narrow action required by that event. Preserve unrelated user changes and update the relevant local CLI/task/knowledge system with evidence, changed files, commits, and blockers.",
|
|
4740
|
+
"If the event is informational or does not require action, record that finding and stop without making changes.",
|
|
4741
|
+
"",
|
|
4742
|
+
`Event context JSON: ${compactJson(eventContext)}`,
|
|
4743
|
+
`Full event envelope JSON: ${input.eventJson}`
|
|
4744
|
+
].join(`
|
|
4745
|
+
`);
|
|
4746
|
+
const verifierPrompt = [
|
|
4747
|
+
`/goal Verify handling of Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}).`,
|
|
4748
|
+
"",
|
|
4749
|
+
"You are the verifier agent for an event-triggered OpenLoops workflow.",
|
|
4750
|
+
"Use fresh context. Inspect the event, repository/project state, worker evidence, tests, and any created tasks or notes. Act as an adversarial reviewer focused on correctness, regressions, security, missing evidence, and incomplete requirements.",
|
|
4751
|
+
"If the work is valid, record verification evidence in the relevant local system. If it is not valid, add precise follow-up tasks/comments and leave the event handling state open or blocked with clear evidence.",
|
|
4752
|
+
"",
|
|
4753
|
+
`Event context JSON: ${compactJson(eventContext)}`,
|
|
4754
|
+
`Full event envelope JSON: ${input.eventJson}`
|
|
4755
|
+
].join(`
|
|
4756
|
+
`);
|
|
4757
|
+
return {
|
|
4758
|
+
name: `event-${input.eventSource}-${input.eventType}-${input.eventId.slice(0, 8)}-worker-verifier`.replace(/[^a-zA-Z0-9._:-]+/g, "-"),
|
|
4759
|
+
description: `Event-triggered worker/verifier workflow for ${input.eventSource}/${input.eventType}`,
|
|
4760
|
+
version: 1,
|
|
4761
|
+
steps: [
|
|
4762
|
+
{
|
|
4763
|
+
id: "worker",
|
|
4764
|
+
name: "Worker",
|
|
4765
|
+
description: "Handle the Hasna event and record evidence.",
|
|
4766
|
+
target: agentTarget(input, workerPrompt),
|
|
4767
|
+
timeoutMs: 45 * 60000
|
|
4768
|
+
},
|
|
4769
|
+
{
|
|
4770
|
+
id: "verifier",
|
|
4771
|
+
name: "Verifier",
|
|
4772
|
+
description: "Adversarially verify event handling.",
|
|
4773
|
+
dependsOn: ["worker"],
|
|
4774
|
+
target: agentTarget(input, verifierPrompt),
|
|
4775
|
+
timeoutMs: 30 * 60000
|
|
4776
|
+
}
|
|
4777
|
+
]
|
|
4778
|
+
};
|
|
4779
|
+
}
|
|
4780
|
+
function renderLoopTemplate(id, values) {
|
|
4781
|
+
if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
4782
|
+
return renderTodosTaskWorkerVerifierWorkflow({
|
|
4783
|
+
taskId: values.taskId ?? "",
|
|
4784
|
+
taskTitle: values.taskTitle,
|
|
4785
|
+
taskDescription: values.taskDescription,
|
|
4786
|
+
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4787
|
+
provider: values.provider,
|
|
4788
|
+
authProfile: values.authProfile,
|
|
4789
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4790
|
+
model: values.model,
|
|
4791
|
+
variant: values.variant,
|
|
4792
|
+
agent: values.agent,
|
|
4793
|
+
permissionMode: values.permissionMode,
|
|
4794
|
+
sandbox: values.sandbox,
|
|
4795
|
+
eventId: values.eventId,
|
|
4796
|
+
eventType: values.eventType
|
|
4797
|
+
});
|
|
4798
|
+
}
|
|
4799
|
+
if (id === EVENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
4800
|
+
return renderEventWorkerVerifierWorkflow({
|
|
4801
|
+
eventId: values.eventId ?? "",
|
|
4802
|
+
eventType: values.eventType ?? "",
|
|
4803
|
+
eventSource: values.eventSource ?? "",
|
|
4804
|
+
eventSubject: values.eventSubject,
|
|
4805
|
+
eventMessage: values.eventMessage,
|
|
4806
|
+
eventJson: values.eventJson ?? "",
|
|
4807
|
+
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4808
|
+
provider: values.provider,
|
|
4809
|
+
authProfile: values.authProfile,
|
|
4810
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4811
|
+
model: values.model,
|
|
4812
|
+
variant: values.variant,
|
|
4813
|
+
agent: values.agent,
|
|
4814
|
+
permissionMode: values.permissionMode,
|
|
4815
|
+
sandbox: values.sandbox
|
|
4816
|
+
});
|
|
4817
|
+
}
|
|
4818
|
+
throw new Error(`unknown template: ${id}`);
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4420
4821
|
// src/cli/index.ts
|
|
4421
4822
|
var program = new Command;
|
|
4422
4823
|
program.name("loops").description("Persistent local loops for commands and headless coding agents").version(packageVersion());
|
|
@@ -4535,6 +4936,69 @@ function accountFromOpts(opts) {
|
|
|
4535
4936
|
throw new Error("--account-tool requires --account");
|
|
4536
4937
|
return opts.account ? { profile: opts.account, tool: opts.accountTool } : undefined;
|
|
4537
4938
|
}
|
|
4939
|
+
function parseVars(values) {
|
|
4940
|
+
const vars = {};
|
|
4941
|
+
for (const value of values ?? []) {
|
|
4942
|
+
const index = value.indexOf("=");
|
|
4943
|
+
if (index <= 0)
|
|
4944
|
+
throw new Error(`invalid --var value, expected key=value: ${value}`);
|
|
4945
|
+
vars[value.slice(0, index)] = value.slice(index + 1);
|
|
4946
|
+
}
|
|
4947
|
+
return vars;
|
|
4948
|
+
}
|
|
4949
|
+
function collectValues(value, previous = []) {
|
|
4950
|
+
previous.push(value);
|
|
4951
|
+
return previous;
|
|
4952
|
+
}
|
|
4953
|
+
function eventData(event) {
|
|
4954
|
+
const data = event.data;
|
|
4955
|
+
if (data && typeof data === "object" && !Array.isArray(data))
|
|
4956
|
+
return data;
|
|
4957
|
+
return {};
|
|
4958
|
+
}
|
|
4959
|
+
function stringField(value) {
|
|
4960
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
4961
|
+
}
|
|
4962
|
+
function slugSegment(value, fallback = "event") {
|
|
4963
|
+
return value.toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || fallback;
|
|
4964
|
+
}
|
|
4965
|
+
function taskEventField(data, keys) {
|
|
4966
|
+
for (const key of keys) {
|
|
4967
|
+
const direct = stringField(data[key]);
|
|
4968
|
+
if (direct)
|
|
4969
|
+
return direct;
|
|
4970
|
+
}
|
|
4971
|
+
const task = data.task;
|
|
4972
|
+
if (task && typeof task === "object" && !Array.isArray(task)) {
|
|
4973
|
+
for (const key of keys) {
|
|
4974
|
+
const direct = stringField(task[key]);
|
|
4975
|
+
if (direct)
|
|
4976
|
+
return direct;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
const payload = data.payload;
|
|
4980
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
4981
|
+
for (const key of keys) {
|
|
4982
|
+
const direct = stringField(payload[key]);
|
|
4983
|
+
if (direct)
|
|
4984
|
+
return direct;
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
async function readEventEnvelopeFromStdin() {
|
|
4990
|
+
const raw = process.env.HASNA_EVENT_JSON || await Bun.stdin.text();
|
|
4991
|
+
const event = JSON.parse(raw);
|
|
4992
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
4993
|
+
throw new Error("event JSON must be an object");
|
|
4994
|
+
if (!stringField(event.id))
|
|
4995
|
+
throw new Error("event.id is required");
|
|
4996
|
+
if (!stringField(event.type))
|
|
4997
|
+
throw new Error("event.type is required");
|
|
4998
|
+
if (!stringField(event.source))
|
|
4999
|
+
throw new Error("event.source is required");
|
|
5000
|
+
return event;
|
|
5001
|
+
}
|
|
4538
5002
|
function providerAuthProfileFromOpts(opts, provider) {
|
|
4539
5003
|
if (!opts.authProfile)
|
|
4540
5004
|
return;
|
|
@@ -4542,6 +5006,40 @@ function providerAuthProfileFromOpts(opts, provider) {
|
|
|
4542
5006
|
throw new Error("--auth-profile is currently supported only for --provider codewith");
|
|
4543
5007
|
return opts.authProfile;
|
|
4544
5008
|
}
|
|
5009
|
+
function sandboxFromOpts(opts, provider) {
|
|
5010
|
+
if (!opts.sandbox)
|
|
5011
|
+
return;
|
|
5012
|
+
const codexLike = ["read-only", "workspace-write", "danger-full-access"];
|
|
5013
|
+
const cursorLike = ["enabled", "disabled"];
|
|
5014
|
+
if (["codewith", "codex"].includes(provider)) {
|
|
5015
|
+
if (!codexLike.includes(opts.sandbox)) {
|
|
5016
|
+
throw new Error("--sandbox must be read-only, workspace-write, or danger-full-access for codewith/codex");
|
|
5017
|
+
}
|
|
5018
|
+
return opts.sandbox;
|
|
5019
|
+
}
|
|
5020
|
+
if (provider === "cursor") {
|
|
5021
|
+
if (!cursorLike.includes(opts.sandbox)) {
|
|
5022
|
+
throw new Error("--sandbox must be enabled or disabled for cursor");
|
|
5023
|
+
}
|
|
5024
|
+
return opts.sandbox;
|
|
5025
|
+
}
|
|
5026
|
+
throw new Error("--sandbox is currently supported only for --provider codewith, codex, or cursor");
|
|
5027
|
+
}
|
|
5028
|
+
function permissionModeFromOpts(opts, provider) {
|
|
5029
|
+
if (!opts.permissionMode)
|
|
5030
|
+
return;
|
|
5031
|
+
const mode = opts.permissionMode;
|
|
5032
|
+
if (!["default", "plan", "auto", "bypass"].includes(mode)) {
|
|
5033
|
+
throw new Error("--permission-mode must be default, plan, auto, or bypass");
|
|
5034
|
+
}
|
|
5035
|
+
if (mode === "plan" && !["claude", "cursor"].includes(provider)) {
|
|
5036
|
+
throw new Error("--permission-mode plan is currently supported only for claude or cursor");
|
|
5037
|
+
}
|
|
5038
|
+
if (mode === "auto" && provider !== "claude") {
|
|
5039
|
+
throw new Error("--permission-mode auto is currently supported only for claude");
|
|
5040
|
+
}
|
|
5041
|
+
return mode;
|
|
5042
|
+
}
|
|
4545
5043
|
var create = program.command("create").description("create loops");
|
|
4546
5044
|
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("command <name>").description("create a deterministic shell command loop").requiredOption("--cmd <command>", "command string to execute").option("--cwd <dir>", "working directory").option("--timeout <duration>", "run timeout").option("--no-shell", "execute without a shell"))))).action((name, opts) => {
|
|
4547
5045
|
const store = new Store;
|
|
@@ -4560,7 +5058,7 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
4560
5058
|
store.close();
|
|
4561
5059
|
}
|
|
4562
5060
|
});
|
|
4563
|
-
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("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
|
|
5061
|
+
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("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
|
|
4564
5062
|
const provider = opts.provider;
|
|
4565
5063
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
4566
5064
|
throw new Error("unsupported provider");
|
|
@@ -4576,10 +5074,13 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
4576
5074
|
prompt: opts.prompt,
|
|
4577
5075
|
cwd: opts.cwd,
|
|
4578
5076
|
model: opts.model,
|
|
5077
|
+
variant: opts.variant,
|
|
4579
5078
|
agent: opts.agent,
|
|
4580
5079
|
authProfile: providerAuthProfileFromOpts(opts, provider),
|
|
4581
5080
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
4582
5081
|
configIsolation: opts.configIsolation,
|
|
5082
|
+
permissionMode: permissionModeFromOpts(opts, provider),
|
|
5083
|
+
sandbox: sandboxFromOpts(opts, provider),
|
|
4583
5084
|
account: accountFromOpts(opts)
|
|
4584
5085
|
};
|
|
4585
5086
|
const loop = store.createLoop(baseCreateInput(name, opts, target));
|
|
@@ -4603,8 +5104,175 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
|
|
|
4603
5104
|
}
|
|
4604
5105
|
});
|
|
4605
5106
|
var workflows = program.command("workflows").alias("workflow").description("manage workflow specs and runs");
|
|
5107
|
+
var templates = program.command("templates").alias("template").description("render and store reusable loop/workflow templates");
|
|
5108
|
+
var events = program.command("events").description("handle Hasna event envelopes from stdin or command transport");
|
|
4606
5109
|
var machines = program.command("machines").description("inspect OpenMachines topology for loop assignment");
|
|
4607
5110
|
var goal = program.command("goal").description("inspect goal runs");
|
|
5111
|
+
templates.command("list").alias("ls").description("list built-in OpenLoops templates").action(() => {
|
|
5112
|
+
const values = listLoopTemplates();
|
|
5113
|
+
if (isJson())
|
|
5114
|
+
print(values);
|
|
5115
|
+
else {
|
|
5116
|
+
for (const template of values) {
|
|
5117
|
+
console.log(`${template.id} ${template.kind} ${template.description}`);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
});
|
|
5121
|
+
templates.command("show <id>").description("show a built-in template").action((id) => {
|
|
5122
|
+
const template = getLoopTemplate(id);
|
|
5123
|
+
if (!template)
|
|
5124
|
+
throw new Error(`template not found: ${id}`);
|
|
5125
|
+
print(template, `${template.id} ${template.kind}`);
|
|
5126
|
+
});
|
|
5127
|
+
templates.command("render <id>").description("render a template as workflow JSON").option("--var <key=value>", "template variable; may be repeated", collectValues, []).action((id, opts) => {
|
|
5128
|
+
const workflow = renderLoopTemplate(id, parseVars(opts.var));
|
|
5129
|
+
print(workflow, JSON.stringify(workflow, null, 2));
|
|
5130
|
+
});
|
|
5131
|
+
templates.command("create-workflow <id>").description("render and store a template as a workflow").option("--var <key=value>", "template variable; may be repeated", collectValues, []).action((id, opts) => {
|
|
5132
|
+
const store = new Store;
|
|
5133
|
+
try {
|
|
5134
|
+
const body = renderLoopTemplate(id, parseVars(opts.var));
|
|
5135
|
+
const workflow = store.createWorkflow(body);
|
|
5136
|
+
print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
|
|
5137
|
+
} finally {
|
|
5138
|
+
store.close();
|
|
5139
|
+
}
|
|
5140
|
+
});
|
|
5141
|
+
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
5142
|
+
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("--account <profile>", "OpenAccounts profile name").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("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
5143
|
+
const event = await readEventEnvelopeFromStdin();
|
|
5144
|
+
const data = eventData(event);
|
|
5145
|
+
const taskId = taskEventField(data, ["id", "task_id", "taskId"]);
|
|
5146
|
+
if (!taskId)
|
|
5147
|
+
throw new Error("todos task event is missing task id in data.id, data.task_id, data.task.id, or data.payload.id");
|
|
5148
|
+
const taskTitle = taskEventField(data, ["title", "task_title", "taskTitle"]);
|
|
5149
|
+
const taskDescription = taskEventField(data, ["description", "body"]);
|
|
5150
|
+
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd"]) ?? process.cwd();
|
|
5151
|
+
const provider = opts.provider;
|
|
5152
|
+
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
|
|
5153
|
+
throw new Error("unsupported provider");
|
|
5154
|
+
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode }, provider);
|
|
5155
|
+
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
5156
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
5157
|
+
const workflowBody = renderTodosTaskWorkerVerifierWorkflow({
|
|
5158
|
+
taskId,
|
|
5159
|
+
taskTitle,
|
|
5160
|
+
taskDescription,
|
|
5161
|
+
projectPath,
|
|
5162
|
+
provider,
|
|
5163
|
+
authProfile,
|
|
5164
|
+
account: accountFromOpts(opts),
|
|
5165
|
+
model: opts.model,
|
|
5166
|
+
variant: opts.variant,
|
|
5167
|
+
agent: opts.agent,
|
|
5168
|
+
permissionMode,
|
|
5169
|
+
sandbox,
|
|
5170
|
+
eventId: event.id,
|
|
5171
|
+
eventType: event.type
|
|
5172
|
+
});
|
|
5173
|
+
const eventSuffix = event.id.slice(0, 8);
|
|
5174
|
+
workflowBody.name = `${opts.namePrefix}:${taskId.slice(0, 8)}:${eventSuffix}:workflow`;
|
|
5175
|
+
workflowBody.description = `Task-triggered worker/verifier workflow for ${taskTitle ?? taskId} from ${event.source}/${event.type}`;
|
|
5176
|
+
const loopName = `${opts.namePrefix}:${taskId.slice(0, 8)}:${eventSuffix}:run`;
|
|
5177
|
+
const loopInput = {
|
|
5178
|
+
name: loopName,
|
|
5179
|
+
description: `Run ${workflowBody.name} once for task ${taskId}`,
|
|
5180
|
+
schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
|
|
5181
|
+
target: { type: "workflow", workflowId: "<created-workflow-id>" },
|
|
5182
|
+
overlap: "skip",
|
|
5183
|
+
maxAttempts: 1,
|
|
5184
|
+
retryDelayMs: 60000,
|
|
5185
|
+
leaseMs: 90 * 60000
|
|
5186
|
+
};
|
|
5187
|
+
if (opts.dryRun) {
|
|
5188
|
+
print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5189
|
+
return;
|
|
5190
|
+
}
|
|
5191
|
+
const store = new Store;
|
|
5192
|
+
try {
|
|
5193
|
+
const existingLoop = store.findLoopByName(loopName);
|
|
5194
|
+
if (existingLoop) {
|
|
5195
|
+
const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
|
|
5196
|
+
print({ deduped: true, event, workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined, loop: publicLoop(existingLoop) }, `deduped existing loop ${existingLoop.id} (${existingLoop.name})`);
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
5200
|
+
const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
|
|
5201
|
+
const loop = store.createLoop({
|
|
5202
|
+
...loopInput,
|
|
5203
|
+
target: { type: "workflow", workflowId: workflow.id }
|
|
5204
|
+
});
|
|
5205
|
+
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
5206
|
+
} finally {
|
|
5207
|
+
store.close();
|
|
5208
|
+
}
|
|
5209
|
+
});
|
|
5210
|
+
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("--account <profile>", "OpenAccounts profile name").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("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
5211
|
+
const event = await readEventEnvelopeFromStdin();
|
|
5212
|
+
const data = eventData(event);
|
|
5213
|
+
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
|
|
5214
|
+
const provider = opts.provider;
|
|
5215
|
+
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider))
|
|
5216
|
+
throw new Error("unsupported provider");
|
|
5217
|
+
const permissionMode = permissionModeFromOpts({ permissionMode: opts.permissionMode }, provider);
|
|
5218
|
+
const sandbox = sandboxFromOpts({ sandbox: opts.sandbox }, provider);
|
|
5219
|
+
const authProfile = providerAuthProfileFromOpts({ authProfile: opts.authProfile }, provider);
|
|
5220
|
+
const workflowBody = renderEventWorkerVerifierWorkflow({
|
|
5221
|
+
eventId: event.id,
|
|
5222
|
+
eventType: event.type,
|
|
5223
|
+
eventSource: event.source,
|
|
5224
|
+
eventSubject: stringField(event.subject),
|
|
5225
|
+
eventMessage: stringField(event.message),
|
|
5226
|
+
eventJson: JSON.stringify(event),
|
|
5227
|
+
projectPath,
|
|
5228
|
+
provider,
|
|
5229
|
+
authProfile,
|
|
5230
|
+
account: accountFromOpts(opts),
|
|
5231
|
+
model: opts.model,
|
|
5232
|
+
variant: opts.variant,
|
|
5233
|
+
agent: opts.agent,
|
|
5234
|
+
permissionMode,
|
|
5235
|
+
sandbox
|
|
5236
|
+
});
|
|
5237
|
+
const eventSuffix = event.id.slice(0, 8);
|
|
5238
|
+
const source = slugSegment(event.source, "source");
|
|
5239
|
+
const type = slugSegment(event.type, "type");
|
|
5240
|
+
workflowBody.name = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:workflow`;
|
|
5241
|
+
workflowBody.description = `Event-triggered worker/verifier workflow for ${event.source}/${event.type}`;
|
|
5242
|
+
const loopName = `${opts.namePrefix}:${source}:${type}:${eventSuffix}:run`;
|
|
5243
|
+
const loopInput = {
|
|
5244
|
+
name: loopName,
|
|
5245
|
+
description: `Run ${workflowBody.name} once for event ${event.id}`,
|
|
5246
|
+
schedule: { type: "once", at: new Date(Date.now() + 1000).toISOString() },
|
|
5247
|
+
target: { type: "workflow", workflowId: "<created-workflow-id>" },
|
|
5248
|
+
overlap: "skip",
|
|
5249
|
+
maxAttempts: 1,
|
|
5250
|
+
retryDelayMs: 60000,
|
|
5251
|
+
leaseMs: 90 * 60000
|
|
5252
|
+
};
|
|
5253
|
+
if (opts.dryRun) {
|
|
5254
|
+
print({ event, workflow: workflowBody, loop: loopInput }, `dry-run ${loopName}`);
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
const store = new Store;
|
|
5258
|
+
try {
|
|
5259
|
+
const existingLoop = store.findLoopByName(loopName);
|
|
5260
|
+
if (existingLoop) {
|
|
5261
|
+
const existingWorkflow2 = existingLoop.target.type === "workflow" ? store.getWorkflow(existingLoop.target.workflowId) : undefined;
|
|
5262
|
+
print({ deduped: true, event, workflow: existingWorkflow2 ? publicWorkflow(existingWorkflow2) : undefined, loop: publicLoop(existingLoop) }, `deduped existing loop ${existingLoop.id} (${existingLoop.name})`);
|
|
5263
|
+
return;
|
|
5264
|
+
}
|
|
5265
|
+
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
5266
|
+
const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
|
|
5267
|
+
const loop = store.createLoop({
|
|
5268
|
+
...loopInput,
|
|
5269
|
+
target: { type: "workflow", workflowId: workflow.id }
|
|
5270
|
+
});
|
|
5271
|
+
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
5272
|
+
} finally {
|
|
5273
|
+
store.close();
|
|
5274
|
+
}
|
|
5275
|
+
});
|
|
4608
5276
|
goal.command("show <idOrName>").description("show a goal or configured loop/workflow goal").action((idOrName) => {
|
|
4609
5277
|
const store = new Store;
|
|
4610
5278
|
try {
|
|
@@ -4718,11 +5386,11 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
|
|
|
4718
5386
|
try {
|
|
4719
5387
|
const run = store.requireWorkflowRun(runId);
|
|
4720
5388
|
const steps = store.listWorkflowStepRuns(run.id);
|
|
4721
|
-
const
|
|
5389
|
+
const events2 = store.listWorkflowEvents(run.id);
|
|
4722
5390
|
const value = {
|
|
4723
5391
|
workflowRun: publicWorkflowRun(run),
|
|
4724
5392
|
steps: steps.map((step) => publicWorkflowStepRun(step)),
|
|
4725
|
-
events:
|
|
5393
|
+
events: events2.map(publicWorkflowEvent)
|
|
4726
5394
|
};
|
|
4727
5395
|
if (isJson())
|
|
4728
5396
|
print(value);
|
|
@@ -4732,7 +5400,7 @@ workflows.command("inspect <runId>").description("show a workflow run with steps
|
|
|
4732
5400
|
const publicStep = publicWorkflowStepRun(step);
|
|
4733
5401
|
console.log(` ${String(step.sequence).padStart(2, "0")} ${step.status.padEnd(10)} ${step.stepId} ${publicStep.error ?? ""}`);
|
|
4734
5402
|
}
|
|
4735
|
-
console.log(` events=${
|
|
5403
|
+
console.log(` events=${events2.length}`);
|
|
4736
5404
|
}
|
|
4737
5405
|
} finally {
|
|
4738
5406
|
store.close();
|
|
@@ -4784,11 +5452,11 @@ workflows.command("runs [idOrName]").option("--limit <n>", "limit", "50").action
|
|
|
4784
5452
|
workflows.command("events <runId>").option("--limit <n>", "limit", "200").action((runId, opts) => {
|
|
4785
5453
|
const store = new Store;
|
|
4786
5454
|
try {
|
|
4787
|
-
const
|
|
5455
|
+
const events2 = store.listWorkflowEvents(runId, Number(opts.limit));
|
|
4788
5456
|
if (isJson())
|
|
4789
|
-
print(
|
|
5457
|
+
print(events2.map(publicWorkflowEvent));
|
|
4790
5458
|
else {
|
|
4791
|
-
for (const event of
|
|
5459
|
+
for (const event of events2) {
|
|
4792
5460
|
console.log(`${String(event.sequence).padStart(3, "0")} ${event.eventType.padEnd(14)} ${event.stepId ?? "-"} ${event.createdAt}`);
|
|
4793
5461
|
}
|
|
4794
5462
|
}
|