@hasna/loops 0.3.51 → 0.3.53

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/sdk/index.js CHANGED
@@ -621,7 +621,7 @@ function writeWorkflowRunManifest(args) {
621
621
  var DEFAULT_RECOVERY_BATCH_LIMIT = 100;
622
622
  var DEFAULT_RECOVERY_SCAN_MULTIPLIER = 5;
623
623
  var LIVE_EXPIRED_RUN_GRACE_MS = 60000;
624
- var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "event-worker-verifier"]);
624
+ var GENERATED_ROUTE_TEMPLATE_IDS = new Set(["todos-task-worker-verifier", "task-lifecycle", "event-worker-verifier"]);
625
625
  var GENERATED_ROUTE_KEYS = new Set(["todos-task", "generic-event"]);
626
626
  function rowToLoop(row) {
627
627
  return {
@@ -1565,6 +1565,65 @@ class Store {
1565
1565
  throw new Error(`workflow invocation not found after create: ${id}`);
1566
1566
  return rowToWorkflowInvocation(row);
1567
1567
  }
1568
+ refreshWorkflowInvocationForWorkItem(workItemId, input) {
1569
+ const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
1570
+ if (!sourceDedupeKey)
1571
+ throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1572
+ const now = nowIso();
1573
+ const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1574
+ const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1575
+ const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1576
+ const result = this.db.query(`UPDATE workflow_invocations
1577
+ SET workflow_id=COALESCE($workflowId, workflow_id),
1578
+ template_id=COALESCE($templateId, template_id),
1579
+ source_id=COALESCE($sourceId, source_id),
1580
+ source_json=$sourceJson,
1581
+ subject_kind=$subjectKind,
1582
+ subject_id=COALESCE($subjectId, subject_id),
1583
+ subject_path=COALESCE($subjectPath, subject_path),
1584
+ subject_url=COALESCE($subjectUrl, subject_url),
1585
+ subject_json=$subjectJson,
1586
+ intent=$intent,
1587
+ scope_json=COALESCE($scopeJson, scope_json),
1588
+ output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
1589
+ updated_at=$updated
1590
+ WHERE source_kind=$sourceKind
1591
+ AND source_dedupe_key=$sourceDedupeKey
1592
+ AND EXISTS (
1593
+ SELECT 1
1594
+ FROM workflow_work_items
1595
+ WHERE id=$workItemId
1596
+ AND invocation_id=workflow_invocations.id
1597
+ AND status IN (${placeholders})
1598
+ )`).run({
1599
+ $workItemId: workItemId,
1600
+ $sourceKind: input.sourceRef.kind,
1601
+ $sourceDedupeKey: sourceDedupeKey,
1602
+ $workflowId: input.workflowId ?? null,
1603
+ $templateId: input.templateId ?? null,
1604
+ $sourceId: input.sourceRef.id ?? null,
1605
+ $sourceJson: JSON.stringify(input.sourceRef),
1606
+ $subjectKind: input.subjectRef.kind,
1607
+ $subjectId: input.subjectRef.id ?? null,
1608
+ $subjectPath: input.subjectRef.path ?? null,
1609
+ $subjectUrl: input.subjectRef.url ?? null,
1610
+ $subjectJson: JSON.stringify(input.subjectRef),
1611
+ $intent: input.intent,
1612
+ $scopeJson: input.scope ? JSON.stringify(input.scope) : null,
1613
+ $outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
1614
+ $updated: now,
1615
+ ...statusBindings
1616
+ });
1617
+ if (result.changes !== 1)
1618
+ throw new Error(`workflow work item is not refreshable: ${workItemId}`);
1619
+ const updated = this.db.query(`SELECT workflow_invocations.*
1620
+ FROM workflow_invocations
1621
+ JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
1622
+ WHERE workflow_work_items.id = ?`).get(workItemId);
1623
+ if (!updated)
1624
+ throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
1625
+ return rowToWorkflowInvocation(updated);
1626
+ }
1568
1627
  getWorkflowInvocation(id) {
1569
1628
  const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
1570
1629
  return row ? rowToWorkflowInvocation(row) : undefined;
package/docs/USAGE.md CHANGED
@@ -345,6 +345,7 @@ into a deduped one-shot workflow loop when route capacity allows:
345
345
 
346
346
  ```bash
347
347
  cat task-created-event.json | loops events handle todos-task \
348
+ --template task-lifecycle \
348
349
  --provider codewith \
349
350
  --auth-profile-pool account004,account005,account006 \
350
351
  --permission-mode bypass \
@@ -362,6 +363,20 @@ approval-required, or `no-auto` tasks. This guard exists even when the upstream
362
363
  `@hasna/events` webhook filter is misconfigured, so task existence alone is not
363
364
  permission to execute agent work.
364
365
 
366
+ By default, `todos-task` routes use `todos-task-worker-verifier` for backwards
367
+ compatibility. Use `--template task-lifecycle` when the task should run the full
368
+ triage -> planner -> worker -> verifier lifecycle. The route rejects unrelated
369
+ workflow templates such as `pr-review` so a todos task cannot accidentally use a
370
+ template with the wrong contract.
371
+ The lifecycle template inserts deterministic gate steps after triage and after
372
+ planning. If either agent marks the task blocked, omits its contextual
373
+ `openloops:triage=go task=<id> event=<event-id>` /
374
+ `openloops:planner=go task=<id> event=<event-id>` marker comment, or the task
375
+ is marked no-auto/manual/approval-required, the next agent step is not started.
376
+ Use `--triage-auth-profile`, `--planner-auth-profile`,
377
+ `--worker-auth-profile`, and `--verifier-auth-profile` for exact Codewith role
378
+ profiles, or use `--auth-profile-pool` for deterministic role rotation.
379
+
365
380
  Use route throttles to avoid stampeding agents when a producer creates many
366
381
  tasks at once:
367
382
 
@@ -426,6 +441,7 @@ locks, or non-pending states stay queued in todos and are not routed:
426
441
  ```bash
427
442
  loops events drain todos-task \
428
443
  --todos-project "$HOME/.hasna/loops" \
444
+ --template task-lifecycle \
429
445
  --task-list repoops-pr-queue \
430
446
  --tags auto:route \
431
447
  --project-path-prefix /home/hasna/workspace/hasna/opensource \
@@ -461,6 +477,7 @@ For the Hasna OSS task-created route, keep the drain deterministic and narrow:
461
477
  loops routes schedule todos-task oss-task-route-drain \
462
478
  --every 5m \
463
479
  --todos-project "$HOME/.hasna/loops" \
480
+ --template task-lifecycle \
464
481
  --project-path-prefix /home/hasna/workspace/hasna/opensource \
465
482
  --tags auto:route \
466
483
  --provider codewith \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.51",
3
+ "version": "0.3.53",
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",