@workflow-manager/runner 0.1.0 → 0.3.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 +123 -12
- package/dist/adapters.d.ts +4 -0
- package/dist/adapters.js +11 -0
- package/dist/claudeCodeExecutor.d.ts +4 -0
- package/dist/claudeCodeExecutor.js +173 -0
- package/dist/cliRunRenderer.d.ts +36 -0
- package/dist/cliRunRenderer.js +286 -0
- package/dist/engine.d.ts +4 -2
- package/dist/engine.js +622 -44
- package/dist/events.d.ts +1 -1
- package/dist/events.js +4 -2
- package/dist/index.js +371 -41
- package/dist/manPage.d.ts +1 -0
- package/dist/manPage.js +147 -0
- package/dist/mockExecutor.d.ts +2 -2
- package/dist/mockExecutor.js +62 -13
- package/dist/opencodeExecutor.d.ts +2 -2
- package/dist/opencodeExecutor.js +101 -138
- package/dist/parser.js +98 -2
- package/dist/piAgentExecutor.d.ts +4 -0
- package/dist/piAgentExecutor.js +298 -0
- package/dist/remote/api.d.ts +1 -0
- package/dist/remote/commands.d.ts +2 -0
- package/dist/remote/commands.js +76 -4
- package/dist/runnerApi.d.ts +7 -0
- package/dist/runnerApi.js +221 -0
- package/dist/runnerSession.d.ts +62 -0
- package/dist/runnerSession.js +260 -0
- package/dist/runtimePreflight.d.ts +16 -0
- package/dist/runtimePreflight.js +189 -0
- package/dist/skillResolver.d.ts +6 -0
- package/dist/skillResolver.js +75 -0
- package/dist/types.d.ts +148 -2
- package/man/wfm.1 +54 -4
- package/package.json +28 -4
- package/skills/commit-discipline/SKILL.md +109 -0
- package/skills/doc-sync/SKILL.md +83 -0
- package/skills/repo-hygiene/SKILL.md +70 -0
- package/skills/spec-driven-development/SKILL.md +33 -0
- package/skills/workflow-manager-cli/SKILL.md +14 -9
package/dist/events.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import type { RunEvent } from "./types.js";
|
|
|
2
2
|
export declare class EventLog {
|
|
3
3
|
private sequence;
|
|
4
4
|
private events;
|
|
5
|
-
push(runId: string, type: RunEvent["type"], payload?: Record<string, unknown>, stepRunId?: string, actor?: string):
|
|
5
|
+
push(runId: string, type: RunEvent["type"], payload?: Record<string, unknown>, stepRunId?: string, actor?: string): RunEvent;
|
|
6
6
|
all(): RunEvent[];
|
|
7
7
|
}
|
package/dist/events.js
CHANGED
|
@@ -4,7 +4,7 @@ export class EventLog {
|
|
|
4
4
|
events = [];
|
|
5
5
|
push(runId, type, payload = {}, stepRunId, actor = "system") {
|
|
6
6
|
this.sequence += 1;
|
|
7
|
-
|
|
7
|
+
const event = {
|
|
8
8
|
id: randomUUID(),
|
|
9
9
|
runId,
|
|
10
10
|
stepRunId,
|
|
@@ -13,7 +13,9 @@ export class EventLog {
|
|
|
13
13
|
occurredAt: new Date().toISOString(),
|
|
14
14
|
actor,
|
|
15
15
|
payload,
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
|
+
this.events.push(event);
|
|
18
|
+
return event;
|
|
17
19
|
}
|
|
18
20
|
all() {
|
|
19
21
|
return [...this.events];
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { CliRunRenderer } from "./cliRunRenderer.js";
|
|
9
|
+
import { startRunnerApiServer } from "./runnerApi.js";
|
|
10
|
+
import { RunnerSessionStore } from "./runnerSession.js";
|
|
5
11
|
import { parseWorkflowFile, validateWorkflow } from "./parser.js";
|
|
6
|
-
import {
|
|
12
|
+
import { MAN_PAGE_SOURCE } from "./manPage.js";
|
|
13
|
+
import { promptForApprovalDecision, runWorkflow } from "./engine.js";
|
|
7
14
|
import { cmdAuth, cmdPublish, cmdPull, cmdRemoteInfo, cmdSearch } from "./remote/commands.js";
|
|
8
15
|
import { emitRunTelemetryBestEffort } from "./remote/telemetry.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"8) What retry/rollback policy should be default and where should exceptions apply?",
|
|
18
|
-
];
|
|
16
|
+
import { adapterImplementationStatuses, runtimeDoctorChecks, validateRuntimeRequirements, } from "./runtimePreflight.js";
|
|
17
|
+
function cliDisplayName() {
|
|
18
|
+
const invokedAs = path.basename(process.argv[1] ?? "");
|
|
19
|
+
if (invokedAs === "workflow-manager" || invokedAs === "wfm") {
|
|
20
|
+
return invokedAs;
|
|
21
|
+
}
|
|
22
|
+
return "wfm";
|
|
23
|
+
}
|
|
19
24
|
function usage() {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const cli = cliDisplayName();
|
|
26
|
+
console.log(`${cli} commands:
|
|
27
|
+
doctor [workflow.md|workflow.json] [--json]
|
|
28
|
+
agent [path] [--force]
|
|
22
29
|
scaffold [path] [--format markdown|json]
|
|
23
30
|
validate <workflow.md|workflow.json>
|
|
24
|
-
run <workflow.md|workflow.json> [--input input.json] [--objective "string"] [--confirm stepA,stepB:human] [--auto-confirm-all]
|
|
31
|
+
run <workflow.md|workflow.json> [--input input.json] [--objective "string"] [--confirm stepA,stepB:human] [--auto-confirm-all] [--port 43121] [--verbose] [--json]
|
|
32
|
+
approve [--url http://127.0.0.1:43121] [--token token] [--run-id run_123] [--step review] [--actor alice] [--note "LGTM"]
|
|
33
|
+
resume [--url http://127.0.0.1:43121] [--token token] [--run-id run_123] [--step review] [--actor alice] [--note "continue"]
|
|
34
|
+
cancel [--url http://127.0.0.1:43121] [--token token] [--run-id run_123] [--step review] [--actor alice] [--note "stop this run"]
|
|
25
35
|
auth <login|whoami|logout> [--token <token>]
|
|
26
36
|
publish <workflow.md|workflow.json> [--slug slug] [--title title] [--description text] [--visibility public|private] [--version version] [--tag a,b] [--draft]
|
|
27
37
|
pull <owner/slug> [--version version] [--output path]
|
|
@@ -38,15 +48,34 @@ function getFlag(name) {
|
|
|
38
48
|
function hasFlag(name) {
|
|
39
49
|
return process.argv.includes(name);
|
|
40
50
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
function getFlagFromArgs(args, name) {
|
|
52
|
+
const idx = args.indexOf(name);
|
|
53
|
+
if (idx >= 0 && idx + 1 < args.length)
|
|
54
|
+
return args[idx + 1];
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function combineRunObservers(...observers) {
|
|
58
|
+
const active = observers.filter((observer) => observer !== undefined);
|
|
59
|
+
if (active.length === 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
onEvent(event) {
|
|
64
|
+
for (const observer of active) {
|
|
65
|
+
observer.onEvent(event);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
onSnapshot(snapshot, stepDetails) {
|
|
69
|
+
for (const observer of active) {
|
|
70
|
+
observer.onSnapshot(snapshot, stepDetails);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
onLog(log) {
|
|
74
|
+
for (const observer of active) {
|
|
75
|
+
observer.onLog(log);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
50
79
|
}
|
|
51
80
|
const WORKFLOW_SCAFFOLD_JSON = {
|
|
52
81
|
key: "workflow-manager-sample",
|
|
@@ -73,7 +102,6 @@ const WORKFLOW_SCAFFOLD_JSON = {
|
|
|
73
102
|
dependsOn: [],
|
|
74
103
|
validation: { mode: "human", required: true, autoConfirm: false },
|
|
75
104
|
taskSpec: {
|
|
76
|
-
adapterKey: "opencode",
|
|
77
105
|
init: {
|
|
78
106
|
context: { repo: "example/repo" },
|
|
79
107
|
skills: ["architecture", "planning"],
|
|
@@ -157,7 +185,6 @@ steps:
|
|
|
157
185
|
required: true
|
|
158
186
|
autoConfirm: false
|
|
159
187
|
taskSpec:
|
|
160
|
-
adapterKey: opencode
|
|
161
188
|
init:
|
|
162
189
|
context:
|
|
163
190
|
repo: example/repo
|
|
@@ -221,6 +248,36 @@ steps:
|
|
|
221
248
|
|
|
222
249
|
Edit frontmatter to configure orchestration behavior.
|
|
223
250
|
`;
|
|
251
|
+
const AGENT_RULES_TEMPLATE = `# WFM Agent Rules
|
|
252
|
+
|
|
253
|
+
Use these rules when creating, validating, running, or publishing workflow-manager workflows with the \`wfm\` CLI.
|
|
254
|
+
|
|
255
|
+
## Core Flow
|
|
256
|
+
|
|
257
|
+
1. Start with \`wfm doctor\` to inspect local adapter and API key setup.
|
|
258
|
+
2. Create a starter workflow with \`wfm scaffold ./workflow.md\` or \`wfm scaffold ./workflow.json --format json\`.
|
|
259
|
+
3. Edit stable workflow keys, step objectives, dependencies, validation modes, and adapter initialization.
|
|
260
|
+
4. Run \`wfm validate <workflow>\` before every run or publish.
|
|
261
|
+
5. Run \`wfm doctor <workflow>\` before using real host-backed adapters.
|
|
262
|
+
6. Run with \`wfm run <workflow>\`; use \`--verbose\` when agent logs are needed.
|
|
263
|
+
7. Publish only after validation succeeds with \`wfm publish <workflow>\`.
|
|
264
|
+
|
|
265
|
+
## Workflow Authoring
|
|
266
|
+
|
|
267
|
+
- Keep workflow, step, and adapter keys stable; external tools and tests may reference them.
|
|
268
|
+
- Omit \`taskSpec.adapterKey\` for the default \`pi-agent\` adapter.
|
|
269
|
+
- Use \`adapterKey: mock\` for deterministic tests and examples.
|
|
270
|
+
- Put skills, MCP endpoints, system prompts, model, and context under \`taskSpec.init\`.
|
|
271
|
+
- Model-backed steps should declare required environment variables through provider-specific model names or \`taskSpec.payload.requiredEnv\`.
|
|
272
|
+
- Human or external validation should be explicit in the workflow file.
|
|
273
|
+
|
|
274
|
+
## Running Safely
|
|
275
|
+
|
|
276
|
+
- Do not use \`--auto-confirm-all\` unless the workflow is intentionally non-interactive.
|
|
277
|
+
- Prefer \`wfm doctor <workflow>\` before runs that use \`pi-agent\`, real \`opencode\`, or real \`claude-code\`.
|
|
278
|
+
- Use the attach API output from \`wfm run\` for approval, resume, and cancel commands.
|
|
279
|
+
- Preserve Markdown workflows when humans will review notes; use JSON for generated or machine-edited workflows.
|
|
280
|
+
`;
|
|
224
281
|
function resolveScaffoldFormat(targetPath, explicitFormat) {
|
|
225
282
|
if (explicitFormat === "markdown" || explicitFormat === "json") {
|
|
226
283
|
return explicitFormat;
|
|
@@ -243,6 +300,31 @@ function parseScaffoldArgs(args) {
|
|
|
243
300
|
}
|
|
244
301
|
return { targetPath, format };
|
|
245
302
|
}
|
|
303
|
+
function parseAgentArgs(args) {
|
|
304
|
+
let targetPath;
|
|
305
|
+
let force = false;
|
|
306
|
+
for (const arg of args) {
|
|
307
|
+
if (arg === "--force" || arg === "-f") {
|
|
308
|
+
force = true;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (!arg.startsWith("-") && !targetPath) {
|
|
312
|
+
targetPath = arg;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { targetPath, force };
|
|
316
|
+
}
|
|
317
|
+
function cmdAgent(targetPath, force = false) {
|
|
318
|
+
const resolvedPath = path.resolve(targetPath ?? "./AGENTS.md");
|
|
319
|
+
if (fs.existsSync(resolvedPath) && !force) {
|
|
320
|
+
console.error(`Agent rules already exist at ${resolvedPath}. Pass --force to overwrite.`);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
324
|
+
fs.writeFileSync(resolvedPath, AGENT_RULES_TEMPLATE, "utf-8");
|
|
325
|
+
console.log(`Wrote WFM agent rules: ${resolvedPath}`);
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
246
328
|
function cmdScaffold(targetPath, format) {
|
|
247
329
|
const resolvedPath = targetPath ? path.resolve(targetPath) : path.resolve("./example-workflow.md");
|
|
248
330
|
const normalizedFormat = format?.toLowerCase();
|
|
@@ -259,18 +341,36 @@ function cmdScaffold(targetPath, format) {
|
|
|
259
341
|
return 0;
|
|
260
342
|
}
|
|
261
343
|
function cmdMan() {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
344
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
345
|
+
const packageRoot = path.dirname(path.dirname(modulePath));
|
|
346
|
+
const candidatePaths = [path.join(packageRoot, "man", "wfm.1"), path.resolve("./man/wfm.1")];
|
|
347
|
+
const manPagePath = candidatePaths.find((candidatePath) => fs.existsSync(candidatePath));
|
|
348
|
+
let fallbackSource = MAN_PAGE_SOURCE;
|
|
349
|
+
let tempDir;
|
|
350
|
+
const resolvedManPagePath = manPagePath ??
|
|
351
|
+
(() => {
|
|
352
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wfm-man-"));
|
|
353
|
+
const tempManPagePath = path.join(tempDir, "wfm.1");
|
|
354
|
+
fs.writeFileSync(tempManPagePath, MAN_PAGE_SOURCE, "utf-8");
|
|
355
|
+
return tempManPagePath;
|
|
356
|
+
})();
|
|
357
|
+
if (manPagePath) {
|
|
358
|
+
fallbackSource = fs.readFileSync(manPagePath, "utf-8");
|
|
266
359
|
}
|
|
267
|
-
|
|
268
|
-
|
|
360
|
+
try {
|
|
361
|
+
const result = spawnSync("man", [resolvedManPagePath], { stdio: "inherit" });
|
|
362
|
+
if (result.status === 0) {
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
console.log("\n' man ' command unavailable, printing page contents:\n");
|
|
366
|
+
console.log(fallbackSource);
|
|
269
367
|
return 0;
|
|
270
368
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
369
|
+
finally {
|
|
370
|
+
if (tempDir) {
|
|
371
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
274
374
|
}
|
|
275
375
|
function cmdValidate(filePath) {
|
|
276
376
|
try {
|
|
@@ -290,10 +390,151 @@ function cmdValidate(filePath) {
|
|
|
290
390
|
return 1;
|
|
291
391
|
}
|
|
292
392
|
}
|
|
393
|
+
function renderStatus(status) {
|
|
394
|
+
if (status === "ok")
|
|
395
|
+
return "OK";
|
|
396
|
+
if (status === "missing")
|
|
397
|
+
return "MISSING";
|
|
398
|
+
return "INFO";
|
|
399
|
+
}
|
|
400
|
+
function cmdDoctor(args) {
|
|
401
|
+
const workflowPath = args.find((arg) => !arg.startsWith("-"));
|
|
402
|
+
const json = args.includes("--json");
|
|
403
|
+
const hostChecks = runtimeDoctorChecks();
|
|
404
|
+
const adapterStatuses = adapterImplementationStatuses();
|
|
405
|
+
let workflow = null;
|
|
406
|
+
let workflowErrors = [];
|
|
407
|
+
let runtimeErrors = [];
|
|
408
|
+
if (workflowPath) {
|
|
409
|
+
try {
|
|
410
|
+
workflow = parseWorkflowFile(path.resolve(workflowPath));
|
|
411
|
+
workflowErrors = validateWorkflow(workflow);
|
|
412
|
+
if (workflowErrors.length === 0) {
|
|
413
|
+
runtimeErrors = validateRuntimeRequirements(workflow);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
workflowErrors = [err.message];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const baselineErrors = workflowPath
|
|
421
|
+
? []
|
|
422
|
+
: hostChecks
|
|
423
|
+
.filter((check) => check.required && check.status === "missing")
|
|
424
|
+
.map((check) => `${check.label}: ${check.detail}`);
|
|
425
|
+
const exitCode = baselineErrors.length > 0 || workflowErrors.length > 0 || runtimeErrors.length > 0 ? 1 : 0;
|
|
426
|
+
if (json) {
|
|
427
|
+
console.log(JSON.stringify({
|
|
428
|
+
ok: exitCode === 0,
|
|
429
|
+
hostChecks,
|
|
430
|
+
adapterStatuses,
|
|
431
|
+
workflow: workflow
|
|
432
|
+
? {
|
|
433
|
+
key: workflow.key,
|
|
434
|
+
title: workflow.title,
|
|
435
|
+
errors: workflowErrors,
|
|
436
|
+
runtimeErrors,
|
|
437
|
+
}
|
|
438
|
+
: null,
|
|
439
|
+
baselineErrors,
|
|
440
|
+
}, null, 2));
|
|
441
|
+
return exitCode;
|
|
442
|
+
}
|
|
443
|
+
console.log("Workflow Manager Doctor");
|
|
444
|
+
console.log("\nHost runtime:");
|
|
445
|
+
for (const check of hostChecks) {
|
|
446
|
+
const required = check.required ? "required" : "optional";
|
|
447
|
+
console.log(`- ${renderStatus(check.status)} ${check.label} (${required}): ${check.detail}`);
|
|
448
|
+
}
|
|
449
|
+
console.log("\nAdapter implementation:");
|
|
450
|
+
for (const adapter of adapterStatuses) {
|
|
451
|
+
console.log(`- ${adapter.adapter}: ${adapter.status} - ${adapter.detail}`);
|
|
452
|
+
}
|
|
453
|
+
if (workflowPath) {
|
|
454
|
+
console.log(`\nWorkflow: ${workflowPath}`);
|
|
455
|
+
if (workflowErrors.length > 0) {
|
|
456
|
+
console.log("- INVALID schema:");
|
|
457
|
+
for (const error of workflowErrors) {
|
|
458
|
+
console.log(` - ${error}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (runtimeErrors.length > 0) {
|
|
462
|
+
console.log("- INVALID runtime:");
|
|
463
|
+
for (const error of runtimeErrors) {
|
|
464
|
+
console.log(` - ${error}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
console.log("- OK workflow schema and runtime requirements");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return exitCode;
|
|
472
|
+
}
|
|
473
|
+
async function runnerControlRequest(action, args) {
|
|
474
|
+
const baseUrl = getFlagFromArgs(args, "--url") ?? process.env.WFM_RUNNER_URL;
|
|
475
|
+
const token = getFlagFromArgs(args, "--token") ?? process.env.WFM_RUNNER_TOKEN;
|
|
476
|
+
const stepKey = getFlagFromArgs(args, "--step");
|
|
477
|
+
const actor = getFlagFromArgs(args, "--actor");
|
|
478
|
+
const note = getFlagFromArgs(args, "--note");
|
|
479
|
+
const source = getFlagFromArgs(args, "--source") ?? "cli";
|
|
480
|
+
if (!baseUrl) {
|
|
481
|
+
console.error(`Missing --url. You can also set WFM_RUNNER_URL.`);
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
if (!token) {
|
|
485
|
+
console.error(`Missing --token. You can also set WFM_RUNNER_TOKEN.`);
|
|
486
|
+
return 1;
|
|
487
|
+
}
|
|
488
|
+
const headers = {
|
|
489
|
+
Authorization: `Bearer ${token}`,
|
|
490
|
+
};
|
|
491
|
+
let runId = getFlagFromArgs(args, "--run-id");
|
|
492
|
+
if (!runId) {
|
|
493
|
+
const sessionResponse = await fetch(`${baseUrl}/session`, { headers });
|
|
494
|
+
if (!sessionResponse.ok) {
|
|
495
|
+
const message = await sessionResponse.text();
|
|
496
|
+
console.error(`Failed to discover run id: ${message}`);
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
const session = (await sessionResponse.json());
|
|
500
|
+
runId = session.run?.runId;
|
|
501
|
+
}
|
|
502
|
+
if (!runId) {
|
|
503
|
+
console.error("Could not determine run id. Pass --run-id explicitly.");
|
|
504
|
+
return 1;
|
|
505
|
+
}
|
|
506
|
+
const response = await fetch(`${baseUrl}/runs/${runId}/${action}`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: {
|
|
509
|
+
...headers,
|
|
510
|
+
"Content-Type": "application/json",
|
|
511
|
+
},
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
stepKey,
|
|
514
|
+
actor,
|
|
515
|
+
note,
|
|
516
|
+
source,
|
|
517
|
+
}),
|
|
518
|
+
});
|
|
519
|
+
const payloadText = await response.text();
|
|
520
|
+
const payload = payloadText ? JSON.parse(payloadText) : {};
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
const message = typeof payload.message === "string" ? payload.message : `Request failed with status ${response.status}`;
|
|
523
|
+
console.error(`${action} failed: ${message}`);
|
|
524
|
+
return 1;
|
|
525
|
+
}
|
|
526
|
+
const decision = typeof payload.decision === "string" ? payload.decision : action === "cancel" ? "cancelled" : "approved";
|
|
527
|
+
const resolvedStep = typeof payload.stepKey === "string" ? payload.stepKey : stepKey ?? "current";
|
|
528
|
+
console.log(`${decision} ${resolvedStep}`);
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
293
531
|
async function cmdRun(filePath) {
|
|
294
532
|
const resolvedPath = path.resolve(filePath);
|
|
295
533
|
const startedAt = Date.now();
|
|
296
534
|
let workflow;
|
|
535
|
+
let runnerServer;
|
|
536
|
+
let sessionStore;
|
|
537
|
+
let liveRenderer;
|
|
297
538
|
try {
|
|
298
539
|
workflow = parseWorkflowFile(resolvedPath);
|
|
299
540
|
const errors = validateWorkflow(workflow);
|
|
@@ -308,20 +549,98 @@ async function cmdRun(filePath) {
|
|
|
308
549
|
return 1;
|
|
309
550
|
}
|
|
310
551
|
const objective = getFlag("--objective");
|
|
311
|
-
const
|
|
312
|
-
|
|
552
|
+
const inputRaw = getFlag("--input");
|
|
553
|
+
let input = {};
|
|
554
|
+
if (inputRaw) {
|
|
555
|
+
const parsed = inputRaw.trimStart().startsWith("{")
|
|
556
|
+
? JSON.parse(inputRaw.replace(/[\n\r]/g, " "))
|
|
557
|
+
: JSON.parse(fs.readFileSync(path.resolve(inputRaw), "utf-8"));
|
|
558
|
+
if (typeof parsed !== "object" || Array.isArray(parsed) || parsed === null) {
|
|
559
|
+
console.error("--input must be a JSON object");
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
input = parsed;
|
|
563
|
+
}
|
|
313
564
|
const confirmRaw = getFlag("--confirm") ?? "";
|
|
314
565
|
const confirmations = confirmRaw
|
|
315
566
|
.split(",")
|
|
316
567
|
.map((x) => x.trim())
|
|
317
568
|
.filter(Boolean);
|
|
318
|
-
const
|
|
569
|
+
const runId = randomUUID();
|
|
570
|
+
const requestedPortRaw = getFlag("--port");
|
|
571
|
+
const requestedPort = requestedPortRaw === undefined ? 0 : Number.parseInt(requestedPortRaw, 10);
|
|
572
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
573
|
+
console.error("--port must be an integer between 0 and 65535");
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
sessionStore = new RunnerSessionStore({
|
|
577
|
+
runId,
|
|
578
|
+
workflow,
|
|
579
|
+
objective: objective ?? workflow.title,
|
|
580
|
+
objectives: workflow.objectives ?? [],
|
|
581
|
+
});
|
|
582
|
+
liveRenderer = new CliRunRenderer({
|
|
583
|
+
workflow,
|
|
584
|
+
verbose: hasFlag("--verbose"),
|
|
585
|
+
});
|
|
586
|
+
runnerServer = await startRunnerApiServer(sessionStore, requestedPort);
|
|
587
|
+
const session = sessionStore.sessionInfo();
|
|
588
|
+
process.stderr.write(`Attach API: ${session.baseUrl} (token ${session.attachToken})\n`);
|
|
589
|
+
const result = await runWorkflow(workflow, {
|
|
590
|
+
runId,
|
|
319
591
|
objective,
|
|
320
592
|
input,
|
|
321
593
|
confirmations,
|
|
322
594
|
autoConfirmAll: hasFlag("--auto-confirm-all"),
|
|
595
|
+
interactive: process.stdin.isTTY,
|
|
596
|
+
workflowFilePath: resolvedPath,
|
|
597
|
+
approvalPrompt: async (request) => {
|
|
598
|
+
liveRenderer?.pauseHeartbeat();
|
|
599
|
+
try {
|
|
600
|
+
const decision = await promptForApprovalDecision(request.stepKey, request.reason, request.validation ?? "external", request.preview ?? null, "cli", request.signal);
|
|
601
|
+
if (!decision) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const metadata = {
|
|
605
|
+
actor: decision.actor,
|
|
606
|
+
note: decision.note,
|
|
607
|
+
source: decision.source,
|
|
608
|
+
};
|
|
609
|
+
const outcome = decision.decision === "cancelled"
|
|
610
|
+
? sessionStore?.cancel(request.stepKey, metadata)
|
|
611
|
+
: request.validation === "external"
|
|
612
|
+
? sessionStore?.resume(request.stepKey, metadata)
|
|
613
|
+
: sessionStore?.approve(request.stepKey, metadata);
|
|
614
|
+
if (outcome && !outcome.ok) {
|
|
615
|
+
process.stderr.write(`Could not apply terminal decision for ${request.stepKey}: ${outcome.reason ?? "unknown error"}\n`);
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
liveRenderer?.resumeHeartbeat();
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
observer: combineRunObservers(sessionStore, liveRenderer),
|
|
624
|
+
controller: sessionStore,
|
|
323
625
|
});
|
|
324
|
-
|
|
626
|
+
liveRenderer.close();
|
|
627
|
+
if (hasFlag("--json")) {
|
|
628
|
+
console.log(JSON.stringify({ session: sessionStore.sessionInfo(), ...result }, null, 2));
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const icon = result.status === "succeeded" ? "✓" : result.status === "waiting_for_approval" ? "◌" : "✗";
|
|
632
|
+
process.stderr.write(`\n${icon} ${result.status} — ${workflow.title}\n\n`);
|
|
633
|
+
for (const sr of result.stepRuns) {
|
|
634
|
+
const stepIcon = sr.status === "succeeded" ? "✓" : sr.status === "waiting_for_approval" ? "◌" : "✗";
|
|
635
|
+
const step = workflow.steps.find((s) => s.key === sr.stepKey);
|
|
636
|
+
const adapterKey = step?.kind === "task" ? (step.taskSpec?.adapterKey ?? "pi-agent") : "approval";
|
|
637
|
+
process.stderr.write(` ${stepIcon} ${sr.stepKey.padEnd(20)} ${adapterKey}\n`);
|
|
638
|
+
}
|
|
639
|
+
if (result.status !== "succeeded") {
|
|
640
|
+
process.stderr.write(`\nRun --json to see full output.\n`);
|
|
641
|
+
}
|
|
642
|
+
process.stderr.write("\n");
|
|
643
|
+
}
|
|
325
644
|
await emitRunTelemetryBestEffort({
|
|
326
645
|
definition: workflow,
|
|
327
646
|
sourceFilePath: resolvedPath,
|
|
@@ -332,6 +651,7 @@ async function cmdRun(filePath) {
|
|
|
332
651
|
return result.status === "succeeded" ? 0 : 2;
|
|
333
652
|
}
|
|
334
653
|
catch (err) {
|
|
654
|
+
liveRenderer?.close();
|
|
335
655
|
console.error(`Run error: ${err.message}`);
|
|
336
656
|
if (workflow) {
|
|
337
657
|
await emitRunTelemetryBestEffort({
|
|
@@ -343,6 +663,10 @@ async function cmdRun(filePath) {
|
|
|
343
663
|
}
|
|
344
664
|
return 1;
|
|
345
665
|
}
|
|
666
|
+
finally {
|
|
667
|
+
liveRenderer?.close();
|
|
668
|
+
await runnerServer?.close().catch(() => undefined);
|
|
669
|
+
}
|
|
346
670
|
}
|
|
347
671
|
async function main() {
|
|
348
672
|
const cmd = process.argv[2];
|
|
@@ -350,9 +674,12 @@ async function main() {
|
|
|
350
674
|
usage();
|
|
351
675
|
process.exit(0);
|
|
352
676
|
}
|
|
353
|
-
if (cmd === "
|
|
354
|
-
|
|
355
|
-
|
|
677
|
+
if (cmd === "doctor") {
|
|
678
|
+
process.exit(cmdDoctor(process.argv.slice(3)));
|
|
679
|
+
}
|
|
680
|
+
if (cmd === "agent") {
|
|
681
|
+
const { targetPath, force } = parseAgentArgs(process.argv.slice(3));
|
|
682
|
+
process.exit(cmdAgent(targetPath, force));
|
|
356
683
|
}
|
|
357
684
|
if (cmd === "scaffold") {
|
|
358
685
|
const { targetPath, format } = parseScaffoldArgs(process.argv.slice(3));
|
|
@@ -377,6 +704,9 @@ async function main() {
|
|
|
377
704
|
}
|
|
378
705
|
process.exit(await cmdRun(file));
|
|
379
706
|
}
|
|
707
|
+
if (cmd === "approve" || cmd === "resume" || cmd === "cancel") {
|
|
708
|
+
process.exit(await runnerControlRequest(cmd, process.argv.slice(3)));
|
|
709
|
+
}
|
|
380
710
|
if (cmd === "auth") {
|
|
381
711
|
process.exit(await cmdAuth(process.argv.slice(3)));
|
|
382
712
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MAN_PAGE_SOURCE = ".TH WFM 1 \"April 2026\" \"@workflow-manager/runner\" \"User Commands\"\n.SH NAME\nwfm \\- run markdown or json workflows from the CLI\n.SH SYNOPSIS\n.B wfm\n.I command\n[options]\n.SH DESCRIPTION\nwfm parses a workflow definition file, validates it, and executes\nit with deterministic in-memory orchestration.\n\nWorkflow files can be Markdown with YAML frontmatter or JSON.\n.SH COMMANDS\n.TP\n.B doctor [workflow.md|workflow.json] [--json]\nInspect host adapter setup, LLM access keys, and current adapter implementation status. When a workflow path is provided, also validate schema and runtime requirements without executing steps.\n.TP\n.B agent [path] [--force]\nCreate WFM-focused agent rules. The default path is ./AGENTS.md. Existing files are not overwritten unless --force is passed.\n.TP\n.B scaffold [path] [--format markdown|json]\nCreate a starter workflow file. Format defaults to markdown unless the output\npath ends in .json.\n.TP\n.B validate <workflow.md|workflow.json>\nValidate workflow structure and report schema errors.\n.TP\n.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json]\nRun the workflow with live CLI progress and optional JSON output.\n.TP\n.B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nApprove the current waiting runner step through the local attach API.\n.TP\n.B resume [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nAlias for approve, intended for external resume flows.\n.TP\n.B cancel [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]\nCancel the current waiting runner step through the local attach API.\n.TP\n.B auth <login|whoami|logout> [--token value]\nManage remote registry authentication for CLI publish and pull flows.\n.TP\n.B publish <workflow.md|workflow.json> [--slug slug] [--title text] [--description text] [--visibility public|private] [--version label] [--tag a,b] [--draft]\nPublish a validated local workflow to the remote registry.\n.TP\n.B pull <owner/slug> [--version label] [--output path]\nDownload a remote workflow and write it to a local file.\n.TP\n.B search [query]\nSearch public workflows from the remote registry.\n.TP\n.B remote info <owner/slug>\nShow metadata and source information for a remote workflow.\n.TP\n.B man\nOpen this man page.\n.SH RUN OPTIONS\n.TP\n.B --input <path>\nJSON file merged into global workflow input state.\n.TP\n.B --objective <text>\nOverride the default run objective.\n.TP\n.B --confirm <stepA,stepB:human,...>\nProvide explicit confirmations for steps that require validation.\n.TP\n.B --auto-confirm-all\nBypass confirmation gating for all steps.\n.TP\n.B --port <number>\nBind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.\n.TP\n.B --verbose\nStream per-step agent output and execution updates to stderr while the workflow runs.\n.TP\n.B --json\nPrint the final run result as JSON on stdout while keeping live progress on stderr.\n.TP\nHuman approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.\n.TP\n.B --url <value>\nRunner attach API base URL for approve, resume, or cancel commands.\n.TP\n.B --token <value>\nRunner attach API bearer token for approve, resume, or cancel commands.\n.TP\n.B --run-id <value>\nRunner id to control. If omitted, the CLI reads it from /session.\n.TP\n.B --step <value>\nOptional step key when controlling a specific waiting step.\n.TP\n.B --actor <value>\nActor name recorded in approval audit events.\n.TP\n.B --note <text>\nOptional approval or cancellation note recorded in the event payload.\n.SH EXAMPLES\n.TP\nValidate markdown workflow:\n.B wfm validate ./example-workflow.md\n.TP\nValidate json workflow:\n.B wfm validate ./example-workflow.json\n.TP\nScaffold json workflow file:\n.B wfm scaffold ./new-workflow.json --format json\n.TP\nAuthenticate with a CLI token:\n.B wfm auth login --token wm_exampletoken\n.TP\nPublish a workflow:\n.B wfm publish ./example-workflow.json --visibility public --tag example,automation\n.TP\nPull a remote workflow:\n.B wfm pull alice/remote-bunny --output ./remote-bunny.json\n.TP\nSearch the remote registry:\n.B wfm search bunny\n.TP\nRun with explicit confirmations:\n.B wfm run ./example-workflow.json --confirm discover:human,qa_gate:human\n.TP\nInspect host setup:\n.B wfm doctor\n.TP\nCheck a workflow before running it:\n.B wfm doctor ./example-workflow.json\n.TP\nCreate agent rules:\n.B wfm agent ./AGENTS.md\n.SH FILES\n.TP\n.B man/wfm.1\nThe manual page source shipped with this repository.\n.SH EXIT STATUS\n.TP\n.B 0\nSuccessful command execution.\n.TP\n.B 1\nValidation or runtime error.\n.TP\n.B 2\nRun completed in non-success terminal status.\n";
|