@hasna/loops 0.3.21 → 0.3.23
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/dist/cli/index.js +156 -35
- package/dist/daemon/index.js +57 -6
- package/dist/index.js +56 -5
- package/dist/sdk/index.js +56 -5
- package/docs/USAGE.md +24 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2833,6 +2833,7 @@ function commandSpec(target) {
|
|
|
2833
2833
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2834
2834
|
account: agentTarget.account,
|
|
2835
2835
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2836
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2836
2837
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2837
2838
|
stdin: agentTarget.prompt,
|
|
2838
2839
|
allowlist: agentTarget.allowlist
|
|
@@ -2914,6 +2915,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2914
2915
|
if (spec.preflightAnyOf?.length) {
|
|
2915
2916
|
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");
|
|
2916
2917
|
}
|
|
2918
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2919
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2920
|
+
}
|
|
2917
2921
|
return lines.join(`
|
|
2918
2922
|
`);
|
|
2919
2923
|
}
|
|
@@ -2929,6 +2933,29 @@ function transportEnv(opts) {
|
|
|
2929
2933
|
env.PATH = normalizeExecutionPath(env);
|
|
2930
2934
|
return env;
|
|
2931
2935
|
}
|
|
2936
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2937
|
+
if (!spec.nativeAuthProfile)
|
|
2938
|
+
return;
|
|
2939
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2940
|
+
return;
|
|
2941
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2942
|
+
encoding: "utf8",
|
|
2943
|
+
env,
|
|
2944
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2945
|
+
timeout: 15000
|
|
2946
|
+
});
|
|
2947
|
+
if (result.error) {
|
|
2948
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2949
|
+
}
|
|
2950
|
+
if ((result.status ?? 1) !== 0) {
|
|
2951
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2952
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2953
|
+
}
|
|
2954
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2955
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2956
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2932
2959
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2933
2960
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2934
2961
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -3070,6 +3097,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
3070
3097
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
3071
3098
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
3072
3099
|
}
|
|
3100
|
+
preflightNativeAuthProfile(spec, env);
|
|
3073
3101
|
return {
|
|
3074
3102
|
command: spec.command,
|
|
3075
3103
|
accountProfile: spec.account?.profile,
|
|
@@ -3111,6 +3139,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3111
3139
|
durationMs: 0
|
|
3112
3140
|
};
|
|
3113
3141
|
}
|
|
3142
|
+
try {
|
|
3143
|
+
preflightNativeAuthProfile(spec, env);
|
|
3144
|
+
} catch (err) {
|
|
3145
|
+
return {
|
|
3146
|
+
status: "failed",
|
|
3147
|
+
stdout: "",
|
|
3148
|
+
stderr: "",
|
|
3149
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3150
|
+
startedAt,
|
|
3151
|
+
finishedAt: nowIso(),
|
|
3152
|
+
durationMs: 0
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3114
3155
|
const child = spawn(spec.command, spec.args, {
|
|
3115
3156
|
cwd: spec.cwd,
|
|
3116
3157
|
env,
|
|
@@ -3767,11 +3808,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3767
3808
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3768
3809
|
}
|
|
3769
3810
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3770
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3811
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3812
|
+
try {
|
|
3813
|
+
return {
|
|
3814
|
+
workflowStepId: step.id,
|
|
3815
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3816
|
+
workflowId: workflow.id,
|
|
3817
|
+
workflowName: workflow.name,
|
|
3818
|
+
workflowStepId: step.id
|
|
3819
|
+
}, opts)
|
|
3820
|
+
};
|
|
3821
|
+
} catch (error) {
|
|
3822
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3823
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3824
|
+
}
|
|
3825
|
+
});
|
|
3775
3826
|
}
|
|
3776
3827
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3777
3828
|
if (loop.target.type !== "workflow") {
|
|
@@ -5092,7 +5143,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5092
5143
|
// package.json
|
|
5093
5144
|
var package_default = {
|
|
5094
5145
|
name: "@hasna/loops",
|
|
5095
|
-
version: "0.3.
|
|
5146
|
+
version: "0.3.23",
|
|
5096
5147
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5097
5148
|
type: "module",
|
|
5098
5149
|
main: "dist/index.js",
|
|
@@ -5576,6 +5627,55 @@ function print(value, human) {
|
|
|
5576
5627
|
else
|
|
5577
5628
|
console.log(human);
|
|
5578
5629
|
}
|
|
5630
|
+
function printCreatedLoop(loop, human, preflight) {
|
|
5631
|
+
if (preflight !== undefined)
|
|
5632
|
+
print({ loop: publicLoop(loop), preflight }, human);
|
|
5633
|
+
else
|
|
5634
|
+
print(publicLoop(loop), human);
|
|
5635
|
+
}
|
|
5636
|
+
function preflightFailed(error, context) {
|
|
5637
|
+
if (!isJson())
|
|
5638
|
+
throw error;
|
|
5639
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5640
|
+
print({
|
|
5641
|
+
ok: false,
|
|
5642
|
+
created: false,
|
|
5643
|
+
preflight: {
|
|
5644
|
+
ok: false,
|
|
5645
|
+
error: redact(message, 320)
|
|
5646
|
+
},
|
|
5647
|
+
...context
|
|
5648
|
+
});
|
|
5649
|
+
process.exit(1);
|
|
5650
|
+
}
|
|
5651
|
+
function preflightLoopTarget(target, context, metadata, opts) {
|
|
5652
|
+
try {
|
|
5653
|
+
return preflightTarget(target, metadata, opts);
|
|
5654
|
+
} catch (error) {
|
|
5655
|
+
preflightFailed(error, context);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
function preflightStoredWorkflow(workflow, context, opts) {
|
|
5659
|
+
try {
|
|
5660
|
+
return preflightWorkflow(workflow, opts);
|
|
5661
|
+
} catch (error) {
|
|
5662
|
+
preflightFailed(error, context);
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
function workflowSpecForPreflight(body, id = "validation") {
|
|
5666
|
+
const now = new Date().toISOString();
|
|
5667
|
+
return {
|
|
5668
|
+
id,
|
|
5669
|
+
name: body.name,
|
|
5670
|
+
description: body.description,
|
|
5671
|
+
version: body.version ?? 1,
|
|
5672
|
+
status: "active",
|
|
5673
|
+
goal: body.goal,
|
|
5674
|
+
steps: body.steps,
|
|
5675
|
+
createdAt: now,
|
|
5676
|
+
updatedAt: now
|
|
5677
|
+
};
|
|
5678
|
+
}
|
|
5579
5679
|
function printTextOutput(value) {
|
|
5580
5680
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
5581
5681
|
console.log(line);
|
|
@@ -6020,7 +6120,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
6020
6120
|
return mode;
|
|
6021
6121
|
}
|
|
6022
6122
|
var create = program.command("create").description("create loops");
|
|
6023
|
-
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) => {
|
|
6123
|
+
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").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
6024
6124
|
const store = new Store;
|
|
6025
6125
|
try {
|
|
6026
6126
|
const target = {
|
|
@@ -6031,13 +6131,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
6031
6131
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
6032
6132
|
account: accountFromOpts(opts)
|
|
6033
6133
|
};
|
|
6034
|
-
const
|
|
6035
|
-
|
|
6134
|
+
const input = baseCreateInput(name, opts, target);
|
|
6135
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6136
|
+
const loop = store.createLoop(input);
|
|
6137
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
6036
6138
|
} finally {
|
|
6037
6139
|
store.close();
|
|
6038
6140
|
}
|
|
6039
6141
|
});
|
|
6040
|
-
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe"))))).action((name, opts) => {
|
|
6142
|
+
addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.command("agent <name>").description("create a headless coding-agent loop").requiredOption("--provider <provider>", "claude, cursor, codewith, aicopilot, opencode, or codex").requiredOption("--prompt <prompt>", "agent prompt").option("--cwd <dir>", "working directory").option("--model <model>", "model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--timeout <duration>", "run timeout").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass").option("--sandbox <mode>", "provider sandbox: codewith/codex use read-only/workspace-write/danger-full-access; cursor uses enabled/disabled").option("--allow-tool <name>", "advisory per-session tool allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--allow-command <name>", "advisory per-session command allowlist metadata; may be repeated or comma-separated", collectValues, []).option("--config-isolation <mode>", "safe or none", "safe").option("--preflight", "check target executables/accounts before storing the loop"))))).action((name, opts) => {
|
|
6041
6143
|
const provider = opts.provider;
|
|
6042
6144
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
6043
6145
|
throw new Error("unsupported provider");
|
|
@@ -6063,13 +6165,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
6063
6165
|
allowlist: allowlistFromOpts(opts),
|
|
6064
6166
|
account: accountFromOpts(opts)
|
|
6065
6167
|
};
|
|
6066
|
-
const
|
|
6067
|
-
|
|
6168
|
+
const input = baseCreateInput(name, opts, target);
|
|
6169
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6170
|
+
const loop = store.createLoop(input);
|
|
6171
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
6068
6172
|
} finally {
|
|
6069
6173
|
store.close();
|
|
6070
6174
|
}
|
|
6071
6175
|
});
|
|
6072
|
-
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")))).action((name, opts) => {
|
|
6176
|
+
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name").option("--preflight", "check workflow step executables/accounts before storing the loop")))).action((name, opts) => {
|
|
6073
6177
|
const store = new Store;
|
|
6074
6178
|
try {
|
|
6075
6179
|
const workflow = store.requireWorkflow(opts.workflow);
|
|
@@ -6077,8 +6181,10 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
|
|
|
6077
6181
|
type: "workflow",
|
|
6078
6182
|
workflowId: workflow.id
|
|
6079
6183
|
};
|
|
6080
|
-
const
|
|
6081
|
-
|
|
6184
|
+
const input = baseCreateInput(name, opts, target);
|
|
6185
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
|
|
6186
|
+
const loop = store.createLoop(input);
|
|
6187
|
+
printCreatedLoop(loop, `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`, preflight);
|
|
6082
6188
|
} finally {
|
|
6083
6189
|
store.close();
|
|
6084
6190
|
}
|
|
@@ -6119,7 +6225,7 @@ templates.command("create-workflow <id>").description("render and store a templa
|
|
|
6119
6225
|
}
|
|
6120
6226
|
});
|
|
6121
6227
|
var eventsHandle = events.command("handle").description("handle a Hasna event envelope");
|
|
6122
|
-
eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--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) => {
|
|
6228
|
+
eventsHandle.command("todos-task").description("create a one-shot worker/verifier workflow loop for a todos task event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:todos-task").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
6123
6229
|
const event = await readEventEnvelopeFromStdin();
|
|
6124
6230
|
const data = eventData(event);
|
|
6125
6231
|
const metadata = eventMetadata(event);
|
|
@@ -6188,7 +6294,12 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
6188
6294
|
leaseMs: 90 * 60000
|
|
6189
6295
|
};
|
|
6190
6296
|
if (opts.dryRun) {
|
|
6191
|
-
|
|
6297
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(workflowBody, "event-preflight"), {
|
|
6298
|
+
name: workflowBody.name,
|
|
6299
|
+
type: "todos-task-event-workflow",
|
|
6300
|
+
event: event.id
|
|
6301
|
+
}, {}) : undefined;
|
|
6302
|
+
print({ deduped: false, idempotencyKey, event, workflow: workflowBody, loop: loopInput, preflight }, `dry-run ${loopName}`);
|
|
6192
6303
|
return;
|
|
6193
6304
|
}
|
|
6194
6305
|
const store = new Store;
|
|
@@ -6207,17 +6318,23 @@ eventsHandle.command("todos-task").description("create a one-shot worker/verifie
|
|
|
6207
6318
|
return;
|
|
6208
6319
|
}
|
|
6209
6320
|
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
6321
|
+
const workflowPreflightSpec = existingWorkflow ?? workflowSpecForPreflight(workflowBody, "event-preflight");
|
|
6322
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflowPreflightSpec, {
|
|
6323
|
+
name: workflowBody.name,
|
|
6324
|
+
type: "todos-task-event-workflow",
|
|
6325
|
+
event: event.id
|
|
6326
|
+
}, {}) : undefined;
|
|
6210
6327
|
const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
|
|
6211
6328
|
const loop = store.createLoop({
|
|
6212
6329
|
...loopInput,
|
|
6213
6330
|
target: { type: "workflow", workflowId: workflow.id }
|
|
6214
6331
|
});
|
|
6215
|
-
print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
|
|
6332
|
+
print({ deduped: false, idempotencyKey, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop), preflight }, `created ${loop.id} (${loop.name}) workflow=${workflow.name} event=${event.id} idempotency=${idempotencyKey}`);
|
|
6216
6333
|
} finally {
|
|
6217
6334
|
store.close();
|
|
6218
6335
|
}
|
|
6219
6336
|
});
|
|
6220
|
-
eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--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) => {
|
|
6337
|
+
eventsHandle.command("generic").description("create a one-shot worker/verifier workflow loop for any Hasna event").option("--provider <provider>", "agent provider", "codewith").option("--auth-profile <profile>", "provider-native auth profile; currently supported for codewith").option("--auth-profile-pool <profiles>", "comma-separated provider-native auth profile pool").option("--worker-auth-profile <profile>", "provider-native auth profile for worker step").option("--verifier-auth-profile <profile>", "provider-native auth profile for verifier step").option("--account <profile>", "OpenAccounts profile name").option("--account-pool <profiles>", "comma-separated OpenAccounts profile pool").option("--worker-account <profile>", "OpenAccounts profile for worker step").option("--verifier-account <profile>", "OpenAccounts profile for verifier step").option("--account-tool <tool>", "OpenAccounts tool id").option("--model <model>", "provider model").option("--variant <variant>", "provider-specific model variant or reasoning effort").option("--agent <agent>", "provider-specific agent").option("--permission-mode <mode>", "provider permission mode: default, plan, auto, or bypass", "bypass").option("--sandbox <mode>", "provider sandbox").option("--project-path <path>", "fallback project/repo working directory").option("--name-prefix <prefix>", "workflow/loop name prefix", "event:generic").option("--preflight", "check generated workflow steps before storing the workflow loop").option("--dry-run", "print the workflow and loop input without storing anything").action(async (opts) => {
|
|
6221
6338
|
const event = await readEventEnvelopeFromStdin();
|
|
6222
6339
|
const data = eventData(event);
|
|
6223
6340
|
const projectPath = opts.projectPath ?? taskEventField(data, ["working_dir", "workingDir", "project_path", "projectPath", "cwd", "repo_path", "repoPath"]) ?? process.cwd();
|
|
@@ -6267,7 +6384,12 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
|
|
|
6267
6384
|
leaseMs: 90 * 60000
|
|
6268
6385
|
};
|
|
6269
6386
|
if (opts.dryRun) {
|
|
6270
|
-
|
|
6387
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(workflowBody, "event-preflight"), {
|
|
6388
|
+
name: workflowBody.name,
|
|
6389
|
+
type: "generic-event-workflow",
|
|
6390
|
+
event: event.id
|
|
6391
|
+
}, {}) : undefined;
|
|
6392
|
+
print({ event, workflow: workflowBody, loop: loopInput, preflight }, `dry-run ${loopName}`);
|
|
6271
6393
|
return;
|
|
6272
6394
|
}
|
|
6273
6395
|
const store = new Store;
|
|
@@ -6279,12 +6401,18 @@ eventsHandle.command("generic").description("create a one-shot worker/verifier w
|
|
|
6279
6401
|
return;
|
|
6280
6402
|
}
|
|
6281
6403
|
const existingWorkflow = store.findWorkflowByName(workflowBody.name);
|
|
6404
|
+
const workflowPreflightSpec = existingWorkflow ?? workflowSpecForPreflight(workflowBody, "event-preflight");
|
|
6405
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflowPreflightSpec, {
|
|
6406
|
+
name: workflowBody.name,
|
|
6407
|
+
type: "generic-event-workflow",
|
|
6408
|
+
event: event.id
|
|
6409
|
+
}, {}) : undefined;
|
|
6282
6410
|
const workflow = existingWorkflow ?? store.createWorkflow(workflowBody);
|
|
6283
6411
|
const loop = store.createLoop({
|
|
6284
6412
|
...loopInput,
|
|
6285
6413
|
target: { type: "workflow", workflowId: workflow.id }
|
|
6286
6414
|
});
|
|
6287
|
-
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop) }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
6415
|
+
print({ deduped: false, event, workflow: publicWorkflow(workflow), loop: publicLoop(loop), preflight }, `created ${loop.id} (${loop.name}) workflow=${workflow.name}`);
|
|
6288
6416
|
} finally {
|
|
6289
6417
|
store.close();
|
|
6290
6418
|
}
|
|
@@ -6349,27 +6477,20 @@ machines.command("show <id>").description("resolve a machine assignment").action
|
|
|
6349
6477
|
});
|
|
6350
6478
|
workflows.command("validate <file>").description("validate a workflow JSON file without storing or running it").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables").action((file, opts) => {
|
|
6351
6479
|
const body = workflowBodyFromJson(JSON.parse(readFileSync2(file, "utf8")), opts.name);
|
|
6352
|
-
const
|
|
6353
|
-
const workflow = {
|
|
6354
|
-
id: "validation",
|
|
6355
|
-
name: body.name,
|
|
6356
|
-
description: body.description,
|
|
6357
|
-
version: body.version ?? 1,
|
|
6358
|
-
status: "active",
|
|
6359
|
-
goal: body.goal,
|
|
6360
|
-
steps: body.steps,
|
|
6361
|
-
createdAt: now,
|
|
6362
|
-
updatedAt: now
|
|
6363
|
-
};
|
|
6480
|
+
const workflow = workflowSpecForPreflight(body);
|
|
6364
6481
|
const preflight = opts.preflight ? preflightWorkflow(workflow) : undefined;
|
|
6365
6482
|
print({ valid: true, workflow: publicWorkflow(workflow), preflight }, `valid workflow ${workflow.name} steps=${workflow.steps.length}`);
|
|
6366
6483
|
});
|
|
6367
|
-
workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").action((file, opts) => {
|
|
6484
|
+
workflows.command("create <file>").description("validate and store a workflow JSON file").option("--name <name>", "override workflow name from the file").option("--preflight", "also check account env and target executables before storing").action((file, opts) => {
|
|
6368
6485
|
const store = new Store;
|
|
6369
6486
|
try {
|
|
6370
6487
|
const body = workflowBodyFromJson(JSON.parse(readFileSync2(file, "utf8")), opts.name);
|
|
6488
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflowSpecForPreflight(body, "creation-preflight"), { name: body.name, type: "workflow" }, {}) : undefined;
|
|
6371
6489
|
const workflow = store.createWorkflow(body);
|
|
6372
|
-
|
|
6490
|
+
if (preflight !== undefined)
|
|
6491
|
+
print({ workflow: publicWorkflow(workflow), preflight }, `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
|
|
6492
|
+
else
|
|
6493
|
+
print(publicWorkflow(workflow), `created workflow ${workflow.id} (${workflow.name}) steps=${workflow.steps.length}`);
|
|
6373
6494
|
} finally {
|
|
6374
6495
|
store.close();
|
|
6375
6496
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -2724,6 +2724,7 @@ function commandSpec(target) {
|
|
|
2724
2724
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2725
2725
|
account: agentTarget.account,
|
|
2726
2726
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2727
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2727
2728
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2728
2729
|
stdin: agentTarget.prompt,
|
|
2729
2730
|
allowlist: agentTarget.allowlist
|
|
@@ -2805,6 +2806,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2805
2806
|
if (spec.preflightAnyOf?.length) {
|
|
2806
2807
|
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");
|
|
2807
2808
|
}
|
|
2809
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2810
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2811
|
+
}
|
|
2808
2812
|
return lines.join(`
|
|
2809
2813
|
`);
|
|
2810
2814
|
}
|
|
@@ -2820,6 +2824,29 @@ function transportEnv(opts) {
|
|
|
2820
2824
|
env.PATH = normalizeExecutionPath(env);
|
|
2821
2825
|
return env;
|
|
2822
2826
|
}
|
|
2827
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2828
|
+
if (!spec.nativeAuthProfile)
|
|
2829
|
+
return;
|
|
2830
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2831
|
+
return;
|
|
2832
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2833
|
+
encoding: "utf8",
|
|
2834
|
+
env,
|
|
2835
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2836
|
+
timeout: 15000
|
|
2837
|
+
});
|
|
2838
|
+
if (result.error) {
|
|
2839
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2840
|
+
}
|
|
2841
|
+
if ((result.status ?? 1) !== 0) {
|
|
2842
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2843
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2844
|
+
}
|
|
2845
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2846
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2847
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2823
2850
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2824
2851
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2825
2852
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2961,6 +2988,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2961
2988
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2962
2989
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2963
2990
|
}
|
|
2991
|
+
preflightNativeAuthProfile(spec, env);
|
|
2964
2992
|
return {
|
|
2965
2993
|
command: spec.command,
|
|
2966
2994
|
accountProfile: spec.account?.profile,
|
|
@@ -3002,6 +3030,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
3002
3030
|
durationMs: 0
|
|
3003
3031
|
};
|
|
3004
3032
|
}
|
|
3033
|
+
try {
|
|
3034
|
+
preflightNativeAuthProfile(spec, env);
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
return {
|
|
3037
|
+
status: "failed",
|
|
3038
|
+
stdout: "",
|
|
3039
|
+
stderr: "",
|
|
3040
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3041
|
+
startedAt,
|
|
3042
|
+
finishedAt: nowIso(),
|
|
3043
|
+
durationMs: 0
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3005
3046
|
const child = spawn(spec.command, spec.args, {
|
|
3006
3047
|
cwd: spec.cwd,
|
|
3007
3048
|
env,
|
|
@@ -3658,11 +3699,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3658
3699
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3659
3700
|
}
|
|
3660
3701
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3661
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3702
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3703
|
+
try {
|
|
3704
|
+
return {
|
|
3705
|
+
workflowStepId: step.id,
|
|
3706
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3707
|
+
workflowId: workflow.id,
|
|
3708
|
+
workflowName: workflow.name,
|
|
3709
|
+
workflowStepId: step.id
|
|
3710
|
+
}, opts)
|
|
3711
|
+
};
|
|
3712
|
+
} catch (error) {
|
|
3713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3714
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3666
3717
|
}
|
|
3667
3718
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3668
3719
|
if (loop.target.type !== "workflow") {
|
|
@@ -4419,7 +4470,7 @@ function enableStartup(result) {
|
|
|
4419
4470
|
// package.json
|
|
4420
4471
|
var package_default = {
|
|
4421
4472
|
name: "@hasna/loops",
|
|
4422
|
-
version: "0.3.
|
|
4473
|
+
version: "0.3.23",
|
|
4423
4474
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4424
4475
|
type: "module",
|
|
4425
4476
|
main: "dist/index.js",
|
package/dist/index.js
CHANGED
|
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
|
|
|
2714
2714
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2715
2715
|
account: agentTarget.account,
|
|
2716
2716
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2717
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2717
2718
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2718
2719
|
stdin: agentTarget.prompt,
|
|
2719
2720
|
allowlist: agentTarget.allowlist
|
|
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2795
2796
|
if (spec.preflightAnyOf?.length) {
|
|
2796
2797
|
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");
|
|
2797
2798
|
}
|
|
2799
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2800
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2801
|
+
}
|
|
2798
2802
|
return lines.join(`
|
|
2799
2803
|
`);
|
|
2800
2804
|
}
|
|
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
|
|
|
2810
2814
|
env.PATH = normalizeExecutionPath(env);
|
|
2811
2815
|
return env;
|
|
2812
2816
|
}
|
|
2817
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2818
|
+
if (!spec.nativeAuthProfile)
|
|
2819
|
+
return;
|
|
2820
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2821
|
+
return;
|
|
2822
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2823
|
+
encoding: "utf8",
|
|
2824
|
+
env,
|
|
2825
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2826
|
+
timeout: 15000
|
|
2827
|
+
});
|
|
2828
|
+
if (result.error) {
|
|
2829
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2830
|
+
}
|
|
2831
|
+
if ((result.status ?? 1) !== 0) {
|
|
2832
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2833
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2834
|
+
}
|
|
2835
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2836
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2837
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2813
2840
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2814
2841
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2815
2842
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2951
2978
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2952
2979
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2953
2980
|
}
|
|
2981
|
+
preflightNativeAuthProfile(spec, env);
|
|
2954
2982
|
return {
|
|
2955
2983
|
command: spec.command,
|
|
2956
2984
|
accountProfile: spec.account?.profile,
|
|
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2992
3020
|
durationMs: 0
|
|
2993
3021
|
};
|
|
2994
3022
|
}
|
|
3023
|
+
try {
|
|
3024
|
+
preflightNativeAuthProfile(spec, env);
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
return {
|
|
3027
|
+
status: "failed",
|
|
3028
|
+
stdout: "",
|
|
3029
|
+
stderr: "",
|
|
3030
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3031
|
+
startedAt,
|
|
3032
|
+
finishedAt: nowIso(),
|
|
3033
|
+
durationMs: 0
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
2995
3036
|
const child = spawn(spec.command, spec.args, {
|
|
2996
3037
|
cwd: spec.cwd,
|
|
2997
3038
|
env,
|
|
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3648
3689
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3649
3690
|
}
|
|
3650
3691
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3651
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3692
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3693
|
+
try {
|
|
3694
|
+
return {
|
|
3695
|
+
workflowStepId: step.id,
|
|
3696
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3697
|
+
workflowId: workflow.id,
|
|
3698
|
+
workflowName: workflow.name,
|
|
3699
|
+
workflowStepId: step.id
|
|
3700
|
+
}, opts)
|
|
3701
|
+
};
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3704
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3656
3707
|
}
|
|
3657
3708
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3658
3709
|
if (loop.target.type !== "workflow") {
|
package/dist/sdk/index.js
CHANGED
|
@@ -2714,6 +2714,7 @@ function commandSpec(target) {
|
|
|
2714
2714
|
timeoutMs: agentTarget.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
2715
2715
|
account: agentTarget.account,
|
|
2716
2716
|
accountTool: agentTarget.account?.tool ?? accountToolForProvider(agentTarget.provider),
|
|
2717
|
+
nativeAuthProfile: agentTarget.authProfile ? { provider: agentTarget.provider, profile: agentTarget.authProfile } : undefined,
|
|
2717
2718
|
preflightAnyOf: agentTarget.provider === "cursor" ? ["cursor", "agent"] : undefined,
|
|
2718
2719
|
stdin: agentTarget.prompt,
|
|
2719
2720
|
allowlist: agentTarget.allowlist
|
|
@@ -2795,6 +2796,9 @@ function remotePreflightScript(spec, metadata) {
|
|
|
2795
2796
|
if (spec.preflightAnyOf?.length) {
|
|
2796
2797
|
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");
|
|
2797
2798
|
}
|
|
2799
|
+
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
2800
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILES="$(${shellQuote(spec.command)} profile list)" || {`, ` printf '%s\\n' ${shellQuote("codewith auth profile preflight failed")} >&2`, " exit 1", "}", `if ! printf '%s\\n' "$__OPENLOOPS_CODEWITH_PROFILES" | awk 'NR > 1 { print $1 }' | grep -Fx ${shellQuote(spec.nativeAuthProfile.profile)} >/dev/null; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`)} >&2`, " exit 1", "fi");
|
|
2801
|
+
}
|
|
2798
2802
|
return lines.join(`
|
|
2799
2803
|
`);
|
|
2800
2804
|
}
|
|
@@ -2810,6 +2814,29 @@ function transportEnv(opts) {
|
|
|
2810
2814
|
env.PATH = normalizeExecutionPath(env);
|
|
2811
2815
|
return env;
|
|
2812
2816
|
}
|
|
2817
|
+
function preflightNativeAuthProfile(spec, env) {
|
|
2818
|
+
if (!spec.nativeAuthProfile)
|
|
2819
|
+
return;
|
|
2820
|
+
if (spec.nativeAuthProfile.provider !== "codewith")
|
|
2821
|
+
return;
|
|
2822
|
+
const result = spawnSync2(spec.command, ["profile", "list"], {
|
|
2823
|
+
encoding: "utf8",
|
|
2824
|
+
env,
|
|
2825
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2826
|
+
timeout: 15000
|
|
2827
|
+
});
|
|
2828
|
+
if (result.error) {
|
|
2829
|
+
throw new Error(`codewith auth profile preflight failed: ${result.error.message}`);
|
|
2830
|
+
}
|
|
2831
|
+
if ((result.status ?? 1) !== 0) {
|
|
2832
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
2833
|
+
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
2834
|
+
}
|
|
2835
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter(Boolean));
|
|
2836
|
+
if (!profiles.has(spec.nativeAuthProfile.profile)) {
|
|
2837
|
+
throw new Error(`codewith auth profile not found: ${spec.nativeAuthProfile.profile}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2813
2840
|
function preflightRemoteSpec(spec, machine, metadata, opts) {
|
|
2814
2841
|
const plan = (opts.machineCommandResolver ?? resolveMachineCommand)(machine.id, "bash -s");
|
|
2815
2842
|
const result = spawnSync2(plan.command, plan.args, {
|
|
@@ -2951,6 +2978,7 @@ function preflightTarget(target, metadata = {}, opts = {}) {
|
|
|
2951
2978
|
if (spec.preflightAnyOf?.length && !spec.preflightAnyOf.some((command) => executableExists(command, env))) {
|
|
2952
2979
|
throw new Error(`none of required executables found: ${spec.preflightAnyOf.join(", ")}`);
|
|
2953
2980
|
}
|
|
2981
|
+
preflightNativeAuthProfile(spec, env);
|
|
2954
2982
|
return {
|
|
2955
2983
|
command: spec.command,
|
|
2956
2984
|
accountProfile: spec.account?.profile,
|
|
@@ -2992,6 +3020,19 @@ async function executeTarget(target, metadata = {}, opts = {}) {
|
|
|
2992
3020
|
durationMs: 0
|
|
2993
3021
|
};
|
|
2994
3022
|
}
|
|
3023
|
+
try {
|
|
3024
|
+
preflightNativeAuthProfile(spec, env);
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
return {
|
|
3027
|
+
status: "failed",
|
|
3028
|
+
stdout: "",
|
|
3029
|
+
stderr: "",
|
|
3030
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3031
|
+
startedAt,
|
|
3032
|
+
finishedAt: nowIso(),
|
|
3033
|
+
durationMs: 0
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
2995
3036
|
const child = spawn(spec.command, spec.args, {
|
|
2996
3037
|
cwd: spec.cwd,
|
|
2997
3038
|
env,
|
|
@@ -3648,11 +3689,21 @@ async function executeWorkflow(store, workflow, opts = {}) {
|
|
|
3648
3689
|
return workflowResult(finalRun, terminalStatus, startedAt, finishedAt, JSON.stringify({ workflowRun: finalRun, steps }, null, 2), blockingError);
|
|
3649
3690
|
}
|
|
3650
3691
|
function preflightWorkflow(workflow, opts = {}) {
|
|
3651
|
-
return workflowExecutionOrder(workflow).map((step) =>
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3692
|
+
return workflowExecutionOrder(workflow).map((step) => {
|
|
3693
|
+
try {
|
|
3694
|
+
return {
|
|
3695
|
+
workflowStepId: step.id,
|
|
3696
|
+
...preflightTarget(targetWithStepAccount(step), {
|
|
3697
|
+
workflowId: workflow.id,
|
|
3698
|
+
workflowName: workflow.name,
|
|
3699
|
+
workflowStepId: step.id
|
|
3700
|
+
}, opts)
|
|
3701
|
+
};
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3704
|
+
throw new Error(`workflow step ${step.id} preflight failed: ${message}`);
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3656
3707
|
}
|
|
3657
3708
|
async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
3658
3709
|
if (loop.target.type !== "workflow") {
|
package/docs/USAGE.md
CHANGED
|
@@ -49,6 +49,30 @@ Run a deterministic command every minute:
|
|
|
49
49
|
loops create command repo-status --every 1m --cmd "git status --short" --cwd /path/to/repo
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Validate the target before storing the loop:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
loops create command repo-status \
|
|
56
|
+
--every 1m \
|
|
57
|
+
--cmd git \
|
|
58
|
+
--no-shell \
|
|
59
|
+
--preflight
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`--preflight` is available on `loops create command`, `loops create agent`,
|
|
63
|
+
`loops create workflow`, `loops workflows create`, and event-router commands such
|
|
64
|
+
as `loops events handle todos-task` and `loops events handle generic`. It checks
|
|
65
|
+
target executables and configured account profiles before loop or workflow rows
|
|
66
|
+
are stored, so a missing command, provider binary, OpenAccounts profile, native
|
|
67
|
+
Codewith auth profile, or workflow step dependency fails without creating a
|
|
68
|
+
scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
|
|
69
|
+
preflight evidence.
|
|
70
|
+
|
|
71
|
+
For shell command loops, preflight can only verify the shell plus configured
|
|
72
|
+
accounts because the command string is interpreted later by the shell. Use
|
|
73
|
+
`--no-shell` or workflow command `args` when you need executable-level
|
|
74
|
+
validation before storing the loop.
|
|
75
|
+
|
|
52
76
|
Run a Claude loop every morning:
|
|
53
77
|
|
|
54
78
|
```bash
|
package/package.json
CHANGED