@workflow-manager/runner 0.1.0

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 ADDED
@@ -0,0 +1,196 @@
1
+ # workflow-manager
2
+
3
+ CLI runner for in-memory and Markdown/JSON workflow orchestration.
4
+
5
+ ## What it does
6
+
7
+ - Parses workflow definitions from Markdown frontmatter or JSON
8
+ - Validates structure, dependencies, adapters, and validation modes
9
+ - Executes workflow steps with deterministic run state transitions
10
+ - Emits a full event timeline and JSON run result
11
+ - Records authenticated CLI run telemetry for success, failure, and workflow effectiveness
12
+ - Publishes and pulls shared workflows from the remote registry
13
+
14
+ ## Architecture
15
+
16
+ - `src/index.ts`: CLI commands (`questions`, `scaffold`, `validate`, `run`)
17
+ - `src/parser.ts`: parsing + validation
18
+ - `src/engine.ts`: execution loop, confirmations, retries, rollback/restart
19
+ - `src/mockExecutor.ts`: mock step executor for simulation
20
+ - `src/events.ts`: event sequencing/logging
21
+ - `src/types.ts`: contracts and status enums
22
+ - `apps/remote-registry/`: React + Vite remote registry app
23
+ - `supabase/`: migrations, local stack config, and Edge Functions
24
+ - `doc/`: VitePress documentation site
25
+ - `skills/`: agent skills shipped inside the main npm package for TanStack Intent and compatible loaders
26
+
27
+ ## Quick start
28
+
29
+ ```bash
30
+ bun install
31
+ bun run build
32
+ bun link
33
+
34
+ wfm scaffold ./example-workflow.md
35
+ wfm validate ./example-workflow.md
36
+ wfm run ./example-workflow.md --auto-confirm-all
37
+
38
+ # JSON workflow support
39
+ wfm scaffold ./example-workflow.json --format json
40
+ wfm validate ./example-workflow.json
41
+ wfm run ./example-workflow.json --auto-confirm-all
42
+
43
+ # Remote registry
44
+ wfm auth login --token <token>
45
+ wfm search bunny
46
+ wfm publish ./example-workflow.json --visibility public --tag storytelling,example
47
+ wfm pull alice/remote-bunny --output ./remote-bunny.json
48
+ ```
49
+
50
+ ## Build
51
+
52
+ ```bash
53
+ bun run build
54
+ bun test
55
+ ```
56
+
57
+ Build a standalone Bun binary:
58
+
59
+ ```bash
60
+ bun run build:bin
61
+ ```
62
+
63
+ Build all release binaries locally:
64
+
65
+ ```bash
66
+ bun run build:bin:all
67
+ ```
68
+
69
+ ## Testing
70
+
71
+ Run unit tests:
72
+
73
+ ```bash
74
+ bun run test:unit
75
+ ```
76
+
77
+ Run the story workflow e2e tests (JSON and Markdown fixtures):
78
+
79
+ ```bash
80
+ bun run test:e2e
81
+ ```
82
+
83
+ The e2e suite also runs an `opencode` adapter variant for both JSON and Markdown workflows and asserts adapter routing in run events.
84
+
85
+ Run real OpenCode adapter e2e (requires `opencode` CLI installed):
86
+
87
+ ```bash
88
+ bun run test:e2e:real
89
+ ```
90
+
91
+ The real adapter test is opt-in and triggered by `WORKFLOW_MANAGER_REAL_OPENCODE=1`.
92
+
93
+ Run full test suite:
94
+
95
+ ```bash
96
+ bun test
97
+ ```
98
+
99
+ Documentation site:
100
+
101
+ ```bash
102
+ bun run docs:dev
103
+ bun run docs:build
104
+ bun run docs:preview
105
+ ```
106
+
107
+ Docs are now treated as a manual Netlify release flow. Build `doc/.vitepress/dist` locally and deploy it manually when needed.
108
+
109
+ Remote registry app:
110
+
111
+ ```bash
112
+ bun run remote-registry:dev
113
+ bun run remote-registry:build
114
+ ```
115
+
116
+ `workflow-manager-ui` is the Netlify auto-release target. The root `netlify.toml` now builds `apps/remote-registry/` so connected Netlify Git deploys can produce preview deploys for PRs and production deploys from merges to `main`.
117
+
118
+ Manual help:
119
+
120
+ ```bash
121
+ wfm man
122
+ ```
123
+
124
+ Remote registry commands:
125
+
126
+ ```bash
127
+ wfm auth whoami
128
+ wfm auth logout
129
+ wfm remote info alice/remote-bunny
130
+ ```
131
+
132
+ ## Agent skills
133
+
134
+ The published `@workflow-manager/runner` npm package now ships the CLI runner and the bundled agent skills together.
135
+
136
+ - bundled skill: `skills/workflow-manager-cli/SKILL.md`
137
+ - discovery keyword: `tanstack-intent`
138
+ - install flow: install `@workflow-manager/runner`, then run `npx @tanstack/intent@latest list` and `npx @tanstack/intent@latest install`
139
+
140
+ Example usage:
141
+
142
+ ```bash
143
+ npm install @workflow-manager/runner
144
+ npx @tanstack/intent@latest list
145
+ npx @tanstack/intent@latest install
146
+ ```
147
+
148
+ See `skills/README.md` for the packaged skill layout, TanStack Intent integration, and release checks.
149
+
150
+ The deployed registry dashboard also supports browser-based token creation, workflow publishing, and creator analytics.
151
+
152
+ Current dashboard capabilities include:
153
+
154
+ - creator workflow analytics
155
+ - analytics refresh and trend views
156
+ - authenticated CLI run telemetry insights
157
+ - token list and revoke controls
158
+ - browser-based workflow publishing for JSON and Markdown sources
159
+ - workflow metadata and version management
160
+
161
+ ## Contribution
162
+
163
+ - Keep workflow contracts backward-compatible when possible (`src/types.ts`)
164
+ - Update docs under `doc/` when changing schema or runtime behavior
165
+ - Add or update tests in `tests/` when touching parser or engine logic
166
+
167
+ Netlify auto-release is configured for the UI in `netlify.toml`:
168
+
169
+ - base directory: `apps/remote-registry`
170
+ - build command: `bun run build`
171
+ - publish directory: `dist`
172
+ - PRs: Deploy Previews
173
+ - `main`: Production deploys for `workflow-manager-ui`
174
+
175
+ The docs site is no longer the auto-release target and should be deployed manually.
176
+
177
+ ## Release
178
+
179
+ - Push a semantic tag like `v0.2.0` to trigger the GitHub Actions release workflow.
180
+ - Publish the root npm package when you want the CLI runner and `skills/` bundle to ship together.
181
+ - The workflow runs tests and build, then compiles binaries for:
182
+ - macOS arm64: `wfm-macos-arm64`
183
+ - Linux x64: `wfm-linux-x64`
184
+ - Windows x64: `wfm-windows-x64.exe`
185
+ - Assets are attached to the GitHub Release for that tag.
186
+
187
+ ## Documentation
188
+
189
+ VitePress docs are in `doc/` and focus on:
190
+
191
+ - how the workflow manager works
192
+ - runtime architecture
193
+ - workflow schema
194
+ - practical workflow examples and implementation patterns
195
+ - CLI manual help usage
196
+ - remote registry architecture, agents, and milestones
@@ -0,0 +1,2 @@
1
+ import type { RunOptions, RunResult, WorkflowDefinition } from "./types.js";
2
+ export declare function runWorkflow(definition: WorkflowDefinition, options?: RunOptions): RunResult;
package/dist/engine.js ADDED
@@ -0,0 +1,217 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventLog } from "./events.js";
3
+ import { executeMockStep } from "./mockExecutor.js";
4
+ import { executeOpencodeStep, shouldUseRealOpencode } from "./opencodeExecutor.js";
5
+ function nodeType(step) {
6
+ if (step.kind === "approval")
7
+ return "HUMAN";
8
+ if (step.kind === "system")
9
+ return "SYSTEM";
10
+ return "AGENT";
11
+ }
12
+ function stepObjective(step, workflowObjective) {
13
+ return step.objective ?? `${workflowObjective} :: ${step.key}`;
14
+ }
15
+ function requiresValidation(step) {
16
+ if (step.approvalSpec?.validation?.required)
17
+ return step.approvalSpec.validation.mode ?? "human";
18
+ if (step.validation?.required)
19
+ return step.validation.mode ?? "human";
20
+ if (step.kind === "approval")
21
+ return step.approvalSpec?.validation?.mode ?? "human";
22
+ return step.validation?.mode ?? "none";
23
+ }
24
+ function canConfirm(step, options, output) {
25
+ const mode = requiresValidation(step);
26
+ if (mode === "none" && output.execution_status !== "YIELD_EXTERNAL")
27
+ return { ok: true };
28
+ if (options.autoConfirmAll)
29
+ return { ok: true };
30
+ const list = new Set(options.confirmations ?? []);
31
+ const modeToken = `${step.key}:${mode}`;
32
+ if (list.has(step.key) || list.has(modeToken))
33
+ return { ok: true };
34
+ const autoConfirm = step.validation?.autoConfirm ?? step.approvalSpec?.validation?.autoConfirm ?? false;
35
+ if (autoConfirm)
36
+ return { ok: true };
37
+ return { ok: false, reason: `Missing confirmation for ${step.key} (${mode})` };
38
+ }
39
+ function executeStep(step, input, attempt) {
40
+ const adapterKey = step.taskSpec?.adapterKey ?? "mock";
41
+ if (adapterKey === "opencode" && shouldUseRealOpencode(step)) {
42
+ return executeOpencodeStep(step, input, attempt);
43
+ }
44
+ return executeMockStep(step, input, attempt);
45
+ }
46
+ export function runWorkflow(definition, options) {
47
+ const runId = randomUUID();
48
+ const actor = options?.actor ?? "cli";
49
+ const primaryObjective = options?.objective ?? definition.title;
50
+ const workflowObjectives = definition.objectives ?? [];
51
+ const globalState = { ...(options?.input ?? {}) };
52
+ const eventLog = new EventLog();
53
+ let runStatus = "queued";
54
+ const stepRuns = new Map();
55
+ for (const step of definition.steps) {
56
+ stepRuns.set(step.key, {
57
+ stepKey: step.key,
58
+ status: "pending",
59
+ attempt: 0,
60
+ confirmed: false,
61
+ });
62
+ }
63
+ eventLog.push(runId, "run.created", { workflowKey: definition.key }, undefined, actor);
64
+ runStatus = "running";
65
+ eventLog.push(runId, "run.started", { objective: primaryObjective, objectives: workflowObjectives }, undefined, actor);
66
+ let index = 0;
67
+ let guard = 0;
68
+ const maxSteps = Math.max(definition.steps.length * 30, 30);
69
+ while (index < definition.steps.length) {
70
+ guard += 1;
71
+ if (guard > maxSteps) {
72
+ runStatus = "failed";
73
+ eventLog.push(runId, "run.failed", { reason: "Execution guard exceeded" });
74
+ break;
75
+ }
76
+ const step = definition.steps[index];
77
+ const stepRun = stepRuns.get(step.key);
78
+ if (!stepRun)
79
+ throw new Error(`Missing step run for ${step.key}`);
80
+ const dependencies = step.dependsOn ?? [];
81
+ const depsComplete = dependencies.every((depKey) => stepRuns.get(depKey)?.status === "succeeded");
82
+ if (!depsComplete) {
83
+ runStatus = "failed";
84
+ eventLog.push(runId, "run.failed", { reason: `Dependencies not satisfied for ${step.key}` }, step.key);
85
+ break;
86
+ }
87
+ stepRun.status = "runnable";
88
+ eventLog.push(runId, "step.runnable", { stepKey: step.key }, step.key);
89
+ stepRun.status = "running";
90
+ stepRun.attempt += 1;
91
+ eventLog.push(runId, "step.claimed", { attempt: stepRun.attempt }, step.key);
92
+ eventLog.push(runId, "step.execution_started", { attempt: stepRun.attempt }, step.key);
93
+ const previousOutput = {};
94
+ for (const dep of dependencies) {
95
+ previousOutput[dep] = stepRuns.get(dep)?.output ?? null;
96
+ }
97
+ const inputEnvelope = {
98
+ global_context: {
99
+ workflow_id: runId,
100
+ primary_objective: primaryObjective,
101
+ workflow_objectives: workflowObjectives,
102
+ global_state: globalState,
103
+ },
104
+ step_context: {
105
+ step_id: step.key,
106
+ step_objective: stepObjective(step, primaryObjective),
107
+ previous_output: previousOutput,
108
+ assigned_node_type: nodeType(step),
109
+ },
110
+ priming_configuration: {
111
+ required_skills: step.taskSpec?.init?.skills ?? [],
112
+ mcp_endpoints: step.taskSpec?.init?.mcps ?? [],
113
+ system_prompts: step.taskSpec?.init?.systemPrompts ?? [],
114
+ context: step.taskSpec?.init?.context,
115
+ adapter: step.taskSpec?.adapterKey ?? "mock",
116
+ model: step.taskSpec?.init?.model,
117
+ },
118
+ };
119
+ const output = executeStep(step, inputEnvelope, stepRun.attempt);
120
+ eventLog.push(runId, "step.execution_finished", {
121
+ status: output.execution_status,
122
+ action: output.qa_routing.action,
123
+ adapter: step.taskSpec?.adapterKey ?? "mock",
124
+ init: {
125
+ skills: step.taskSpec?.init?.skills ?? [],
126
+ mcps: step.taskSpec?.init?.mcps ?? [],
127
+ context: step.taskSpec?.init?.context ?? {},
128
+ },
129
+ }, step.key);
130
+ const confirmation = canConfirm(step, options ?? {}, output);
131
+ if (!confirmation.ok) {
132
+ stepRun.status = "waiting_for_approval";
133
+ runStatus = "waiting_for_approval";
134
+ eventLog.push(runId, "step.waiting_for_approval", { reason: confirmation.reason, validation: requiresValidation(step) }, step.key);
135
+ eventLog.push(runId, "run.waiting_for_approval", { reason: "confirmation required" }, step.key, actor);
136
+ break;
137
+ }
138
+ stepRun.confirmed = true;
139
+ eventLog.push(runId, "step.confirmed", { by: actor, validation: requiresValidation(step) }, step.key, actor);
140
+ if (output.execution_status === "YIELD_EXTERNAL") {
141
+ stepRun.status = "waiting_for_approval";
142
+ runStatus = "waiting_for_approval";
143
+ eventLog.push(runId, "step.waiting_for_approval", { reason: "external intervention" }, step.key);
144
+ eventLog.push(runId, "approval.resolved", { decision: "approved" }, step.key, actor);
145
+ stepRun.status = "succeeded";
146
+ runStatus = "running";
147
+ stepRun.output = output.mutated_payload;
148
+ globalState[step.key] = output.mutated_payload;
149
+ index += 1;
150
+ continue;
151
+ }
152
+ if (output.execution_status === "FAILED") {
153
+ stepRun.status = "failed";
154
+ runStatus = "failed";
155
+ eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "step failed" }, step.key);
156
+ break;
157
+ }
158
+ if (output.execution_status === "QA_REJECTED") {
159
+ const retryMax = step.retryPolicy?.maxAttempts ?? definition.defaultRetryPolicy?.maxAttempts ?? 1;
160
+ if (output.qa_routing.action === "RETRY_CURRENT") {
161
+ if (stepRun.attempt < retryMax) {
162
+ stepRun.status = "pending";
163
+ eventLog.push(runId, "step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
164
+ continue;
165
+ }
166
+ stepRun.status = "failed";
167
+ runStatus = "failed";
168
+ eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "max retry exceeded" }, step.key);
169
+ break;
170
+ }
171
+ if (output.qa_routing.action === "ROLLBACK_PREVIOUS") {
172
+ if (index === 0) {
173
+ stepRun.status = "failed";
174
+ runStatus = "failed";
175
+ eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "cannot rollback before first step" }, step.key);
176
+ break;
177
+ }
178
+ const prevStep = definition.steps[index - 1];
179
+ const prevRun = stepRuns.get(prevStep.key);
180
+ prevRun.status = "pending";
181
+ prevRun.confirmed = false;
182
+ eventLog.push(runId, "step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
183
+ stepRun.status = "pending";
184
+ stepRun.confirmed = false;
185
+ index -= 1;
186
+ continue;
187
+ }
188
+ if (output.qa_routing.action === "RESTART_ALL") {
189
+ for (const s of definition.steps) {
190
+ const sr = stepRuns.get(s.key);
191
+ sr.status = "pending";
192
+ sr.attempt = 0;
193
+ sr.confirmed = false;
194
+ delete sr.output;
195
+ }
196
+ eventLog.push(runId, "step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
197
+ index = 0;
198
+ continue;
199
+ }
200
+ }
201
+ stepRun.status = "succeeded";
202
+ stepRun.output = output.mutated_payload;
203
+ globalState[step.key] = output.mutated_payload;
204
+ index += 1;
205
+ }
206
+ if (runStatus === "running") {
207
+ runStatus = "succeeded";
208
+ eventLog.push(runId, "run.completed", { steps: definition.steps.length }, undefined, actor);
209
+ }
210
+ return {
211
+ runId,
212
+ status: runStatus,
213
+ outputs: globalState,
214
+ stepRuns: definition.steps.map((s) => stepRuns.get(s.key)),
215
+ events: eventLog.all(),
216
+ };
217
+ }
@@ -0,0 +1,7 @@
1
+ import type { RunEvent } from "./types.js";
2
+ export declare class EventLog {
3
+ private sequence;
4
+ private events;
5
+ push(runId: string, type: RunEvent["type"], payload?: Record<string, unknown>, stepRunId?: string, actor?: string): void;
6
+ all(): RunEvent[];
7
+ }
package/dist/events.js ADDED
@@ -0,0 +1,21 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export class EventLog {
3
+ sequence = 0;
4
+ events = [];
5
+ push(runId, type, payload = {}, stepRunId, actor = "system") {
6
+ this.sequence += 1;
7
+ this.events.push({
8
+ id: randomUUID(),
9
+ runId,
10
+ stepRunId,
11
+ type,
12
+ sequenceNumber: this.sequence,
13
+ occurredAt: new Date().toISOString(),
14
+ actor,
15
+ payload,
16
+ });
17
+ }
18
+ all() {
19
+ return [...this.events];
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};