@hasna/workflows 0.1.4 → 0.2.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/README.md CHANGED
@@ -1,112 +1,194 @@
1
1
  # @hasna/workflows
2
2
 
3
- Universal graph workflow app: a graph language (with `while` nodes), a
4
- three-table store, session WAL with torn-run repair, memoization, a secrets
5
- write-gate, a claims/leases daemon, and four lane adapters (claude, codex,
6
- cursor, grok) — one domain implementation, four interface surfaces.
7
-
8
- ## Surfaces
9
-
10
- | surface | bin / export | what it is |
11
- |---|---|---|
12
- | CLI | `workflows` | init, validate, graph (render), run (--context/--input/--idempotency-key), runs list/show/cancel/resume/events, nodes list/show, sessions list/pull, machines list/status, lanes list/probe, daemon start/status/stop, memos list/clear, resume (interrupted-run restore), serve, repair (+ version, health, info) |
13
- | MCP | `workflows-mcp` | stdio MCP server for coding agents (7 tools incl. workflows_validate, workflows_run, workflows_runs_list, workflows_lanes_list) |
14
- | Serve | `workflows-serve` (or `workflows serve`) | HTTP server (`/health`, `/ready`, `/version`, authenticated `/trigger`, `/openapi.json`); binds `127.0.0.1` by default; `/trigger` requires `WORKFLOWS_API_KEY` + Bearer token |
15
- | SDK | `./sdk` (`@hasna/workflows/sdk`) | importable module surface (exports `WorkflowsService`, `createWorkflowsService`, `createRequestHandler`, `workflowsTools`, `callWorkflowTool`) |
16
-
17
- All three bins answer `--version` / `--help` before binding or serving.
18
-
19
- ## The graph language (v1)
20
-
21
- A workflow is a JSON graph with five node types — `start`, `step`, `decision`,
22
- `while`, `end` — one `start`, at least one `end`, explicit edges only
23
- (`next`/`then`/`else`), and no explicit cycles: repetition lives in the `while`
24
- node's condition, which is REQUIRED to carry a finite `maxIterations` bound.
25
- `validate` reports every rule violation with a dotted path. Conditions are a
26
- bounded expression language (comparisons, `and`/`or`/`not`, parens, dotted
27
- paths like `steps.build.exitCode`, and the loop counter `i`).
28
-
29
- ```json
30
- {
31
- "name": "retry-until-green",
32
- "version": "1.0.0",
33
- "nodes": [
34
- { "id": "start", "type": "start", "next": "w" },
35
- { "id": "w", "type": "while", "condition": "steps.check.ok != true",
36
- "body": ["work", "check"], "maxIterations": 5, "next": "done" },
37
- { "id": "work", "type": "step", "lane": "claude", "prompt": "fix it" },
38
- { "id": "check", "type": "step", "command": "bun test" },
39
- { "id": "done", "type": "end" }
40
- ]
41
- }
3
+ Workflows executes bounded graphs and records their runs, nodes and results. The CLI,
4
+ stdio MCP server and `@hasna/workflows/sdk` use the hosted HTTP API. Their default
5
+ authority is `https://api.hasna.com/workflows/v1`. A missing credential or an
6
+ unreachable service fails visibly; clients never create a local application database.
7
+
8
+ ## Set up with your agent
9
+
10
+ Copy this prompt to your agent:
11
+
12
+ > Install the published `@hasna/workflows` package. Connect its `workflows-mcp`
13
+ > stdio server using this harness's supported MCP configuration. Target
14
+ > `https://api.hasna.com/workflows/v1` and use this station's own supported
15
+ > credential resolver. Run `workflows setup` with your credential owner's environment
16
+ > projection, then verify `workflows whoami`, `workflows ready --json` and
17
+ > `workflows runs list --json`, then call `workflows_ready` through the harness.
18
+ > Finish only when those real consumers work. Never copy a credential from another
19
+ > station or fall back to a local database.
20
+
21
+ Raw setup is also available:
22
+
23
+ ```sh
24
+ npm install --prefix <retained-version-runtime> @hasna/workflows
25
+ workflows --help
26
+ workflows setup
27
+ workflows whoami
28
+ workflows ready --json
29
+ workflows runs list --json
42
30
  ```
43
31
 
44
- ## Execution model
45
-
46
- - **Store**: exactly three SQLite tables — `runs`, `run_nodes`, `memos`
47
- (input-hash memoization across runs).
48
- - **Session WAL**: append-only JSONL journal with per-entry sha256 checksums;
49
- torn tails are truncated and reported on replay.
50
- - **Torn-run repair**: a run left `running` with no live claim is marked
51
- `interrupted` (attempts + 1, up to a bound) or failed; `repair` exposes it.
52
- An interrupted run continues from its durable cursor either via the daemon
53
- (which dispatches interrupted runs) or via top-level `resume <run-id>`,
54
- which restores it and reuses memoized node outputs.
55
- - **Daemon**: fencing-tokened, expiring, WAL-recorded claims; heartbeat and
56
- release are fencing-checked; the reaper expires stale leases, repairs torn
57
- runs, dispatches pending + interrupted runs, and advances each run ONE step
58
- per bounded cycle (maxDispatchPerCycle / maxStepsPerCycle / maxCycles /
59
- maxIterations — every invocation carries a finite budget).
60
- - **Secrets write-gate**: no node output, run result, or context containing a
61
- credential-shaped value is ever persisted; the write is refused with the
62
- path and detector named.
63
- - **Lanes**: exactly four adapters — claude (@anthropic-ai/claude-agent-sdk),
64
- codex (@openai/codex-sdk), cursor (@cursor/sdk, local mode), grok (no xAI
65
- Grok SDK exists on npm — measured; local grok CLI). Each falls back to its
66
- local CLI and reports LANE_DEPENDENCY_MISSING (exit 127) when neither
67
- substrate exists.
68
-
69
- ## Usage
70
-
71
- ```bash
72
- workflows --version
73
- workflows init # creates workflows/ + sessions/ + workflows.db and scaffolds a sample graph
74
- workflows validate graph.json
75
- workflows graph graph.json # render (text | --format dot | --format json)
76
- workflows run graph.json --context '{"repo":"x"}' --input go=yes --idempotency-key k1
77
- workflows runs list
78
- workflows runs show <run-id>
79
- workflows runs events <run-id> # the run's WAL event stream
80
- workflows nodes show <run-id> <node-id>
81
- workflows sessions pull # session WAL state (entries, torn, live claims)
82
- workflows machines status # this machine's store/WAL/daemon status
83
- workflows lanes probe claude # wired-vs-not-ready-with-reason maturity check
84
- workflows resume <run-id> # restore an INTERRUPTED run from its durable cursor
85
- workflows daemon start --once # or a continuous reap loop
86
- workflows memos clear --yes
87
- workflows serve # HTTP server (same as workflows-serve)
88
- workflows-mcp # stdio MCP server
89
- workflows-serve # HTTP server, port from HASNA_WORKFLOWS_PORT (default 8790)
32
+ The shared `@hasna/contracts` resolver owns credentials. It supports the canonical
33
+ `HASNA_WORKFLOWS_API_KEY` / `HASNA_WORKFLOWS_API_URL` environment names and the
34
+ `WORKFLOWS_` aliases, its owner-only disk tier at
35
+ `~/.hasna/workflows/config/credentials`, documented Hasna home/profile overrides,
36
+ and its supported macOS Keychain tier. Keep API keys out of commands, logs and MCP
37
+ configuration. A credential bound to one authority is refused at another authority.
38
+ Legacy unbound internal credentials stay bound to the Hasna gateway.
39
+
40
+ `workflows setup` reads a key projected into `HASNA_WORKFLOWS_API_KEY`, verifies
41
+ one authenticated API read, writes owner-only credentials atomically, and verifies
42
+ another read through the disk tier. It refuses a selected override, pointer or
43
+ profile. Replacing different credentials requires `--replace`; the exact original
44
+ is preserved and read back before replacement. `workflows whoami` works offline and
45
+ reports the authority, credential source and calling agent without printing a key.
46
+
47
+ For personal sign-in, `workflows login` verifies and stores a key projected by
48
+ your credential owner into `HASNA_WORKFLOWS_API_KEY`. A supplied key can also be
49
+ piped into `workflows login --with-api-key`; never put a key in arguments.
50
+ This API-key-only server does not offer a browser or device sign-in flow.
51
+ `workflows logout` removes only the active credential that login stored,
52
+ preserves its readable original and keeps the API URL. It leaves setup-owned,
53
+ environment, pointer and other-profile credentials with their owners. Environment
54
+ credentials still set produce a non-zero result and name their source.
55
+ A supplied key remains valid on the server until its credential owner revokes it;
56
+ `--revoke` signs out locally and reports that individual-key revocation is unavailable.
57
+
58
+ An explicit URL can include or omit `/v1`; the client adds it exactly once:
59
+
60
+ ```sh
61
+ workflows --api-url https://api.hasna.com/workflows/v1 runs list --json
90
62
  ```
91
63
 
64
+ For self-hosting, supply your own HTTPS URL and its paired credential using the
65
+ resolver. Loopback HTTP is permitted for isolated development. Remote HTTP and
66
+ redirects carrying credentials are refused.
67
+
68
+ ## HTTP SDK
69
+
70
+ The SDK works in Node and Bun:
71
+
92
72
  ```ts
93
- import { createWorkflowsService } from "@hasna/workflows/sdk";
73
+ import { createWorkflowsClient } from "@hasna/workflows/sdk";
94
74
 
95
- const svc = createWorkflowsService();
96
- console.log(svc.health());
75
+ const workflows = createWorkflowsClient();
76
+ const readiness = await workflows.ready();
77
+ const runs = await workflows.listRuns({ limit: 100 });
97
78
  ```
98
79
 
99
- ## Configuration
80
+ Its typed operations are generated from the server's OpenAPI document using
81
+ `@hasna/contracts`, with the same credential-bound transport as the CLI and MCP.
82
+ `bun run check:sdk` refuses stale generated code; `bun run generate:sdk` updates it.
100
83
 
101
- | variable | meaning | default |
102
- |---|---|---|
103
- | `HASNA_WORKFLOWS_PORT` / `WORKFLOWS_PORT` | serve port | 8790 |
104
- | `HASNA_WORKFLOWS_HOST` / `WORKFLOWS_HOST` | serve bind host | 127.0.0.1 |
105
- | `HASNA_WORKFLOWS_DATA_DIR` / `WORKFLOWS_DATA_DIR` | data directory | `~/.hasna/workflows` (the @hasna/paths XDG data home `~/.local/share/hasna/workflows` is adopted once `HASNA_DATA_HOME` is set or the store is migrated there) |
84
+ `createWorkflowsClient({ apiUrl })` provides an explicit authority override. Its
85
+ run API includes `triggerRun`, `getRun`, `listNodes`, `events`, `cancelRun` and
86
+ `resumeRun`. Trigger idempotency keys cover the entire stored history and bind to
87
+ the graph, context and cycle budget. Reusing a key for a different request returns
88
+ HTTP 409. Run context cannot contain the reserved `__wf` or `steps` fields.
89
+
90
+ A step's optional `cwd` names its working directory on the server executing the
91
+ workflow. It applies to shell commands and model lanes; prefer an absolute path.
92
+ Codex prompts keep their native repository, sandbox and approval defaults. Supply
93
+ `cwd` for work in an existing trusted repository. For a prompt that needs no file
94
+ changes and may run outside a repository, explicitly set `codexMode: "read-only"`:
95
+
96
+ ```ts
97
+ import { createWorkflowsClient, type WorkflowGraph } from "@hasna/workflows/sdk";
98
+
99
+ const workflows = createWorkflowsClient();
100
+ const graph: WorkflowGraph = {
101
+ name: "read-only-answer", version: "1",
102
+ nodes: [
103
+ { id: "start", type: "start", next: "answer" },
104
+ { id: "answer", type: "step", lane: "codex", codexMode: "read-only",
105
+ prompt: "Return the requested answer without changing files.", next: "end" },
106
+ { id: "end", type: "end" },
107
+ ],
108
+ };
109
+ await workflows.triggerRun(graph, {}, { idempotencyKey: "read-only-answer-1" });
110
+ ```
111
+
112
+ This explicit mode uses Codex's read-only sandbox, permits a nonrepository
113
+ directory and refuses approval escalation. It applies only to Codex prompt steps;
114
+ shell commands cannot select it. An empty Codex response fails the step even when
115
+ the native process exits successfully. Memoization distinguishes the working
116
+ directory, lane and execution mode.
117
+
118
+ ## MCP
119
+
120
+ Register `workflows-mcp` as a stdio command in Codex, Claude Code, OpenCode, Sumi,
121
+ Cursor or another MCP harness. Use the harness's supported configuration writer;
122
+ keep existing servers and machine-owned credentials intact. Initialization,
123
+ `tools/list`, graph validation and version inspection require no credential.
124
+ Hosted tools resolve this station's credential when invoked. No token belongs in
125
+ the harness configuration.
126
+ `--stdio` is accepted. Run tools require a stable `idempotencyKey`. Each tool accepts
127
+ an optional `agent`; otherwise identity comes from the native harness context or
128
+ the station. CLI and SDK callers may also set their agent explicitly.
129
+
130
+ ## Server and migration
131
+
132
+ `workflows-serve` owns execution and persistence. `HASNA_WORKFLOWS_HOST`,
133
+ `HASNA_WORKFLOWS_PORT`, `HASNA_WORKFLOWS_DATA_DIR` and
134
+ `HASNA_WORKFLOWS_API_KEY` configure the server. Set
135
+ `HASNA_WORKFLOWS_DATABASE_URL` to its PostgreSQL database and optionally
136
+ `HASNA_WORKFLOWS_DATABASE_SCHEMA` (default `workflows`). Alias names remain supported;
137
+ database URL aliases must agree. Storage and mode environment selectors never
138
+ enable local fallback. Run `workflows-serve init-db` explicitly to create a fresh
139
+ schema; normal startup and readiness never migrate storage.
140
+ Use durable storage for the data directory and one deployed executor; ephemeral
141
+ container storage is unsuitable. Model lanes require the selected harness's
142
+ native subscription on the executor. They do not inherit paid model API keys.
143
+
144
+ When the server sits behind an authenticated gateway, set
145
+ `HASNA_WORKFLOWS_ORIGIN_AUTH` to the gateway's scoped origin identity. It must be
146
+ 32 to 256 ASCII letters, digits, underscores or hyphens. All API routes, including
147
+ legacy aliases, then require `x-hasna-origin-auth`; only unversioned `/health` and
148
+ `/ready` remain public. The origin identity never replaces caller Bearer authentication.
149
+
150
+ The versioned API serves health, readiness, version, graph validation, triggers,
151
+ run details, nodes, events, cancellation, resume and inspection routes under `/v1`.
152
+ Legacy unversioned server routes remain aliases. Readiness returns HTTP 503 unless
153
+ authentication and the actual storage schema are available. Cancellation fences
154
+ late results. Resume executes the pinned graph from the saved cursor. A timeout
155
+ whose cancellation is unconfirmed cannot automatically retry or resume.
156
+ PostgreSQL advisory ownership is shared between executors. A lost connection
157
+ leaves a durable execution hold for operator reconciliation; it never silently
158
+ restarts a potentially active command. Deploy one executor with its preserved
159
+ journal so that all event reads use the same authoritative history.
160
+
161
+ The server stores runs, node history and memo results in three PostgreSQL tables,
162
+ with an append-only session journal in its durable data directory. Journal writers
163
+ serialize through a SQLite mutex that contains no application tables. Before torn-tail
164
+ repair, the exact original is copied and read back at
165
+ `session.wal.preserved-<sha256>`. The required `bun run test:pg` gate uses a fresh
166
+ `WORKFLOWS_TEST_DATABASE_URL` and proves actual storage and HTTP execution. It fails
167
+ when the database is not configured. The ordinary test suite reports these cases
168
+ as skipped when no test database is supplied.
169
+
170
+ Existing embedded applications import `createWorkflowsService` from
171
+ `@hasna/workflows` and require Bun. The `./sdk` export now denotes the HTTP client.
172
+ Server-only maintenance commands use `workflows-serve admin ...`; operator
173
+ maintenance never selects the client's hosted credentials or changes its target.
174
+
175
+ An embedded server example:
176
+
177
+ ```ts
178
+ import { createWorkflowsService } from "@hasna/workflows";
179
+ const service = createWorkflowsService({ storageBackend: "sqlite", dataDir: "/srv/workflows", apiKey: process.env.HASNA_WORKFLOWS_API_KEY });
180
+ console.log(service.health());
181
+ ```
106
182
 
107
- The server data backend is sqlite (default) or postgresql when
108
- `HASNA_WORKFLOWS_DATABASE_URL` is set.
183
+ A workflow graph contains one start node, steps or decisions, optional bounded
184
+ while loops, and an end node. `workflows init workflow.json` writes a sample graph
185
+ without creating application state. `workflows validate workflow.json` and
186
+ `workflows graph workflow.json` work offline. `workflows run workflow.json`
187
+ executes on the configured server, and `workflows runs events <run-id>` reads its
188
+ durable execution history.
109
189
 
110
- ## License
190
+ Lanes support Claude, Codex, Cursor in local mode, and a Grok CLI when installed.
191
+ A wired substrate is distinct from verified sign-in and usable model capacity;
192
+ missing substrate or authentication fails visibly.
111
193
 
112
- Apache-2.0
194
+ A [Hasna](https://hasna.com) product.