@hasna/loops 0.4.11 → 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/mcp/index.js CHANGED
@@ -1323,6 +1323,7 @@ function rowToWorkflowWorkItem(row) {
1323
1323
  subjectRef: row.subject_ref,
1324
1324
  projectKey: row.project_key ?? undefined,
1325
1325
  projectGroup: row.project_group ?? undefined,
1326
+ routeScope: row.route_scope ?? undefined,
1326
1327
  priority: row.priority,
1327
1328
  status: row.status,
1328
1329
  attempts: row.attempts,
@@ -1603,6 +1604,13 @@ class Store {
1603
1604
  this.addColumnIfMissing("loop_runs", "claim_token", "TEXT");
1604
1605
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL");
1605
1606
  }
1607
+ },
1608
+ {
1609
+ id: "0008_work_item_route_scope",
1610
+ apply: () => {
1611
+ this.addColumnIfMissing("workflow_work_items", "route_scope", "TEXT");
1612
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_work_items_scope ON workflow_work_items(route_scope, status)");
1613
+ }
1606
1614
  }
1607
1615
  ];
1608
1616
  }
@@ -1750,6 +1758,7 @@ class Store {
1750
1758
  subject_ref TEXT NOT NULL,
1751
1759
  project_key TEXT,
1752
1760
  project_group TEXT,
1761
+ route_scope TEXT,
1753
1762
  priority INTEGER NOT NULL,
1754
1763
  status TEXT NOT NULL,
1755
1764
  attempts INTEGER NOT NULL,
@@ -1767,6 +1776,13 @@ class Store {
1767
1776
  CREATE INDEX IF NOT EXISTS idx_workflow_work_items_project ON workflow_work_items(project_key, status);
1768
1777
  CREATE INDEX IF NOT EXISTS idx_workflow_work_items_group ON workflow_work_items(project_group, status);
1769
1778
  CREATE INDEX IF NOT EXISTS idx_workflow_work_items_invocation ON workflow_work_items(invocation_id);
1779
+ -- idx_workflow_work_items_scope (route_scope, status) is created ONLY by
1780
+ -- migration 0008_work_item_route_scope, never here: this baseline DDL
1781
+ -- re-runs on EVERY open (0001 is not skip-guarded), and on a pre-0008
1782
+ -- database the CREATE TABLE above is a no-op, so an index on route_scope
1783
+ -- here would execute before the column exists and crash the open
1784
+ -- ("no such column: route_scope"). New columns may be folded into the
1785
+ -- CREATE TABLE (fresh-db only); their indexes must live in the migration.
1770
1786
 
1771
1787
  CREATE TABLE IF NOT EXISTS workflow_step_runs (
1772
1788
  id TEXT PRIMARY KEY,
@@ -2558,10 +2574,10 @@ class Store {
2558
2574
  const id = genId();
2559
2575
  const status = input.status ?? "queued";
2560
2576
  this.db.query(`INSERT INTO workflow_work_items (id, route_key, idempotency_key, invocation_id, source_type, source_ref,
2561
- subject_ref, project_key, project_group, priority, status, attempts, next_attempt_at, lease_expires_at,
2577
+ subject_ref, project_key, project_group, route_scope, priority, status, attempts, next_attempt_at, lease_expires_at,
2562
2578
  workflow_id, loop_id, workflow_run_id, last_reason, created_at, updated_at)
2563
2579
  VALUES ($id, $routeKey, $idempotencyKey, $invocationId, $sourceType, $sourceRef, $subjectRef,
2564
- $projectKey, $projectGroup, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
2580
+ $projectKey, $projectGroup, $routeScope, $priority, $status, 0, $nextAttemptAt, NULL, NULL, NULL, NULL,
2565
2581
  $lastReason, $created, $updated)
2566
2582
  ON CONFLICT(route_key, idempotency_key) DO UPDATE SET
2567
2583
  invocation_id=excluded.invocation_id,
@@ -2570,6 +2586,7 @@ class Store {
2570
2586
  subject_ref=excluded.subject_ref,
2571
2587
  project_key=excluded.project_key,
2572
2588
  project_group=excluded.project_group,
2589
+ route_scope=excluded.route_scope,
2573
2590
  priority=excluded.priority,
2574
2591
  status=CASE
2575
2592
  WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
@@ -2611,6 +2628,7 @@ class Store {
2611
2628
  $subjectRef: input.subjectRef,
2612
2629
  $projectKey: input.projectKey ?? null,
2613
2630
  $projectGroup: input.projectGroup ?? null,
2631
+ $routeScope: input.routeScope ?? null,
2614
2632
  $priority: input.priority ?? 0,
2615
2633
  $status: status,
2616
2634
  $nextAttemptAt: input.nextAttemptAt ?? null,
@@ -2648,11 +2666,21 @@ class Store {
2648
2666
  countActiveWorkflowWorkItems(args = {}) {
2649
2667
  const active = ["admitted", "running"];
2650
2668
  const placeholders = active.map(() => "?").join(",");
2651
- const global = this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders})`).get(...active)?.count ?? 0;
2669
+ const routeScope = args.routeScope?.trim() || undefined;
2670
+ const global = routeScope ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND route_scope = ?`).get(...active, routeScope)?.count ?? 0 : this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders})`).get(...active)?.count ?? 0;
2652
2671
  const project = args.projectKey ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_key = ?`).get(...active, args.projectKey)?.count ?? 0 : 0;
2653
2672
  const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
2654
2673
  return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
2655
2674
  }
2675
+ countRunningWorkflowStepsByAuthProfile() {
2676
+ const rows = this.db.query("SELECT account_profile, COUNT(*) AS count FROM workflow_step_runs WHERE status = 'running' AND account_profile IS NOT NULL GROUP BY account_profile").all();
2677
+ const counts = {};
2678
+ for (const row of rows) {
2679
+ if (row.account_profile)
2680
+ counts[row.account_profile] = row.count;
2681
+ }
2682
+ return counts;
2683
+ }
2656
2684
  requeueWorkflowWorkItem(id, patch = {}) {
2657
2685
  const current = this.getWorkflowWorkItem(id);
2658
2686
  if (!current)
@@ -3088,6 +3116,9 @@ class Store {
3088
3116
  }
3089
3117
  input.workflow.steps.forEach((step, sequence) => {
3090
3118
  const account = step.account ?? step.target.account;
3119
+ const agentTarget = step.target.type === "agent" ? step.target : undefined;
3120
+ const resolvedProfile = account?.profile ?? agentTarget?.authProfile ?? null;
3121
+ const resolvedTool = account?.tool ?? (agentTarget?.authProfile ? agentTarget.provider : null);
3091
3122
  this.db.query(`INSERT INTO workflow_step_runs (id, workflow_run_id, step_id, sequence, status, started_at, finished_at,
3092
3123
  exit_code, pid, duration_ms, stdout, stderr, error, account_profile, account_tool, created_at, updated_at)
3093
3124
  VALUES ($id, $workflowRunId, $stepId, $sequence, 'pending', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
@@ -3096,8 +3127,8 @@ class Store {
3096
3127
  $workflowRunId: runId,
3097
3128
  $stepId: step.id,
3098
3129
  $sequence: sequence,
3099
- $accountProfile: account?.profile ?? null,
3100
- $accountTool: account?.tool ?? null,
3130
+ $accountProfile: resolvedProfile,
3131
+ $accountTool: resolvedTool,
3101
3132
  $created: now,
3102
3133
  $updated: now
3103
3134
  });
@@ -4234,6 +4265,328 @@ class Store {
4234
4265
  }
4235
4266
  }
4236
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
+ }
4237
4590
 
4238
4591
  // src/mcp/index.ts
4239
4592
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -5536,6 +5889,21 @@ function runDoctor(store) {
5536
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" });
5537
5890
  const failedRuns = store.countRuns("failed");
5538
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
+ });
5539
5907
  for (const loop of store.listLoops({ status: "active" })) {
5540
5908
  try {
5541
5909
  if (loop.target.type === "workflow") {
@@ -7478,136 +7846,6 @@ async function tick(deps) {
7478
7846
  }
7479
7847
  return { claimed, completed, skipped, recovered, expired };
7480
7848
  }
7481
- // package.json
7482
- var package_default = {
7483
- name: "@hasna/loops",
7484
- version: "0.4.11",
7485
- description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7486
- type: "module",
7487
- main: "dist/index.js",
7488
- types: "dist/index.d.ts",
7489
- bin: {
7490
- loops: "dist/cli/index.js",
7491
- "loops-daemon": "dist/daemon/index.js",
7492
- "loops-api": "dist/api/index.js",
7493
- "loops-runner": "dist/runner/index.js",
7494
- "loops-mcp": "dist/mcp/index.js"
7495
- },
7496
- exports: {
7497
- ".": {
7498
- types: "./dist/index.d.ts",
7499
- import: "./dist/index.js"
7500
- },
7501
- "./sdk": {
7502
- types: "./dist/sdk/index.d.ts",
7503
- import: "./dist/sdk/index.js"
7504
- },
7505
- "./mcp": {
7506
- types: "./dist/mcp/index.d.ts",
7507
- import: "./dist/mcp/index.js"
7508
- },
7509
- "./api": {
7510
- types: "./dist/api/index.d.ts",
7511
- import: "./dist/api/index.js"
7512
- },
7513
- "./runner": {
7514
- types: "./dist/runner/index.d.ts",
7515
- import: "./dist/runner/index.js"
7516
- },
7517
- "./mode": {
7518
- types: "./dist/lib/mode.d.ts",
7519
- import: "./dist/lib/mode.js"
7520
- },
7521
- "./storage": {
7522
- types: "./dist/lib/store.d.ts",
7523
- import: "./dist/lib/store.js"
7524
- },
7525
- "./storage/contract": {
7526
- types: "./dist/lib/storage/contract.d.ts",
7527
- import: "./dist/lib/storage/contract.js"
7528
- },
7529
- "./storage/sqlite": {
7530
- types: "./dist/lib/storage/sqlite.d.ts",
7531
- import: "./dist/lib/storage/sqlite.js"
7532
- },
7533
- "./storage/postgres": {
7534
- types: "./dist/lib/storage/postgres.d.ts",
7535
- import: "./dist/lib/storage/postgres.js"
7536
- },
7537
- "./storage/postgres-schema": {
7538
- types: "./dist/lib/storage/postgres-schema.d.ts",
7539
- import: "./dist/lib/storage/postgres-schema.js"
7540
- }
7541
- },
7542
- files: [
7543
- "dist",
7544
- "README.md",
7545
- "CHANGELOG.md",
7546
- "docs",
7547
- "LICENSE"
7548
- ],
7549
- scripts: {
7550
- 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",
7551
- "build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
7552
- typecheck: "tsc --noEmit",
7553
- test: "bun test",
7554
- "test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
7555
- prepare: "test -d dist || bun run build",
7556
- "dev:cli": "bun run src/cli/index.ts",
7557
- "dev:daemon": "bun run src/daemon/index.ts",
7558
- prepublishOnly: "bun run build && bun run typecheck && bun test && bun run test:boundary"
7559
- },
7560
- keywords: [
7561
- "loops",
7562
- "scheduler",
7563
- "daemon",
7564
- "agents",
7565
- "claude",
7566
- "cursor",
7567
- "codewith",
7568
- "opencode",
7569
- "cli"
7570
- ],
7571
- repository: {
7572
- type: "git",
7573
- url: "git+https://github.com/hasna/loops.git"
7574
- },
7575
- homepage: "https://github.com/hasna/loops#readme",
7576
- bugs: {
7577
- url: "https://github.com/hasna/loops/issues"
7578
- },
7579
- author: "Hasna <andrei@hasna.com>",
7580
- license: "Apache-2.0",
7581
- engines: {
7582
- bun: ">=1.0.0"
7583
- },
7584
- dependencies: {
7585
- "@hasna/events": "^0.1.9",
7586
- "@modelcontextprotocol/sdk": "^1.29.0",
7587
- "@openrouter/ai-sdk-provider": "2.9.1",
7588
- ai: "6.0.204",
7589
- commander: "^13.1.0",
7590
- zod: "4.4.3"
7591
- },
7592
- optionalDependencies: {
7593
- "@hasna/machines": "0.0.49"
7594
- },
7595
- devDependencies: {
7596
- "@types/bun": "latest",
7597
- typescript: "^5.7.3"
7598
- },
7599
- publishConfig: {
7600
- registry: "https://registry.npmjs.org",
7601
- access: "public"
7602
- }
7603
- };
7604
-
7605
- // src/lib/version.ts
7606
- function packageVersion() {
7607
- if (typeof package_default.version !== "string" || package_default.version.trim() === "")
7608
- throw new Error("package.json version is missing");
7609
- return package_default.version;
7610
- }
7611
7849
 
7612
7850
  // src/mcp/index.ts
7613
7851
  var LOOP_STATUSES = ["active", "paused", "stopped", "expired"];