@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/sdk/index.js
CHANGED
|
@@ -4266,7 +4266,7 @@ class Store {
|
|
|
4266
4266
|
// package.json
|
|
4267
4267
|
var package_default = {
|
|
4268
4268
|
name: "@hasna/loops",
|
|
4269
|
-
version: "0.4.
|
|
4269
|
+
version: "0.4.13",
|
|
4270
4270
|
description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
4271
4271
|
type: "module",
|
|
4272
4272
|
main: "dist/index.js",
|
|
@@ -4457,6 +4457,50 @@ function sourceOfTruthForMode(mode) {
|
|
|
4457
4457
|
return "self_hosted_control_plane";
|
|
4458
4458
|
return "cloud_control_plane";
|
|
4459
4459
|
}
|
|
4460
|
+
var ROUTE_ADMISSION_GATES = [
|
|
4461
|
+
"max_dispatch",
|
|
4462
|
+
"max_active",
|
|
4463
|
+
"max_active_per_project",
|
|
4464
|
+
"max_active_per_project_group",
|
|
4465
|
+
"max_active_scope",
|
|
4466
|
+
"max_per_profile"
|
|
4467
|
+
];
|
|
4468
|
+
function remoteSchedulerBackendForMode(mode, config) {
|
|
4469
|
+
if (mode === "local")
|
|
4470
|
+
return "none";
|
|
4471
|
+
if (mode === "cloud")
|
|
4472
|
+
return config.cloudApiUrl ? "hosted_control_plane_contract" : "unconfigured";
|
|
4473
|
+
if (config.databaseUrlPresent)
|
|
4474
|
+
return "postgres_contract";
|
|
4475
|
+
if (config.apiUrl)
|
|
4476
|
+
return "api_control_plane_contract";
|
|
4477
|
+
return "unconfigured";
|
|
4478
|
+
}
|
|
4479
|
+
function schedulerStateForMode(args) {
|
|
4480
|
+
const nonLocal = args.deploymentMode !== "local";
|
|
4481
|
+
return {
|
|
4482
|
+
authority: args.sourceOfTruth,
|
|
4483
|
+
localStore: {
|
|
4484
|
+
backend: "sqlite",
|
|
4485
|
+
role: args.localRole,
|
|
4486
|
+
runArtifacts: "local_files",
|
|
4487
|
+
routeAdmissionState: "workflow_work_items"
|
|
4488
|
+
},
|
|
4489
|
+
remoteStore: {
|
|
4490
|
+
backend: remoteSchedulerBackendForMode(args.deploymentMode, args.config),
|
|
4491
|
+
configured: nonLocal && args.controlPlaneConfigured,
|
|
4492
|
+
applySupported: false,
|
|
4493
|
+
objectArtifacts: nonLocal ? "object_store_contract" : "none",
|
|
4494
|
+
mutatesAws: false
|
|
4495
|
+
},
|
|
4496
|
+
routeAdmission: {
|
|
4497
|
+
stateStore: nonLocal ? "control_plane_contract" : "local_sqlite",
|
|
4498
|
+
activeStatuses: ["admitted", "running"],
|
|
4499
|
+
gates: ROUTE_ADMISSION_GATES,
|
|
4500
|
+
dryRunEvaluatesLiveCounts: false
|
|
4501
|
+
}
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4460
4504
|
function displayControlPlaneUrl(value) {
|
|
4461
4505
|
if (!value)
|
|
4462
4506
|
return;
|
|
@@ -4482,6 +4526,9 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
4482
4526
|
if (deploymentMode === "self_hosted" && !controlPlaneConfigured) {
|
|
4483
4527
|
warnings.push("self_hosted mode needs LOOPS_API_URL or LOOPS_DATABASE_URL before it can become authoritative");
|
|
4484
4528
|
}
|
|
4529
|
+
if (deploymentMode === "self_hosted" && config.databaseUrlPresent && !config.apiUrl) {
|
|
4530
|
+
warnings.push("LOOPS_DATABASE_URL selects the self_hosted storage contract; loops-runner still needs LOOPS_API_URL to claim remote work");
|
|
4531
|
+
}
|
|
4485
4532
|
if (deploymentMode === "cloud" && config.apiUrl && !config.cloudApiUrl) {
|
|
4486
4533
|
warnings.push("LOOPS_API_URL selects self_hosted; cloud mode uses LOOPS_CLOUD_API_URL");
|
|
4487
4534
|
}
|
|
@@ -4515,6 +4562,13 @@ function buildDeploymentStatus(opts = {}) {
|
|
|
4515
4562
|
required: deploymentMode !== "local",
|
|
4516
4563
|
role: deploymentMode === "local" ? "daemon" : "control_plane_worker"
|
|
4517
4564
|
},
|
|
4565
|
+
schedulerState: schedulerStateForMode({
|
|
4566
|
+
deploymentMode,
|
|
4567
|
+
sourceOfTruth: sourceOfTruthForMode(deploymentMode),
|
|
4568
|
+
localRole: deploymentMode === "local" ? "authoritative" : "cache_and_spool",
|
|
4569
|
+
controlPlaneConfigured,
|
|
4570
|
+
config
|
|
4571
|
+
}),
|
|
4518
4572
|
warnings
|
|
4519
4573
|
};
|
|
4520
4574
|
}
|
|
@@ -4527,6 +4581,7 @@ function deploymentStatusLine(status) {
|
|
|
4527
4581
|
`source=${status.deploymentModeSource}`,
|
|
4528
4582
|
`truth=${status.sourceOfTruth}`,
|
|
4529
4583
|
`local=${status.localStore.role}`,
|
|
4584
|
+
`scheduler=${status.schedulerState.routeAdmission.stateStore}`,
|
|
4530
4585
|
`control_plane=${configured}`
|
|
4531
4586
|
].join(" ");
|
|
4532
4587
|
}
|
|
@@ -5827,6 +5882,21 @@ function runDoctor(store) {
|
|
|
5827
5882
|
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" });
|
|
5828
5883
|
const failedRuns = store.countRuns("failed");
|
|
5829
5884
|
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` });
|
|
5885
|
+
const deployment = buildDeploymentStatus();
|
|
5886
|
+
const schedulerState = deployment.schedulerState;
|
|
5887
|
+
checks.push({
|
|
5888
|
+
id: "scheduler-state",
|
|
5889
|
+
status: deployment.deploymentMode === "local" || deployment.controlPlane.configured ? "ok" : "warn",
|
|
5890
|
+
message: `scheduler state authority=${schedulerState.authority} local=${schedulerState.localStore.role} remote=${schedulerState.remoteStore.backend}`,
|
|
5891
|
+
detail: [
|
|
5892
|
+
`route_state=${schedulerState.routeAdmission.stateStore}`,
|
|
5893
|
+
`active_statuses=${schedulerState.routeAdmission.activeStatuses.join(",")}`,
|
|
5894
|
+
`gates=${schedulerState.routeAdmission.gates.join(",")}`,
|
|
5895
|
+
`artifacts=${schedulerState.localStore.runArtifacts}`,
|
|
5896
|
+
`remote_artifacts=${schedulerState.remoteStore.objectArtifacts}`,
|
|
5897
|
+
`remote_apply=${String(schedulerState.remoteStore.applySupported)}`
|
|
5898
|
+
].join(" ")
|
|
5899
|
+
});
|
|
5830
5900
|
for (const loop of store.listLoops({ status: "active" })) {
|
|
5831
5901
|
try {
|
|
5832
5902
|
if (loop.target.type === "workflow") {
|
package/docs/DEPLOYMENT_MODES.md
CHANGED
|
@@ -68,7 +68,7 @@ loops import ./loops-export.json --apply
|
|
|
68
68
|
Human status output is intentionally compact:
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
deploymentMode=local active source=default truth=local_sqlite local=authoritative control_plane=none
|
|
71
|
+
deploymentMode=local active source=default truth=local_sqlite local=authoritative scheduler=local_sqlite control_plane=none
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
JSON uses these field names:
|
|
@@ -85,6 +85,22 @@ JSON uses these field names:
|
|
|
85
85
|
presence signal.
|
|
86
86
|
- `controlPlane.apiUrl`: a display-safe URL without credentials, query string,
|
|
87
87
|
or fragment.
|
|
88
|
+
- `schedulerState.localStore`: always names the local SQLite store and local
|
|
89
|
+
run artifact files. In `local` mode this store is authoritative; in
|
|
90
|
+
non-local modes it is a cache, offline spool, and audit copy.
|
|
91
|
+
- `schedulerState.remoteStore`: names the remote scheduler contract:
|
|
92
|
+
`api_control_plane_contract`, `postgres_contract`,
|
|
93
|
+
`hosted_control_plane_contract`, `unconfigured`, or `none`. Remote apply is
|
|
94
|
+
`false` in this public package until a control-plane host wires a full
|
|
95
|
+
storage adapter.
|
|
96
|
+
- `schedulerState.remoteStore.objectArtifacts`: `object_store_contract` means
|
|
97
|
+
remote artifact/object storage is a control-plane contract. The public package
|
|
98
|
+
does not create or mutate S3 buckets, AWS resources, or hosted credentials.
|
|
99
|
+
- `schedulerState.routeAdmission`: names the active route-state store and the
|
|
100
|
+
bounded gates (`max_dispatch`, `max_active`, `max_active_per_project`,
|
|
101
|
+
`max_active_per_project_group`, `max_active_scope`, and `max_per_profile`).
|
|
102
|
+
Live active counts use admitted/running work items; dry-runs do not open or
|
|
103
|
+
migrate the live store to compute counts.
|
|
88
104
|
|
|
89
105
|
`loops-api` is a separate process in the same public package. It is not a
|
|
90
106
|
separate package at this stage because self-hosted users and the hosted service
|
|
@@ -150,6 +166,12 @@ would generate new ids, so it is not a no-loss migration. Local SQLite remains
|
|
|
150
166
|
authoritative until a safe import is applied; in non-local modes it may remain
|
|
151
167
|
a cache, offline spool, and audit copy.
|
|
152
168
|
|
|
169
|
+
`LOOPS_DATABASE_URL` selects the self-hosted Postgres scheduler-state contract,
|
|
170
|
+
but it does not make the standalone CLI mutate a remote database by itself.
|
|
171
|
+
Remote execution still flows through a configured control-plane API and runner
|
|
172
|
+
protocol. `loops-runner` needs `LOOPS_API_URL` or `HASNA_LOOPS_API_URL` to claim
|
|
173
|
+
work; a database URL alone is migration/readiness configuration.
|
|
174
|
+
|
|
153
175
|
`loops self-hosted runner-register` is also preview-only unless `--apply` is
|
|
154
176
|
present. The dry run prints the runner id, machine id, labels, and
|
|
155
177
|
capabilities that would be posted, without exposing tokens.
|
package/docs/USAGE.md
CHANGED
|
@@ -25,6 +25,16 @@ planes:
|
|
|
25
25
|
status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
|
|
26
26
|
`HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
|
|
27
27
|
|
|
28
|
+
Scheduler state is explicit in status JSON. `schedulerState.localStore` is
|
|
29
|
+
SQLite plus local run artifact files: authoritative in `local`, cache/spool in
|
|
30
|
+
non-local modes. `schedulerState.remoteStore` names the non-local contract
|
|
31
|
+
(`api_control_plane_contract`, `postgres_contract`, or
|
|
32
|
+
`hosted_control_plane_contract`) and reports `applySupported=false` because this
|
|
33
|
+
public package does not directly mutate remote Postgres, S3/object storage, AWS
|
|
34
|
+
resources, or hosted credentials. Route admission remains bounded by
|
|
35
|
+
`max_dispatch`, `max_active`, `max_active_per_project`,
|
|
36
|
+
`max_active_per_project_group`, `max_active_scope`, and `max_per_profile`.
|
|
37
|
+
|
|
28
38
|
Useful status commands:
|
|
29
39
|
|
|
30
40
|
```bash
|
|
@@ -674,11 +684,12 @@ idempotency key before rendering worktree plans or checking route limits. In
|
|
|
674
684
|
dry-run mode, throttle counts are not evaluated because opening the live loop
|
|
675
685
|
store can create or migrate the local database.
|
|
676
686
|
Terminal routed work items such as failed, dead-letter, cancelled, or succeeded
|
|
677
|
-
history
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
687
|
+
history are re-admitted only when the todos task is still actionable, the
|
|
688
|
+
per-attempt backoff has elapsed, and the redispatch cap has not been reached.
|
|
689
|
+
Operators can still force a retry with `loops routes requeue <work-item-id>
|
|
690
|
+
--reason "<cause fixed>"`. The next route-created output records `requeue`
|
|
691
|
+
evidence with the previous work item id, previous attempts, reason, new attempt,
|
|
692
|
+
workflow id, and loop id.
|
|
682
693
|
|
|
683
694
|
When a sandboxed Codewith/Codex worker must update app stores outside the repo
|
|
684
695
|
worktree, pass those stores explicitly with `--add-dir` or template `addDirs`.
|
package/package.json
CHANGED