@hasna/loops 0.3.12 → 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/index.js
CHANGED
|
@@ -368,6 +368,35 @@ function validateTarget(value, label) {
|
|
|
368
368
|
if (value.provider !== "codewith")
|
|
369
369
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
370
370
|
}
|
|
371
|
+
if (value.variant !== undefined)
|
|
372
|
+
assertString(value.variant, `${label}.variant`);
|
|
373
|
+
if (value.permissionMode !== undefined) {
|
|
374
|
+
assertString(value.permissionMode, `${label}.permissionMode`);
|
|
375
|
+
const permissionModes = ["default", "plan", "auto", "bypass"];
|
|
376
|
+
if (!permissionModes.includes(value.permissionMode)) {
|
|
377
|
+
throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
|
|
378
|
+
}
|
|
379
|
+
if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
|
|
380
|
+
throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
|
|
381
|
+
}
|
|
382
|
+
if (value.permissionMode === "auto" && value.provider !== "claude") {
|
|
383
|
+
throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (value.sandbox !== undefined) {
|
|
387
|
+
assertString(value.sandbox, `${label}.sandbox`);
|
|
388
|
+
const codexLike = ["read-only", "workspace-write", "danger-full-access"];
|
|
389
|
+
const cursorLike = ["enabled", "disabled"];
|
|
390
|
+
if (["codewith", "codex"].includes(value.provider)) {
|
|
391
|
+
if (!codexLike.includes(value.sandbox))
|
|
392
|
+
throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
|
|
393
|
+
} else if (value.provider === "cursor") {
|
|
394
|
+
if (!cursorLike.includes(value.sandbox))
|
|
395
|
+
throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
|
|
396
|
+
} else {
|
|
397
|
+
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
371
400
|
return value;
|
|
372
401
|
}
|
|
373
402
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -2353,7 +2382,7 @@ function providerCommand(provider) {
|
|
|
2353
2382
|
case "claude":
|
|
2354
2383
|
return "claude";
|
|
2355
2384
|
case "cursor":
|
|
2356
|
-
return "
|
|
2385
|
+
return "sh";
|
|
2357
2386
|
case "codewith":
|
|
2358
2387
|
return "codewith";
|
|
2359
2388
|
case "aicopilot":
|
|
@@ -2364,22 +2393,107 @@ function providerCommand(provider) {
|
|
|
2364
2393
|
return "codex";
|
|
2365
2394
|
}
|
|
2366
2395
|
}
|
|
2396
|
+
function codewithLikeSandbox(target) {
|
|
2397
|
+
const sandbox = target.sandbox ?? (target.permissionMode === "bypass" ? "danger-full-access" : "workspace-write");
|
|
2398
|
+
if (sandbox !== "read-only" && sandbox !== "workspace-write" && sandbox !== "danger-full-access") {
|
|
2399
|
+
throw new Error(`${target.provider} sandbox must be read-only, workspace-write, or danger-full-access`);
|
|
2400
|
+
}
|
|
2401
|
+
return sandbox;
|
|
2402
|
+
}
|
|
2403
|
+
function configStringValue(value) {
|
|
2404
|
+
return JSON.stringify(value);
|
|
2405
|
+
}
|
|
2406
|
+
function assertStringOption(value, label) {
|
|
2407
|
+
if (value !== undefined && typeof value !== "string")
|
|
2408
|
+
throw new Error(`${label} must be a string`);
|
|
2409
|
+
}
|
|
2410
|
+
function assertSupportedAgentOptions(target) {
|
|
2411
|
+
assertStringOption(target.variant, `${target.provider}.variant`);
|
|
2412
|
+
assertStringOption(target.model, `${target.provider}.model`);
|
|
2413
|
+
assertStringOption(target.agent, `${target.provider}.agent`);
|
|
2414
|
+
assertStringOption(target.authProfile, `${target.provider}.authProfile`);
|
|
2415
|
+
if (target.authProfile !== undefined && target.provider !== "codewith") {
|
|
2416
|
+
throw new Error(`${target.provider}.authProfile is supported only for codewith`);
|
|
2417
|
+
}
|
|
2418
|
+
if (target.permissionMode && !["default", "plan", "auto", "bypass"].includes(target.permissionMode)) {
|
|
2419
|
+
throw new Error(`${target.provider}.permissionMode must be default, plan, auto, or bypass`);
|
|
2420
|
+
}
|
|
2421
|
+
if (target.sandbox && !["read-only", "workspace-write", "danger-full-access", "enabled", "disabled"].includes(target.sandbox)) {
|
|
2422
|
+
throw new Error(`${target.provider}.sandbox is not supported: ${target.sandbox}`);
|
|
2423
|
+
}
|
|
2424
|
+
if (["codewith", "codex"].includes(target.provider)) {
|
|
2425
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2426
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2427
|
+
}
|
|
2428
|
+
if (target.sandbox)
|
|
2429
|
+
codewithLikeSandbox(target);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (target.provider === "claude") {
|
|
2433
|
+
if (target.sandbox !== undefined)
|
|
2434
|
+
throw new Error("claude.sandbox is not supported");
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (target.provider === "cursor") {
|
|
2438
|
+
if (target.permissionMode === "auto")
|
|
2439
|
+
throw new Error("cursor.permissionMode auto is not supported; use provider-specific extraArgs for Cursor auto-review");
|
|
2440
|
+
if (target.sandbox !== undefined && target.sandbox !== "enabled" && target.sandbox !== "disabled") {
|
|
2441
|
+
throw new Error("cursor.sandbox must be enabled or disabled");
|
|
2442
|
+
}
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (target.permissionMode && !["default", "bypass"].includes(target.permissionMode)) {
|
|
2446
|
+
throw new Error(`${target.provider}.permissionMode supports only default or bypass`);
|
|
2447
|
+
}
|
|
2448
|
+
if (target.sandbox !== undefined)
|
|
2449
|
+
throw new Error(`${target.provider}.sandbox is not supported`);
|
|
2450
|
+
}
|
|
2367
2451
|
function agentArgs(target) {
|
|
2452
|
+
assertSupportedAgentOptions(target);
|
|
2368
2453
|
const isolation = target.configIsolation ?? "safe";
|
|
2454
|
+
const permissionMode = target.permissionMode ?? "default";
|
|
2369
2455
|
const args = [];
|
|
2370
2456
|
switch (target.provider) {
|
|
2371
2457
|
case "claude":
|
|
2372
2458
|
if (isolation === "safe")
|
|
2373
2459
|
args.push("--safe-mode", "--setting-sources", "local", "--no-session-persistence");
|
|
2460
|
+
if (permissionMode !== "default") {
|
|
2461
|
+
const mode = permissionMode === "bypass" ? "bypassPermissions" : permissionMode === "plan" || permissionMode === "auto" ? permissionMode : undefined;
|
|
2462
|
+
if (mode)
|
|
2463
|
+
args.push("--permission-mode", mode);
|
|
2464
|
+
}
|
|
2374
2465
|
args.push("-p", "--output-format", "json");
|
|
2375
2466
|
if (target.model)
|
|
2376
2467
|
args.push("--model", target.model);
|
|
2468
|
+
if (target.variant)
|
|
2469
|
+
args.push("--effort", target.variant);
|
|
2377
2470
|
if (target.agent)
|
|
2378
2471
|
args.push("--agent", target.agent);
|
|
2379
2472
|
args.push(...target.extraArgs ?? []);
|
|
2380
2473
|
return args;
|
|
2381
2474
|
case "cursor":
|
|
2382
|
-
args.push("
|
|
2475
|
+
args.push("-c", [
|
|
2476
|
+
"set -eu",
|
|
2477
|
+
"if command -v cursor >/dev/null 2>&1; then",
|
|
2478
|
+
' exec cursor agent "$@"',
|
|
2479
|
+
"elif command -v agent >/dev/null 2>&1; then",
|
|
2480
|
+
' exec agent "$@"',
|
|
2481
|
+
"else",
|
|
2482
|
+
" echo 'Executable not found in PATH: cursor agent or agent' >&2",
|
|
2483
|
+
" exit 127",
|
|
2484
|
+
"fi"
|
|
2485
|
+
].join(`
|
|
2486
|
+
`), "openloops-cursor", "-p");
|
|
2487
|
+
if (permissionMode === "plan")
|
|
2488
|
+
args.push("--mode", "plan");
|
|
2489
|
+
if (permissionMode === "bypass")
|
|
2490
|
+
args.push("--force");
|
|
2491
|
+
const cursorSandbox = target.sandbox ?? (isolation === "safe" ? "enabled" : undefined);
|
|
2492
|
+
if (cursorSandbox) {
|
|
2493
|
+
if (cursorSandbox !== "enabled" && cursorSandbox !== "disabled")
|
|
2494
|
+
throw new Error("cursor sandbox must be enabled or disabled");
|
|
2495
|
+
args.push("--sandbox", cursorSandbox);
|
|
2496
|
+
}
|
|
2383
2497
|
if (target.model)
|
|
2384
2498
|
args.push("--model", target.model);
|
|
2385
2499
|
if (target.agent)
|
|
@@ -2387,7 +2501,10 @@ function agentArgs(target) {
|
|
|
2387
2501
|
args.push(...target.extraArgs ?? []);
|
|
2388
2502
|
return args;
|
|
2389
2503
|
case "codewith":
|
|
2390
|
-
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []
|
|
2504
|
+
args.push(...target.authProfile ? ["--auth-profile", target.authProfile] : []);
|
|
2505
|
+
if (target.variant)
|
|
2506
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2507
|
+
args.push("--ask-for-approval", "never", "exec", "--json", "--ephemeral", "--sandbox", codewithLikeSandbox(target), "--skip-git-repo-check");
|
|
2391
2508
|
if (isolation === "safe")
|
|
2392
2509
|
args.push("--ignore-rules");
|
|
2393
2510
|
if (target.cwd)
|
|
@@ -2399,7 +2516,9 @@ function agentArgs(target) {
|
|
|
2399
2516
|
args.push(...target.extraArgs ?? []);
|
|
2400
2517
|
return args;
|
|
2401
2518
|
case "codex":
|
|
2402
|
-
|
|
2519
|
+
if (target.variant)
|
|
2520
|
+
args.push("-c", `model_reasoning_effort=${configStringValue(target.variant)}`);
|
|
2521
|
+
args.push("exec", "--json", "--ephemeral", "--ask-for-approval", "never", "--sandbox", codewithLikeSandbox(target));
|
|
2403
2522
|
if (isolation === "safe")
|
|
2404
2523
|
args.push("--ignore-rules");
|
|
2405
2524
|
if (target.cwd)
|
|
@@ -2412,10 +2531,14 @@ function agentArgs(target) {
|
|
|
2412
2531
|
args.push("run", "--format", "json");
|
|
2413
2532
|
if (isolation === "safe")
|
|
2414
2533
|
args.push("--pure");
|
|
2534
|
+
if (permissionMode === "bypass")
|
|
2535
|
+
args.push("--dangerously-skip-permissions");
|
|
2415
2536
|
if (target.cwd)
|
|
2416
2537
|
args.push("--dir", target.cwd);
|
|
2417
2538
|
if (target.model)
|
|
2418
2539
|
args.push("--model", target.model);
|
|
2540
|
+
if (target.variant)
|
|
2541
|
+
args.push("--variant", target.variant);
|
|
2419
2542
|
if (target.agent)
|
|
2420
2543
|
args.push("--agent", target.agent);
|
|
2421
2544
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2424,10 +2547,14 @@ function agentArgs(target) {
|
|
|
2424
2547
|
args.push("run", "--format", "json");
|
|
2425
2548
|
if (isolation === "safe")
|
|
2426
2549
|
args.push("--pure");
|
|
2550
|
+
if (permissionMode === "bypass")
|
|
2551
|
+
args.push("--dangerously-skip-permissions");
|
|
2427
2552
|
if (target.cwd)
|
|
2428
2553
|
args.push("--dir", target.cwd);
|
|
2429
2554
|
if (target.model)
|
|
2430
2555
|
args.push("--model", target.model);
|
|
2556
|
+
if (target.variant)
|
|
2557
|
+
args.push("--variant", target.variant);
|
|
2431
2558
|
if (target.agent)
|
|
2432
2559
|
args.push("--agent", target.agent);
|
|
2433
2560
|
args.push(...target.extraArgs ?? []);
|
|
@@ -2456,6 +2583,7 @@ function commandSpec(target) {
|
|
|
2456
2583
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2457
2584
|
account: agentTarget.account,
|
|
2458
2585
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2586
|
+
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2459
2587
|
stdin: agentTarget.prompt
|
|
2460
2588
|
};
|
|
2461
2589
|
}
|
|
@@ -2523,11 +2651,15 @@ function remoteScript(spec, metadata) {
|
|
|
2523
2651
|
`;
|
|
2524
2652
|
}
|
|
2525
2653
|
function remotePreflightScript(spec, metadata) {
|
|
2526
|
-
|
|
2654
|
+
const lines = [
|
|
2527
2655
|
...remoteBootstrapLines(spec, metadata),
|
|
2528
2656
|
"command -v bash >/dev/null 2>&1",
|
|
2529
2657
|
`command -v ${shellQuote(spec.shell ? "sh" : spec.command)} >/dev/null 2>&1`
|
|
2530
|
-
]
|
|
2658
|
+
];
|
|
2659
|
+
if (spec.preflightAnyOf?.length) {
|
|
2660
|
+
lines.push(`if ! ${spec.preflightAnyOf.map((command) => `command -v ${shellQuote(command)} >/dev/null 2>&1`).join(" && ! ")}; then`, ` echo 'none of required executables found: ${spec.preflightAnyOf.join(", ")}' >&2`, " exit 127", "fi");
|
|
2661
|
+
}
|
|
2662
|
+
return lines.join(`
|
|
2531
2663
|
`);
|
|
2532
2664
|
}
|
|
2533
2665
|
function transportEnv(opts) {
|
|
@@ -2680,6 +2812,9 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2680
2812
|
if (!spec.shell && !executableExists(spec.command, env)) {
|
|
2681
2813
|
throw new Error(commandNotFoundMessage(spec.command, env));
|
|
2682
2814
|
}
|
|
2815
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2816
|
+
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2817
|
+
}
|
|
2683
2818
|
return {
|
|
2684
2819
|
command: spec.command,
|
|
2685
2820
|
accountProfile: spec.account?.profile,
|
|
@@ -2710,6 +2845,17 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2710
2845
|
durationMs: 0
|
|
2711
2846
|
};
|
|
2712
2847
|
}
|
|
2848
|
+
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2849
|
+
return {
|
|
2850
|
+
status: "failed",
|
|
2851
|
+
stdout,
|
|
2852
|
+
stderr,
|
|
2853
|
+
error: `none of required executables found: ${spec.preflightAnyOf.join(", ")}`,
|
|
2854
|
+
startedAt,
|
|
2855
|
+
finishedAt: nowIso(),
|
|
2856
|
+
durationMs: 0
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2713
2859
|
const child = spawn(spec.command, spec.args, {
|
|
2714
2860
|
cwd: spec.cwd,
|
|
2715
2861
|
env,
|
|
@@ -3813,6 +3959,241 @@ class LoopsClient {
|
|
|
3813
3959
|
function loops(opts = {}) {
|
|
3814
3960
|
return new LoopsClient(opts);
|
|
3815
3961
|
}
|
|
3962
|
+
// src/lib/templates.ts
|
|
3963
|
+
var TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
3964
|
+
var EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
3965
|
+
var TEMPLATE_SUMMARIES = [
|
|
3966
|
+
{
|
|
3967
|
+
id: TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
3968
|
+
name: "Todos Task Worker + Verifier",
|
|
3969
|
+
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.",
|
|
3970
|
+
kind: "workflow",
|
|
3971
|
+
variables: [
|
|
3972
|
+
{ name: "taskId", required: true, description: "Todos task id to execute." },
|
|
3973
|
+
{ name: "taskTitle", description: "Human-readable task title." },
|
|
3974
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
3975
|
+
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
3976
|
+
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
3977
|
+
{ name: "model", description: "Provider model." },
|
|
3978
|
+
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
3979
|
+
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
3980
|
+
{ name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
|
|
3981
|
+
]
|
|
3982
|
+
},
|
|
3983
|
+
{
|
|
3984
|
+
id: EVENT_WORKER_VERIFIER_TEMPLATE_ID,
|
|
3985
|
+
name: "Hasna Event Worker + Verifier",
|
|
3986
|
+
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.",
|
|
3987
|
+
kind: "workflow",
|
|
3988
|
+
variables: [
|
|
3989
|
+
{ name: "eventId", required: true, description: "Hasna event id." },
|
|
3990
|
+
{ name: "eventType", required: true, description: "Hasna event type." },
|
|
3991
|
+
{ name: "eventSource", required: true, description: "Hasna event source." },
|
|
3992
|
+
{ name: "eventJson", required: true, description: "Full event envelope JSON." },
|
|
3993
|
+
{ name: "projectPath", required: true, description: "Repository or project working directory." },
|
|
3994
|
+
{ name: "provider", default: "codewith", description: "Agent provider: codewith, claude, cursor, opencode, aicopilot, or codex." },
|
|
3995
|
+
{ name: "authProfile", description: "Provider-native auth profile, currently Codewith." },
|
|
3996
|
+
{ name: "model", description: "Provider model." },
|
|
3997
|
+
{ name: "variant", description: "Provider reasoning/model effort variant." },
|
|
3998
|
+
{ name: "permissionMode", default: "bypass", description: "Provider permission mode: default, plan, auto, or bypass." },
|
|
3999
|
+
{ name: "sandbox", default: "danger-full-access", description: "Provider sandbox mode." }
|
|
4000
|
+
]
|
|
4001
|
+
}
|
|
4002
|
+
];
|
|
4003
|
+
function compactJson(value) {
|
|
4004
|
+
return JSON.stringify(value);
|
|
4005
|
+
}
|
|
4006
|
+
function taskLabel(input) {
|
|
4007
|
+
const head = input.taskTitle?.trim() || input.taskId;
|
|
4008
|
+
return head.length > 160 ? `${head.slice(0, 157)}...` : head;
|
|
4009
|
+
}
|
|
4010
|
+
function agentTarget(input, prompt) {
|
|
4011
|
+
const provider = input.provider ?? "codewith";
|
|
4012
|
+
const sandbox = input.sandbox ?? (provider === "codewith" || provider === "codex" ? "danger-full-access" : provider === "cursor" ? "disabled" : undefined);
|
|
4013
|
+
return {
|
|
4014
|
+
type: "agent",
|
|
4015
|
+
provider,
|
|
4016
|
+
prompt,
|
|
4017
|
+
cwd: input.projectPath,
|
|
4018
|
+
model: input.model,
|
|
4019
|
+
variant: input.variant,
|
|
4020
|
+
agent: input.agent,
|
|
4021
|
+
authProfile: provider === "codewith" ? input.authProfile : undefined,
|
|
4022
|
+
configIsolation: "safe",
|
|
4023
|
+
permissionMode: input.permissionMode ?? "bypass",
|
|
4024
|
+
sandbox,
|
|
4025
|
+
account: input.account,
|
|
4026
|
+
timeoutMs: 45 * 60000
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
function listLoopTemplates() {
|
|
4030
|
+
return TEMPLATE_SUMMARIES.map((template) => structuredClone(template));
|
|
4031
|
+
}
|
|
4032
|
+
function getLoopTemplate(id) {
|
|
4033
|
+
return listLoopTemplates().find((template) => template.id === id || template.name === id);
|
|
4034
|
+
}
|
|
4035
|
+
function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
4036
|
+
if (!input.taskId?.trim())
|
|
4037
|
+
throw new Error("taskId is required");
|
|
4038
|
+
if (!input.projectPath?.trim())
|
|
4039
|
+
throw new Error("projectPath is required");
|
|
4040
|
+
const taskContext = {
|
|
4041
|
+
taskId: input.taskId,
|
|
4042
|
+
taskTitle: input.taskTitle,
|
|
4043
|
+
taskDescription: input.taskDescription,
|
|
4044
|
+
eventId: input.eventId,
|
|
4045
|
+
eventType: input.eventType,
|
|
4046
|
+
projectPath: input.projectPath
|
|
4047
|
+
};
|
|
4048
|
+
const workerPrompt = [
|
|
4049
|
+
`/goal Complete todos task ${input.taskId} in ${input.projectPath}.`,
|
|
4050
|
+
"",
|
|
4051
|
+
"You are the worker agent for a task-triggered OpenLoops workflow.",
|
|
4052
|
+
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
4053
|
+
"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.",
|
|
4054
|
+
"Do not mark the task complete unless the work is genuinely done and validated.",
|
|
4055
|
+
"",
|
|
4056
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
4057
|
+
].join(`
|
|
4058
|
+
`);
|
|
4059
|
+
const verifierPrompt = [
|
|
4060
|
+
`/goal Verify todos task ${input.taskId} after the worker step.`,
|
|
4061
|
+
"",
|
|
4062
|
+
"You are the verifier agent for a task-triggered OpenLoops workflow.",
|
|
4063
|
+
"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.",
|
|
4064
|
+
"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.",
|
|
4065
|
+
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
4066
|
+
"",
|
|
4067
|
+
`Task context JSON: ${compactJson(taskContext)}`
|
|
4068
|
+
].join(`
|
|
4069
|
+
`);
|
|
4070
|
+
return {
|
|
4071
|
+
name: `todos-task-${input.taskId.slice(0, 8)}-worker-verifier`,
|
|
4072
|
+
description: `Task-triggered worker/verifier workflow for ${taskLabel(input)}`,
|
|
4073
|
+
version: 1,
|
|
4074
|
+
steps: [
|
|
4075
|
+
{
|
|
4076
|
+
id: "worker",
|
|
4077
|
+
name: "Worker",
|
|
4078
|
+
description: "Implement the todos task and record evidence.",
|
|
4079
|
+
target: agentTarget(input, workerPrompt),
|
|
4080
|
+
timeoutMs: 45 * 60000
|
|
4081
|
+
},
|
|
4082
|
+
{
|
|
4083
|
+
id: "verifier",
|
|
4084
|
+
name: "Verifier",
|
|
4085
|
+
description: "Adversarially verify worker output and update todos.",
|
|
4086
|
+
dependsOn: ["worker"],
|
|
4087
|
+
target: agentTarget(input, verifierPrompt),
|
|
4088
|
+
timeoutMs: 30 * 60000
|
|
4089
|
+
}
|
|
4090
|
+
]
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
function renderEventWorkerVerifierWorkflow(input) {
|
|
4094
|
+
if (!input.eventId?.trim())
|
|
4095
|
+
throw new Error("eventId is required");
|
|
4096
|
+
if (!input.eventType?.trim())
|
|
4097
|
+
throw new Error("eventType is required");
|
|
4098
|
+
if (!input.eventSource?.trim())
|
|
4099
|
+
throw new Error("eventSource is required");
|
|
4100
|
+
if (!input.eventJson?.trim())
|
|
4101
|
+
throw new Error("eventJson is required");
|
|
4102
|
+
if (!input.projectPath?.trim())
|
|
4103
|
+
throw new Error("projectPath is required");
|
|
4104
|
+
const eventContext = {
|
|
4105
|
+
eventId: input.eventId,
|
|
4106
|
+
eventType: input.eventType,
|
|
4107
|
+
eventSource: input.eventSource,
|
|
4108
|
+
eventSubject: input.eventSubject,
|
|
4109
|
+
eventMessage: input.eventMessage,
|
|
4110
|
+
projectPath: input.projectPath
|
|
4111
|
+
};
|
|
4112
|
+
const workerPrompt = [
|
|
4113
|
+
`/goal Handle Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}) in ${input.projectPath}.`,
|
|
4114
|
+
"",
|
|
4115
|
+
"You are the worker agent for an event-triggered OpenLoops workflow.",
|
|
4116
|
+
"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.",
|
|
4117
|
+
"If the event is informational or does not require action, record that finding and stop without making changes.",
|
|
4118
|
+
"",
|
|
4119
|
+
`Event context JSON: ${compactJson(eventContext)}`,
|
|
4120
|
+
`Full event envelope JSON: ${input.eventJson}`
|
|
4121
|
+
].join(`
|
|
4122
|
+
`);
|
|
4123
|
+
const verifierPrompt = [
|
|
4124
|
+
`/goal Verify handling of Hasna event ${input.eventSource}/${input.eventType} (${input.eventId}).`,
|
|
4125
|
+
"",
|
|
4126
|
+
"You are the verifier agent for an event-triggered OpenLoops workflow.",
|
|
4127
|
+
"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.",
|
|
4128
|
+
"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.",
|
|
4129
|
+
"",
|
|
4130
|
+
`Event context JSON: ${compactJson(eventContext)}`,
|
|
4131
|
+
`Full event envelope JSON: ${input.eventJson}`
|
|
4132
|
+
].join(`
|
|
4133
|
+
`);
|
|
4134
|
+
return {
|
|
4135
|
+
name: `event-${input.eventSource}-${input.eventType}-${input.eventId.slice(0, 8)}-worker-verifier`.replace(/[^a-zA-Z0-9._:-]+/g, "-"),
|
|
4136
|
+
description: `Event-triggered worker/verifier workflow for ${input.eventSource}/${input.eventType}`,
|
|
4137
|
+
version: 1,
|
|
4138
|
+
steps: [
|
|
4139
|
+
{
|
|
4140
|
+
id: "worker",
|
|
4141
|
+
name: "Worker",
|
|
4142
|
+
description: "Handle the Hasna event and record evidence.",
|
|
4143
|
+
target: agentTarget(input, workerPrompt),
|
|
4144
|
+
timeoutMs: 45 * 60000
|
|
4145
|
+
},
|
|
4146
|
+
{
|
|
4147
|
+
id: "verifier",
|
|
4148
|
+
name: "Verifier",
|
|
4149
|
+
description: "Adversarially verify event handling.",
|
|
4150
|
+
dependsOn: ["worker"],
|
|
4151
|
+
target: agentTarget(input, verifierPrompt),
|
|
4152
|
+
timeoutMs: 30 * 60000
|
|
4153
|
+
}
|
|
4154
|
+
]
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
function renderLoopTemplate(id, values) {
|
|
4158
|
+
if (id === TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
4159
|
+
return renderTodosTaskWorkerVerifierWorkflow({
|
|
4160
|
+
taskId: values.taskId ?? "",
|
|
4161
|
+
taskTitle: values.taskTitle,
|
|
4162
|
+
taskDescription: values.taskDescription,
|
|
4163
|
+
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4164
|
+
provider: values.provider,
|
|
4165
|
+
authProfile: values.authProfile,
|
|
4166
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4167
|
+
model: values.model,
|
|
4168
|
+
variant: values.variant,
|
|
4169
|
+
agent: values.agent,
|
|
4170
|
+
permissionMode: values.permissionMode,
|
|
4171
|
+
sandbox: values.sandbox,
|
|
4172
|
+
eventId: values.eventId,
|
|
4173
|
+
eventType: values.eventType
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
if (id === EVENT_WORKER_VERIFIER_TEMPLATE_ID) {
|
|
4177
|
+
return renderEventWorkerVerifierWorkflow({
|
|
4178
|
+
eventId: values.eventId ?? "",
|
|
4179
|
+
eventType: values.eventType ?? "",
|
|
4180
|
+
eventSource: values.eventSource ?? "",
|
|
4181
|
+
eventSubject: values.eventSubject,
|
|
4182
|
+
eventMessage: values.eventMessage,
|
|
4183
|
+
eventJson: values.eventJson ?? "",
|
|
4184
|
+
projectPath: values.projectPath ?? values.cwd ?? process.cwd(),
|
|
4185
|
+
provider: values.provider,
|
|
4186
|
+
authProfile: values.authProfile,
|
|
4187
|
+
account: values.account ? { profile: values.account, tool: values.accountTool } : undefined,
|
|
4188
|
+
model: values.model,
|
|
4189
|
+
variant: values.variant,
|
|
4190
|
+
agent: values.agent,
|
|
4191
|
+
permissionMode: values.permissionMode,
|
|
4192
|
+
sandbox: values.sandbox
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
throw new Error(`unknown template: ${id}`);
|
|
4196
|
+
}
|
|
3816
4197
|
// src/lib/doctor.ts
|
|
3817
4198
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
3818
4199
|
import { accessSync as accessSync2, constants as constants2 } from "fs";
|
|
@@ -3945,17 +4326,35 @@ async function stopDaemon(opts = {}) {
|
|
|
3945
4326
|
// src/lib/doctor.ts
|
|
3946
4327
|
var PROVIDER_COMMANDS = [
|
|
3947
4328
|
"claude",
|
|
3948
|
-
"cursor
|
|
4329
|
+
"cursor agent",
|
|
3949
4330
|
"codewith",
|
|
3950
4331
|
"aicopilot",
|
|
3951
4332
|
"opencode",
|
|
3952
4333
|
"codex"
|
|
3953
4334
|
];
|
|
3954
4335
|
function hasCommand(command) {
|
|
4336
|
+
if (command === "cursor agent") {
|
|
4337
|
+
return hasCommand("cursor") || hasCommand("agent");
|
|
4338
|
+
}
|
|
3955
4339
|
const result = spawnSync3("sh", ["-c", 'command -v "$1" >/dev/null', "sh", command], { stdio: "ignore" });
|
|
3956
4340
|
return (result.status ?? 1) === 0;
|
|
3957
4341
|
}
|
|
3958
4342
|
function commandVersion(command) {
|
|
4343
|
+
if (command === "cursor agent") {
|
|
4344
|
+
const cursorResult = spawnSync3("cursor", ["agent", "--version"], {
|
|
4345
|
+
encoding: "utf8",
|
|
4346
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4347
|
+
});
|
|
4348
|
+
if ((cursorResult.status ?? 1) === 0)
|
|
4349
|
+
return (cursorResult.stdout || cursorResult.stderr).trim().split(/\r?\n/)[0];
|
|
4350
|
+
const agentResult = spawnSync3("agent", ["--version"], {
|
|
4351
|
+
encoding: "utf8",
|
|
4352
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4353
|
+
});
|
|
4354
|
+
if ((agentResult.status ?? 1) === 0)
|
|
4355
|
+
return (agentResult.stdout || agentResult.stderr).trim().split(/\r?\n/)[0];
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
3959
4358
|
const result = spawnSync3(command, ["--version"], {
|
|
3960
4359
|
encoding: "utf8",
|
|
3961
4360
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4040,6 +4439,9 @@ export {
|
|
|
4040
4439
|
rollupSummary,
|
|
4041
4440
|
resolveLoopMachine,
|
|
4042
4441
|
resolveGoalModel,
|
|
4442
|
+
renderTodosTaskWorkerVerifierWorkflow,
|
|
4443
|
+
renderLoopTemplate,
|
|
4444
|
+
renderEventWorkerVerifierWorkflow,
|
|
4043
4445
|
refreshLoopMachine,
|
|
4044
4446
|
readyNodeKeys,
|
|
4045
4447
|
preflightWorkflow,
|
|
@@ -4049,13 +4451,17 @@ export {
|
|
|
4049
4451
|
nextCronRun,
|
|
4050
4452
|
loops,
|
|
4051
4453
|
listOpenMachines,
|
|
4454
|
+
listLoopTemplates,
|
|
4052
4455
|
isTerminal as isGoalTerminal,
|
|
4053
4456
|
initialNextRun,
|
|
4457
|
+
getLoopTemplate,
|
|
4054
4458
|
executeWorkflow,
|
|
4055
4459
|
executeTarget,
|
|
4056
4460
|
executeLoopTarget,
|
|
4057
4461
|
executeLoop,
|
|
4058
4462
|
computeNextAfter,
|
|
4463
|
+
TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID,
|
|
4059
4464
|
Store,
|
|
4060
|
-
LoopsClient
|
|
4465
|
+
LoopsClient,
|
|
4466
|
+
EVENT_WORKER_VERIFIER_TEMPLATE_ID
|
|
4061
4467
|
};
|
package/dist/lib/store.js
CHANGED
|
@@ -368,6 +368,35 @@ function validateTarget(value, label) {
|
|
|
368
368
|
if (value.provider !== "codewith")
|
|
369
369
|
throw new Error(`${label}.authProfile is currently supported only for provider codewith`);
|
|
370
370
|
}
|
|
371
|
+
if (value.variant !== undefined)
|
|
372
|
+
assertString(value.variant, `${label}.variant`);
|
|
373
|
+
if (value.permissionMode !== undefined) {
|
|
374
|
+
assertString(value.permissionMode, `${label}.permissionMode`);
|
|
375
|
+
const permissionModes = ["default", "plan", "auto", "bypass"];
|
|
376
|
+
if (!permissionModes.includes(value.permissionMode)) {
|
|
377
|
+
throw new Error(`${label}.permissionMode must be one of ${permissionModes.join(", ")}`);
|
|
378
|
+
}
|
|
379
|
+
if (value.permissionMode === "plan" && !["claude", "cursor"].includes(value.provider)) {
|
|
380
|
+
throw new Error(`${label}.permissionMode plan is currently supported only for provider claude or cursor`);
|
|
381
|
+
}
|
|
382
|
+
if (value.permissionMode === "auto" && value.provider !== "claude") {
|
|
383
|
+
throw new Error(`${label}.permissionMode auto is currently supported only for provider claude`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (value.sandbox !== undefined) {
|
|
387
|
+
assertString(value.sandbox, `${label}.sandbox`);
|
|
388
|
+
const codexLike = ["read-only", "workspace-write", "danger-full-access"];
|
|
389
|
+
const cursorLike = ["enabled", "disabled"];
|
|
390
|
+
if (["codewith", "codex"].includes(value.provider)) {
|
|
391
|
+
if (!codexLike.includes(value.sandbox))
|
|
392
|
+
throw new Error(`${label}.sandbox must be one of ${codexLike.join(", ")}`);
|
|
393
|
+
} else if (value.provider === "cursor") {
|
|
394
|
+
if (!cursorLike.includes(value.sandbox))
|
|
395
|
+
throw new Error(`${label}.sandbox must be one of ${cursorLike.join(", ")}`);
|
|
396
|
+
} else {
|
|
397
|
+
throw new Error(`${label}.sandbox is currently supported only for provider codewith, codex, or cursor`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
371
400
|
return value;
|
|
372
401
|
}
|
|
373
402
|
throw new Error(`${label}.type must be command or agent`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AccountRef, AgentPermissionMode, AgentProvider, AgentSandbox, CreateWorkflowInput, LoopTemplateSummary } from "../types.js";
|
|
2
|
+
export declare const TODOS_TASK_WORKER_VERIFIER_TEMPLATE_ID = "todos-task-worker-verifier";
|
|
3
|
+
export declare const EVENT_WORKER_VERIFIER_TEMPLATE_ID = "event-worker-verifier";
|
|
4
|
+
export interface TodosTaskWorkflowTemplateInput {
|
|
5
|
+
taskId: string;
|
|
6
|
+
taskTitle?: string;
|
|
7
|
+
taskDescription?: string;
|
|
8
|
+
projectPath: string;
|
|
9
|
+
provider?: AgentProvider;
|
|
10
|
+
authProfile?: string;
|
|
11
|
+
account?: AccountRef;
|
|
12
|
+
model?: string;
|
|
13
|
+
variant?: string;
|
|
14
|
+
agent?: string;
|
|
15
|
+
permissionMode?: AgentPermissionMode;
|
|
16
|
+
sandbox?: AgentSandbox;
|
|
17
|
+
eventId?: string;
|
|
18
|
+
eventType?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface EventWorkflowTemplateInput {
|
|
21
|
+
eventId: string;
|
|
22
|
+
eventType: string;
|
|
23
|
+
eventSource: string;
|
|
24
|
+
eventSubject?: string;
|
|
25
|
+
eventMessage?: string;
|
|
26
|
+
eventJson: string;
|
|
27
|
+
projectPath: string;
|
|
28
|
+
provider?: AgentProvider;
|
|
29
|
+
authProfile?: string;
|
|
30
|
+
account?: AccountRef;
|
|
31
|
+
model?: string;
|
|
32
|
+
variant?: string;
|
|
33
|
+
agent?: string;
|
|
34
|
+
permissionMode?: AgentPermissionMode;
|
|
35
|
+
sandbox?: AgentSandbox;
|
|
36
|
+
}
|
|
37
|
+
export declare function listLoopTemplates(): LoopTemplateSummary[];
|
|
38
|
+
export declare function getLoopTemplate(id: string): LoopTemplateSummary | undefined;
|
|
39
|
+
export declare function renderTodosTaskWorkerVerifierWorkflow(input: TodosTaskWorkflowTemplateInput): CreateWorkflowInput;
|
|
40
|
+
export declare function renderEventWorkerVerifierWorkflow(input: EventWorkflowTemplateInput): CreateWorkflowInput;
|
|
41
|
+
export declare function renderLoopTemplate(id: string, values: Record<string, string | undefined>): CreateWorkflowInput;
|