@hasna/loops 0.4.9 → 0.4.11
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/CHANGELOG.md +45 -0
- package/dist/api/index.js +1 -1
- package/dist/cli/index.js +265 -44
- package/dist/daemon/daemon.d.ts +18 -0
- package/dist/daemon/index.js +99 -24
- package/dist/index.js +65 -15
- package/dist/lib/mode.js +1 -1
- package/dist/lib/route/pr-review.d.ts +24 -1
- package/dist/lib/scheduler.d.ts +13 -0
- package/dist/lib/storage/index.js +30 -11
- package/dist/lib/storage/sqlite.js +30 -11
- package/dist/lib/store.js +30 -11
- package/dist/mcp/index.js +65 -15
- package/dist/runner/index.js +22 -4
- package/dist/sdk/index.js +65 -15
- package/package.json +1 -1
package/dist/sdk/index.js
CHANGED
|
@@ -54,8 +54,12 @@ function nowIso() {
|
|
|
54
54
|
import { mkdirSync } from "fs";
|
|
55
55
|
import { homedir } from "os";
|
|
56
56
|
import { join } from "path";
|
|
57
|
+
function homeDir() {
|
|
58
|
+
const home = process.env.HOME?.trim();
|
|
59
|
+
return home ? home : homedir();
|
|
60
|
+
}
|
|
57
61
|
function dataDir() {
|
|
58
|
-
return process.env.LOOPS_DATA_DIR || join(
|
|
62
|
+
return process.env.LOOPS_DATA_DIR || join(homeDir(), ".hasna", "loops");
|
|
59
63
|
}
|
|
60
64
|
function ensureDataDir() {
|
|
61
65
|
const dir = dataDir();
|
|
@@ -72,10 +76,10 @@ function daemonLogPath() {
|
|
|
72
76
|
return join(dataDir(), "daemon.log");
|
|
73
77
|
}
|
|
74
78
|
function systemdServicePath() {
|
|
75
|
-
return join(
|
|
79
|
+
return join(homeDir(), ".config", "systemd", "user", "loops-daemon.service");
|
|
76
80
|
}
|
|
77
81
|
function launchdPlistPath() {
|
|
78
|
-
return join(
|
|
82
|
+
return join(homeDir(), "Library", "LaunchAgents", "com.hasna.loops.daemon.plist");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
// src/lib/process-identity.ts
|
|
@@ -1472,6 +1476,21 @@ function workItemStatusForLoopRun(status, attempt, maxAttempts) {
|
|
|
1472
1476
|
function scrubbedOrNull(value) {
|
|
1473
1477
|
return value == null ? null : scrubSecrets(value);
|
|
1474
1478
|
}
|
|
1479
|
+
var MAX_PERSISTED_RUN_OUTPUT_CHARS = 64 * 1024;
|
|
1480
|
+
function clampPersistedRunOutput(value) {
|
|
1481
|
+
if (value == null || value.length <= MAX_PERSISTED_RUN_OUTPUT_CHARS)
|
|
1482
|
+
return value;
|
|
1483
|
+
const half = Math.floor(MAX_PERSISTED_RUN_OUTPUT_CHARS / 2);
|
|
1484
|
+
const head = value.slice(0, half);
|
|
1485
|
+
const tail = value.slice(value.length - half);
|
|
1486
|
+
const omitted = value.length - head.length - tail.length;
|
|
1487
|
+
return `${head}
|
|
1488
|
+
\u2026[${omitted} chars truncated by loops run-output retention]\u2026
|
|
1489
|
+
${tail}`;
|
|
1490
|
+
}
|
|
1491
|
+
function persistedRunOutput(value) {
|
|
1492
|
+
return clampPersistedRunOutput(scrubbedOrNull(value));
|
|
1493
|
+
}
|
|
1475
1494
|
function chmodIfExists(path, mode) {
|
|
1476
1495
|
try {
|
|
1477
1496
|
if (existsSync(path))
|
|
@@ -3218,8 +3237,8 @@ class Store {
|
|
|
3218
3237
|
))`).run({
|
|
3219
3238
|
$workflowRunId: workflowRunId,
|
|
3220
3239
|
$stepId: stepId,
|
|
3221
|
-
$stdout: progress.stdout === undefined ? null :
|
|
3222
|
-
$stderr: progress.stderr === undefined ? null :
|
|
3240
|
+
$stdout: progress.stdout === undefined ? null : persistedRunOutput(progress.stdout),
|
|
3241
|
+
$stderr: progress.stderr === undefined ? null : persistedRunOutput(progress.stderr),
|
|
3223
3242
|
$updated: now,
|
|
3224
3243
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
3225
3244
|
$now: now
|
|
@@ -3280,8 +3299,8 @@ class Store {
|
|
|
3280
3299
|
$finished: finishedAt,
|
|
3281
3300
|
$exitCode: patch.exitCode ?? null,
|
|
3282
3301
|
$durationMs: patch.durationMs ?? null,
|
|
3283
|
-
$stdout:
|
|
3284
|
-
$stderr:
|
|
3302
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3303
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3285
3304
|
$error: error ?? null,
|
|
3286
3305
|
$updated: finishedAt,
|
|
3287
3306
|
$daemonLeaseId: opts.daemonLeaseId ?? null,
|
|
@@ -3659,8 +3678,8 @@ class Store {
|
|
|
3659
3678
|
$pid: patch.pid ?? null,
|
|
3660
3679
|
$exitCode: patch.exitCode ?? null,
|
|
3661
3680
|
$durationMs: patch.durationMs ?? null,
|
|
3662
|
-
$stdout:
|
|
3663
|
-
$stderr:
|
|
3681
|
+
$stdout: persistedRunOutput(patch.stdout),
|
|
3682
|
+
$stderr: persistedRunOutput(patch.stderr),
|
|
3664
3683
|
$error: error ?? null,
|
|
3665
3684
|
$updated: finishedAt,
|
|
3666
3685
|
$claimedBy: opts.claimedBy ?? null,
|
|
@@ -4057,8 +4076,8 @@ class Store {
|
|
|
4057
4076
|
$processStartedAt: run.processStartedAt ?? null,
|
|
4058
4077
|
$exitCode: run.exitCode ?? null,
|
|
4059
4078
|
$durationMs: run.durationMs ?? null,
|
|
4060
|
-
$stdout:
|
|
4061
|
-
$stderr:
|
|
4079
|
+
$stdout: persistedRunOutput(run.stdout),
|
|
4080
|
+
$stderr: persistedRunOutput(run.stderr),
|
|
4062
4081
|
$error: scrubbedOrNull(run.error),
|
|
4063
4082
|
$goalRunId: run.goalRunId ?? null,
|
|
4064
4083
|
$created: run.createdAt,
|
|
@@ -4216,7 +4235,7 @@ class Store {
|
|
|
4216
4235
|
// package.json
|
|
4217
4236
|
var package_default = {
|
|
4218
4237
|
name: "@hasna/loops",
|
|
4219
|
-
version: "0.4.
|
|
4238
|
+
version: "0.4.11",
|
|
4220
4239
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4221
4240
|
type: "module",
|
|
4222
4241
|
main: "dist/index.js",
|
|
@@ -5040,6 +5059,20 @@ function notifySpawn(pid, opts) {
|
|
|
5040
5059
|
function shellQuote(value) {
|
|
5041
5060
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5042
5061
|
}
|
|
5062
|
+
function codewithProfileCandidateFromLine(line) {
|
|
5063
|
+
const cols = line.trim().split(/\s+/);
|
|
5064
|
+
if (cols[0] === "*")
|
|
5065
|
+
return cols[1];
|
|
5066
|
+
return cols[0];
|
|
5067
|
+
}
|
|
5068
|
+
function codewithProfileForError(profile) {
|
|
5069
|
+
return /[\u0000-\u001F\u007F]/.test(profile) ? JSON.stringify(profile) ?? profile : profile;
|
|
5070
|
+
}
|
|
5071
|
+
function assertCodewithAuthProfileSupported(profile) {
|
|
5072
|
+
if (profile.includes("\x00")) {
|
|
5073
|
+
throw new Error(`codewith auth profile contains unsupported NUL byte: ${codewithProfileForError(profile)}`);
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5043
5076
|
function metadataEnv(metadata) {
|
|
5044
5077
|
const env = {};
|
|
5045
5078
|
if (metadata.loopId)
|
|
@@ -5106,6 +5139,9 @@ function commandSpec(target, opts) {
|
|
|
5106
5139
|
};
|
|
5107
5140
|
}
|
|
5108
5141
|
const agentTarget = target;
|
|
5142
|
+
if (agentTarget.provider === "codewith" && agentTarget.authProfile) {
|
|
5143
|
+
assertCodewithAuthProfileSupported(agentTarget.authProfile);
|
|
5144
|
+
}
|
|
5109
5145
|
const adapter = providerAdapter(agentTarget.provider);
|
|
5110
5146
|
const invocation = adapter.buildInvocation(agentTarget);
|
|
5111
5147
|
return {
|
|
@@ -5278,7 +5314,8 @@ function remotePreflightScript(spec, metadata) {
|
|
|
5278
5314
|
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");
|
|
5279
5315
|
}
|
|
5280
5316
|
if (spec.nativeAuthProfile?.provider === "codewith") {
|
|
5281
|
-
|
|
5317
|
+
const profileForError = codewithProfileForError(spec.nativeAuthProfile.profile);
|
|
5318
|
+
lines.push(`__OPENLOOPS_CODEWITH_PROFILE=${shellQuote(spec.nativeAuthProfile.profile)}`, "export __OPENLOOPS_CODEWITH_PROFILE", `__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 { candidate = ($1 == "*" ? $2 : $1); if (candidate == ENVIRON["__OPENLOOPS_CODEWITH_PROFILE"]) found = 1 } END { exit(found ? 0 : 1) }'; then`, ` printf '%s\\n' ${shellQuote(`codewith auth profile not found: ${profileForError}`)} >&2`, " exit 1", "fi");
|
|
5282
5319
|
}
|
|
5283
5320
|
return lines.join(`
|
|
5284
5321
|
`);
|
|
@@ -5303,9 +5340,9 @@ function assertCodewithProfileListed(profile, result) {
|
|
|
5303
5340
|
const detail = (result.stderr || result.stdout || `exit ${result.status ?? "unknown"}`).trim();
|
|
5304
5341
|
throw new Error(`codewith auth profile preflight failed${detail ? `: ${detail}` : ""}`);
|
|
5305
5342
|
}
|
|
5306
|
-
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(
|
|
5343
|
+
const profiles = new Set((result.stdout || "").split(/\r?\n/).slice(1).map(codewithProfileCandidateFromLine).filter(Boolean));
|
|
5307
5344
|
if (!profiles.has(profile)) {
|
|
5308
|
-
throw new Error(`codewith auth profile not found: ${profile}`);
|
|
5345
|
+
throw new Error(`codewith auth profile not found: ${codewithProfileForError(profile)}`);
|
|
5309
5346
|
}
|
|
5310
5347
|
}
|
|
5311
5348
|
function preflightNativeAuthProfileSync(spec, env) {
|
|
@@ -7755,6 +7792,9 @@ async function executeLoopTarget(store, loop, run, opts = {}) {
|
|
|
7755
7792
|
}
|
|
7756
7793
|
|
|
7757
7794
|
// src/lib/scheduler.ts
|
|
7795
|
+
function loopLane(loop) {
|
|
7796
|
+
return loop.target.type === "command" ? "command" : "agent";
|
|
7797
|
+
}
|
|
7758
7798
|
function manualRunScheduledFor(loop, now = new Date) {
|
|
7759
7799
|
if (loop.archivedAt)
|
|
7760
7800
|
return now.toISOString();
|
|
@@ -8147,14 +8187,23 @@ function claimDueRuns(deps) {
|
|
|
8147
8187
|
const maxClaims = Math.max(0, deps.maxClaims ?? Number.POSITIVE_INFINITY);
|
|
8148
8188
|
if (maxClaims === 0)
|
|
8149
8189
|
return { claims, claimed, completed: [], skipped, recovered, expired };
|
|
8190
|
+
const laneLimits = deps.laneLimits;
|
|
8191
|
+
const laneClaims = { command: 0, agent: 0 };
|
|
8192
|
+
const laneCap = (lane) => laneLimits === undefined ? Number.POSITIVE_INFINITY : Math.max(0, laneLimits[lane] ?? Number.POSITIVE_INFINITY);
|
|
8193
|
+
const laneFull = (lane) => laneClaims[lane] >= laneCap(lane);
|
|
8150
8194
|
for (const loop of deps.store.dueLoops(now)) {
|
|
8151
8195
|
if (claims.length >= maxClaims)
|
|
8152
8196
|
break;
|
|
8197
|
+
const lane = loopLane(loop);
|
|
8198
|
+
if (laneFull(lane))
|
|
8199
|
+
continue;
|
|
8153
8200
|
const plan = dueSlots(loop, now);
|
|
8154
8201
|
let loopSkips = 0;
|
|
8155
8202
|
for (const slot of plan.slots) {
|
|
8156
8203
|
if (claims.length >= maxClaims)
|
|
8157
8204
|
break;
|
|
8205
|
+
if (laneFull(lane))
|
|
8206
|
+
break;
|
|
8158
8207
|
if (loopSkips >= MAX_SKIPS_PER_LOOP_PER_TICK)
|
|
8159
8208
|
break;
|
|
8160
8209
|
const run = claimSlot(deps, loop, slot);
|
|
@@ -8163,6 +8212,7 @@ function claimDueRuns(deps) {
|
|
|
8163
8212
|
if ("loop" in run) {
|
|
8164
8213
|
claims.push(run);
|
|
8165
8214
|
claimed.push(run.run);
|
|
8215
|
+
laneClaims[lane] += 1;
|
|
8166
8216
|
} else if (run.status === "skipped") {
|
|
8167
8217
|
skipped.push(run);
|
|
8168
8218
|
loopSkips += 1;
|