@hasna/loops 0.3.37 → 0.3.39

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/types.d.ts CHANGED
@@ -43,6 +43,26 @@ export type ScheduleSpec = OnceSchedule | IntervalSchedule | CronSchedule | Dyna
43
43
  export interface RuntimePreflightPolicy {
44
44
  beforeRun?: boolean;
45
45
  }
46
+ export interface OpenAutomationsRuntimeBinding {
47
+ integration: "open-automations";
48
+ role: "runtime";
49
+ handoff: "claim-queue";
50
+ queueOwner: "open-automations";
51
+ runtimeOwner: "open-loops";
52
+ statusCommand: "automations status";
53
+ claimCommand: "automations queue claim";
54
+ completeCommand: "automations queue complete";
55
+ failCommand: "automations queue fail";
56
+ eventHandoff: {
57
+ envelopeCommand: "automations webhooks event";
58
+ handlerCommand: "loops events handle generic";
59
+ pipeExample: string;
60
+ boundary: string;
61
+ };
62
+ requiredEnvironment: string[];
63
+ guarantees: string[];
64
+ nonGoals: string[];
65
+ }
46
66
  export interface CommandTarget {
47
67
  type: "command";
48
68
  command: string;
@@ -51,6 +71,7 @@ export interface CommandTarget {
51
71
  shell?: boolean;
52
72
  env?: Record<string, string>;
53
73
  timeoutMs?: number;
74
+ idleTimeoutMs?: number;
54
75
  account?: AccountRef;
55
76
  preflight?: RuntimePreflightPolicy;
56
77
  }
@@ -94,6 +115,7 @@ export interface AgentTarget {
94
115
  authProfile?: string;
95
116
  extraArgs?: string[];
96
117
  timeoutMs?: number;
118
+ idleTimeoutMs?: number;
97
119
  configIsolation?: AgentConfigIsolation;
98
120
  permissionMode?: AgentPermissionMode;
99
121
  sandbox?: AgentSandbox;
@@ -115,6 +137,90 @@ export type LoopTarget = ExecutableTarget | WorkflowTarget;
115
137
  export type WorkflowStatus = "active" | "archived";
116
138
  export type WorkflowRunStatus = "running" | "succeeded" | "failed" | "timed_out" | "cancelled";
117
139
  export type WorkflowStepRunStatus = "pending" | "running" | "succeeded" | "failed" | "timed_out" | "skipped" | "cancelled";
140
+ export type WorkflowInvocationSourceKind = "task" | "event" | "schedule" | "manual" | "pr" | "review" | "knowledge";
141
+ export type WorkflowInvocationSubjectKind = "repo" | "pr" | "task" | "doc" | "run" | "metric";
142
+ export type WorkflowInvocationIntent = "route" | "mutate" | "review" | "evaluate" | "report";
143
+ export interface WorkflowInvocationRef {
144
+ kind: string;
145
+ id?: string;
146
+ path?: string;
147
+ url?: string;
148
+ dedupeKey?: string;
149
+ raw?: Record<string, unknown>;
150
+ }
151
+ export interface WorkflowInvocationScope {
152
+ projectPath?: string;
153
+ projectGroup?: string;
154
+ worktreePolicy?: AgentWorktreeMode;
155
+ permissions?: string;
156
+ accountPolicy?: string;
157
+ concurrencyGroup?: string;
158
+ [key: string]: unknown;
159
+ }
160
+ export interface WorkflowInvocationOutputPolicy {
161
+ report?: "always" | "on_change" | "on_failure";
162
+ createTask?: "never" | "on_actionable" | "on_failure" | "always";
163
+ [key: string]: unknown;
164
+ }
165
+ export interface WorkflowInvocation {
166
+ id: string;
167
+ workflowId?: string;
168
+ templateId?: string;
169
+ sourceRef: WorkflowInvocationRef;
170
+ subjectRef: WorkflowInvocationRef;
171
+ intent: WorkflowInvocationIntent;
172
+ scope?: WorkflowInvocationScope;
173
+ outputPolicy?: WorkflowInvocationOutputPolicy;
174
+ createdAt: string;
175
+ updatedAt: string;
176
+ }
177
+ export interface CreateWorkflowInvocationInput {
178
+ id?: string;
179
+ workflowId?: string;
180
+ templateId?: string;
181
+ sourceRef: WorkflowInvocationRef;
182
+ subjectRef: WorkflowInvocationRef;
183
+ intent: WorkflowInvocationIntent;
184
+ scope?: WorkflowInvocationScope;
185
+ outputPolicy?: WorkflowInvocationOutputPolicy;
186
+ }
187
+ export type WorkflowWorkItemStatus = "queued" | "deferred" | "admitted" | "running" | "succeeded" | "failed" | "dead_letter" | "cancelled";
188
+ export interface WorkflowWorkItem {
189
+ id: string;
190
+ routeKey: string;
191
+ idempotencyKey: string;
192
+ invocationId: string;
193
+ sourceType: string;
194
+ sourceRef: string;
195
+ subjectRef: string;
196
+ projectKey?: string;
197
+ projectGroup?: string;
198
+ priority: number;
199
+ status: WorkflowWorkItemStatus;
200
+ attempts: number;
201
+ nextAttemptAt?: string;
202
+ leaseExpiresAt?: string;
203
+ workflowId?: string;
204
+ loopId?: string;
205
+ workflowRunId?: string;
206
+ lastReason?: string;
207
+ createdAt: string;
208
+ updatedAt: string;
209
+ }
210
+ export interface UpsertWorkflowWorkItemInput {
211
+ routeKey: string;
212
+ idempotencyKey: string;
213
+ invocationId: string;
214
+ sourceType: string;
215
+ sourceRef: string;
216
+ subjectRef: string;
217
+ projectKey?: string;
218
+ projectGroup?: string;
219
+ priority?: number;
220
+ status?: Extract<WorkflowWorkItemStatus, "queued" | "deferred">;
221
+ nextAttemptAt?: string;
222
+ lastReason?: string;
223
+ }
118
224
  export interface WorkflowStep {
119
225
  id: string;
120
226
  name?: string;
@@ -164,8 +270,11 @@ export interface WorkflowRun {
164
270
  workflowName: string;
165
271
  loopId?: string;
166
272
  loopRunId?: string;
273
+ invocationId?: string;
274
+ workItemId?: string;
167
275
  scheduledFor?: string;
168
276
  idempotencyKey?: string;
277
+ manifestPath?: string;
169
278
  goalRunId?: string;
170
279
  status: WorkflowRunStatus;
171
280
  startedAt?: string;
package/docs/USAGE.md CHANGED
@@ -108,7 +108,7 @@ loops create agent supply-chain-watch \
108
108
  --provider codewith \
109
109
  --every 15m \
110
110
  --cwd /path/to/repo \
111
- --sandbox danger-full-access \
111
+ --sandbox workspace-write \
112
112
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
113
113
  ```
114
114
 
@@ -120,7 +120,7 @@ loops create agent supply-chain-watch \
120
120
  --auth-profile account001 \
121
121
  --every 15m \
122
122
  --cwd /path/to/repo \
123
- --sandbox danger-full-access \
123
+ --sandbox workspace-write \
124
124
  --prompt "Check for suspicious dependency or supply-chain changes. Report only concrete findings."
125
125
  ```
126
126
 
@@ -216,6 +216,9 @@ Built-in templates turn common orchestration flows into reusable workflow JSON.
216
216
  `event-worker-verifier` handles any Hasna event envelope and then verifies the
217
217
  handling. `bounded-agent-worker-verifier` is for recurring bounded agent work:
218
218
  one worker runs a narrow objective, then a fresh verifier audits the result.
219
+ The catalog also includes `task-lifecycle`, `pr-review`, `scheduled-audit`,
220
+ `knowledge-refresh`, `report-only`, `incident-response`, and
221
+ `deterministic-check-create-task` for common operator workflows.
219
222
 
220
223
  ```bash
221
224
  loops templates list
@@ -225,7 +228,7 @@ loops templates render todos-task-worker-verifier \
225
228
  --var projectPath=/path/to/repo \
226
229
  --var provider=codewith \
227
230
  --var authProfilePool=account004,account005,account006 \
228
- --var sandbox=danger-full-access
231
+ --var sandbox=workspace-write
229
232
  loops templates create-workflow todos-task-worker-verifier \
230
233
  --var taskId=<task-id> \
231
234
  --var projectPath=/path/to/repo
@@ -240,15 +243,23 @@ loops templates render bounded-agent-worker-verifier \
240
243
  --var projectPath=/path/to/repo \
241
244
  --var provider=codewith \
242
245
  --var authProfilePool=account004,account005 \
243
- --var sandbox=danger-full-access
246
+ --var sandbox=workspace-write
247
+ loops templates render pr-review \
248
+ --var prUrl=https://github.com/hasna/loops/pull/123 \
249
+ --var projectPath=/path/to/repo
250
+ loops templates render deterministic-check-create-task \
251
+ --var projectPath=/path/to/repo \
252
+ --var checkCommand='your deterministic check and todos upsert command'
244
253
  ```
245
254
 
246
- Task/event agent templates default to `worktreeMode=auto`. When `projectPath`
247
- is an existing git repository, OpenLoops inserts a `prepare-worktree` command
248
- step before the worker and runs the worker/verifier from a deterministic
249
- worktree under `~/.hasna/loops/worktrees/<repo>/<run>`. The generated agent
250
- target includes worktree metadata (`mode`, `cwd`, `path`, `branch`,
251
- `originalCwd`) so dry-runs and workflow inspection expose the exact checkout.
255
+ Repo-mutating task/event routes should set `worktreeMode=required` so the
256
+ workflow fails fast instead of falling back to the main checkout. When
257
+ `projectPath` is an existing git repository, OpenLoops inserts a
258
+ `prepare-worktree` command step before the worker and runs the worker/verifier
259
+ from a deterministic worktree under `~/.hasna/loops/worktrees/<repo>/<run>`.
260
+ The generated agent target includes worktree metadata (`mode`, `cwd`, `path`,
261
+ `branch`, `originalCwd`) so dry-runs and workflow inspection expose the exact
262
+ checkout.
252
263
 
253
264
  Use explicit main/default checkout mode only when the task truly requires it:
254
265
 
@@ -259,21 +270,23 @@ loops templates render todos-task-worker-verifier \
259
270
  --var worktreeMode=main
260
271
  ```
261
272
 
262
- Use `worktreeMode=required` when non-worktree execution should fail fast, or
273
+ Use `worktreeMode=auto` only for compatibility or mixed routes where a
274
+ non-git/non-mutating project is expected and the fallback is recorded. Use
263
275
  `worktreeMode=off` for non-git projects. `worktreeRoot` and
264
276
  `worktreeBranchPrefix` can override the storage root and branch prefix.
265
277
 
266
278
  For event-driven task automation, `loops events handle todos-task` reads a
267
- Hasna event envelope from stdin or `HASNA_EVENT_JSON`, renders the template, and
268
- schedules a deduped one-shot workflow loop:
279
+ Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
280
+ `WorkflowInvocation`, upserts an admission work item, and admits that work item
281
+ into a deduped one-shot workflow loop when route capacity allows:
269
282
 
270
283
  ```bash
271
284
  cat task-created-event.json | loops events handle todos-task \
272
285
  --provider codewith \
273
286
  --auth-profile-pool account004,account005,account006 \
274
287
  --permission-mode bypass \
275
- --sandbox danger-full-access \
276
- --worktree-mode auto
288
+ --sandbox workspace-write \
289
+ --worktree-mode required
277
290
  ```
278
291
 
279
292
  Task routing is explicit opt-in. The handler skips the event without creating a
@@ -297,19 +310,40 @@ cat task-created-event.json | loops events handle todos-task \
297
310
  --max-active 12
298
311
  ```
299
312
 
300
- The limits count active routed worker/verifier workflow loops once per workflow.
313
+ The limits count active admitted/running OpenLoops work items once per workflow.
301
314
  `--max-active-per-project` gates new work for the same project path,
302
315
  `--max-active-per-project-group` shares a pool across related projects such as
303
316
  `oss`, and `--max-active` is the global routed-workflow cap. Project matching
304
317
  uses the canonical git top-level path when available, so repo subdirectories
305
- share the same project cap. A throttled event is skipped with JSON evidence and
306
- `queuedAtSource=true` instead of creating another worker loop; the source task
307
- remains the durable queue item and should be replayed/drained later by the task
308
- scheduler. Re-delivering the event later is safe because event handlers dedupe
309
- by task/event id before rendering worktree plans or checking route limits. In
318
+ share the same project cap. A throttled event records a deferred OpenLoops
319
+ admission work item with JSON evidence instead of creating another worker loop.
320
+ Re-delivering the event later is safe because handlers dedupe by the work-item
321
+ idempotency key before rendering worktree plans or checking route limits. In
310
322
  dry-run mode, throttle counts are not evaluated because opening the live loop
311
323
  store can create or migrate the local database.
312
324
 
325
+ Inspect route state with:
326
+
327
+ ```bash
328
+ cat task-created-event.json | loops routes preview todos-task --sandbox workspace-write
329
+ cat task-created-event.json | loops routes create todos-task --sandbox workspace-write
330
+ loops routes drain todos-task --task-list oss --max-dispatch 2 --compact
331
+ loops routes schedule todos-task route-drain-oss-5m --every 5m --task-list oss --max-dispatch 1 --compact
332
+ loops routes list --route-key todos-task
333
+ loops routes show <work-item-id>
334
+ loops routes invocations
335
+ ```
336
+
337
+ When a workflow run starts from an admitted work item, OpenLoops writes a
338
+ manifest under:
339
+
340
+ ```text
341
+ .hasna/loops/runs/<project-slug>/<subject-key>/<run-id>/manifest.json
342
+ ```
343
+
344
+ `subject-key` is a safe derived path segment (`kind-safeSlug-shortHash`), not
345
+ the raw subject reference. The raw `subjectRef` is stored inside the manifest.
346
+
313
347
  When tasks were created while capacity was full, or when bulk producers created
314
348
  many tasks at once, use the drain command instead of replaying every webhook by
315
349
  hand. It scans `todos ready --json`, so tasks with incomplete dependencies,
@@ -329,7 +363,7 @@ loops events drain todos-task \
329
363
  --max-active-per-project 1 \
330
364
  --max-active-per-project-group 4 \
331
365
  --max-active 12 \
332
- --worktree-mode auto \
366
+ --worktree-mode required \
333
367
  --evidence-dir /home/hasna/.hasna/loops/reports/task-drain
334
368
  ```
335
369
 
@@ -354,19 +388,26 @@ cat event.json | loops events handle generic \
354
388
  --provider codewith \
355
389
  --auth-profile-pool account004,account005,account006 \
356
390
  --permission-mode bypass \
357
- --sandbox danger-full-access \
358
- --project-path /path/to/repo
391
+ --sandbox workspace-write \
392
+ --project-path /path/to/repo \
393
+ --worktree-mode required
359
394
  ```
360
395
 
361
396
  This is the intended deterministic-to-agentic path: a producer creates a todos
362
- task, `@hasna/events` delivers `task.created`, OpenLoops creates a worker and a
363
- verifier workflow, and the workflow updates todos with evidence. Use account
364
- pools so worker and verifier steps do not burn the same profile; OpenLoops picks
365
- deterministically and uses a different verifier profile when the pool has at
366
- least two entries. Use `--dry-run` to inspect the rendered workflow and loop
367
- input without storing anything, including the worktree path and branch for
397
+ task, `@hasna/events` delivers `task.created`, OpenLoops records the invocation
398
+ and admission item, OpenLoops creates a worker/verifier workflow when admitted,
399
+ and the workflow updates todos with evidence. Use account pools so worker and
400
+ verifier steps do not burn the same profile; OpenLoops picks deterministically
401
+ and uses a different verifier profile when the pool has at least two entries.
402
+ Use `--dry-run` to inspect the rendered invocation, work item, workflow, and
403
+ loop input without storing anything, including the worktree path and branch for
368
404
  git-backed tasks.
369
405
 
406
+ Generated worker/verifier workflows fail closed when `sandbox=danger-full-access`
407
+ is requested without `manualBreakGlass=true`. Use `workspace-write` for
408
+ unattended task/event routes. Full access is an explicit manual emergency path,
409
+ not a default automation mode.
410
+
370
411
  ## Transcript-Driven Loops
371
412
 
372
413
  OpenLoops can turn long-form media or meeting transcripts into recurring workflow work when paired with `iapp-transcriber`. The template at `docs/workflows/transcript-feedback-to-loops.json` transcribes an authorized media URL, asks an agent to extract recurring loop candidates, authors workflow specs, and validates generated workflows before scheduling. Copy it into the target repo, replace `/path/to/repo` with that repo's absolute path, and provide `TRANSCRIBER_SOURCE_URL` through the runner environment or a private, uncommitted workflow copy before storing or scheduling it. Do not commit private or signed media URLs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.37",
3
+ "version": "0.3.39",
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",