@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/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.12",
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"];
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hasna/loops",
6
- version: "0.4.12",
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
  }