@hasna/loops 0.3.60 → 0.4.1
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 +265 -0
- package/LICENSE +197 -13
- package/README.md +95 -85
- package/dist/api/index.d.ts +11 -0
- package/dist/api/index.js +333 -0
- package/dist/cli/index.js +8013 -6659
- package/dist/daemon/control.d.ts +21 -1
- package/dist/daemon/daemon.d.ts +5 -0
- package/dist/daemon/index.js +2836 -1222
- package/dist/daemon/install.d.ts +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.js +5715 -4541
- package/dist/lib/accounts.d.ts +6 -1
- package/dist/lib/agent-adapter.d.ts +74 -0
- package/dist/lib/backup.d.ts +25 -0
- package/dist/lib/errors.d.ts +21 -0
- package/dist/lib/executor.d.ts +10 -1
- package/dist/lib/goal/metadata.d.ts +16 -0
- package/dist/lib/goal/model-factory.d.ts +1 -0
- package/dist/lib/goal/prompts.d.ts +3 -1
- package/dist/lib/goal/types.d.ts +3 -0
- package/dist/lib/health.d.ts +1 -1
- package/dist/lib/ids.d.ts +8 -1
- package/dist/lib/machines.d.ts +6 -0
- package/dist/lib/mode.d.ts +48 -0
- package/dist/lib/mode.js +260 -0
- package/dist/lib/process-identity.d.ts +16 -0
- package/dist/lib/{schedule.d.ts → recurrence.d.ts} +1 -0
- package/dist/lib/redact.d.ts +21 -0
- package/dist/lib/route/cursors.d.ts +18 -0
- package/dist/lib/route/drain.d.ts +6 -0
- package/dist/lib/route/fields.d.ts +27 -0
- package/dist/lib/route/gates.d.ts +26 -0
- package/dist/lib/route/index.d.ts +18 -0
- package/dist/lib/route/options.d.ts +31 -0
- package/dist/lib/route/parse.d.ts +8 -0
- package/dist/lib/route/pr-review.d.ts +11 -0
- package/dist/lib/route/provider.d.ts +41 -0
- package/dist/lib/route/route-event.d.ts +23 -0
- package/dist/lib/route/route-tasks.d.ts +75 -0
- package/dist/lib/route/throttle.d.ts +47 -0
- package/dist/lib/route/todos-cli.d.ts +21 -0
- package/dist/lib/route/types.d.ts +88 -0
- package/dist/lib/run-artifacts.d.ts +17 -2
- package/dist/lib/run-envelope.d.ts +41 -0
- package/dist/lib/scheduler.d.ts +78 -2
- package/dist/lib/store.d.ts +63 -0
- package/dist/lib/store.js +1007 -287
- package/dist/lib/template-kit.d.ts +70 -0
- package/dist/lib/templates-custom.d.ts +34 -0
- package/dist/lib/templates.d.ts +13 -81
- package/dist/lib/workflow-runner.d.ts +2 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +3231 -1382
- package/dist/runner/index.d.ts +9 -0
- package/dist/runner/index.js +295 -0
- package/dist/sdk/index.d.ts +29 -3
- package/dist/sdk/index.js +2743 -1044
- package/dist/test-helpers.d.ts +37 -0
- package/dist/types.d.ts +3 -1
- package/docs/DEPLOYMENT_MODES.md +140 -0
- package/docs/USAGE.md +102 -46
- package/package.json +23 -5
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared deadline-based polling and process-kill verification helpers for the
|
|
3
|
+
* test suite. Test-only: nothing in src/ production code may import this file.
|
|
4
|
+
*/
|
|
5
|
+
export interface WaitUntilOptions {
|
|
6
|
+
/** Total time to wait before failing (default 5s). */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Poll interval (default 5ms). */
|
|
9
|
+
intervalMs?: number;
|
|
10
|
+
/** Included in the timeout error to identify the stuck condition. */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Poll `check` until it returns a truthy value, with a hard deadline.
|
|
15
|
+
* Returns the first truthy value; throws once the deadline passes so a stuck
|
|
16
|
+
* condition fails the test instead of silently falling through.
|
|
17
|
+
*/
|
|
18
|
+
export declare function waitUntil<T>(check: () => T | undefined | null | false | Promise<T | undefined | null | false>, opts?: WaitUntilOptions): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Shell command that blocks until `gate` exists, then writes `text` to
|
|
21
|
+
* `marker`. Replaces fixed `sleep N; printf ...` fixtures: the child stays
|
|
22
|
+
* alive (blocked on the gate) for exactly as long as the test needs, and a
|
|
23
|
+
* kill can be proven by opening the gate and observing that the marker never
|
|
24
|
+
* appears (see {@link expectMarkerNeverWritten}).
|
|
25
|
+
*/
|
|
26
|
+
export declare function gatedWriteCommand(gate: string, marker: string, opts?: {
|
|
27
|
+
text?: string;
|
|
28
|
+
append?: boolean;
|
|
29
|
+
}): string;
|
|
30
|
+
/** Open a {@link gatedWriteCommand} gate so a still-alive child can finish. */
|
|
31
|
+
export declare function openGate(gate: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Prove a gated child process was killed: open the gate and verify the marker
|
|
34
|
+
* still does not appear. A surviving child polls the gate every 20ms, so the
|
|
35
|
+
* default 250ms settle window gives it >10x the time it would need to write.
|
|
36
|
+
*/
|
|
37
|
+
export declare function expectMarkerNeverWritten(gate: string, marker: string, settleMs?: number): Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface OpenAutomationsRuntimeBinding {
|
|
|
55
55
|
failCommand: "automations queue fail";
|
|
56
56
|
eventHandoff: {
|
|
57
57
|
envelopeCommand: "automations webhooks event";
|
|
58
|
-
handlerCommand: "loops
|
|
58
|
+
handlerCommand: "loops routes create generic";
|
|
59
59
|
pipeExample: string;
|
|
60
60
|
boundary: string;
|
|
61
61
|
};
|
|
@@ -370,6 +370,8 @@ export interface LoopRun {
|
|
|
370
370
|
claimedBy?: string;
|
|
371
371
|
leaseExpiresAt?: string;
|
|
372
372
|
pid?: number;
|
|
373
|
+
pgid?: number;
|
|
374
|
+
processStartedAt?: string;
|
|
373
375
|
exitCode?: number;
|
|
374
376
|
durationMs?: number;
|
|
375
377
|
stdout?: string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# OpenLoops Deployment Modes
|
|
2
|
+
|
|
3
|
+
OpenLoops supports one active source of truth at a time. The public package
|
|
4
|
+
defines the mode vocabulary, local cache behavior, API shape, and runner
|
|
5
|
+
contract. Hosted multi-tenant operation is implemented outside this public
|
|
6
|
+
package.
|
|
7
|
+
|
|
8
|
+
## Modes
|
|
9
|
+
|
|
10
|
+
| Mode | Source of truth | Local storage role | Executor |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `local` | SQLite in `LOOPS_DATA_DIR` | Authoritative | `loops-daemon` |
|
|
13
|
+
| `self_hosted` | A user-operated `loops-api` control plane contract | Cache and offline spool | `loops-runner` foundation |
|
|
14
|
+
| `cloud` | A configured hosted control plane contract | Cache and offline spool | `loops-runner` foundation |
|
|
15
|
+
|
|
16
|
+
`local` remains the default. It must keep working without network access,
|
|
17
|
+
tokens, Postgres, or hosted infrastructure.
|
|
18
|
+
|
|
19
|
+
`self_hosted` is for users or teams running their own control plane. The public
|
|
20
|
+
`@hasna/loops` package owns the API and runner contract for this mode.
|
|
21
|
+
|
|
22
|
+
`cloud` is the hosted control-plane contract. The public package exposes the
|
|
23
|
+
client and runner contract, but tenant auth, account administration, and hosted
|
|
24
|
+
infrastructure stay outside this package. The public package must not depend on
|
|
25
|
+
private hosted packages or resource names. This release exposes status
|
|
26
|
+
surfaces only; non-local claim/lease execution is follow-up work.
|
|
27
|
+
|
|
28
|
+
## Mode Resolution
|
|
29
|
+
|
|
30
|
+
`LOOPS_MODE` or `HASNA_LOOPS_MODE` may be set to `local`, `self_hosted`, or
|
|
31
|
+
`cloud`. Hyphenated `self-hosted` is normalized to `self_hosted`.
|
|
32
|
+
|
|
33
|
+
When no explicit mode is set, OpenLoops resolves the mode from configuration:
|
|
34
|
+
|
|
35
|
+
1. `LOOPS_CLOUD_API_URL` or `HASNA_LOOPS_CLOUD_API_URL` selects `cloud`.
|
|
36
|
+
2. `LOOPS_API_URL`, `HASNA_LOOPS_API_URL`, `LOOPS_DATABASE_URL`, or
|
|
37
|
+
`HASNA_LOOPS_DATABASE_URL` selects `self_hosted`.
|
|
38
|
+
3. Otherwise OpenLoops uses `local`.
|
|
39
|
+
|
|
40
|
+
`LOOPS_API_URL` and `HASNA_LOOPS_API_URL` belong to `self_hosted`.
|
|
41
|
+
`cloud` uses only `LOOPS_CLOUD_API_URL` or `HASNA_LOOPS_CLOUD_API_URL`.
|
|
42
|
+
|
|
43
|
+
Tokens are represented only as presence signals in status output. Self-hosted
|
|
44
|
+
status uses `LOOPS_API_TOKEN` or `HASNA_LOOPS_API_TOKEN`. Cloud status uses
|
|
45
|
+
`LOOPS_CLOUD_TOKEN` or `HASNA_LOOPS_CLOUD_TOKEN`. URL credentials, query
|
|
46
|
+
strings, and fragments are not returned in status output.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
loops mode
|
|
52
|
+
loops --json mode
|
|
53
|
+
loops self-hosted status
|
|
54
|
+
loops cloud status
|
|
55
|
+
loops-api status
|
|
56
|
+
loops-runner status
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Human status output is intentionally compact:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
deploymentMode=local active source=default truth=local_sqlite local=authoritative control_plane=none
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
JSON uses these field names:
|
|
66
|
+
|
|
67
|
+
- `deploymentMode`: the requested status perspective.
|
|
68
|
+
- `activeDeploymentMode`: the mode selected from the current environment.
|
|
69
|
+
- `deploymentModeSource`: the env var or default that selected the active mode.
|
|
70
|
+
- `sourceOfTruth`: `local_sqlite`, `self_hosted_control_plane`, or
|
|
71
|
+
`cloud_control_plane`.
|
|
72
|
+
- `localStore.role`: `authoritative` in local mode, `cache_and_spool` in
|
|
73
|
+
non-local modes.
|
|
74
|
+
- `controlPlane.configured`: true only when the current mode has enough
|
|
75
|
+
configuration to be usable. Cloud requires both a cloud URL and a cloud token
|
|
76
|
+
presence signal.
|
|
77
|
+
- `controlPlane.apiUrl`: a display-safe URL without credentials, query string,
|
|
78
|
+
or fragment.
|
|
79
|
+
|
|
80
|
+
`loops-api` is a separate process in the same public package. It is not a
|
|
81
|
+
separate package at this stage because self-hosted users and the hosted service
|
|
82
|
+
must share the same public contract.
|
|
83
|
+
|
|
84
|
+
`loops-runner` is the process that connects a machine to a non-local control
|
|
85
|
+
plane. The initial foundation exposes status only. The Postgres schema, claim
|
|
86
|
+
protocol, runner heartbeat/finalization protocol, and import/export migration
|
|
87
|
+
must land before `loops-runner` is advertised as an executing worker.
|
|
88
|
+
|
|
89
|
+
## Machine Placement
|
|
90
|
+
|
|
91
|
+
A loop can eventually target:
|
|
92
|
+
|
|
93
|
+
- one specific machine,
|
|
94
|
+
- a machine pool selected by capability labels,
|
|
95
|
+
- or multiple machines intentionally.
|
|
96
|
+
|
|
97
|
+
Single-run loops need a lease so only one runner executes each scheduled slot.
|
|
98
|
+
Multi-machine loops must record per-machine run evidence so duplicate work is
|
|
99
|
+
distinguishable from intentional fan-out.
|
|
100
|
+
|
|
101
|
+
This is separate from existing local OpenMachines dispatch. In `local` mode,
|
|
102
|
+
`loops-daemon` can still dispatch a loop target to a configured remote machine
|
|
103
|
+
through the existing OpenMachines transport. In `self_hosted` or `cloud`,
|
|
104
|
+
machine execution is runner-pull: a registered `loops-runner` claims work from
|
|
105
|
+
the control plane. Operators should not treat local remote dispatch as cloud
|
|
106
|
+
mode.
|
|
107
|
+
|
|
108
|
+
## Follow-Up Work
|
|
109
|
+
|
|
110
|
+
Non-local execution needs these follow-up releases before it is live:
|
|
111
|
+
|
|
112
|
+
- Postgres/control-plane persistence for loops, schedules, runners, claims, and
|
|
113
|
+
evidence.
|
|
114
|
+
- Runner placement, capability labels, leases, heartbeat, and finalization.
|
|
115
|
+
- Import/export migration between local SQLite state and a control plane.
|
|
116
|
+
- Hosted product integration outside the public package.
|
|
117
|
+
|
|
118
|
+
## Public Package Boundary
|
|
119
|
+
|
|
120
|
+
The public package owns:
|
|
121
|
+
|
|
122
|
+
- local SQLite scheduling and daemon execution,
|
|
123
|
+
- the mode resolver and status surfaces,
|
|
124
|
+
- the self-hosted API contract,
|
|
125
|
+
- the runner contract,
|
|
126
|
+
- local cache/spool semantics,
|
|
127
|
+
- import/export and migration commands,
|
|
128
|
+
- SDK, MCP, and CLI primitives.
|
|
129
|
+
|
|
130
|
+
Hosted product code owns:
|
|
131
|
+
|
|
132
|
+
- tenant account management,
|
|
133
|
+
- hosted authentication and invitations,
|
|
134
|
+
- hosted observability and admin operations,
|
|
135
|
+
- cloud infrastructure deployment,
|
|
136
|
+
- product UI and customer lifecycle flows.
|
|
137
|
+
|
|
138
|
+
The public package must document cloud behavior as a contract unless a hosted
|
|
139
|
+
URL and token are explicitly configured. It must not claim that cloud service is
|
|
140
|
+
live by default.
|
package/docs/USAGE.md
CHANGED
|
@@ -11,8 +11,36 @@ It supports deterministic command loops, JSON-defined workflows, and guarded CLI
|
|
|
11
11
|
- `opencode run`
|
|
12
12
|
- `codex exec`
|
|
13
13
|
|
|
14
|
+
## Deployment Modes
|
|
15
|
+
|
|
16
|
+
OpenLoops defaults to `local`, where SQLite in `LOOPS_DATA_DIR` is
|
|
17
|
+
authoritative and `loops-daemon` executes scheduled work. The package also
|
|
18
|
+
defines `self_hosted` and `cloud` contracts for future non-local control
|
|
19
|
+
planes:
|
|
20
|
+
|
|
21
|
+
- `self_hosted`: user-operated `loops-api` control-plane contract; this
|
|
22
|
+
release exposes API/runner status foundations only.
|
|
23
|
+
- `cloud`: hosted control-plane contract; this release exposes client/runner
|
|
24
|
+
status only, and requires `LOOPS_CLOUD_API_URL` plus `LOOPS_CLOUD_TOKEN` or
|
|
25
|
+
`HASNA_LOOPS_CLOUD_TOKEN` before status can report ready.
|
|
26
|
+
|
|
27
|
+
Useful status commands:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
loops mode
|
|
31
|
+
loops --json mode
|
|
32
|
+
loops self-hosted status
|
|
33
|
+
loops cloud status
|
|
34
|
+
loops-api status
|
|
35
|
+
loops-runner status
|
|
36
|
+
```
|
|
37
|
+
|
|
14
38
|
## Install
|
|
15
39
|
|
|
40
|
+
**OpenLoops requires the [Bun](https://bun.sh) runtime (`bun >= 1.0`).** The
|
|
41
|
+
installed binaries are `loops`, `loops-daemon`, `loops-api`, `loops-runner`, and
|
|
42
|
+
`loops-mcp`; each uses a `#!/usr/bin/env bun` shebang.
|
|
43
|
+
|
|
16
44
|
From npm:
|
|
17
45
|
|
|
18
46
|
```bash
|
|
@@ -57,8 +85,9 @@ The package also exports the server factory for embedded hosts:
|
|
|
57
85
|
import { createLoopsMcpServer } from "@hasna/loops/mcp";
|
|
58
86
|
```
|
|
59
87
|
|
|
60
|
-
Available read tools include `loops_list`, `loops_show`, `
|
|
61
|
-
`loops_doctor`, `
|
|
88
|
+
Available read tools include `loops_list`, `loops_show`, `loops_runs`,
|
|
89
|
+
`loops_doctor`, `loops_workflows_list`, `loops_workflow_read`, and
|
|
90
|
+
`loops_workflow_validate`.
|
|
62
91
|
Resources are available at `loops://runtime` and `loops://tools`.
|
|
63
92
|
Those tools use the same `Store`, public redaction helpers, and workflow parser
|
|
64
93
|
as the CLI and SDK, so read output and validation behavior stay aligned across
|
|
@@ -66,15 +95,19 @@ surfaces.
|
|
|
66
95
|
|
|
67
96
|
Mutation tools are disabled by default. Start the server with
|
|
68
97
|
`LOOPS_MCP_ALLOW_MUTATIONS=true` only for a trusted local MCP host that should be
|
|
69
|
-
allowed to change loop state.
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
98
|
+
allowed to change loop state. The guarded mutation tools use canonical names:
|
|
99
|
+
`loops_pause`, `loops_resume`, `loops_stop`, `loops_run_now`, `loops_archive`,
|
|
100
|
+
`loops_unarchive`, `loops_create_command`, and `loops_create_workflow`.
|
|
101
|
+
Deprecated `loop_*` aliases are still registered where compatibility needs them,
|
|
102
|
+
but callers should use the `loops_*` names. Mutation tools do not require or
|
|
103
|
+
accept confirmation-string parameters; the server-side environment opt-in is the
|
|
104
|
+
gate. MCP `loops_run_now` schedules the loop for immediate daemon pickup; inline
|
|
105
|
+
execution remains CLI-only.
|
|
73
106
|
|
|
74
107
|
Keep host-affecting or long-running operations on the CLI: daemon
|
|
75
|
-
start/stop/install/logs, inline `run-now`, `tick`, loop
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
start/stop/install/logs, inline `run-now`, `tick`, loop deletion, workflow
|
|
109
|
+
create/migrate/cancel/recover, agent loop creation, template materialization,
|
|
110
|
+
and event-route drains.
|
|
78
111
|
|
|
79
112
|
## Create Loops
|
|
80
113
|
|
|
@@ -95,8 +128,8 @@ loops create command repo-status \
|
|
|
95
128
|
```
|
|
96
129
|
|
|
97
130
|
`--preflight` is available on `loops create command`, `loops create agent`,
|
|
98
|
-
`loops create workflow`, `loops workflows create`, and
|
|
99
|
-
|
|
131
|
+
`loops create workflow`, `loops workflows create`, and route commands such as
|
|
132
|
+
`loops routes create todos-task` and `loops routes create generic`. It checks
|
|
100
133
|
target executables and configured account profiles before loop or workflow rows
|
|
101
134
|
are stored, so a missing command, provider binary, OpenAccounts profile, native
|
|
102
135
|
Codewith auth profile, or workflow step dependency fails without creating a
|
|
@@ -313,6 +346,15 @@ Use `shell: true` only when you intentionally want shell parsing:
|
|
|
313
346
|
{ "type": "command", "command": "git status --short", "shell": true }
|
|
314
347
|
```
|
|
315
348
|
|
|
349
|
+
Gate steps can end a workflow early without failing it. A step opts into
|
|
350
|
+
blocked-exit semantics either explicitly via `"blockedExitCodes": [12]` on the
|
|
351
|
+
step, or by naming convention: a step whose `id` or `name` contains "gate" as a
|
|
352
|
+
standalone word (for example `gate`, `triage-gate`, `gate_check`) treats exit
|
|
353
|
+
code 12 as "blocked" — the step and its dependents are recorded as skipped and
|
|
354
|
+
the workflow still succeeds. Substring matches such as `gateway`, `aggregate`,
|
|
355
|
+
or `delegate` do NOT inherit gate behavior; use `blockedExitCodes` explicitly
|
|
356
|
+
for those. Set `"blockedExitCodes": []` on a gate-named step to opt out.
|
|
357
|
+
|
|
316
358
|
## Templates And Task Events
|
|
317
359
|
|
|
318
360
|
Built-in templates turn common orchestration flows into reusable workflow JSON.
|
|
@@ -331,11 +373,11 @@ loops templates render todos-task-worker-verifier \
|
|
|
331
373
|
--var taskTitle="Fix parser" \
|
|
332
374
|
--var projectPath=/path/to/repo \
|
|
333
375
|
--var provider=codewith \
|
|
334
|
-
--var authProfilePool=
|
|
376
|
+
--var authProfilePool=account001,account002,account003 \
|
|
335
377
|
--var sandbox=workspace-write \
|
|
336
378
|
--var todosProjectPath=$HOME/.hasna/loops \
|
|
337
379
|
--var addDirs=$HOME/.hasna/todos,$HOME/.hasna/loops
|
|
338
|
-
loops
|
|
380
|
+
loops workflows create --template todos-task-worker-verifier \
|
|
339
381
|
--var taskId=<task-id> \
|
|
340
382
|
--var projectPath=/path/to/repo
|
|
341
383
|
loops templates render event-worker-verifier \
|
|
@@ -348,7 +390,7 @@ loops templates render bounded-agent-worker-verifier \
|
|
|
348
390
|
--var objective="Check docs drift and queue tasks for gaps" \
|
|
349
391
|
--var projectPath=/path/to/repo \
|
|
350
392
|
--var provider=codewith \
|
|
351
|
-
--var authProfilePool=
|
|
393
|
+
--var authProfilePool=account001,account002 \
|
|
352
394
|
--var sandbox=workspace-write
|
|
353
395
|
loops templates render pr-review \
|
|
354
396
|
--var prUrl=https://github.com/hasna/loops/pull/123 \
|
|
@@ -432,14 +474,15 @@ loops templates show custom-report
|
|
|
432
474
|
loops templates render custom-report \
|
|
433
475
|
--var objective="Check docs drift" \
|
|
434
476
|
--var projectPath=/path/to/repo
|
|
435
|
-
loops
|
|
477
|
+
loops workflows create --template custom-report \
|
|
436
478
|
--var objective="Check docs drift" \
|
|
437
479
|
--var projectPath=/path/to/repo
|
|
438
480
|
```
|
|
439
481
|
|
|
440
482
|
Use `--source builtin`, `--source custom`, or `--source all` on
|
|
441
|
-
`list`, `show`, `render`, and
|
|
442
|
-
|
|
483
|
+
`templates list`, `templates show`, `templates render`, and
|
|
484
|
+
`workflows create --template` when automation needs an explicit source. Custom
|
|
485
|
+
template ids and names cannot override built-ins.
|
|
443
486
|
Custom templates fail closed for `danger-full-access`, dangerous passthrough
|
|
444
487
|
arguments, and implicit Codewith/Codex full-access defaults. If a custom
|
|
445
488
|
Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
|
|
@@ -448,18 +491,21 @@ explicit break-glass handling for emergency workflows that need full access.
|
|
|
448
491
|
|
|
449
492
|
Repo-mutating task/event routes should set `worktreeMode=required` so the
|
|
450
493
|
workflow fails fast instead of falling back to the main checkout. When
|
|
451
|
-
`projectPath` is an existing git repository,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
494
|
+
`projectPath` is an existing git repository, the executor prepares and enters
|
|
495
|
+
a deterministic worktree under `~/.hasna/loops/worktrees/<repo>/<run>` before
|
|
496
|
+
spawning the worker/verifier — locally and on machine-assigned (remote) loops,
|
|
497
|
+
where the dispatch script runs the equivalent `git worktree add`/reuse checks
|
|
498
|
+
on the remote machine. The generated agent target includes worktree metadata
|
|
499
|
+
(`mode`, `cwd`, `path`, `branch`, `originalCwd`) so dry-runs and workflow
|
|
500
|
+
inspection expose the exact checkout.
|
|
501
|
+
|
|
502
|
+
Before a worker starts, worktree preparation verifies that any existing
|
|
503
|
+
managed path is a real git worktree with the same top-level checkout, the same
|
|
504
|
+
git common directory as the source repo, and the expected generated branch. A
|
|
505
|
+
detached HEAD or unexpected branch fails closed with evidence
|
|
506
|
+
(`worktreeMode=required`) instead of silently running a mutating workflow in
|
|
507
|
+
the wrong state; `worktreeMode=auto` falls back to the original checkout and
|
|
508
|
+
records the fallback.
|
|
463
509
|
|
|
464
510
|
Use explicit main/default checkout mode only when the task truly requires it:
|
|
465
511
|
|
|
@@ -479,16 +525,16 @@ PR review and merge route workers may fetch, rebase, or merge base branches
|
|
|
479
525
|
inside the isolated worktree when the task requires it, but they must not
|
|
480
526
|
mutate the primary main checkout.
|
|
481
527
|
|
|
482
|
-
For event-driven task automation, `loops
|
|
528
|
+
For event-driven task automation, `loops routes create todos-task` reads a
|
|
483
529
|
Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
|
|
484
530
|
`WorkflowInvocation`, upserts an admission work item, and admits that work item
|
|
485
531
|
into a deduped one-shot workflow loop when route capacity allows:
|
|
486
532
|
|
|
487
533
|
```bash
|
|
488
|
-
cat task-created-event.json | loops
|
|
534
|
+
cat task-created-event.json | loops routes create todos-task \
|
|
489
535
|
--template task-lifecycle \
|
|
490
536
|
--provider codewith \
|
|
491
|
-
--auth-profile-pool
|
|
537
|
+
--auth-profile-pool account001,account002,account003 \
|
|
492
538
|
--permission-mode bypass \
|
|
493
539
|
--sandbox workspace-write \
|
|
494
540
|
--todos-project "$HOME/.hasna/loops" \
|
|
@@ -518,7 +564,7 @@ selected.
|
|
|
518
564
|
loops routes drain todos-task \
|
|
519
565
|
--dry-run \
|
|
520
566
|
--provider-rule area=frontend:claude:claude-ui-a,claude-ui-b \
|
|
521
|
-
--provider-rule area=backend:codewith:
|
|
567
|
+
--provider-rule area=backend:codewith:account001,account002 \
|
|
522
568
|
--worktree-mode required
|
|
523
569
|
```
|
|
524
570
|
|
|
@@ -556,9 +602,9 @@ Use route throttles to avoid stampeding agents when a producer creates many
|
|
|
556
602
|
tasks at once:
|
|
557
603
|
|
|
558
604
|
```bash
|
|
559
|
-
cat task-created-event.json | loops
|
|
605
|
+
cat task-created-event.json | loops routes create todos-task \
|
|
560
606
|
--provider codewith \
|
|
561
|
-
--auth-profile-pool
|
|
607
|
+
--auth-profile-pool account001,account002,account003 \
|
|
562
608
|
--project-group oss \
|
|
563
609
|
--max-active-per-project 1 \
|
|
564
610
|
--max-active-per-project-group 4 \
|
|
@@ -621,14 +667,14 @@ hand. It scans `todos ready --json`, so tasks with incomplete dependencies,
|
|
|
621
667
|
locks, or non-pending states stay queued in todos and are not routed:
|
|
622
668
|
|
|
623
669
|
```bash
|
|
624
|
-
loops
|
|
670
|
+
loops routes drain todos-task \
|
|
625
671
|
--todos-project "$HOME/.hasna/loops" \
|
|
626
672
|
--template task-lifecycle \
|
|
627
673
|
--task-list repoops-pr-queue \
|
|
628
674
|
--tags auto:route \
|
|
629
|
-
--project-path-prefix /
|
|
675
|
+
--project-path-prefix "$HOME/workspace/example/opensource" \
|
|
630
676
|
--provider codewith \
|
|
631
|
-
--auth-profile-pool
|
|
677
|
+
--auth-profile-pool account001,account002,account003 \
|
|
632
678
|
--add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
|
|
633
679
|
--project-group oss \
|
|
634
680
|
--max-dispatch 2 \
|
|
@@ -653,17 +699,17 @@ when requested, and leaves excess ready tasks in todos for a later drain pass.
|
|
|
653
699
|
Use `--dry-run` to preview candidates and rendered workflows without mutating
|
|
654
700
|
OpenLoops state.
|
|
655
701
|
|
|
656
|
-
For
|
|
702
|
+
For an OSS task-created route, keep the drain deterministic and narrow:
|
|
657
703
|
|
|
658
704
|
```bash
|
|
659
705
|
loops routes schedule todos-task oss-task-route-drain \
|
|
660
706
|
--every 5m \
|
|
661
707
|
--todos-project "$HOME/.hasna/loops" \
|
|
662
708
|
--template task-lifecycle \
|
|
663
|
-
--project-path-prefix /
|
|
709
|
+
--project-path-prefix "$HOME/workspace/example/opensource" \
|
|
664
710
|
--tags auto:route \
|
|
665
711
|
--provider codewith \
|
|
666
|
-
--auth-profile-pool
|
|
712
|
+
--auth-profile-pool account001,account002,account003 \
|
|
667
713
|
--add-dir "$HOME/.hasna/todos,$HOME/.hasna/loops" \
|
|
668
714
|
--project-group oss \
|
|
669
715
|
--max-dispatch 2 \
|
|
@@ -676,7 +722,7 @@ loops routes schedule todos-task oss-task-route-drain \
|
|
|
676
722
|
--compact
|
|
677
723
|
```
|
|
678
724
|
|
|
679
|
-
Only tasks under
|
|
725
|
+
Only tasks under `$HOME/workspace/example/opensource` that explicitly opt
|
|
680
726
|
in with the `auto:route` tag, `route_enabled=true`, or
|
|
681
727
|
`automation.allowed=true` should be routed. Keep repo-mutating worker/verifier
|
|
682
728
|
runs on a Codewith account pool with `--worktree-mode required`. Do not dispatch
|
|
@@ -697,9 +743,9 @@ For other Hasna apps that expose `@hasna/events` webhooks, use the generic
|
|
|
697
743
|
handler:
|
|
698
744
|
|
|
699
745
|
```bash
|
|
700
|
-
cat event.json | loops
|
|
746
|
+
cat event.json | loops routes create generic \
|
|
701
747
|
--provider codewith \
|
|
702
|
-
--auth-profile-pool
|
|
748
|
+
--auth-profile-pool account001,account002,account003 \
|
|
703
749
|
--permission-mode bypass \
|
|
704
750
|
--sandbox workspace-write \
|
|
705
751
|
--project-path /path/to/repo \
|
|
@@ -785,8 +831,9 @@ record an `auto_route_skipped_reason`. Without `--auto-route`, route commands
|
|
|
785
831
|
only upsert deduped tasks and do not launch agents.
|
|
786
832
|
|
|
787
833
|
Failure classifications are: `rate_limit`, `auth`, `model_not_found`,
|
|
788
|
-
`context_length`, `schema_response_format`, `node_init`, `
|
|
789
|
-
`
|
|
834
|
+
`context_length`, `schema_response_format`, `node_init`, `preflight`,
|
|
835
|
+
`route_functional`, `timeout`, `sigsegv`, `skipped_previous_active`,
|
|
836
|
+
`circuit_breaker`, and `unknown`.
|
|
790
837
|
|
|
791
838
|
## Hygiene
|
|
792
839
|
|
|
@@ -904,6 +951,15 @@ The adapters intentionally use provider command surfaces instead of pretending e
|
|
|
904
951
|
- `--permission-mode` maps `plan`, `auto`, and `bypass` where the provider supports it. Claude uses native permission modes, Cursor maps bypass to `--force`, and OpenCode/AICopilot map bypass to `--dangerously-skip-permissions`.
|
|
905
952
|
- `--variant` is provider-specific reasoning/model effort. Claude maps it to `--effort`, Codewith/Codex map it to `model_reasoning_effort`, and OpenCode/AICopilot pass `--variant`.
|
|
906
953
|
- Daemon and scheduled runs prepend common user executable directories such as `~/.local/bin` and `~/.bun/bin` before resolving provider CLIs.
|
|
954
|
+
- Agent targets that set neither `timeoutMs` nor `idleTimeoutMs` get a default
|
|
955
|
+
idle watchdog: 30 minutes without stdout/stderr for streaming providers
|
|
956
|
+
(codex, cursor), and 4 hours for providers whose CLIs buffer all output until
|
|
957
|
+
completion (claude, opencode, aicopilot) or whose durable progress
|
|
958
|
+
fingerprint can stay flat during long work (codewith). Override the default
|
|
959
|
+
with `LOOPS_AGENT_IDLE_TIMEOUT_MS=<ms>`, disable it with
|
|
960
|
+
`LOOPS_AGENT_IDLE_TIMEOUT_MS=0` (or `none`/`off`), or set explicit
|
|
961
|
+
`timeoutMs`/`idleTimeoutMs` per target — `"timeoutMs": null` opts a target
|
|
962
|
+
out of both the wall-clock default and the idle watchdog.
|
|
907
963
|
|
|
908
964
|
For production loops that can mutate repos, prefer the built-in
|
|
909
965
|
`worktreeMode=auto`/`required` path and explicit prompts that name allowed write
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/loops",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"loops": "dist/cli/index.js",
|
|
10
10
|
"loops-daemon": "dist/daemon/index.js",
|
|
11
|
+
"loops-api": "dist/api/index.js",
|
|
12
|
+
"loops-runner": "dist/runner/index.js",
|
|
11
13
|
"loops-mcp": "dist/mcp/index.js"
|
|
12
14
|
},
|
|
13
15
|
"exports": {
|
|
@@ -23,6 +25,18 @@
|
|
|
23
25
|
"types": "./dist/mcp/index.d.ts",
|
|
24
26
|
"import": "./dist/mcp/index.js"
|
|
25
27
|
},
|
|
28
|
+
"./api": {
|
|
29
|
+
"types": "./dist/api/index.d.ts",
|
|
30
|
+
"import": "./dist/api/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./runner": {
|
|
33
|
+
"types": "./dist/runner/index.d.ts",
|
|
34
|
+
"import": "./dist/runner/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./mode": {
|
|
37
|
+
"types": "./dist/lib/mode.d.ts",
|
|
38
|
+
"import": "./dist/lib/mode.js"
|
|
39
|
+
},
|
|
26
40
|
"./storage": {
|
|
27
41
|
"types": "./dist/lib/store.d.ts",
|
|
28
42
|
"import": "./dist/lib/store.js"
|
|
@@ -31,18 +45,20 @@
|
|
|
31
45
|
"files": [
|
|
32
46
|
"dist",
|
|
33
47
|
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
34
49
|
"docs",
|
|
35
50
|
"LICENSE"
|
|
36
51
|
],
|
|
37
52
|
"scripts": {
|
|
38
|
-
"build": "rm -rf dist && bun build src/cli/index.ts src/daemon/index.ts src/mcp/index.ts src/index.ts src/sdk/index.ts src/lib/store.ts --outdir dist --target bun --packages external && chmod +x dist/cli/index.js dist/daemon/index.js dist/mcp/index.js && tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
53
|
+
"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 --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",
|
|
39
54
|
"build:bin": "bun build src/cli/index.ts --compile --outfile dist/loops",
|
|
40
55
|
"typecheck": "tsc --noEmit",
|
|
41
56
|
"test": "bun test",
|
|
42
|
-
"
|
|
57
|
+
"test:boundary": "bun run scripts/no-private-cloud-boundary.mjs",
|
|
58
|
+
"prepare": "test -d dist || bun run build",
|
|
43
59
|
"dev:cli": "bun run src/cli/index.ts",
|
|
44
60
|
"dev:daemon": "bun run src/daemon/index.ts",
|
|
45
|
-
"prepublishOnly": "bun run build"
|
|
61
|
+
"prepublishOnly": "bun run build && bun run test:boundary"
|
|
46
62
|
},
|
|
47
63
|
"keywords": [
|
|
48
64
|
"loops",
|
|
@@ -70,13 +86,15 @@
|
|
|
70
86
|
},
|
|
71
87
|
"dependencies": {
|
|
72
88
|
"@hasna/events": "^0.1.9",
|
|
73
|
-
"@hasna/machines": "0.0.49",
|
|
74
89
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
75
90
|
"@openrouter/ai-sdk-provider": "2.9.1",
|
|
76
91
|
"ai": "6.0.204",
|
|
77
92
|
"commander": "^13.1.0",
|
|
78
93
|
"zod": "4.4.3"
|
|
79
94
|
},
|
|
95
|
+
"optionalDependencies": {
|
|
96
|
+
"@hasna/machines": "0.0.49"
|
|
97
|
+
},
|
|
80
98
|
"devDependencies": {
|
|
81
99
|
"@types/bun": "latest",
|
|
82
100
|
"typescript": "^5.7.3"
|