@hasna/workflows 0.1.0 → 0.1.2
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 +75 -18
- package/dist/agent-tools.js +1937 -1
- package/dist/cli/index.js +2762 -7
- package/dist/handlers.js +96 -1
- package/dist/index.js +2062 -9
- package/dist/mcp/index.js +1994 -16
- package/dist/serve/index.js +1992 -9
- package/dist/service.js +1911 -7
- package/hasna.contract.json +2 -2
- package/package.json +7 -5
- package/types/agent-tools.d.ts +2 -6
- package/types/daemon.d.ts +119 -0
- package/types/expr.d.ts +58 -0
- package/types/graph.d.ts +80 -0
- package/types/handlers.d.ts +1 -5
- package/types/lanes/claude.d.ts +16 -0
- package/types/lanes/codex.d.ts +12 -0
- package/types/lanes/cursor.d.ts +13 -0
- package/types/lanes/grok.d.ts +16 -0
- package/types/lanes/index.d.ts +14 -0
- package/types/lanes/types.d.ts +64 -0
- package/types/render.d.ts +12 -0
- package/types/secrets.d.ts +30 -0
- package/types/service.d.ts +18 -0
- package/types/session.d.ts +51 -0
- package/types/store.d.ts +86 -0
- package/types/wal.d.ts +85 -0
package/hasna.contract.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"name": "workflows",
|
|
4
4
|
"class": "service",
|
|
5
5
|
"contractVersion": "v1",
|
|
6
|
-
"kitVersion": "0.14.
|
|
7
|
-
"description": "
|
|
6
|
+
"kitVersion": "0.14.1",
|
|
7
|
+
"description": "Universal graph workflow app: graph language (start/step/decision/while/end), three-table run store, session WAL, claims/leases daemon, four lane adapters (claude, codex, cursor, grok), and a full CLI on four surfaces (CLI, MCP, serve, SDK).",
|
|
8
8
|
"bins": [
|
|
9
9
|
"workflows",
|
|
10
10
|
"workflows-mcp",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/workflows",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Universal graph workflow app
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Universal graph workflow app — graph language (start/step/decision/while/end), three-table store, session WAL, daemon with claims/leases, four lane adapters (claude/codex/cursor/grok), and a full CLI on four surfaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -65,16 +65,18 @@
|
|
|
65
65
|
"url": "https://github.com/hasna/apps/issues"
|
|
66
66
|
},
|
|
67
67
|
"engines": {
|
|
68
|
-
"bun": ">=1.0.0"
|
|
69
|
-
"node": ">=20.0.0"
|
|
68
|
+
"bun": ">=1.0.0"
|
|
70
69
|
},
|
|
71
70
|
"author": "Hasna",
|
|
72
71
|
"license": "Apache-2.0",
|
|
73
72
|
"dependencies": {
|
|
73
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.234",
|
|
74
|
+
"@cursor/sdk": "^1.0.28",
|
|
75
|
+
"@openai/codex-sdk": "^0.147.0",
|
|
74
76
|
"commander": "^13.1.0"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
|
-
"@hasna/contracts": "0.14.
|
|
79
|
+
"@hasna/contracts": "0.14.1",
|
|
78
80
|
"@types/bun": "latest",
|
|
79
81
|
"@types/pg": "^8.15.6",
|
|
80
82
|
"pg": "^8.16.3",
|
package/types/agent-tools.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP tool registry for @hasna/workflows — the agent interface layer over
|
|
3
|
-
* WorkflowsService. Slice 1 ships identity/health tools; the graph language,
|
|
4
|
-
* store, and daemon slices add their tools here.
|
|
5
|
-
*/
|
|
6
1
|
import type { WorkflowsService } from "./service.js";
|
|
7
2
|
export interface McpTool {
|
|
8
3
|
name: string;
|
|
@@ -10,6 +5,7 @@ export interface McpTool {
|
|
|
10
5
|
inputSchema: {
|
|
11
6
|
type: "object";
|
|
12
7
|
properties: Record<string, unknown>;
|
|
8
|
+
required?: string[];
|
|
13
9
|
};
|
|
14
10
|
}
|
|
15
11
|
export interface ToolCallResult {
|
|
@@ -20,4 +16,4 @@ export interface ToolCallResult {
|
|
|
20
16
|
isError: boolean;
|
|
21
17
|
}
|
|
22
18
|
export declare function workflowsTools(_service: WorkflowsService): McpTool[];
|
|
23
|
-
export declare function callWorkflowTool(service: WorkflowsService, name: string,
|
|
19
|
+
export declare function callWorkflowTool(service: WorkflowsService, name: string, args: Record<string, unknown>): Promise<ToolCallResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type WorkflowGraph } from "./graph.js";
|
|
2
|
+
import type { RunRow, WorkflowsStore } from "./store.js";
|
|
3
|
+
import { SessionWAL } from "./wal.js";
|
|
4
|
+
import { type LaneJob, type LaneResult } from "./lanes/types.js";
|
|
5
|
+
export type { LaneJob, LaneKind, LaneResult } from "./lanes/types.js";
|
|
6
|
+
export { LANE_KINDS } from "./lanes/types.js";
|
|
7
|
+
export type LaneRunner = (job: LaneJob) => Promise<LaneResult>;
|
|
8
|
+
export interface Lease {
|
|
9
|
+
runId: string;
|
|
10
|
+
worker: string;
|
|
11
|
+
expiresAtMs: number;
|
|
12
|
+
fencing: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DaemonOptions {
|
|
15
|
+
worker?: string;
|
|
16
|
+
leaseTtlMs?: number;
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
maxDispatchPerCycle?: number;
|
|
19
|
+
maxStepsPerCycle?: number;
|
|
20
|
+
time?: () => number;
|
|
21
|
+
laneRunner?: LaneRunner;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
/** When set, reap() dispatches/advances ONLY this run (foreground
|
|
24
|
+
* single-run isolation). Live-verify observation 2026-08-25: the
|
|
25
|
+
* foreground driver reaped every pending/running run in the shared store —
|
|
26
|
+
* an orphaned run executed its step inside the foreground run's cycle and
|
|
27
|
+
* blocked it. */
|
|
28
|
+
isolatedRunId?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ReapReport {
|
|
31
|
+
expired: number;
|
|
32
|
+
interrupted: number;
|
|
33
|
+
requeued: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
dispatched: number;
|
|
36
|
+
advanced: number;
|
|
37
|
+
completed: number;
|
|
38
|
+
}
|
|
39
|
+
export type StepOutcome = {
|
|
40
|
+
kind: "advanced";
|
|
41
|
+
nodeId: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "completed";
|
|
44
|
+
} | {
|
|
45
|
+
kind: "failed";
|
|
46
|
+
error: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "noop";
|
|
49
|
+
} | {
|
|
50
|
+
kind: "blocked";
|
|
51
|
+
};
|
|
52
|
+
/** Execute a shell command with a bounded timeout (the built-in step executor). */
|
|
53
|
+
export declare function runCommand(job: LaneJob): LaneResult;
|
|
54
|
+
export declare class WorkflowsDaemon {
|
|
55
|
+
private readonly store;
|
|
56
|
+
private readonly wal;
|
|
57
|
+
readonly worker: string;
|
|
58
|
+
private readonly leaseTtlMs;
|
|
59
|
+
private readonly maxAttempts;
|
|
60
|
+
private readonly maxDispatchPerCycle;
|
|
61
|
+
private readonly maxStepsPerCycle;
|
|
62
|
+
private readonly time;
|
|
63
|
+
private readonly laneRunner;
|
|
64
|
+
private readonly env;
|
|
65
|
+
private readonly leases;
|
|
66
|
+
private readonly fencingCounter;
|
|
67
|
+
private graphCache;
|
|
68
|
+
private isolatedRunId;
|
|
69
|
+
constructor(store: WorkflowsStore, wal: SessionWAL, options?: DaemonOptions);
|
|
70
|
+
/** Foreground single-run mode: reap() touches only this run from now on. */
|
|
71
|
+
isolateRun(runId: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Recover graph definitions from the WAL. A replacement daemon (crash
|
|
74
|
+
* recovery) must be able to advance runs whose graphs were registered by
|
|
75
|
+
* the previous instance — the in-memory cache alone made `runs resume` and
|
|
76
|
+
* torn-run repair dead paths across processes (measured live 2026-08-25:
|
|
77
|
+
* "graph ... not found in daemon graph cache"). The latest registration of
|
|
78
|
+
* a name wins.
|
|
79
|
+
*/
|
|
80
|
+
private replayRegisteredGraphs;
|
|
81
|
+
liveLeases(): ReadonlyMap<string, Lease>;
|
|
82
|
+
/** Acquire (or refresh) the lease on a run. Returns null while another
|
|
83
|
+
* worker's lease is live. */
|
|
84
|
+
claim(runId: string): Lease | null;
|
|
85
|
+
/** Extend a lease. Only the fencing-holding worker may. */
|
|
86
|
+
heartbeat(runId: string, fencing: number): boolean;
|
|
87
|
+
/** Release a lease. Only the fencing-holding worker may. */
|
|
88
|
+
release(runId: string, fencing: number): boolean;
|
|
89
|
+
/** One bounded cycle: expire leases -> repair torn runs -> dispatch pending
|
|
90
|
+
* runs -> advance every claimed run one step. */
|
|
91
|
+
reap(): Promise<ReapReport>;
|
|
92
|
+
/** Validate + persist a new run. Returns the pending run row. */
|
|
93
|
+
startRun(graph: WorkflowGraph, context: unknown): RunRow;
|
|
94
|
+
/** Execute ONE step of the graph for a claimed run. */
|
|
95
|
+
advanceRun(runId: string): Promise<StepOutcome>;
|
|
96
|
+
private loadGraph;
|
|
97
|
+
/** The condition scope: user context + steps + loop counter (i). */
|
|
98
|
+
private conditionScope;
|
|
99
|
+
private persistContext;
|
|
100
|
+
private executeStep;
|
|
101
|
+
/** Advance past a finished step node. Inside a while body with no next,
|
|
102
|
+
* the iteration completes and the while node re-enters. */
|
|
103
|
+
private afterNode;
|
|
104
|
+
private executeWhile;
|
|
105
|
+
private recordStepResult;
|
|
106
|
+
private completeRun;
|
|
107
|
+
private failRun;
|
|
108
|
+
}
|
|
109
|
+
export interface RunToCompletionOptions {
|
|
110
|
+
laneRunner?: LaneRunner;
|
|
111
|
+
time?: () => number;
|
|
112
|
+
env?: Record<string, string>;
|
|
113
|
+
maxCycles?: number;
|
|
114
|
+
/** Pre-set the daemon's isolation; the foreground driver always isolates
|
|
115
|
+
* to its own run anyway (see isolateRun in runGraphToCompletion). */
|
|
116
|
+
isolatedRunId?: string;
|
|
117
|
+
}
|
|
118
|
+
/** Drive a run to a terminal state with bounded cycles. Returns the final row. */
|
|
119
|
+
export declare function runGraphToCompletion(store: WorkflowsStore, wal: SessionWAL, graph: WorkflowGraph, context: unknown, options?: RunToCompletionOptions): Promise<RunRow>;
|
package/types/expr.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, bounded expression language for decision/while conditions.
|
|
3
|
+
*
|
|
4
|
+
* Grammar (recursive descent):
|
|
5
|
+
* expr := or
|
|
6
|
+
* or := and ("or" and)*
|
|
7
|
+
* and := not ("and" not)*
|
|
8
|
+
* not := "not" not | comparison
|
|
9
|
+
* comparison := primary (("==" | "!=" | "<" | "<=" | ">" | ">=") primary)?
|
|
10
|
+
* primary := "(" expr ")" | literal | path
|
|
11
|
+
* literal := number | string | true | false
|
|
12
|
+
* path := identifier ("." identifier)* — resolves against the run context
|
|
13
|
+
*
|
|
14
|
+
* Values: numbers, strings, booleans. Comparisons require the same type on
|
|
15
|
+
* both sides. A path that does not resolve evaluates to undefined, so the
|
|
16
|
+
* canonical while-loop guard `steps.check.ok != true` is TRUE before the
|
|
17
|
+
* body has run — a missing step is simply not ok. Ordering comparisons
|
|
18
|
+
* against an unknown path are an error; equality is defined.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ExprSyntaxError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class ExprEvalError extends Error {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
export type ExprValue = number | string | boolean;
|
|
27
|
+
export interface Token {
|
|
28
|
+
kind: "number" | "string" | "ident" | "op" | "lparen" | "rparen";
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function tokenize(input: string): Token[];
|
|
32
|
+
export type AstNode = {
|
|
33
|
+
kind: "literal";
|
|
34
|
+
value: ExprValue;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "path";
|
|
37
|
+
segments: string[];
|
|
38
|
+
} | {
|
|
39
|
+
kind: "comparison";
|
|
40
|
+
op: string;
|
|
41
|
+
left: AstNode;
|
|
42
|
+
right: AstNode;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "and";
|
|
45
|
+
left: AstNode;
|
|
46
|
+
right: AstNode;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "or";
|
|
49
|
+
left: AstNode;
|
|
50
|
+
right: AstNode;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "not";
|
|
53
|
+
operand: AstNode;
|
|
54
|
+
};
|
|
55
|
+
/** Parse and return the AST without evaluating (used by validate()). */
|
|
56
|
+
export declare function parseExpr(input: string): AstNode;
|
|
57
|
+
/** Evaluate a condition expression against a run context. */
|
|
58
|
+
export declare function evaluateExpr(input: string, context: Record<string, unknown>): ExprValue;
|
package/types/graph.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export declare const GRAPH_NODE_TYPES: readonly ["start", "step", "decision", "while", "end"];
|
|
2
|
+
export type GraphNodeType = (typeof GRAPH_NODE_TYPES)[number];
|
|
3
|
+
export declare const LANE_KINDS: readonly ["claude", "codex", "cursor", "grok"];
|
|
4
|
+
export type LaneKind = (typeof LANE_KINDS)[number];
|
|
5
|
+
export interface StartNode {
|
|
6
|
+
id: string;
|
|
7
|
+
type: "start";
|
|
8
|
+
next?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StepNode {
|
|
11
|
+
id: string;
|
|
12
|
+
type: "step";
|
|
13
|
+
/** Lane that executes the work. Default "claude". */
|
|
14
|
+
lane?: LaneKind;
|
|
15
|
+
/** Prompt handed to the lane agent. Required unless `command` is set. */
|
|
16
|
+
prompt?: string;
|
|
17
|
+
/** Shell command alternative to a lane prompt. */
|
|
18
|
+
command?: string;
|
|
19
|
+
next?: string;
|
|
20
|
+
/** Retries on lane failure. Default 0. */
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
/** Memoize the node output by input hash across runs. Default false. */
|
|
23
|
+
memo?: boolean;
|
|
24
|
+
/** Per-invocation timeout budget in ms. Default 120000. */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DecisionNode {
|
|
28
|
+
id: string;
|
|
29
|
+
type: "decision";
|
|
30
|
+
/** Boolean expression over the run context (see expr.ts). */
|
|
31
|
+
condition: string;
|
|
32
|
+
then?: string;
|
|
33
|
+
else?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface WhileNode {
|
|
36
|
+
id: string;
|
|
37
|
+
type: "while";
|
|
38
|
+
/** Loop-while-true; `i` is the loop counter in the condition scope. */
|
|
39
|
+
condition: string;
|
|
40
|
+
/** Node ids executed per iteration; steps/decisions only in v1. */
|
|
41
|
+
body: string[];
|
|
42
|
+
/** REQUIRED finite loop bound (SOL finite-budget rule). */
|
|
43
|
+
maxIterations: number;
|
|
44
|
+
/** Exit edge taken once the condition fails or the bound is reached. */
|
|
45
|
+
next?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface EndNode {
|
|
48
|
+
id: string;
|
|
49
|
+
type: "end";
|
|
50
|
+
}
|
|
51
|
+
export type GraphNode = StartNode | StepNode | DecisionNode | WhileNode | EndNode;
|
|
52
|
+
export interface WorkflowGraph {
|
|
53
|
+
name: string;
|
|
54
|
+
version: string;
|
|
55
|
+
nodes: GraphNode[];
|
|
56
|
+
}
|
|
57
|
+
export interface GraphIssue {
|
|
58
|
+
/** Dotted path to the offending member, e.g. "nodes.build" or "nodes.w.body". */
|
|
59
|
+
path: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
export interface GraphValidation {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
issues: GraphIssue[];
|
|
65
|
+
}
|
|
66
|
+
export declare function findNode(graph: WorkflowGraph, id: string): GraphNode | undefined;
|
|
67
|
+
export interface Edge {
|
|
68
|
+
from: string;
|
|
69
|
+
to: string;
|
|
70
|
+
}
|
|
71
|
+
/** Every explicit edge in the graph (next/then/else). While-body edges are
|
|
72
|
+
* internal and validated separately; the while's repetition is expressed by
|
|
73
|
+
* its condition, not by an edge. */
|
|
74
|
+
export declare function collectEdges(graph: WorkflowGraph): Edge[];
|
|
75
|
+
/** Nodes reachable from the (unique) start node over explicit edges. */
|
|
76
|
+
export declare function reachableNodes(graph: WorkflowGraph): string[];
|
|
77
|
+
/** Explicit cycles via depth-first search with white/grey/black coloring. */
|
|
78
|
+
export declare function findCycles(graph: WorkflowGraph): string[][];
|
|
79
|
+
/** Validate a graph. Every rule has a named, checkable failure. */
|
|
80
|
+
export declare function validateGraph(graph: WorkflowGraph): GraphValidation;
|
package/types/handlers.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP request handler for workflows-serve. Interface layer over
|
|
3
|
-
* WorkflowsService; all business logic lives in the service.
|
|
4
|
-
*/
|
|
5
1
|
import type { WorkflowsService } from "./service.js";
|
|
6
2
|
export declare function jsonResponse(body: unknown, status?: number): Response;
|
|
7
|
-
export declare function createRequestHandler(service: WorkflowsService): (req: Request) => Response
|
|
3
|
+
export declare function createRequestHandler(service: WorkflowsService): (req: Request) => Response | Promise<Response>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude lane adapter — @anthropic-ai/claude-agent-sdk.
|
|
3
|
+
*
|
|
4
|
+
* The SDK's `query({ prompt, options })` returns an async generator of
|
|
5
|
+
* SDKMessage; assistant text blocks are accumulated into the result output.
|
|
6
|
+
* When the SDK package is unavailable, the adapter falls back to the local
|
|
7
|
+
* `claude` CLI in print mode (`claude -p <prompt>`).
|
|
8
|
+
*/
|
|
9
|
+
import { type LaneAdapter } from "./types.js";
|
|
10
|
+
export interface ClaudeAdapterDeps {
|
|
11
|
+
/** SDK loader — injectable for tests; defaults to the real package import. */
|
|
12
|
+
sdkLoader?: () => Promise<unknown>;
|
|
13
|
+
/** CLI fallback binary name. Default "claude". */
|
|
14
|
+
cliPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createClaudeAdapter(deps?: ClaudeAdapterDeps): LaneAdapter;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex lane adapter — @openai/codex-sdk.
|
|
3
|
+
*
|
|
4
|
+
* `new Codex({ config, env })` -> `thread.run(prompt)` -> `Turn` whose
|
|
5
|
+
* `finalResponse` is the completed agent output. CLI fallback: `codex exec`.
|
|
6
|
+
*/
|
|
7
|
+
import { type LaneAdapter } from "./types.js";
|
|
8
|
+
export interface CodexAdapterDeps {
|
|
9
|
+
sdkLoader?: () => Promise<unknown>;
|
|
10
|
+
cliPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createCodexAdapter(deps?: CodexAdapterDeps): LaneAdapter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cursor lane adapter — @cursor/sdk, LOCAL mode (owner spec 2026-08-25).
|
|
3
|
+
*
|
|
4
|
+
* `new Cursor(...)` -> `Agent` -> `agent.send(prompt, options)` -> `Run`
|
|
5
|
+
* whose `.result` is the final text and `.status` one of
|
|
6
|
+
* finished | error | cancelled. CLI fallback: `cursor-agent`.
|
|
7
|
+
*/
|
|
8
|
+
import { type LaneAdapter } from "./types.js";
|
|
9
|
+
export interface CursorAdapterDeps {
|
|
10
|
+
sdkLoader?: () => Promise<unknown>;
|
|
11
|
+
cliPath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createCursorAdapter(deps?: CursorAdapterDeps): LaneAdapter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grok lane adapter — xAI Grok SDK.
|
|
3
|
+
*
|
|
4
|
+
* MEASURED 2026-08-25: no xAI Grok SDK exists on the npm registry under any
|
|
5
|
+
* candidate name (@xai/grok-sdk, @xai/sdk, @grok-ai/sdk, xai-sdk all absent;
|
|
6
|
+
* the only "grok-sdk" row is a squatted 0.0.1-security package and is never
|
|
7
|
+
* used). The adapter therefore drives the local `grok` CLI when present and
|
|
8
|
+
* reports LANE_DEPENDENCY_MISSING (exit 127) naming the missing substrate
|
|
9
|
+
* otherwise — the honest degradation contract, so a workflow fails loudly
|
|
10
|
+
* instead of pretending a lane ran.
|
|
11
|
+
*/
|
|
12
|
+
import { type LaneAdapter } from "./types.js";
|
|
13
|
+
export interface GrokAdapterDeps {
|
|
14
|
+
cliPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function createGrokAdapter(deps?: GrokAdapterDeps): LaneAdapter;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type LaneAdapter, type LaneJob, type LaneProbe, type LaneResult, type LaneKind } from "./types.js";
|
|
2
|
+
export type { LaneAdapter, LaneJob, LaneProbe, LaneResult, LaneKind } from "./types.js";
|
|
3
|
+
export { LANE_KINDS, LaneDependencyMissingError, LaneAdapterShapeError, binaryOnPath } from "./types.js";
|
|
4
|
+
export { createClaudeAdapter } from "./claude.js";
|
|
5
|
+
export { createCodexAdapter } from "./codex.js";
|
|
6
|
+
export { createCursorAdapter } from "./cursor.js";
|
|
7
|
+
export { createGrokAdapter } from "./grok.js";
|
|
8
|
+
export declare function resolveLane(kind: LaneKind): LaneAdapter;
|
|
9
|
+
/** Run one lane job through the registry (used by the daemon's default runner). */
|
|
10
|
+
export declare function runLaneJob(job: LaneJob): Promise<LaneResult>;
|
|
11
|
+
/** Re-run one lane's maturity check and report wired / not-ready-with-reason. */
|
|
12
|
+
export declare function probeLane(kind: LaneKind): Promise<LaneProbe>;
|
|
13
|
+
/** Probe all four lanes and return the wired-vs-not-ready registry shape. */
|
|
14
|
+
export declare function laneInventory(): Promise<LaneProbe[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const LANE_KINDS: readonly ["claude", "codex", "cursor", "grok"];
|
|
2
|
+
export type LaneKind = (typeof LANE_KINDS)[number];
|
|
3
|
+
export interface LaneJob {
|
|
4
|
+
lane: LaneKind;
|
|
5
|
+
/** Prompt handed to the lane agent. */
|
|
6
|
+
prompt?: string;
|
|
7
|
+
/** Shell command alternative (handled by the daemon's built-in executor). */
|
|
8
|
+
command?: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
/** Environment for the lane process — never credentials from the vault. */
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
/** Per-invocation budget in ms. Default 120000. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
model?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LaneResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
output: string;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A lane maturity probe: is the substrate actually wired on THIS machine
|
|
25
|
+
* right now, and if not, why. `wired` means a usable substrate exists —
|
|
26
|
+
* the SDK package importable or the CLI binary on PATH — NOT that a live
|
|
27
|
+
* authenticated call would succeed (that is the live-verify gate's job).
|
|
28
|
+
*/
|
|
29
|
+
export interface LaneProbe {
|
|
30
|
+
kind: LaneKind;
|
|
31
|
+
sdk: string;
|
|
32
|
+
wired: boolean;
|
|
33
|
+
/** How the lane would execute when wired (e.g. "sdk" or "cli"). */
|
|
34
|
+
via?: string;
|
|
35
|
+
/** Why the lane is not ready, when not wired. */
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface LaneAdapter {
|
|
39
|
+
kind: LaneKind;
|
|
40
|
+
run(job: LaneJob): Promise<LaneResult>;
|
|
41
|
+
/** Re-run the maturity check for this lane. */
|
|
42
|
+
probe(): Promise<LaneProbe>;
|
|
43
|
+
}
|
|
44
|
+
/** The substrate (SDK package or CLI binary) is not installed/available. */
|
|
45
|
+
export declare class LaneDependencyMissingError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
/** Raised when a lane SDK's shape differs from the documented contract. */
|
|
49
|
+
export declare class LaneAdapterShapeError extends Error {
|
|
50
|
+
constructor(message: string);
|
|
51
|
+
}
|
|
52
|
+
export declare const DEFAULT_LANE_TIMEOUT_MS = 120000;
|
|
53
|
+
/** Is a binary present on PATH (read-only existence check; never runs it)? */
|
|
54
|
+
export declare function binaryOnPath(binary: string): boolean;
|
|
55
|
+
/** Run a promise with a bounded wall-clock budget. */
|
|
56
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, what: string): Promise<T>;
|
|
57
|
+
/** Spawn a CLI binary with a bounded timeout; maps ENOENT to a missing-substrate error. */
|
|
58
|
+
export declare function spawnCli(cmd: string[], opts: {
|
|
59
|
+
cwd?: string;
|
|
60
|
+
env?: Record<string, string>;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
binaryName: string;
|
|
63
|
+
packageHint: string;
|
|
64
|
+
}): LaneResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph rendering (the `workflows graph <file>` command) — deterministic
|
|
3
|
+
* text, DOT, and JSON renderings of a workflow graph. Presentation only:
|
|
4
|
+
* all graph semantics live in graph.ts; this module never mutates.
|
|
5
|
+
*/
|
|
6
|
+
import { type WorkflowGraph } from "./graph.js";
|
|
7
|
+
/** Deterministic text rendering: one line per node in graph order. */
|
|
8
|
+
export declare function renderGraphText(graph: WorkflowGraph): string;
|
|
9
|
+
/** Graphviz DOT rendering (explicit edges solid, while-body edges dashed). */
|
|
10
|
+
export declare function renderGraphDot(graph: WorkflowGraph): string;
|
|
11
|
+
/** Machine-readable rendering: node registry + explicit edges + while bodies. */
|
|
12
|
+
export declare function renderGraphJson(graph: WorkflowGraph): Record<string, unknown>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The secrets write-gate (slice C).
|
|
3
|
+
*
|
|
4
|
+
* Before the daemon persists ANY node output, run result, or run context to
|
|
5
|
+
* the store or the session WAL, it calls assertNoSecrets. A payload carrying
|
|
6
|
+
* a credential-shaped value is refused with the offending path named and the
|
|
7
|
+
* value never rendered. The gate is deliberately conservative: a workflow
|
|
8
|
+
* that stores {"apiKey": "..."} is a workflow that will leak credentials
|
|
9
|
+
* downstream, so the gate blocks the write.
|
|
10
|
+
*
|
|
11
|
+
* NOTE on construction: the regex sources are built from joined fragments so
|
|
12
|
+
* this very file does not contain literal credential prefixes (the repo's CI
|
|
13
|
+
* secret scan would trip on them and the gate would block its own source).
|
|
14
|
+
*/
|
|
15
|
+
export declare class SecretsGateError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export interface SecretFinding {
|
|
19
|
+
/** Dotted JSON path to the offending value, e.g. "$.output" or "$.steps.build.result". */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Detector name. */
|
|
22
|
+
detector: string;
|
|
23
|
+
}
|
|
24
|
+
/** Recursively walk a JSON value and collect credential-shaped findings. */
|
|
25
|
+
export declare function scanForSecrets(value: unknown, path?: string): SecretFinding[];
|
|
26
|
+
/** Refuse a write whose payload carries a credential shape. Throws
|
|
27
|
+
* SecretsGateError naming the paths and detectors, never the values. */
|
|
28
|
+
export declare function assertNoSecrets(value: unknown, what: string): void;
|
|
29
|
+
/** Deep-copy a payload replacing credential-shaped values with a marker. */
|
|
30
|
+
export declare function redactDeep(value: unknown): unknown;
|
package/types/service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkflowGraph } from "./graph.js";
|
|
1
2
|
import type { HealthReport, ReadinessReport, WorkflowsConfig } from "./types.js";
|
|
2
3
|
export declare function packageVersion(): string;
|
|
3
4
|
/** Resolve the service configuration from env + explicit overrides. */
|
|
@@ -10,5 +11,22 @@ export declare class WorkflowsService {
|
|
|
10
11
|
constructor(config?: Partial<WorkflowsConfig>);
|
|
11
12
|
health(): HealthReport;
|
|
12
13
|
ready(): ReadinessReport;
|
|
14
|
+
/**
|
|
15
|
+
* The authenticated trigger: run a graph to a terminal state (bounded
|
|
16
|
+
* cycles) and return its summary. The HTTP layer authenticates; this
|
|
17
|
+
* method holds the business logic. When an idempotency key is supplied and
|
|
18
|
+
* a run carrying it already exists, that run's summary is returned instead
|
|
19
|
+
* of starting a new one.
|
|
20
|
+
*/
|
|
21
|
+
triggerRun(graph: WorkflowGraph, context?: unknown, opts?: {
|
|
22
|
+
maxCycles?: number;
|
|
23
|
+
idempotencyKey?: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
runId: string;
|
|
26
|
+
status: string;
|
|
27
|
+
error: string | null;
|
|
28
|
+
result: unknown;
|
|
29
|
+
reused: boolean;
|
|
30
|
+
}>;
|
|
13
31
|
}
|
|
14
32
|
export declare function createWorkflowsService(config?: Partial<WorkflowsConfig>): WorkflowsService;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SessionWAL } from "./wal.js";
|
|
2
|
+
import type { RunRow, WorkflowsStore } from "./store.js";
|
|
3
|
+
export interface RepairResult {
|
|
4
|
+
/** runs found running with no live claim */
|
|
5
|
+
interrupted: number;
|
|
6
|
+
/** torn runs marked interrupted (restorable via resume or the daemon) */
|
|
7
|
+
requeued: number;
|
|
8
|
+
/** torn runs failed after exhausting attempts */
|
|
9
|
+
failed: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_MAX_ATTEMPTS = 3;
|
|
12
|
+
export declare function repairTornRuns(store: WorkflowsStore, wal: SessionWAL, opts?: {
|
|
13
|
+
maxAttempts?: number;
|
|
14
|
+
now?: number;
|
|
15
|
+
}): RepairResult;
|
|
16
|
+
/** Stable hash of a JSON-serializable payload. */
|
|
17
|
+
export declare function inputHash(payload: unknown): string;
|
|
18
|
+
/** Memo key: graphName:nodeId:<inputHash>. */
|
|
19
|
+
export declare function memoKey(graphName: string, nodeId: string, input: unknown): string;
|
|
20
|
+
/** Look up a memo by input; a hit increments hitCount and returns the row. */
|
|
21
|
+
export declare function tryMemoHit(store: WorkflowsStore, graphName: string, nodeId: string, input: unknown): import("./store.js").MemoRow | undefined;
|
|
22
|
+
/** Persist a node output for future runs with the same input. */
|
|
23
|
+
export declare function recordMemo(store: WorkflowsStore, graphName: string, nodeId: string, input: unknown, output: unknown): void;
|
|
24
|
+
/** Requeue a cancelled run's in-flight nodes (used by cancel + resume). */
|
|
25
|
+
export declare function resetRunNodes(store: WorkflowsStore, run: RunRow): void;
|
|
26
|
+
export interface RestoreResult {
|
|
27
|
+
runId: string;
|
|
28
|
+
status: string;
|
|
29
|
+
/** running/pending node rows returned to pending so the run can continue. */
|
|
30
|
+
nodesRestored: number;
|
|
31
|
+
/** completed node rows whose persisted output the restore reuses (memoized). */
|
|
32
|
+
memoizedNodes: number;
|
|
33
|
+
attempts: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Top-level `resume` — restore an INTERRUPTED run from its durable cursor.
|
|
37
|
+
*
|
|
38
|
+
* Distinct from `runs resume` (which requeues cancelled/failed runs): an
|
|
39
|
+
* interrupted run is one whose worker died mid-flight; its durable context
|
|
40
|
+
* (__wf cursor + steps) and completed node outputs are already persisted, so
|
|
41
|
+
* restore reuses them (memoization) and only running/pending nodes return to
|
|
42
|
+
* pending. The daemon then continues from the last durable checkpoint.
|
|
43
|
+
*/
|
|
44
|
+
export declare function restoreInterruptedRun(store: WorkflowsStore, runId: string): RestoreResult;
|
|
45
|
+
/**
|
|
46
|
+
* Idempotent run lookup: the run whose context carries the given idempotency
|
|
47
|
+
* key, if any. The three-table law forbids an extra column, so the key lives
|
|
48
|
+
* in the reserved __wf namespace of context_json; the scan is bounded by the
|
|
49
|
+
* store's own listRuns cap (1000 rows).
|
|
50
|
+
*/
|
|
51
|
+
export declare function findRunByIdempotencyKey(store: WorkflowsStore, key: string): RunRow | undefined;
|