@workflow-manager/runner 0.6.0 → 0.8.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 +5 -1
- package/dist/cliRunRenderer.js +18 -56
- package/dist/engine.js +201 -68
- package/dist/generated/bundledSkills.d.ts +5 -0
- package/dist/generated/bundledSkills.js +22 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +466 -87
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +47 -9
- package/dist/parser.js +10 -1
- package/dist/runFormat.d.ts +11 -0
- package/dist/runFormat.js +56 -0
- package/dist/runnerApi.js +14 -0
- package/dist/sessionFile.d.ts +11 -0
- package/dist/sessionFile.js +61 -0
- package/dist/tui/ansi.d.ts +23 -0
- package/dist/tui/ansi.js +102 -0
- package/dist/tui/layout.d.ts +18 -0
- package/dist/tui/layout.js +245 -0
- package/dist/tui/logStore.d.ts +22 -0
- package/dist/tui/logStore.js +78 -0
- package/dist/tui/screen.d.ts +35 -0
- package/dist/tui/screen.js +99 -0
- package/dist/tui/theme.d.ts +13 -0
- package/dist/tui/theme.js +55 -0
- package/dist/tui/tuiRunRenderer.d.ts +83 -0
- package/dist/tui/tuiRunRenderer.js +316 -0
- package/dist/types.d.ts +15 -8
- package/package.json +4 -2
- package/skills/workflow-author/README.md +18 -0
- package/skills/workflow-author/SKILL.md +306 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// RunObserver implementation for `wfm run --ui`: owns the mutable TUI state
|
|
2
|
+
// (latest snapshot, per-step logs, selection/follow, transient status
|
|
3
|
+
// messages), wires TerminalScreen input/output, and composes+paints frames
|
|
4
|
+
// via the pure renderFrame(). This is the only piece that talks timers,
|
|
5
|
+
// process streams, and the session approve/resume/cancel controls — layout
|
|
6
|
+
// math and terminal I/O stay in layout.ts / screen.ts.
|
|
7
|
+
import { isTerminalStatus } from "../runFormat.js";
|
|
8
|
+
import { renderFrame } from "./layout.js";
|
|
9
|
+
import { TuiLogStore } from "./logStore.js";
|
|
10
|
+
import { TerminalScreen } from "./screen.js";
|
|
11
|
+
import { createTheme } from "./theme.js";
|
|
12
|
+
import picocolors from "picocolors";
|
|
13
|
+
const DEFAULT_REDRAW_MS = 80;
|
|
14
|
+
const DEFAULT_TICK_MS = 1000;
|
|
15
|
+
const STATUS_MESSAGE_TTL_MS = 4000;
|
|
16
|
+
const STEP_TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled"]);
|
|
17
|
+
function readString(payload, key) {
|
|
18
|
+
const value = payload[key];
|
|
19
|
+
return typeof value === "string" ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
function buildEventMetaText(event) {
|
|
22
|
+
const payload = event.payload;
|
|
23
|
+
if (event.type === "agent.started") {
|
|
24
|
+
const command = readString(payload, "command") ?? "agent";
|
|
25
|
+
const args = Array.isArray(payload.args) ? payload.args.map((value) => String(value)).join(" ") : "";
|
|
26
|
+
const model = readString(payload, "model");
|
|
27
|
+
return `agent started: ${command}${args ? ` ${args}` : ""}${model ? ` model=${model}` : ""}`;
|
|
28
|
+
}
|
|
29
|
+
if (event.type === "agent.finished") {
|
|
30
|
+
const status = readString(payload, "executionStatus") ?? "unknown";
|
|
31
|
+
const exitStatus = typeof payload.exitStatus === "number" || payload.exitStatus === null ? ` exit=${String(payload.exitStatus)}` : "";
|
|
32
|
+
const timedOut = payload.timedOut === true ? " timedOut=true" : "";
|
|
33
|
+
return `agent finished: ${status}${exitStatus}${timedOut}`;
|
|
34
|
+
}
|
|
35
|
+
if (event.type === "step.execution_finished") {
|
|
36
|
+
const status = readString(payload, "status") ?? "unknown";
|
|
37
|
+
const action = readString(payload, "action");
|
|
38
|
+
const feedbackReason = readString(payload, "feedbackReason");
|
|
39
|
+
return `execution finished: ${status}${action ? ` action=${action}` : ""}${feedbackReason ? ` reason=${feedbackReason}` : ""}`;
|
|
40
|
+
}
|
|
41
|
+
if (event.type === "step.validation_started") {
|
|
42
|
+
const adapter = readString(payload, "adapter");
|
|
43
|
+
const criteria = readString(payload, "criteria");
|
|
44
|
+
return `agent validation started${adapter ? ` (${adapter})` : ""}${criteria ? ` criteria=${criteria}` : ""}`;
|
|
45
|
+
}
|
|
46
|
+
if (event.type === "step.validation_finished") {
|
|
47
|
+
const status = readString(payload, "status") ?? "unknown";
|
|
48
|
+
const action = readString(payload, "action");
|
|
49
|
+
const feedbackReason = readString(payload, "feedbackReason");
|
|
50
|
+
return `agent validation: ${status}${action ? ` action=${action}` : ""}${feedbackReason ? ` reason=${feedbackReason}` : ""}`;
|
|
51
|
+
}
|
|
52
|
+
if (event.type === "step.retried") {
|
|
53
|
+
const attempt = typeof payload.attempt === "number" ? payload.attempt : undefined;
|
|
54
|
+
return attempt !== undefined ? `retry scheduled (attempt ${attempt})` : "retry scheduled";
|
|
55
|
+
}
|
|
56
|
+
if (event.type === "approval.resolved") {
|
|
57
|
+
const decision = readString(payload, "decision") ?? "unknown";
|
|
58
|
+
const actor = readString(payload, "actor");
|
|
59
|
+
return `approval resolved: ${decision}${actor ? ` by ${actor}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export class TuiRunRenderer {
|
|
64
|
+
workflow;
|
|
65
|
+
session;
|
|
66
|
+
attachUrl;
|
|
67
|
+
attachToken;
|
|
68
|
+
stdout;
|
|
69
|
+
stdin;
|
|
70
|
+
redrawMs;
|
|
71
|
+
tickMs;
|
|
72
|
+
nowFn;
|
|
73
|
+
theme;
|
|
74
|
+
screen;
|
|
75
|
+
snapshot = null;
|
|
76
|
+
stepDetails = new Map();
|
|
77
|
+
logs = new TuiLogStore();
|
|
78
|
+
selectedStepKey = null;
|
|
79
|
+
follow = true;
|
|
80
|
+
statusMessage = null;
|
|
81
|
+
statusMessageExpiresAt = null;
|
|
82
|
+
dirty = true;
|
|
83
|
+
redrawTimer = null;
|
|
84
|
+
tickTimer = null;
|
|
85
|
+
started = false;
|
|
86
|
+
stopped = false;
|
|
87
|
+
constructor(options) {
|
|
88
|
+
this.workflow = options.workflow;
|
|
89
|
+
this.session = options.session;
|
|
90
|
+
this.attachUrl = options.attachUrl;
|
|
91
|
+
this.attachToken = options.attachToken;
|
|
92
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
93
|
+
this.stdin = options.stdin ?? process.stdin;
|
|
94
|
+
this.redrawMs = options.redrawMs ?? DEFAULT_REDRAW_MS;
|
|
95
|
+
this.tickMs = options.tickMs ?? DEFAULT_TICK_MS;
|
|
96
|
+
this.nowFn = options.now ?? Date.now;
|
|
97
|
+
this.theme = createTheme(options.colorEnabled ?? picocolors.isColorSupported);
|
|
98
|
+
this.screen = new TerminalScreen({
|
|
99
|
+
stdout: this.stdout,
|
|
100
|
+
stdin: this.stdin,
|
|
101
|
+
onKey: this.handleKey,
|
|
102
|
+
onResize: this.handleResize,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
start() {
|
|
106
|
+
if (this.started) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.started = true;
|
|
110
|
+
this.stopped = false;
|
|
111
|
+
this.screen.start();
|
|
112
|
+
if (this.redrawMs > 0) {
|
|
113
|
+
this.redrawTimer = setInterval(() => {
|
|
114
|
+
if (this.dirty) {
|
|
115
|
+
this.renderNow();
|
|
116
|
+
}
|
|
117
|
+
}, this.redrawMs);
|
|
118
|
+
}
|
|
119
|
+
if (this.tickMs > 0) {
|
|
120
|
+
this.tickTimer = setInterval(() => {
|
|
121
|
+
const status = this.snapshot?.status;
|
|
122
|
+
if (status === "running" || status === "waiting_for_approval") {
|
|
123
|
+
this.markDirty();
|
|
124
|
+
}
|
|
125
|
+
}, this.tickMs);
|
|
126
|
+
}
|
|
127
|
+
// Seed an initial (pending / snapshot-null) frame so the alt screen never
|
|
128
|
+
// shows a blank body while waiting on the first onSnapshot.
|
|
129
|
+
this.renderNow();
|
|
130
|
+
}
|
|
131
|
+
stop() {
|
|
132
|
+
if (this.stopped) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.stopped = true;
|
|
136
|
+
this.started = false;
|
|
137
|
+
if (this.redrawTimer) {
|
|
138
|
+
clearInterval(this.redrawTimer);
|
|
139
|
+
this.redrawTimer = null;
|
|
140
|
+
}
|
|
141
|
+
if (this.tickTimer) {
|
|
142
|
+
clearInterval(this.tickTimer);
|
|
143
|
+
this.tickTimer = null;
|
|
144
|
+
}
|
|
145
|
+
this.screen.stop();
|
|
146
|
+
}
|
|
147
|
+
onSnapshot(snapshot, stepDetails) {
|
|
148
|
+
this.snapshot = snapshot;
|
|
149
|
+
this.stepDetails = new Map(stepDetails.map((detail) => [detail.stepKey, detail]));
|
|
150
|
+
if (this.follow) {
|
|
151
|
+
const firstNonTerminal = snapshot.steps.find((step) => !STEP_TERMINAL_STATUSES.has(step.status))?.stepKey;
|
|
152
|
+
this.selectedStepKey = snapshot.currentStepKey ?? firstNonTerminal ?? this.selectedStepKey;
|
|
153
|
+
}
|
|
154
|
+
if (isTerminalStatus(snapshot.status)) {
|
|
155
|
+
this.logs.flushPartialLines();
|
|
156
|
+
}
|
|
157
|
+
this.markDirty();
|
|
158
|
+
}
|
|
159
|
+
onEvent(event) {
|
|
160
|
+
const text = buildEventMetaText(event);
|
|
161
|
+
if (text === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.logs.appendMeta(event.stepRunId, text);
|
|
165
|
+
this.markDirty();
|
|
166
|
+
}
|
|
167
|
+
onLog(log) {
|
|
168
|
+
this.logs.appendChunk(log);
|
|
169
|
+
this.markDirty();
|
|
170
|
+
}
|
|
171
|
+
handleKey = (key) => {
|
|
172
|
+
const name = key.name;
|
|
173
|
+
if (name === "up" || name === "k") {
|
|
174
|
+
this.follow = false;
|
|
175
|
+
this.moveSelection(-1);
|
|
176
|
+
this.markDirty();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (name === "down" || name === "j") {
|
|
180
|
+
this.follow = false;
|
|
181
|
+
this.moveSelection(1);
|
|
182
|
+
this.markDirty();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (name === "f") {
|
|
186
|
+
this.follow = true;
|
|
187
|
+
this.selectedStepKey = this.snapshot?.currentStepKey ?? this.selectedStepKey;
|
|
188
|
+
this.markDirty();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (name === "q" || (key.ctrl && name === "c")) {
|
|
192
|
+
this.handleQuit();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (name === "a") {
|
|
196
|
+
this.handleApprove();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (name === "r") {
|
|
200
|
+
this.handleResume();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (name === "c") {
|
|
204
|
+
this.handleCancel();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.markDirty();
|
|
208
|
+
};
|
|
209
|
+
renderNow() {
|
|
210
|
+
const rows = renderFrame(this.composeFrame());
|
|
211
|
+
this.screen.paint(rows);
|
|
212
|
+
this.dirty = false;
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
handleResize = () => {
|
|
216
|
+
this.markDirty();
|
|
217
|
+
};
|
|
218
|
+
handleApprove() {
|
|
219
|
+
const waiting = this.snapshot?.waitingForApproval;
|
|
220
|
+
if (!waiting) {
|
|
221
|
+
this.markDirty();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (waiting.validation === "external") {
|
|
225
|
+
this.setStatusMessage("external step — press r to resume");
|
|
226
|
+
this.markDirty();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const result = this.session.approve(waiting.stepKey, { actor: "cli", source: "tui" });
|
|
230
|
+
if (!result.ok) {
|
|
231
|
+
this.setStatusMessage(`approve failed: ${result.reason ?? "unknown error"}`);
|
|
232
|
+
}
|
|
233
|
+
this.markDirty();
|
|
234
|
+
}
|
|
235
|
+
handleResume() {
|
|
236
|
+
const waiting = this.snapshot?.waitingForApproval;
|
|
237
|
+
if (!waiting) {
|
|
238
|
+
this.markDirty();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const result = this.session.resume(waiting.stepKey, { actor: "cli", source: "tui" });
|
|
242
|
+
if (!result.ok) {
|
|
243
|
+
this.setStatusMessage(`resume failed: ${result.reason ?? "unknown error"}`);
|
|
244
|
+
}
|
|
245
|
+
this.markDirty();
|
|
246
|
+
}
|
|
247
|
+
handleCancel() {
|
|
248
|
+
const waiting = this.snapshot?.waitingForApproval;
|
|
249
|
+
if (!waiting) {
|
|
250
|
+
this.setStatusMessage("press q to quit; c cancels only a pending approval");
|
|
251
|
+
this.markDirty();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const result = this.session.cancel(waiting.stepKey, { actor: "cli", source: "tui" });
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
this.setStatusMessage(`cancel failed: ${result.reason ?? "unknown error"}`);
|
|
257
|
+
}
|
|
258
|
+
this.markDirty();
|
|
259
|
+
}
|
|
260
|
+
handleQuit() {
|
|
261
|
+
const status = this.snapshot?.status;
|
|
262
|
+
if (status && isTerminalStatus(status)) {
|
|
263
|
+
// Run already finished; cmdRun owns process exit from here.
|
|
264
|
+
this.markDirty();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.session.cancel(undefined, { actor: "cli", source: "tui", note: "cancelled from TUI" });
|
|
268
|
+
this.setStatusMessage("cancelling…");
|
|
269
|
+
this.markDirty();
|
|
270
|
+
}
|
|
271
|
+
moveSelection(delta) {
|
|
272
|
+
const steps = this.snapshot?.steps ?? [];
|
|
273
|
+
if (steps.length === 0) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const currentIndex = this.selectedStepKey ? steps.findIndex((step) => step.stepKey === this.selectedStepKey) : -1;
|
|
277
|
+
const baseIndex = currentIndex >= 0 ? currentIndex : delta > 0 ? -1 : 0;
|
|
278
|
+
const nextIndex = Math.min(Math.max(baseIndex + delta, 0), steps.length - 1);
|
|
279
|
+
this.selectedStepKey = steps[nextIndex].stepKey;
|
|
280
|
+
}
|
|
281
|
+
setStatusMessage(message) {
|
|
282
|
+
this.statusMessage = message;
|
|
283
|
+
this.statusMessageExpiresAt = this.nowFn() + STATUS_MESSAGE_TTL_MS;
|
|
284
|
+
}
|
|
285
|
+
currentStatusMessage(now) {
|
|
286
|
+
if (this.statusMessage && this.statusMessageExpiresAt !== null && now >= this.statusMessageExpiresAt) {
|
|
287
|
+
this.statusMessage = null;
|
|
288
|
+
this.statusMessageExpiresAt = null;
|
|
289
|
+
}
|
|
290
|
+
return this.statusMessage;
|
|
291
|
+
}
|
|
292
|
+
markDirty() {
|
|
293
|
+
this.dirty = true;
|
|
294
|
+
if (this.redrawMs === 0) {
|
|
295
|
+
this.renderNow();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
composeFrame() {
|
|
299
|
+
const { columns, rows } = this.screen.size();
|
|
300
|
+
const now = this.nowFn();
|
|
301
|
+
return {
|
|
302
|
+
snapshot: this.snapshot,
|
|
303
|
+
stepDetails: this.stepDetails,
|
|
304
|
+
logs: this.logs,
|
|
305
|
+
selectedStepKey: this.selectedStepKey,
|
|
306
|
+
follow: this.follow,
|
|
307
|
+
attachUrl: this.attachUrl,
|
|
308
|
+
attachToken: this.attachToken,
|
|
309
|
+
statusMessage: this.currentStatusMessage(now),
|
|
310
|
+
width: columns,
|
|
311
|
+
height: rows,
|
|
312
|
+
now,
|
|
313
|
+
theme: this.theme,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,17 +4,11 @@ export type StepKind = "task" | "approval" | "system";
|
|
|
4
4
|
export type NodeType = "AGENT" | "HUMAN" | "SYSTEM";
|
|
5
5
|
export type ExecutionStatus = "SUCCESS" | "QA_REJECTED" | "YIELD_EXTERNAL" | "FAILED";
|
|
6
6
|
export type QaAction = "PROCEED" | "RETRY_CURRENT" | "ROLLBACK_PREVIOUS" | "RESTART_ALL";
|
|
7
|
-
export type ValidationMode = "none" | "human" | "external";
|
|
7
|
+
export type ValidationMode = "none" | "human" | "external" | "agent";
|
|
8
8
|
export type AdapterKey = "pi-agent" | "mock" | "opencode" | "codex" | "claude-code" | "acp";
|
|
9
9
|
export interface RetryPolicy {
|
|
10
10
|
maxAttempts?: number;
|
|
11
11
|
}
|
|
12
|
-
export interface ValidationSpec {
|
|
13
|
-
mode?: ValidationMode;
|
|
14
|
-
required?: boolean;
|
|
15
|
-
autoConfirm?: boolean;
|
|
16
|
-
confirmerPolicy?: string;
|
|
17
|
-
}
|
|
18
12
|
export interface TaskInitConfig {
|
|
19
13
|
context?: Record<string, unknown> | string;
|
|
20
14
|
skills?: string[];
|
|
@@ -22,6 +16,19 @@ export interface TaskInitConfig {
|
|
|
22
16
|
systemPrompts?: string[];
|
|
23
17
|
model?: string;
|
|
24
18
|
}
|
|
19
|
+
export interface AgentValidationSpec {
|
|
20
|
+
adapterKey?: AdapterKey;
|
|
21
|
+
init?: TaskInitConfig;
|
|
22
|
+
criteria?: string;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
export interface ValidationSpec {
|
|
26
|
+
mode?: ValidationMode;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
autoConfirm?: boolean;
|
|
29
|
+
confirmerPolicy?: string;
|
|
30
|
+
agent?: AgentValidationSpec;
|
|
31
|
+
}
|
|
25
32
|
export interface StepDefinition {
|
|
26
33
|
key: string;
|
|
27
34
|
kind: StepKind;
|
|
@@ -260,7 +267,7 @@ export interface RunEvent {
|
|
|
260
267
|
id: string;
|
|
261
268
|
runId: string;
|
|
262
269
|
stepRunId?: string;
|
|
263
|
-
type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "agent.started" | "agent.stdout" | "agent.stderr" | "agent.finished" | "run.completed" | "run.failed" | "run.cancelled";
|
|
270
|
+
type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.validation_started" | "step.validation_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "agent.started" | "agent.stdout" | "agent.stderr" | "agent.finished" | "run.completed" | "run.failed" | "run.cancelled";
|
|
264
271
|
sequenceNumber: number;
|
|
265
272
|
occurredAt: string;
|
|
266
273
|
actor: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-manager/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"generate:skills": "node ./scripts/generate-bundled-skills.mjs",
|
|
26
27
|
"build:bin": "node ./scripts/build-binary.mjs --outfile ./dist/wfm",
|
|
27
28
|
"build:bin:macos": "node ./scripts/build-binary.mjs --target bun-darwin-arm64 --outfile ./dist/wfm-macos-arm64",
|
|
28
29
|
"build:bin:linux": "node ./scripts/build-binary.mjs --target bun-linux-x64 --outfile ./dist/wfm-linux-x64",
|
|
@@ -73,7 +74,8 @@
|
|
|
73
74
|
"license": "MIT",
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"@zed-industries/agent-client-protocol": "^0.4.5",
|
|
76
|
-
"gray-matter": "^4.0.3"
|
|
77
|
+
"gray-matter": "^4.0.3",
|
|
78
|
+
"picocolors": "^1.1.1"
|
|
77
79
|
},
|
|
78
80
|
"devDependencies": {
|
|
79
81
|
"@biomejs/biome": "^2.4.14",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# workflow-author skill
|
|
2
|
+
|
|
3
|
+
Teaches a host coding agent (Claude Code, opencode, pi) how to turn a natural-language
|
|
4
|
+
task description into a repeatable `wfm` workflow, validate it, run it, and narrate
|
|
5
|
+
progress — including approval gates — back to the user.
|
|
6
|
+
|
|
7
|
+
This skill ships inside the root `@workflow-manager/runner` npm package alongside
|
|
8
|
+
`workflow-manager-cli`. Install it into an agent's skill directory with:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
wfm skill install workflow-author # -> ./.claude/skills/
|
|
12
|
+
wfm skill install workflow-author --agent opencode # -> ./.opencode/skill/
|
|
13
|
+
wfm skill install workflow-author --global # -> ~/.claude/skills/
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
See `SKILL.md` for the full operating manual, and `doc/guide/workflow-schema.md` /
|
|
17
|
+
`doc/guide/runner-api.md` for the underlying schema and attach-API contract this
|
|
18
|
+
skill is built on.
|