@hasna/loops 0.3.56 → 0.3.57
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/README.md +188 -14
- package/dist/cli/index.js +1325 -175
- package/dist/daemon/index.js +143 -19
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4031 -2876
- package/dist/lib/health.d.ts +2 -2
- package/dist/lib/store.d.ts +3 -0
- package/dist/lib/store.js +46 -8
- package/dist/lib/templates.d.ts +4 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.js +5821 -0
- package/dist/sdk/index.js +134 -16
- package/docs/AUTOMATION_RUNTIME_DESIGN.md +228 -0
- package/docs/USAGE.md +123 -15
- package/package.json +9 -3
package/dist/lib/health.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Loop, LoopRun } from "../types.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
-
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
3
|
+
export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
|
|
4
4
|
export interface RunFailureSignal {
|
|
5
5
|
classification: RunFailureClassification;
|
|
6
6
|
fingerprint: string;
|
|
@@ -34,7 +34,7 @@ export interface LoopExpectationResult {
|
|
|
34
34
|
loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt">;
|
|
35
35
|
ok: boolean;
|
|
36
36
|
check: {
|
|
37
|
-
id: "latest-run-succeeded";
|
|
37
|
+
id: "latest-run-succeeded" | "route-functional-health";
|
|
38
38
|
status: "pass" | "fail" | "warn";
|
|
39
39
|
message: string;
|
|
40
40
|
};
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -140,6 +140,9 @@ export declare class Store {
|
|
|
140
140
|
project: number;
|
|
141
141
|
projectGroup?: number;
|
|
142
142
|
};
|
|
143
|
+
requeueWorkflowWorkItem(id: string, patch?: {
|
|
144
|
+
reason?: string;
|
|
145
|
+
}): WorkflowWorkItem;
|
|
143
146
|
admitWorkflowWorkItem(id: string, patch: {
|
|
144
147
|
workflowId: string;
|
|
145
148
|
loopId: string;
|
package/dist/lib/store.js
CHANGED
|
@@ -459,6 +459,9 @@ function validateTarget(value, label, opts) {
|
|
|
459
459
|
}
|
|
460
460
|
if (value.model !== undefined)
|
|
461
461
|
assertString(value.model, `${label}.model`);
|
|
462
|
+
if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
|
|
463
|
+
throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
|
|
464
|
+
}
|
|
462
465
|
if (value.variant !== undefined)
|
|
463
466
|
assertString(value.variant, `${label}.variant`);
|
|
464
467
|
if (value.agent !== undefined)
|
|
@@ -1761,7 +1764,7 @@ class Store {
|
|
|
1761
1764
|
if (!sourceDedupeKey)
|
|
1762
1765
|
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1763
1766
|
const now = nowIso();
|
|
1764
|
-
const claimableStatuses = ["queued", "deferred"
|
|
1767
|
+
const claimableStatuses = ["queued", "deferred"];
|
|
1765
1768
|
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1766
1769
|
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1767
1770
|
const result = this.db.query(`UPDATE workflow_invocations
|
|
@@ -1842,28 +1845,35 @@ class Store {
|
|
|
1842
1845
|
project_group=excluded.project_group,
|
|
1843
1846
|
priority=excluded.priority,
|
|
1844
1847
|
status=CASE
|
|
1845
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
|
|
1848
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
|
|
1846
1849
|
THEN workflow_work_items.status
|
|
1847
1850
|
ELSE excluded.status
|
|
1848
1851
|
END,
|
|
1849
1852
|
workflow_id=CASE
|
|
1850
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
|
|
1853
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
|
|
1851
1854
|
ELSE NULL
|
|
1852
1855
|
END,
|
|
1853
1856
|
loop_id=CASE
|
|
1854
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
|
|
1857
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
|
|
1855
1858
|
ELSE NULL
|
|
1856
1859
|
END,
|
|
1857
1860
|
workflow_run_id=CASE
|
|
1858
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
|
|
1861
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
|
|
1859
1862
|
ELSE NULL
|
|
1860
1863
|
END,
|
|
1861
1864
|
lease_expires_at=CASE
|
|
1862
|
-
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
|
|
1865
|
+
WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
|
|
1863
1866
|
ELSE NULL
|
|
1864
1867
|
END,
|
|
1865
1868
|
next_attempt_at=excluded.next_attempt_at,
|
|
1866
|
-
last_reason=
|
|
1869
|
+
last_reason=CASE
|
|
1870
|
+
WHEN workflow_work_items.attempts > 0
|
|
1871
|
+
AND workflow_work_items.status IN ('queued', 'deferred')
|
|
1872
|
+
AND workflow_work_items.last_reason IS NOT NULL
|
|
1873
|
+
AND excluded.last_reason IS NOT NULL
|
|
1874
|
+
THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
|
|
1875
|
+
ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
|
|
1876
|
+
END,
|
|
1867
1877
|
updated_at=excluded.updated_at`).run({
|
|
1868
1878
|
$id: id,
|
|
1869
1879
|
$routeKey: input.routeKey,
|
|
@@ -1916,11 +1926,39 @@ class Store {
|
|
|
1916
1926
|
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;
|
|
1917
1927
|
return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
|
|
1918
1928
|
}
|
|
1929
|
+
requeueWorkflowWorkItem(id, patch = {}) {
|
|
1930
|
+
const current = this.getWorkflowWorkItem(id);
|
|
1931
|
+
if (!current)
|
|
1932
|
+
throw new Error(`workflow work item not found: ${id}`);
|
|
1933
|
+
const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
|
|
1934
|
+
if (!requeueableStatuses.includes(current.status)) {
|
|
1935
|
+
throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
|
|
1936
|
+
}
|
|
1937
|
+
const now = nowIso();
|
|
1938
|
+
const reason = patch.reason?.trim() || `requeued from ${current.status}`;
|
|
1939
|
+
const placeholders = requeueableStatuses.map(() => "?").join(",");
|
|
1940
|
+
const res = this.db.query(`UPDATE workflow_work_items
|
|
1941
|
+
SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
|
|
1942
|
+
next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
|
|
1943
|
+
WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
|
|
1944
|
+
const item = this.getWorkflowWorkItem(id);
|
|
1945
|
+
if (!item)
|
|
1946
|
+
throw new Error(`workflow work item not found after requeue: ${id}`);
|
|
1947
|
+
if (res.changes !== 1)
|
|
1948
|
+
throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
|
|
1949
|
+
return item;
|
|
1950
|
+
}
|
|
1919
1951
|
admitWorkflowWorkItem(id, patch) {
|
|
1920
1952
|
const now = nowIso();
|
|
1921
1953
|
const res = this.db.query(`UPDATE workflow_work_items
|
|
1922
1954
|
SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
|
|
1923
|
-
next_attempt_at=NULL,
|
|
1955
|
+
next_attempt_at=NULL,
|
|
1956
|
+
lease_expires_at=NULL,
|
|
1957
|
+
last_reason=CASE
|
|
1958
|
+
WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
|
|
1959
|
+
ELSE COALESCE($reason, last_reason)
|
|
1960
|
+
END,
|
|
1961
|
+
updated_at=$updated
|
|
1924
1962
|
WHERE id=$id AND status IN ('queued', 'deferred')`).run({
|
|
1925
1963
|
$id: id,
|
|
1926
1964
|
$workflowId: patch.workflowId,
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface TodosTaskWorkflowTemplateInput {
|
|
|
41
41
|
worktreeRoot?: string;
|
|
42
42
|
worktreeBranchPrefix?: string;
|
|
43
43
|
timeoutMs?: TimeoutMs;
|
|
44
|
+
verifierIdleTimeoutMs?: number;
|
|
45
|
+
prHandoff?: boolean;
|
|
44
46
|
eventId?: string;
|
|
45
47
|
eventType?: string;
|
|
46
48
|
}
|
|
@@ -78,6 +80,7 @@ export interface EventWorkflowTemplateInput {
|
|
|
78
80
|
worktreeRoot?: string;
|
|
79
81
|
worktreeBranchPrefix?: string;
|
|
80
82
|
timeoutMs?: TimeoutMs;
|
|
83
|
+
verifierIdleTimeoutMs?: number;
|
|
81
84
|
}
|
|
82
85
|
export interface BoundedAgentWorkflowTemplateInput {
|
|
83
86
|
name?: string;
|
|
@@ -110,6 +113,7 @@ export interface BoundedAgentWorkflowTemplateInput {
|
|
|
110
113
|
worktreeRoot?: string;
|
|
111
114
|
worktreeBranchPrefix?: string;
|
|
112
115
|
timeoutMs?: TimeoutMs;
|
|
116
|
+
verifierIdleTimeoutMs?: number;
|
|
113
117
|
}
|
|
114
118
|
export type LoopTemplateSourceFilter = LoopTemplateSource | "all";
|
|
115
119
|
export interface ListLoopTemplatesOptions {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
export interface LoopsMcpToolMetadata {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
readOnly: boolean;
|
|
7
|
+
guarded?: boolean;
|
|
8
|
+
requiresEnv?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
|
|
11
|
+
export declare function createLoopsMcpServer(): McpServer;
|
|
12
|
+
export declare function listToolsForCli(): LoopsMcpToolMetadata[];
|