@hasna/loops 0.3.20 → 0.3.22
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 +4 -1
- package/dist/cli/index.js +223 -28
- package/dist/daemon/index.js +57 -6
- package/dist/index.js +59 -5
- package/dist/sdk/index.js +56 -5
- package/docs/USAGE.md +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,7 +324,10 @@ renames only with `--apply`. Apply mode writes a SQLite backup under
|
|
|
324
324
|
groups loops with the same normalized name, cwd, and schedule. `hygiene scripts`
|
|
325
325
|
inventories loops whose command still references `~/.hasna/loops/scripts`.
|
|
326
326
|
`hygiene route-tasks` upserts deduped Todos tasks for hygiene findings with
|
|
327
|
-
stable fingerprints and `no_tmux_dispatch=true` metadata.
|
|
327
|
+
stable fingerprints and `no_tmux_dispatch=true` metadata. Route commands use a
|
|
328
|
+
package-managed cursor under `<LOOPS_DATA_DIR>/route-cursors.json` so bounded
|
|
329
|
+
`--max-actions` runs advance through all findings over repeated scheduled runs
|
|
330
|
+
instead of reprocessing only the first batch.
|
|
328
331
|
|
|
329
332
|
Archive loops when retiring old automation but preserving history:
|
|
330
333
|
|
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") {
|
|
@@ -4757,6 +4808,7 @@ function recommendedTask(loop, run, failure, route) {
|
|
|
4757
4808
|
`Status: ${run.status}`,
|
|
4758
4809
|
`Classification: ${failure.classification}`,
|
|
4759
4810
|
`Fingerprint: ${failure.fingerprint}`,
|
|
4811
|
+
`No-tmux routing: Do not dispatch or paste prompts into tmux panes; use task-triggered headless worker/verifier workflows only.`,
|
|
4760
4812
|
route.cwd ? `Route cwd: ${route.cwd}` : undefined,
|
|
4761
4813
|
route.provider ? `Provider: ${route.provider}` : undefined,
|
|
4762
4814
|
failure.evidence.error ? `Error:
|
|
@@ -5091,7 +5143,7 @@ function buildScriptInventoryReport(store, opts = {}) {
|
|
|
5091
5143
|
// package.json
|
|
5092
5144
|
var package_default = {
|
|
5093
5145
|
name: "@hasna/loops",
|
|
5094
|
-
version: "0.3.
|
|
5146
|
+
version: "0.3.22",
|
|
5095
5147
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5096
5148
|
type: "module",
|
|
5097
5149
|
main: "dist/index.js",
|
|
@@ -5332,6 +5384,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
5332
5384
|
"You are the worker agent for a task-triggered OpenLoops workflow.",
|
|
5333
5385
|
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
5334
5386
|
"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.",
|
|
5387
|
+
"Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
|
|
5335
5388
|
"Do not mark the task complete unless the work is genuinely done and validated.",
|
|
5336
5389
|
"",
|
|
5337
5390
|
`Task context JSON: ${compactJson(taskContext)}`
|
|
@@ -5343,6 +5396,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
5343
5396
|
"You are the verifier agent for a task-triggered OpenLoops workflow.",
|
|
5344
5397
|
"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.",
|
|
5345
5398
|
"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.",
|
|
5399
|
+
"Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
|
|
5346
5400
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
5347
5401
|
"",
|
|
5348
5402
|
`Task context JSON: ${compactJson(taskContext)}`
|
|
@@ -5573,6 +5627,41 @@ function print(value, human) {
|
|
|
5573
5627
|
else
|
|
5574
5628
|
console.log(human);
|
|
5575
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
|
+
}
|
|
5576
5665
|
function printTextOutput(value) {
|
|
5577
5666
|
for (const line of textOutputBlocks(value, { indent: " " }))
|
|
5578
5667
|
console.log(line);
|
|
@@ -5761,6 +5850,53 @@ function stableHash(parts) {
|
|
|
5761
5850
|
return createHash2("sha256").update(parts.map((part) => JSON.stringify(part)).join(`
|
|
5762
5851
|
`)).digest("hex").slice(0, 16);
|
|
5763
5852
|
}
|
|
5853
|
+
function routeCursorsPath() {
|
|
5854
|
+
return join3(dataDir(), "route-cursors.json");
|
|
5855
|
+
}
|
|
5856
|
+
function readRouteCursors() {
|
|
5857
|
+
const path = routeCursorsPath();
|
|
5858
|
+
if (!existsSync3(path))
|
|
5859
|
+
return {};
|
|
5860
|
+
try {
|
|
5861
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
5862
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
5863
|
+
} catch {
|
|
5864
|
+
return {};
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
function writeRouteCursor(key, lastFingerprint) {
|
|
5868
|
+
if (!lastFingerprint)
|
|
5869
|
+
return;
|
|
5870
|
+
const cursors = readRouteCursors();
|
|
5871
|
+
cursors[key] = { lastFingerprint, updatedAt: new Date().toISOString() };
|
|
5872
|
+
writeFileSync3(routeCursorsPath(), JSON.stringify(cursors, null, 2), { mode: 384 });
|
|
5873
|
+
}
|
|
5874
|
+
function selectRouteItems(items, maxActions, cursorKey, fingerprintOf) {
|
|
5875
|
+
const total = items.length;
|
|
5876
|
+
const boundedMax = Math.max(0, Math.floor(Number.isFinite(maxActions) ? maxActions : 0));
|
|
5877
|
+
if (total === 0 || boundedMax === 0) {
|
|
5878
|
+
return { selected: [], cursor: { key: cursorKey, total, maxActions: boundedMax, startIndex: 0 } };
|
|
5879
|
+
}
|
|
5880
|
+
const cursors = readRouteCursors();
|
|
5881
|
+
const previousFingerprint = cursors[cursorKey]?.lastFingerprint;
|
|
5882
|
+
const previousIndex = previousFingerprint ? items.findIndex((item) => fingerprintOf(item) === previousFingerprint) : -1;
|
|
5883
|
+
const startIndex = previousIndex >= 0 ? (previousIndex + 1) % total : 0;
|
|
5884
|
+
const selected = [];
|
|
5885
|
+
const count = Math.min(boundedMax, total);
|
|
5886
|
+
for (let index = 0;index < count; index += 1)
|
|
5887
|
+
selected.push(items[(startIndex + index) % total]);
|
|
5888
|
+
return {
|
|
5889
|
+
selected,
|
|
5890
|
+
cursor: {
|
|
5891
|
+
key: cursorKey,
|
|
5892
|
+
total,
|
|
5893
|
+
maxActions: boundedMax,
|
|
5894
|
+
previousFingerprint,
|
|
5895
|
+
startIndex,
|
|
5896
|
+
lastFingerprint: selected.length ? fingerprintOf(selected[selected.length - 1]) : undefined
|
|
5897
|
+
}
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5764
5900
|
function eventData(event) {
|
|
5765
5901
|
const data = event.data;
|
|
5766
5902
|
if (data && typeof data === "object" && !Array.isArray(data))
|
|
@@ -5842,6 +5978,36 @@ function booleanLike(value) {
|
|
|
5842
5978
|
function hasTruthyField(records, keys) {
|
|
5843
5979
|
return records.some((record) => keys.some((key) => booleanLike(record[key])));
|
|
5844
5980
|
}
|
|
5981
|
+
function automationRecords(data, metadata) {
|
|
5982
|
+
const records = [];
|
|
5983
|
+
const dataAutomation = nestedObject(data, "automation");
|
|
5984
|
+
if (dataAutomation)
|
|
5985
|
+
records.push(dataAutomation);
|
|
5986
|
+
const dataTask = nestedObject(data, "task");
|
|
5987
|
+
const dataTaskAutomation = dataTask ? nestedObject(dataTask, "automation") : undefined;
|
|
5988
|
+
if (dataTaskAutomation)
|
|
5989
|
+
records.push(dataTaskAutomation);
|
|
5990
|
+
const dataPayload = nestedObject(data, "payload");
|
|
5991
|
+
const payloadAutomation = dataPayload ? nestedObject(dataPayload, "automation") : undefined;
|
|
5992
|
+
if (payloadAutomation)
|
|
5993
|
+
records.push(payloadAutomation);
|
|
5994
|
+
const payloadTask = dataPayload ? nestedObject(dataPayload, "task") : undefined;
|
|
5995
|
+
const payloadTaskAutomation = payloadTask ? nestedObject(payloadTask, "automation") : undefined;
|
|
5996
|
+
if (payloadTaskAutomation)
|
|
5997
|
+
records.push(payloadTaskAutomation);
|
|
5998
|
+
const dataMetadata = nestedObject(data, "metadata");
|
|
5999
|
+
const dataMetadataAutomation = dataMetadata ? nestedObject(dataMetadata, "automation") : undefined;
|
|
6000
|
+
if (dataMetadataAutomation)
|
|
6001
|
+
records.push(dataMetadataAutomation);
|
|
6002
|
+
const metadataAutomation = nestedObject(metadata, "automation");
|
|
6003
|
+
if (metadataAutomation)
|
|
6004
|
+
records.push(metadataAutomation);
|
|
6005
|
+
const metadataTask = nestedObject(metadata, "task");
|
|
6006
|
+
const metadataTaskAutomation = metadataTask ? nestedObject(metadataTask, "automation") : undefined;
|
|
6007
|
+
if (metadataTaskAutomation)
|
|
6008
|
+
records.push(metadataTaskAutomation);
|
|
6009
|
+
return records;
|
|
6010
|
+
}
|
|
5845
6011
|
function tagsFromValue(value) {
|
|
5846
6012
|
if (Array.isArray(value))
|
|
5847
6013
|
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
@@ -5860,7 +6026,7 @@ function taskEventTags(records) {
|
|
|
5860
6026
|
function taskRouteEligibility(data, metadata) {
|
|
5861
6027
|
const records = taskEventRecords(data, metadata);
|
|
5862
6028
|
const tags = taskEventTags(records);
|
|
5863
|
-
const hasRouteOptIn = tags.includes("auto:route") || hasTruthyField(records, ["route_enabled", "routeEnabled", "automation_allowed", "automationAllowed", "allowed"]);
|
|
6029
|
+
const hasRouteOptIn = tags.includes("auto:route") || hasTruthyField(records, ["route_enabled", "routeEnabled", "automation_allowed", "automationAllowed"]) || hasTruthyField(automationRecords(data, metadata), ["allowed"]);
|
|
5864
6030
|
if (!hasRouteOptIn)
|
|
5865
6031
|
return { eligible: false, reason: "missing explicit route opt-in", tags };
|
|
5866
6032
|
const status = taskEventField(data, ["status", "task_status", "taskStatus"])?.toLowerCase();
|
|
@@ -5940,7 +6106,7 @@ function permissionModeFromOpts(opts, provider) {
|
|
|
5940
6106
|
return mode;
|
|
5941
6107
|
}
|
|
5942
6108
|
var create = program.command("create").description("create loops");
|
|
5943
|
-
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) => {
|
|
6109
|
+
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) => {
|
|
5944
6110
|
const store = new Store;
|
|
5945
6111
|
try {
|
|
5946
6112
|
const target = {
|
|
@@ -5951,13 +6117,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
5951
6117
|
timeoutMs: opts.timeout ? parseDuration(opts.timeout) : undefined,
|
|
5952
6118
|
account: accountFromOpts(opts)
|
|
5953
6119
|
};
|
|
5954
|
-
const
|
|
5955
|
-
|
|
6120
|
+
const input = baseCreateInput(name, opts, target);
|
|
6121
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "command" }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6122
|
+
const loop = store.createLoop(input);
|
|
6123
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
5956
6124
|
} finally {
|
|
5957
6125
|
store.close();
|
|
5958
6126
|
}
|
|
5959
6127
|
});
|
|
5960
|
-
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) => {
|
|
6128
|
+
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) => {
|
|
5961
6129
|
const provider = opts.provider;
|
|
5962
6130
|
if (!["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"].includes(provider)) {
|
|
5963
6131
|
throw new Error("unsupported provider");
|
|
@@ -5983,13 +6151,15 @@ addGoalOptions(addAccountOptions(addMachineOptions(addScheduleOptions(create.com
|
|
|
5983
6151
|
allowlist: allowlistFromOpts(opts),
|
|
5984
6152
|
account: accountFromOpts(opts)
|
|
5985
6153
|
};
|
|
5986
|
-
const
|
|
5987
|
-
|
|
6154
|
+
const input = baseCreateInput(name, opts, target);
|
|
6155
|
+
const preflight = opts.preflight ? preflightLoopTarget(input.target, { name, type: "agent", provider }, { loopName: name }, { machine: input.machine }) : undefined;
|
|
6156
|
+
const loop = store.createLoop(input);
|
|
6157
|
+
printCreatedLoop(loop, `created loop ${loop.id} (${loop.name}) next=${loop.nextRunAt}`, preflight);
|
|
5988
6158
|
} finally {
|
|
5989
6159
|
store.close();
|
|
5990
6160
|
}
|
|
5991
6161
|
});
|
|
5992
|
-
addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <name>").description("schedule a stored workflow").requiredOption("--workflow <idOrName>", "workflow id or name")))).action((name, opts) => {
|
|
6162
|
+
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) => {
|
|
5993
6163
|
const store = new Store;
|
|
5994
6164
|
try {
|
|
5995
6165
|
const workflow = store.requireWorkflow(opts.workflow);
|
|
@@ -5997,8 +6167,10 @@ addGoalOptions(addMachineOptions(addScheduleOptions(create.command("workflow <na
|
|
|
5997
6167
|
type: "workflow",
|
|
5998
6168
|
workflowId: workflow.id
|
|
5999
6169
|
};
|
|
6000
|
-
const
|
|
6001
|
-
|
|
6170
|
+
const input = baseCreateInput(name, opts, target);
|
|
6171
|
+
const preflight = opts.preflight ? preflightStoredWorkflow(workflow, { name, type: "workflow", workflow: workflow.name }, { machine: input.machine }) : undefined;
|
|
6172
|
+
const loop = store.createLoop(input);
|
|
6173
|
+
printCreatedLoop(loop, `created workflow loop ${loop.id} (${loop.name}) workflow=${workflow.name} next=${loop.nextRunAt}`, preflight);
|
|
6002
6174
|
} finally {
|
|
6003
6175
|
store.close();
|
|
6004
6176
|
}
|
|
@@ -6517,9 +6689,10 @@ health.command("route-tasks").description("upsert deduped todos tasks for failed
|
|
|
6517
6689
|
const store = new Store;
|
|
6518
6690
|
try {
|
|
6519
6691
|
const report = buildHealthReport(store, { limit: Number(opts.limit), includeInactive: Boolean(opts.includeInactive) });
|
|
6520
|
-
const failures = report.expectations.filter((entry) => !entry.ok && entry.recommendedTask)
|
|
6692
|
+
const failures = report.expectations.filter((entry) => !entry.ok && entry.recommendedTask);
|
|
6693
|
+
const selection = selectRouteItems(failures, Number(opts.maxActions), `health:${stableHash([opts.project, opts.taskList, opts.limit, Boolean(opts.includeInactive)])}`, (expectation) => expectation.recommendedTask.dedupeKey);
|
|
6521
6694
|
const listId = opts.dryRun ? undefined : ensureTodosTaskList(opts.project, opts.taskList, "Loop Error Self Heal", "Deduped OpenLoops health expectation failures routed by loops health route-tasks.");
|
|
6522
|
-
const actions =
|
|
6695
|
+
const actions = selection.selected.map((expectation) => {
|
|
6523
6696
|
const task = expectation.recommendedTask;
|
|
6524
6697
|
const metadata = {
|
|
6525
6698
|
source: "openloops.health.route-tasks",
|
|
@@ -6561,7 +6734,15 @@ health.command("route-tasks").description("upsert deduped todos tasks for failed
|
|
|
6561
6734
|
}
|
|
6562
6735
|
return { action: "upserted", fingerprint: task.dedupeKey, task: JSON.parse(result.stdout || "{}") };
|
|
6563
6736
|
});
|
|
6564
|
-
const routed = {
|
|
6737
|
+
const routed = {
|
|
6738
|
+
ok: actions.every((action) => action.action !== "upsert-failed"),
|
|
6739
|
+
inspected: report.summary.loops,
|
|
6740
|
+
failures: failures.length,
|
|
6741
|
+
routing: selection.cursor,
|
|
6742
|
+
actions
|
|
6743
|
+
};
|
|
6744
|
+
if (!opts.dryRun && routed.ok)
|
|
6745
|
+
writeRouteCursor(selection.cursor.key, selection.cursor.lastFingerprint);
|
|
6565
6746
|
if (isJson() || opts.json)
|
|
6566
6747
|
console.log(JSON.stringify(routed, null, 2));
|
|
6567
6748
|
else {
|
|
@@ -6711,25 +6892,36 @@ function buildHygieneRouteTasks(store, opts) {
|
|
|
6711
6892
|
hygiene.command("names").description("check or apply canonical machine-/repo-prefixed loop names").option("--apply", "rename loops in-place").option("--include-stopped", "include stopped loops").option("--include-inactive", "include stopped, expired, and archived loops").option("--limit <n>", "maximum loops to inspect", "1000").option("--json", "print JSON").action((opts) => {
|
|
6712
6893
|
const store = new Store;
|
|
6713
6894
|
try {
|
|
6714
|
-
const backupPath = opts.apply ? backupLoopsDatabase("name-hygiene") : undefined;
|
|
6715
6895
|
const report = buildNameHygieneReport(store, {
|
|
6716
|
-
apply:
|
|
6896
|
+
apply: false,
|
|
6717
6897
|
includeStopped: Boolean(opts.includeStopped),
|
|
6718
6898
|
includeInactive: Boolean(opts.includeInactive),
|
|
6719
6899
|
limit: Number(opts.limit)
|
|
6720
6900
|
});
|
|
6721
|
-
|
|
6901
|
+
let outputReport = report;
|
|
6902
|
+
const backupPath = opts.apply && report.changed > 0 ? backupLoopsDatabase("name-hygiene") : undefined;
|
|
6903
|
+
if (opts.apply && report.changed > 0) {
|
|
6904
|
+
outputReport = buildNameHygieneReport(store, {
|
|
6905
|
+
apply: true,
|
|
6906
|
+
includeStopped: Boolean(opts.includeStopped),
|
|
6907
|
+
includeInactive: Boolean(opts.includeInactive),
|
|
6908
|
+
limit: Number(opts.limit)
|
|
6909
|
+
});
|
|
6910
|
+
} else if (opts.apply) {
|
|
6911
|
+
outputReport = { ...report, applied: true };
|
|
6912
|
+
}
|
|
6913
|
+
const output = backupPath ? { ...outputReport, backupPath } : outputReport;
|
|
6722
6914
|
if (isJson() || opts.json)
|
|
6723
6915
|
console.log(JSON.stringify(output, null, 2));
|
|
6724
6916
|
else {
|
|
6725
|
-
console.log(`hygiene_names checked=${
|
|
6917
|
+
console.log(`hygiene_names checked=${outputReport.checked} changed=${outputReport.changed} applied=${outputReport.applied}`);
|
|
6726
6918
|
if (backupPath)
|
|
6727
6919
|
console.log(`backup=${backupPath}`);
|
|
6728
|
-
for (const change of
|
|
6729
|
-
console.log(`${
|
|
6920
|
+
for (const change of outputReport.changes.filter((entry) => entry.changed)) {
|
|
6921
|
+
console.log(`${outputReport.applied ? "renamed" : "would-rename"} ${change.id} ${change.oldName} -> ${change.newName}`);
|
|
6730
6922
|
}
|
|
6731
6923
|
}
|
|
6732
|
-
if (!
|
|
6924
|
+
if (!outputReport.ok && !outputReport.applied)
|
|
6733
6925
|
process.exitCode = 1;
|
|
6734
6926
|
} finally {
|
|
6735
6927
|
store.close();
|
|
@@ -6787,9 +6979,9 @@ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygie
|
|
|
6787
6979
|
limit: Number(opts.limit),
|
|
6788
6980
|
scriptsDir: opts.scriptsDir
|
|
6789
6981
|
});
|
|
6790
|
-
const
|
|
6982
|
+
const selection = selectRouteItems(route.tasks, Number(opts.maxActions), `hygiene:${stableHash([opts.project, opts.taskList, checks, opts.limit, Boolean(opts.includeInactive), opts.scriptsDir ?? ""])}`, (task) => task.fingerprint);
|
|
6791
6983
|
const listId = opts.dryRun ? undefined : ensureTodosTaskList(opts.project, opts.taskList, "OpenLoops Hygiene", "Deduped OpenLoops hygiene findings routed by loops hygiene route-tasks.");
|
|
6792
|
-
const actions =
|
|
6984
|
+
const actions = selection.selected.map((task) => {
|
|
6793
6985
|
if (opts.dryRun) {
|
|
6794
6986
|
return { action: "would-upsert", check: task.check, title: task.title, fingerprint: task.fingerprint, priority: task.priority, metadata: task.metadata };
|
|
6795
6987
|
}
|
|
@@ -6826,8 +7018,11 @@ hygiene.command("route-tasks").description("upsert deduped todos tasks for hygie
|
|
|
6826
7018
|
checks,
|
|
6827
7019
|
checked: route.checked,
|
|
6828
7020
|
findings: route.findings,
|
|
7021
|
+
routing: selection.cursor,
|
|
6829
7022
|
actions
|
|
6830
7023
|
};
|
|
7024
|
+
if (!opts.dryRun && routed.ok)
|
|
7025
|
+
writeRouteCursor(selection.cursor.key, selection.cursor.lastFingerprint);
|
|
6831
7026
|
if (isJson() || opts.json)
|
|
6832
7027
|
console.log(JSON.stringify(routed, null, 2));
|
|
6833
7028
|
else {
|
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.22",
|
|
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") {
|
|
@@ -4268,6 +4319,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4268
4319
|
"You are the worker agent for a task-triggered OpenLoops workflow.",
|
|
4269
4320
|
"Investigate first before changing files. Use the todos CLI as the source of truth for the task.",
|
|
4270
4321
|
"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.",
|
|
4322
|
+
"Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
|
|
4271
4323
|
"Do not mark the task complete unless the work is genuinely done and validated.",
|
|
4272
4324
|
"",
|
|
4273
4325
|
`Task context JSON: ${compactJson(taskContext)}`
|
|
@@ -4279,6 +4331,7 @@ function renderTodosTaskWorkerVerifierWorkflow(input) {
|
|
|
4279
4331
|
"You are the verifier agent for a task-triggered OpenLoops workflow.",
|
|
4280
4332
|
"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.",
|
|
4281
4333
|
"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.",
|
|
4334
|
+
"Do not dispatch or paste prompts into tmux panes. If additional work is required, create or update deduped todos tasks so task-created routing can start a fresh headless workflow.",
|
|
4282
4335
|
"Do not make broad unrelated changes. Only apply tiny verification fixes when they are necessary and low risk; otherwise create follow-up tasks.",
|
|
4283
4336
|
"",
|
|
4284
4337
|
`Task context JSON: ${compactJson(taskContext)}`
|
|
@@ -4850,6 +4903,7 @@ function recommendedTask(loop, run, failure, route) {
|
|
|
4850
4903
|
`Status: ${run.status}`,
|
|
4851
4904
|
`Classification: ${failure.classification}`,
|
|
4852
4905
|
`Fingerprint: ${failure.fingerprint}`,
|
|
4906
|
+
`No-tmux routing: Do not dispatch or paste prompts into tmux panes; use task-triggered headless worker/verifier workflows only.`,
|
|
4853
4907
|
route.cwd ? `Route cwd: ${route.cwd}` : undefined,
|
|
4854
4908
|
route.provider ? `Provider: ${route.provider}` : undefined,
|
|
4855
4909
|
failure.evidence.error ? `Error:
|
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,28 @@ 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`, and
|
|
63
|
+
`loops create workflow`. It checks target executables and configured account
|
|
64
|
+
profiles before the loop row is stored, so a missing command, provider binary,
|
|
65
|
+
OpenAccounts profile, or workflow step dependency fails without creating a
|
|
66
|
+
scheduled loop. Use `--json` with `--preflight` to capture stable machine-readable
|
|
67
|
+
preflight evidence.
|
|
68
|
+
|
|
69
|
+
For shell command loops, preflight can only verify the shell plus configured
|
|
70
|
+
accounts because the command string is interpreted later by the shell. Use
|
|
71
|
+
`--no-shell` or workflow command `args` when you need executable-level
|
|
72
|
+
validation before storing the loop.
|
|
73
|
+
|
|
52
74
|
Run a Claude loop every morning:
|
|
53
75
|
|
|
54
76
|
```bash
|
|
@@ -333,7 +355,10 @@ inventories loops whose command still references `~/.hasna/loops/scripts`; use
|
|
|
333
355
|
it as a migration gate before deleting local scripts. `hygiene route-tasks`
|
|
334
356
|
upserts deduped Todos tasks for hygiene findings with stable fingerprints and
|
|
335
357
|
`no_tmux_dispatch=true` metadata; use `--dry-run --json` before enabling it as a
|
|
336
|
-
production loop.
|
|
358
|
+
production loop. Route commands store a small cursor in
|
|
359
|
+
`<LOOPS_DATA_DIR>/route-cursors.json` so bounded `--max-actions` runs advance
|
|
360
|
+
through all findings over repeated scheduled runs instead of reprocessing only
|
|
361
|
+
the first batch.
|
|
337
362
|
|
|
338
363
|
Archive loops when retiring old automation but preserving history:
|
|
339
364
|
|
package/package.json
CHANGED