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