@hasna/loops 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +16 -5
- package/dist/api/index.js +56 -1
- package/dist/cli/index.js +121 -5
- package/dist/daemon/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +121 -5
- package/dist/lib/mode.d.ts +27 -0
- package/dist/lib/mode.js +56 -1
- package/dist/mcp/index.js +337 -130
- package/dist/runner/index.js +56 -1
- package/dist/sdk/index.js +71 -1
- package/docs/DEPLOYMENT_MODES.md +23 -1
- package/docs/USAGE.md +16 -5
- package/package.json +1 -1
package/dist/lib/mode.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@hasna/loops",
|
|
5
|
-
version: "0.4.
|
|
5
|
+
version: "0.4.13",
|
|
6
6
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7
7
|
type: "module",
|
|
8
8
|
main: "dist/index.js",
|
|
@@ -193,6 +193,50 @@ function sourceOfTruthForMode(mode) {
|
|
|
193
193
|
return "self_hosted_control_plane";
|
|
194
194
|
return "cloud_control_plane";
|
|
195
195
|
}
|
|
196
|
+
var ROUTE_ADMISSION_GATES = [
|
|
197
|
+
"max_dispatch",
|
|
198
|
+
"max_active",
|
|
199
|
+
"max_active_per_project",
|
|
200
|
+
"max_active_per_project_group",
|
|
201
|
+
"max_active_scope",
|
|
202
|
+
"max_per_profile"
|
|
203
|
+
];
|
|
204
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
205
|
+
if (mode === "local")
|
|
206
|
+
return "none";
|
|
207
|
+
if (mode === "cloud")
|
|
208
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
209
|
+
if (config.databaseUrlPresent)
|
|
210
|
+
return "postgres_contract";
|
|
211
|
+
if (config.apiUrl)
|
|
212
|
+
return "api_control_plane_contract";
|
|
213
|
+
return "unconfigured";
|
|
214
|
+
}
|
|
215
|
+
function schedulerStateForMode(args) {
|
|
216
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
217
|
+
return {
|
|
218
|
+
authority: args.sourceOfTruth,
|
|
219
|
+
localStore: {
|
|
220
|
+
backend: "sqlite",
|
|
221
|
+
role: args.localRole,
|
|
222
|
+
runArtifacts: "local_files",
|
|
223
|
+
routeAdmissionState: "workflow_work_items"
|
|
224
|
+
},
|
|
225
|
+
remoteStore: {
|
|
226
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
227
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
228
|
+
applySupported: false,
|
|
229
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
230
|
+
mutatesAws: false
|
|
231
|
+
},
|
|
232
|
+
routeAdmission: {
|
|
233
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
234
|
+
activeStatuses: ["admitted", "running"],
|
|
235
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
236
|
+
dryRunEvaluatesLiveCounts: false
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
196
240
|
function displayControlPlaneUrl(value) {
|
|
197
241
|
if (!value)
|
|
198
242
|
return;
|
|
@@ -218,6 +262,9 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
218
262
|
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
219
263
|
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
220
264
|
}
|
|
265
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
266
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
267
|
+
}
|
|
221
268
|
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
222
269
|
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
223
270
|
}
|
|
@@ -251,6 +298,13 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
251
298
|
required: deploymentMode !== "local",
|
|
252
299
|
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
253
300
|
},
|
|
301
|
+
schedulerState: schedulerStateForMode({
|
|
302
|
+
deploymentMode,
|
|
303
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
304
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
305
|
+
controlPlaneConfigured,
|
|
306
|
+
config
|
|
307
|
+
}),
|
|
254
308
|
warnings
|
|
255
309
|
};
|
|
256
310
|
}
|
|
@@ -263,6 +317,7 @@ function deploymentStatusLine(status) {
|
|
|
263
317
|
`source=${status.deploymentModeSource}`,
|
|
264
318
|
`truth=${status.sourceOfTruth}`,
|
|
265
319
|
`local=${status.localStore.role}`,
|
|
320
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
266
321
|
`control_plane=${configured}`
|
|
267
322
|
].join(" ");
|
|
268
323
|
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -4265,6 +4265,328 @@ class Store {
|
|
|
4265
4265
|
}
|
|
4266
4266
|
}
|
|
4267
4267
|
}
|
|
4268
|
+
// package.json
|
|
4269
|
+
var package_default = {
|
|
4270
|
+
name: "@hasna/loops",
|
|
4271
|
+
version: "0.4.13",
|
|
4272
|
+
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4273
|
+
type: "module",
|
|
4274
|
+
main: "dist/index.js",
|
|
4275
|
+
types: "dist/index.d.ts",
|
|
4276
|
+
bin: {
|
|
4277
|
+
loops: "dist/cli/index.js",
|
|
4278
|
+
"loops-daemon": "dist/daemon/index.js",
|
|
4279
|
+
"loops-api": "dist/api/index.js",
|
|
4280
|
+
"loops-runner": "dist/runner/index.js",
|
|
4281
|
+
"loops-mcp": "dist/mcp/index.js"
|
|
4282
|
+
},
|
|
4283
|
+
exports: {
|
|
4284
|
+
".": {
|
|
4285
|
+
types: "./dist/index.d.ts",
|
|
4286
|
+
import: "./dist/index.js"
|
|
4287
|
+
},
|
|
4288
|
+
"./sdk": {
|
|
4289
|
+
types: "./dist/sdk/index.d.ts",
|
|
4290
|
+
import: "./dist/sdk/index.js"
|
|
4291
|
+
},
|
|
4292
|
+
"./mcp": {
|
|
4293
|
+
types: "./dist/mcp/index.d.ts",
|
|
4294
|
+
import: "./dist/mcp/index.js"
|
|
4295
|
+
},
|
|
4296
|
+
"./api": {
|
|
4297
|
+
types: "./dist/api/index.d.ts",
|
|
4298
|
+
import: "./dist/api/index.js"
|
|
4299
|
+
},
|
|
4300
|
+
"./runner": {
|
|
4301
|
+
types: "./dist/runner/index.d.ts",
|
|
4302
|
+
import: "./dist/runner/index.js"
|
|
4303
|
+
},
|
|
4304
|
+
"./mode": {
|
|
4305
|
+
types: "./dist/lib/mode.d.ts",
|
|
4306
|
+
import: "./dist/lib/mode.js"
|
|
4307
|
+
},
|
|
4308
|
+
"./storage": {
|
|
4309
|
+
types: "./dist/lib/store.d.ts",
|
|
4310
|
+
import: "./dist/lib/store.js"
|
|
4311
|
+
},
|
|
4312
|
+
"./storage/contract": {
|
|
4313
|
+
types: "./dist/lib/storage/contract.d.ts",
|
|
4314
|
+
import: "./dist/lib/storage/contract.js"
|
|
4315
|
+
},
|
|
4316
|
+
"./storage/sqlite": {
|
|
4317
|
+
types: "./dist/lib/storage/sqlite.d.ts",
|
|
4318
|
+
import: "./dist/lib/storage/sqlite.js"
|
|
4319
|
+
},
|
|
4320
|
+
"./storage/postgres": {
|
|
4321
|
+
types: "./dist/lib/storage/postgres.d.ts",
|
|
4322
|
+
import: "./dist/lib/storage/postgres.js"
|
|
4323
|
+
},
|
|
4324
|
+
"./storage/postgres-schema": {
|
|
4325
|
+
types: "./dist/lib/storage/postgres-schema.d.ts",
|
|
4326
|
+
import: "./dist/lib/storage/postgres-schema.js"
|
|
4327
|
+
}
|
|
4328
|
+
},
|
|
4329
|
+
files: [
|
|
4330
|
+
"dist",
|
|
4331
|
+
"README.md",
|
|
4332
|
+
"CHANGELOG.md",
|
|
4333
|
+
"docs",
|
|
4334
|
+
"LICENSE"
|
|
4335
|
+
],
|
|
4336
|
+
scripts: {
|
|
4337
|
+
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
4338
|
+
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
4339
|
+
typecheck: "tsc --noEmit",
|
|
4340
|
+
test: "bun test",
|
|
4341
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
4342
|
+
prepare: "test -d dist || bun run build",
|
|
4343
|
+
"dev:cli": "bun run src/cli/index.ts",
|
|
4344
|
+
"dev:daemon": "bun run src/daemon/index.ts",
|
|
4345
|
+
prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
|
|
4346
|
+
},
|
|
4347
|
+
keywords: [
|
|
4348
|
+
"loops",
|
|
4349
|
+
"scheduler",
|
|
4350
|
+
"daemon",
|
|
4351
|
+
"agents",
|
|
4352
|
+
"claude",
|
|
4353
|
+
"cursor",
|
|
4354
|
+
"codewith",
|
|
4355
|
+
"opencode",
|
|
4356
|
+
"cli"
|
|
4357
|
+
],
|
|
4358
|
+
repository: {
|
|
4359
|
+
type: "git",
|
|
4360
|
+
url: "git+https://github.com/hasna/loops.git"
|
|
4361
|
+
},
|
|
4362
|
+
homepage: "https://github.com/hasna/loops#readme",
|
|
4363
|
+
bugs: {
|
|
4364
|
+
url: "https://github.com/hasna/loops/issues"
|
|
4365
|
+
},
|
|
4366
|
+
author: "Hasna <andrei@hasna.com>",
|
|
4367
|
+
license: "Apache-2.0",
|
|
4368
|
+
engines: {
|
|
4369
|
+
bun: ">=1.0.0"
|
|
4370
|
+
},
|
|
4371
|
+
dependencies: {
|
|
4372
|
+
"@hasna/events": "^0.1.9",
|
|
4373
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
4374
|
+
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
4375
|
+
ai: "6.0.204",
|
|
4376
|
+
commander: "^13.1.0",
|
|
4377
|
+
zod: "4.4.3"
|
|
4378
|
+
},
|
|
4379
|
+
optionalDependencies: {
|
|
4380
|
+
"@hasna/machines": "0.0.49"
|
|
4381
|
+
},
|
|
4382
|
+
devDependencies: {
|
|
4383
|
+
"@types/bun": "latest",
|
|
4384
|
+
typescript: "^5.7.3"
|
|
4385
|
+
},
|
|
4386
|
+
publishConfig: {
|
|
4387
|
+
registry: "https://registry.npmjs.org",
|
|
4388
|
+
access: "public"
|
|
4389
|
+
}
|
|
4390
|
+
};
|
|
4391
|
+
|
|
4392
|
+
// src/lib/version.ts
|
|
4393
|
+
function packageVersion() {
|
|
4394
|
+
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
4395
|
+
throw new Error("package.json version is missing");
|
|
4396
|
+
return package_default.version;
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
// src/lib/mode.ts
|
|
4400
|
+
var LOOP_DEPLOYMENT_MODES = ["local", "self_hosted", "cloud"];
|
|
4401
|
+
var MODE_ENV_KEYS = ["LOOPS_MODE", "HASNA_LOOPS_MODE"];
|
|
4402
|
+
var API_URL_ENV_KEYS = ["LOOPS_API_URL", "HASNA_LOOPS_API_URL"];
|
|
4403
|
+
var CLOUD_API_URL_ENV_KEYS = ["LOOPS_CLOUD_API_URL", "HASNA_LOOPS_CLOUD_API_URL"];
|
|
4404
|
+
var DATABASE_URL_ENV_KEYS = ["LOOPS_DATABASE_URL", "HASNA_LOOPS_DATABASE_URL"];
|
|
4405
|
+
var API_TOKEN_ENV_KEYS = ["LOOPS_API_TOKEN", "HASNA_LOOPS_API_TOKEN"];
|
|
4406
|
+
var CLOUD_TOKEN_ENV_KEYS = ["LOOPS_CLOUD_TOKEN", "HASNA_LOOPS_CLOUD_TOKEN"];
|
|
4407
|
+
var TOKEN_ENV_KEYS = [...API_TOKEN_ENV_KEYS, ...CLOUD_TOKEN_ENV_KEYS];
|
|
4408
|
+
function envValue(env, keys) {
|
|
4409
|
+
for (const key of keys) {
|
|
4410
|
+
const value = env[key]?.trim();
|
|
4411
|
+
if (value)
|
|
4412
|
+
return { key, value };
|
|
4413
|
+
}
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
function normalizeLoopDeploymentMode(value) {
|
|
4417
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
4418
|
+
if (normalized === "local")
|
|
4419
|
+
return "local";
|
|
4420
|
+
if (normalized === "self_hosted" || normalized === "selfhosted")
|
|
4421
|
+
return "self_hosted";
|
|
4422
|
+
if (normalized === "cloud" || normalized === "saas")
|
|
4423
|
+
return "cloud";
|
|
4424
|
+
throw new Error(`unsupported OpenLoops deployment mode "${value}"; expected local, self_hosted, or cloud`);
|
|
4425
|
+
}
|
|
4426
|
+
function resolveLoopDeploymentMode(env = process.env) {
|
|
4427
|
+
const explicitMode = envValue(env, MODE_ENV_KEYS);
|
|
4428
|
+
if (explicitMode) {
|
|
4429
|
+
return {
|
|
4430
|
+
deploymentMode: normalizeLoopDeploymentMode(explicitMode.value),
|
|
4431
|
+
source: explicitMode.key
|
|
4432
|
+
};
|
|
4433
|
+
}
|
|
4434
|
+
const cloudApiUrl = envValue(env, CLOUD_API_URL_ENV_KEYS);
|
|
4435
|
+
if (cloudApiUrl)
|
|
4436
|
+
return { deploymentMode: "cloud", source: cloudApiUrl.key };
|
|
4437
|
+
const apiUrl = envValue(env, API_URL_ENV_KEYS);
|
|
4438
|
+
if (apiUrl)
|
|
4439
|
+
return { deploymentMode: "self_hosted", source: apiUrl.key };
|
|
4440
|
+
const databaseUrl = envValue(env, DATABASE_URL_ENV_KEYS);
|
|
4441
|
+
if (databaseUrl)
|
|
4442
|
+
return { deploymentMode: "self_hosted", source: databaseUrl.key };
|
|
4443
|
+
return { deploymentMode: "local", source: "default" };
|
|
4444
|
+
}
|
|
4445
|
+
function loopControlPlaneConfig(env = process.env) {
|
|
4446
|
+
return {
|
|
4447
|
+
apiUrl: envValue(env, API_URL_ENV_KEYS)?.value,
|
|
4448
|
+
cloudApiUrl: envValue(env, CLOUD_API_URL_ENV_KEYS)?.value,
|
|
4449
|
+
databaseUrlPresent: Boolean(envValue(env, DATABASE_URL_ENV_KEYS)),
|
|
4450
|
+
apiAuthTokenPresent: Boolean(envValue(env, API_TOKEN_ENV_KEYS)),
|
|
4451
|
+
cloudAuthTokenPresent: Boolean(envValue(env, CLOUD_TOKEN_ENV_KEYS)),
|
|
4452
|
+
authTokenPresent: Boolean(envValue(env, TOKEN_ENV_KEYS))
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
function sourceOfTruthForMode(mode) {
|
|
4456
|
+
if (mode === "local")
|
|
4457
|
+
return "local_sqlite";
|
|
4458
|
+
if (mode === "self_hosted")
|
|
4459
|
+
return "self_hosted_control_plane";
|
|
4460
|
+
return "cloud_control_plane";
|
|
4461
|
+
}
|
|
4462
|
+
var ROUTE_ADMISSION_GATES = [
|
|
4463
|
+
"max_dispatch",
|
|
4464
|
+
"max_active",
|
|
4465
|
+
"max_active_per_project",
|
|
4466
|
+
"max_active_per_project_group",
|
|
4467
|
+
"max_active_scope",
|
|
4468
|
+
"max_per_profile"
|
|
4469
|
+
];
|
|
4470
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
4471
|
+
if (mode === "local")
|
|
4472
|
+
return "none";
|
|
4473
|
+
if (mode === "cloud")
|
|
4474
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
4475
|
+
if (config.databaseUrlPresent)
|
|
4476
|
+
return "postgres_contract";
|
|
4477
|
+
if (config.apiUrl)
|
|
4478
|
+
return "api_control_plane_contract";
|
|
4479
|
+
return "unconfigured";
|
|
4480
|
+
}
|
|
4481
|
+
function schedulerStateForMode(args) {
|
|
4482
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
4483
|
+
return {
|
|
4484
|
+
authority: args.sourceOfTruth,
|
|
4485
|
+
localStore: {
|
|
4486
|
+
backend: "sqlite",
|
|
4487
|
+
role: args.localRole,
|
|
4488
|
+
runArtifacts: "local_files",
|
|
4489
|
+
routeAdmissionState: "workflow_work_items"
|
|
4490
|
+
},
|
|
4491
|
+
remoteStore: {
|
|
4492
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
4493
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
4494
|
+
applySupported: false,
|
|
4495
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
4496
|
+
mutatesAws: false
|
|
4497
|
+
},
|
|
4498
|
+
routeAdmission: {
|
|
4499
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
4500
|
+
activeStatuses: ["admitted", "running"],
|
|
4501
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
4502
|
+
dryRunEvaluatesLiveCounts: false
|
|
4503
|
+
}
|
|
4504
|
+
};
|
|
4505
|
+
}
|
|
4506
|
+
function displayControlPlaneUrl(value) {
|
|
4507
|
+
if (!value)
|
|
4508
|
+
return;
|
|
4509
|
+
try {
|
|
4510
|
+
const url = new URL(value);
|
|
4511
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/g, "");
|
|
4512
|
+
return `${url.origin}${path}`;
|
|
4513
|
+
} catch {
|
|
4514
|
+
return "[invalid-url]";
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
function buildDeploymentStatus(opts = {}) {
|
|
4518
|
+
const env = opts.env ?? process.env;
|
|
4519
|
+
const active = resolveLoopDeploymentMode(env);
|
|
4520
|
+
const deploymentMode = opts.perspective ?? active.deploymentMode;
|
|
4521
|
+
const config = loopControlPlaneConfig(env);
|
|
4522
|
+
const rawApiUrl = deploymentMode === "cloud" ? config.cloudApiUrl : config.apiUrl;
|
|
4523
|
+
const apiUrl = displayControlPlaneUrl(rawApiUrl);
|
|
4524
|
+
const controlPlaneKind = deploymentMode === "local" ? "none" : deploymentMode;
|
|
4525
|
+
const deploymentAuthTokenPresent = deploymentMode === "cloud" ? config.cloudAuthTokenPresent : deploymentMode === "self_hosted" ? config.apiAuthTokenPresent : false;
|
|
4526
|
+
const controlPlaneConfigured = deploymentMode === "local" ? false : deploymentMode === "self_hosted" ? Boolean(config.apiUrl || config.databaseUrlPresent) : Boolean(config.cloudApiUrl && deploymentAuthTokenPresent);
|
|
4527
|
+
const warnings = [];
|
|
4528
|
+
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
4529
|
+
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
4530
|
+
}
|
|
4531
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
4532
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
4533
|
+
}
|
|
4534
|
+
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
4535
|
+
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
4536
|
+
}
|
|
4537
|
+
if (deploymentMode === "cloud" && !config.cloudApiUrl) {
|
|
4538
|
+
warnings.push("cloud mode is a public contract until LOOPS_CLOUD_API_URL is configured");
|
|
4539
|
+
}
|
|
4540
|
+
if (deploymentMode === "cloud" && config.cloudApiUrl && !deploymentAuthTokenPresent) {
|
|
4541
|
+
warnings.push("cloud mode needs LOOPS_CLOUD_TOKEN or HASNA_LOOPS_CLOUD_TOKEN before it can become ready");
|
|
4542
|
+
}
|
|
4543
|
+
if (opts.perspective && opts.perspective !== active.deploymentMode) {
|
|
4544
|
+
warnings.push(`active deployment mode is ${active.deploymentMode}; this is the ${opts.perspective} perspective`);
|
|
4545
|
+
}
|
|
4546
|
+
return {
|
|
4547
|
+
packageVersion: packageVersion(),
|
|
4548
|
+
deploymentMode,
|
|
4549
|
+
activeDeploymentMode: active.deploymentMode,
|
|
4550
|
+
active: deploymentMode === active.deploymentMode,
|
|
4551
|
+
deploymentModeSource: active.source,
|
|
4552
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
4553
|
+
localStore: {
|
|
4554
|
+
role: deploymentMode === "local" ? "authoritative" : "cache_and_spool"
|
|
4555
|
+
},
|
|
4556
|
+
controlPlane: {
|
|
4557
|
+
kind: controlPlaneKind,
|
|
4558
|
+
configured: controlPlaneConfigured,
|
|
4559
|
+
apiUrl,
|
|
4560
|
+
databaseUrlPresent: config.databaseUrlPresent,
|
|
4561
|
+
authTokenPresent: deploymentAuthTokenPresent
|
|
4562
|
+
},
|
|
4563
|
+
runner: {
|
|
4564
|
+
required: deploymentMode !== "local",
|
|
4565
|
+
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
4566
|
+
},
|
|
4567
|
+
schedulerState: schedulerStateForMode({
|
|
4568
|
+
deploymentMode,
|
|
4569
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
4570
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
4571
|
+
controlPlaneConfigured,
|
|
4572
|
+
config
|
|
4573
|
+
}),
|
|
4574
|
+
warnings
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
function deploymentStatusLine(status) {
|
|
4578
|
+
const configured = status.controlPlane.kind === "none" ? "none" : String(status.controlPlane.configured);
|
|
4579
|
+
const active = status.active ? "active" : `active=${status.activeDeploymentMode}`;
|
|
4580
|
+
return [
|
|
4581
|
+
`deploymentMode=${status.deploymentMode}`,
|
|
4582
|
+
active,
|
|
4583
|
+
`source=${status.deploymentModeSource}`,
|
|
4584
|
+
`truth=${status.sourceOfTruth}`,
|
|
4585
|
+
`local=${status.localStore.role}`,
|
|
4586
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
4587
|
+
`control_plane=${configured}`
|
|
4588
|
+
].join(" ");
|
|
4589
|
+
}
|
|
4268
4590
|
|
|
4269
4591
|
// src/mcp/index.ts
|
|
4270
4592
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -5567,6 +5889,21 @@ function runDoctor(store) {
|
|
|
5567
5889
|
checks.push(status.running ? { id: "daemon", status: "ok", message: `daemon is running pid=${status.pid}` } : { id: "daemon", status: status.stale ? "warn" : "ok", message: status.stale ? "daemon pid file is stale" : "daemon is not running" });
|
|
5568
5890
|
const failedRuns = store.countRuns("failed");
|
|
5569
5891
|
checks.push(failedRuns === 0 ? { id: "loop-runs", status: "ok", message: "no failed loop runs recorded" } : { id: "loop-runs", status: "warn", message: `${failedRuns} failed loop run(s) recorded` });
|
|
5892
|
+
const deployment = buildDeploymentStatus();
|
|
5893
|
+
const schedulerState = deployment.schedulerState;
|
|
5894
|
+
checks.push({
|
|
5895
|
+
id: "scheduler-state",
|
|
5896
|
+
status: deployment.deploymentMode === "local" || deployment.controlPlane.configured ? "ok" : "warn",
|
|
5897
|
+
message: `scheduler state authority=${schedulerState.authority} local=${schedulerState.localStore.role} remote=${schedulerState.remoteStore.backend}`,
|
|
5898
|
+
detail: [
|
|
5899
|
+
`route_state=${schedulerState.routeAdmission.stateStore}`,
|
|
5900
|
+
`active_statuses=${schedulerState.routeAdmission.activeStatuses.join(",")}`,
|
|
5901
|
+
`gates=${schedulerState.routeAdmission.gates.join(",")}`,
|
|
5902
|
+
`artifacts=${schedulerState.localStore.runArtifacts}`,
|
|
5903
|
+
`remote_artifacts=${schedulerState.remoteStore.objectArtifacts}`,
|
|
5904
|
+
`remote_apply=${String(schedulerState.remoteStore.applySupported)}`
|
|
5905
|
+
].join(" ")
|
|
5906
|
+
});
|
|
5570
5907
|
for (const loop of store.listLoops({ status: "active" })) {
|
|
5571
5908
|
try {
|
|
5572
5909
|
if (loop.target.type === "workflow") {
|
|
@@ -7509,136 +7846,6 @@ async function tick(deps) {
|
|
|
7509
7846
|
}
|
|
7510
7847
|
return { claimed, completed, skipped, recovered, expired };
|
|
7511
7848
|
}
|
|
7512
|
-
// package.json
|
|
7513
|
-
var package_default = {
|
|
7514
|
-
name: "@hasna/loops",
|
|
7515
|
-
version: "0.4.12",
|
|
7516
|
-
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
7517
|
-
type: "module",
|
|
7518
|
-
main: "dist/index.js",
|
|
7519
|
-
types: "dist/index.d.ts",
|
|
7520
|
-
bin: {
|
|
7521
|
-
loops: "dist/cli/index.js",
|
|
7522
|
-
"loops-daemon": "dist/daemon/index.js",
|
|
7523
|
-
"loops-api": "dist/api/index.js",
|
|
7524
|
-
"loops-runner": "dist/runner/index.js",
|
|
7525
|
-
"loops-mcp": "dist/mcp/index.js"
|
|
7526
|
-
},
|
|
7527
|
-
exports: {
|
|
7528
|
-
".": {
|
|
7529
|
-
types: "./dist/index.d.ts",
|
|
7530
|
-
import: "./dist/index.js"
|
|
7531
|
-
},
|
|
7532
|
-
"./sdk": {
|
|
7533
|
-
types: "./dist/sdk/index.d.ts",
|
|
7534
|
-
import: "./dist/sdk/index.js"
|
|
7535
|
-
},
|
|
7536
|
-
"./mcp": {
|
|
7537
|
-
types: "./dist/mcp/index.d.ts",
|
|
7538
|
-
import: "./dist/mcp/index.js"
|
|
7539
|
-
},
|
|
7540
|
-
"./api": {
|
|
7541
|
-
types: "./dist/api/index.d.ts",
|
|
7542
|
-
import: "./dist/api/index.js"
|
|
7543
|
-
},
|
|
7544
|
-
"./runner": {
|
|
7545
|
-
types: "./dist/runner/index.d.ts",
|
|
7546
|
-
import: "./dist/runner/index.js"
|
|
7547
|
-
},
|
|
7548
|
-
"./mode": {
|
|
7549
|
-
types: "./dist/lib/mode.d.ts",
|
|
7550
|
-
import: "./dist/lib/mode.js"
|
|
7551
|
-
},
|
|
7552
|
-
"./storage": {
|
|
7553
|
-
types: "./dist/lib/store.d.ts",
|
|
7554
|
-
import: "./dist/lib/store.js"
|
|
7555
|
-
},
|
|
7556
|
-
"./storage/contract": {
|
|
7557
|
-
types: "./dist/lib/storage/contract.d.ts",
|
|
7558
|
-
import: "./dist/lib/storage/contract.js"
|
|
7559
|
-
},
|
|
7560
|
-
"./storage/sqlite": {
|
|
7561
|
-
types: "./dist/lib/storage/sqlite.d.ts",
|
|
7562
|
-
import: "./dist/lib/storage/sqlite.js"
|
|
7563
|
-
},
|
|
7564
|
-
"./storage/postgres": {
|
|
7565
|
-
types: "./dist/lib/storage/postgres.d.ts",
|
|
7566
|
-
import: "./dist/lib/storage/postgres.js"
|
|
7567
|
-
},
|
|
7568
|
-
"./storage/postgres-schema": {
|
|
7569
|
-
types: "./dist/lib/storage/postgres-schema.d.ts",
|
|
7570
|
-
import: "./dist/lib/storage/postgres-schema.js"
|
|
7571
|
-
}
|
|
7572
|
-
},
|
|
7573
|
-
files: [
|
|
7574
|
-
"dist",
|
|
7575
|
-
"README.md",
|
|
7576
|
-
"CHANGELOG.md",
|
|
7577
|
-
"docs",
|
|
7578
|
-
"LICENSE"
|
|
7579
|
-
],
|
|
7580
|
-
scripts: {
|
|
7581
|
-
build: "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/api/index.ts src/runner/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts src/lib/mode.ts src/lib/storage/index.ts src/lib/storage/contract.ts src/lib/storage/sqlite.ts src/lib/storage/postgres.ts src/lib/storage/postgres-schema.ts --root src --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/api/index.js dist/runner/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
7582
|
-
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
7583
|
-
typecheck: "tsc --noEmit",
|
|
7584
|
-
test: "bun test",
|
|
7585
|
-
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
7586
|
-
prepare: "test -d dist || bun run build",
|
|
7587
|
-
"dev:cli": "bun run src/cli/index.ts",
|
|
7588
|
-
"dev:daemon": "bun run src/daemon/index.ts",
|
|
7589
|
-
prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
|
|
7590
|
-
},
|
|
7591
|
-
keywords: [
|
|
7592
|
-
"loops",
|
|
7593
|
-
"scheduler",
|
|
7594
|
-
"daemon",
|
|
7595
|
-
"agents",
|
|
7596
|
-
"claude",
|
|
7597
|
-
"cursor",
|
|
7598
|
-
"codewith",
|
|
7599
|
-
"opencode",
|
|
7600
|
-
"cli"
|
|
7601
|
-
],
|
|
7602
|
-
repository: {
|
|
7603
|
-
type: "git",
|
|
7604
|
-
url: "git+https://github.com/hasna/loops.git"
|
|
7605
|
-
},
|
|
7606
|
-
homepage: "https://github.com/hasna/loops#readme",
|
|
7607
|
-
bugs: {
|
|
7608
|
-
url: "https://github.com/hasna/loops/issues"
|
|
7609
|
-
},
|
|
7610
|
-
author: "Hasna <andrei@hasna.com>",
|
|
7611
|
-
license: "Apache-2.0",
|
|
7612
|
-
engines: {
|
|
7613
|
-
bun: ">=1.0.0"
|
|
7614
|
-
},
|
|
7615
|
-
dependencies: {
|
|
7616
|
-
"@hasna/events": "^0.1.9",
|
|
7617
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
7618
|
-
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
7619
|
-
ai: "6.0.204",
|
|
7620
|
-
commander: "^13.1.0",
|
|
7621
|
-
zod: "4.4.3"
|
|
7622
|
-
},
|
|
7623
|
-
optionalDependencies: {
|
|
7624
|
-
"@hasna/machines": "0.0.49"
|
|
7625
|
-
},
|
|
7626
|
-
devDependencies: {
|
|
7627
|
-
"@types/bun": "latest",
|
|
7628
|
-
typescript: "^5.7.3"
|
|
7629
|
-
},
|
|
7630
|
-
publishConfig: {
|
|
7631
|
-
registry: "https://registry.npmjs.org",
|
|
7632
|
-
access: "public"
|
|
7633
|
-
}
|
|
7634
|
-
};
|
|
7635
|
-
|
|
7636
|
-
// src/lib/version.ts
|
|
7637
|
-
function packageVersion() {
|
|
7638
|
-
if (typeof package_default.version !== "string" || package_default.version.trim() === "")
|
|
7639
|
-
throw new Error("package.json version is missing");
|
|
7640
|
-
return package_default.version;
|
|
7641
|
-
}
|
|
7642
7849
|
|
|
7643
7850
|
// src/mcp/index.ts
|
|
7644
7851
|
var LOOP_STATUSES = ["active", "paused", "stopped", "expired"];
|
package/dist/runner/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hasna/loops",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.13",
|
|
7
7
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
8
8
|
type: "module",
|
|
9
9
|
main: "dist/index.js",
|
|
@@ -194,6 +194,50 @@ function sourceOfTruthForMode(mode) {
|
|
|
194
194
|
return "self_hosted_control_plane";
|
|
195
195
|
return "cloud_control_plane";
|
|
196
196
|
}
|
|
197
|
+
var ROUTE_ADMISSION_GATES = [
|
|
198
|
+
"max_dispatch",
|
|
199
|
+
"max_active",
|
|
200
|
+
"max_active_per_project",
|
|
201
|
+
"max_active_per_project_group",
|
|
202
|
+
"max_active_scope",
|
|
203
|
+
"max_per_profile"
|
|
204
|
+
];
|
|
205
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
206
|
+
if (mode === "local")
|
|
207
|
+
return "none";
|
|
208
|
+
if (mode === "cloud")
|
|
209
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
210
|
+
if (config.databaseUrlPresent)
|
|
211
|
+
return "postgres_contract";
|
|
212
|
+
if (config.apiUrl)
|
|
213
|
+
return "api_control_plane_contract";
|
|
214
|
+
return "unconfigured";
|
|
215
|
+
}
|
|
216
|
+
function schedulerStateForMode(args) {
|
|
217
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
218
|
+
return {
|
|
219
|
+
authority: args.sourceOfTruth,
|
|
220
|
+
localStore: {
|
|
221
|
+
backend: "sqlite",
|
|
222
|
+
role: args.localRole,
|
|
223
|
+
runArtifacts: "local_files",
|
|
224
|
+
routeAdmissionState: "workflow_work_items"
|
|
225
|
+
},
|
|
226
|
+
remoteStore: {
|
|
227
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
228
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
229
|
+
applySupported: false,
|
|
230
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
231
|
+
mutatesAws: false
|
|
232
|
+
},
|
|
233
|
+
routeAdmission: {
|
|
234
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
235
|
+
activeStatuses: ["admitted", "running"],
|
|
236
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
237
|
+
dryRunEvaluatesLiveCounts: false
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
197
241
|
function displayControlPlaneUrl(value) {
|
|
198
242
|
if (!value)
|
|
199
243
|
return;
|
|
@@ -219,6 +263,9 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
219
263
|
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
220
264
|
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
221
265
|
}
|
|
266
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
267
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
268
|
+
}
|
|
222
269
|
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
223
270
|
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
224
271
|
}
|
|
@@ -252,6 +299,13 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
252
299
|
required: deploymentMode !== "local",
|
|
253
300
|
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
254
301
|
},
|
|
302
|
+
schedulerState: schedulerStateForMode({
|
|
303
|
+
deploymentMode,
|
|
304
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
305
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
306
|
+
controlPlaneConfigured,
|
|
307
|
+
config
|
|
308
|
+
}),
|
|
255
309
|
warnings
|
|
256
310
|
};
|
|
257
311
|
}
|
|
@@ -264,6 +318,7 @@ function deploymentStatusLine(status) {
|
|
|
264
318
|
`source=${status.deploymentModeSource}`,
|
|
265
319
|
`truth=${status.sourceOfTruth}`,
|
|
266
320
|
`local=${status.localStore.role}`,
|
|
321
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
267
322
|
`control_plane=${configured}`
|
|
268
323
|
].join(" ");
|
|
269
324
|
}
|