@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.
Files changed (40) hide show
  1. package/README.md +123 -12
  2. package/dist/adapters.d.ts +4 -0
  3. package/dist/adapters.js +11 -0
  4. package/dist/claudeCodeExecutor.d.ts +4 -0
  5. package/dist/claudeCodeExecutor.js +173 -0
  6. package/dist/cliRunRenderer.d.ts +36 -0
  7. package/dist/cliRunRenderer.js +286 -0
  8. package/dist/engine.d.ts +4 -2
  9. package/dist/engine.js +622 -44
  10. package/dist/events.d.ts +1 -1
  11. package/dist/events.js +4 -2
  12. package/dist/index.js +371 -41
  13. package/dist/manPage.d.ts +1 -0
  14. package/dist/manPage.js +147 -0
  15. package/dist/mockExecutor.d.ts +2 -2
  16. package/dist/mockExecutor.js +62 -13
  17. package/dist/opencodeExecutor.d.ts +2 -2
  18. package/dist/opencodeExecutor.js +101 -138
  19. package/dist/parser.js +98 -2
  20. package/dist/piAgentExecutor.d.ts +4 -0
  21. package/dist/piAgentExecutor.js +298 -0
  22. package/dist/remote/api.d.ts +1 -0
  23. package/dist/remote/commands.d.ts +2 -0
  24. package/dist/remote/commands.js +76 -4
  25. package/dist/runnerApi.d.ts +7 -0
  26. package/dist/runnerApi.js +221 -0
  27. package/dist/runnerSession.d.ts +62 -0
  28. package/dist/runnerSession.js +260 -0
  29. package/dist/runtimePreflight.d.ts +16 -0
  30. package/dist/runtimePreflight.js +189 -0
  31. package/dist/skillResolver.d.ts +6 -0
  32. package/dist/skillResolver.js +75 -0
  33. package/dist/types.d.ts +148 -2
  34. package/man/wfm.1 +54 -4
  35. package/package.json +28 -4
  36. package/skills/commit-discipline/SKILL.md +109 -0
  37. package/skills/doc-sync/SKILL.md +83 -0
  38. package/skills/repo-hygiene/SKILL.md +70 -0
  39. package/skills/spec-driven-development/SKILL.md +33 -0
  40. 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): void;
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
- this.events.push({
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 { runWorkflow } from "./engine.js";
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
- const DISCOVERY_QUESTIONS = [
10
- "1) What set of workflow objectives should be tracked per run (one or many)?",
11
- "2) Which steps require human validation vs external validation?",
12
- "3) Which approvals are mandatory and who can confirm each approval?",
13
- "4) Which steps require explicit confirmation before proceeding?",
14
- "5) For each step, which adapter should run it (opencode, codex, claude-code, mock)?",
15
- "6) What per-step initialization is needed (context, skills, MCPs, system prompts, model)?",
16
- "7) What output format should this session produce (JSON run report, markdown summary, event timeline)?",
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
- console.log(`wfm commands:
21
- questions
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 cmdQuestions() {
42
- console.log("Project definition questions:");
43
- for (const q of DISCOVERY_QUESTIONS)
44
- console.log(`- ${q}`);
45
- console.log("\nExpected output of this session:");
46
- console.log("- Finalized workflow file (markdown or json) with per-step objectives");
47
- console.log("- Validation/approval confirmation map per step");
48
- console.log("- Adapter init map (opencode/codex/claude-code with context+skills+mcps)");
49
- console.log("- Runnable CLI command examples + run JSON output");
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 manPagePath = path.resolve("./man/wfm.1");
263
- if (!fs.existsSync(manPagePath)) {
264
- console.error(`Man page not found at ${manPagePath}`);
265
- return 1;
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
- const result = spawnSync("man", [manPagePath], { stdio: "inherit" });
268
- if (result.status === 0) {
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
- console.log("\n' man ' command unavailable, printing page contents:\n");
272
- console.log(fs.readFileSync(manPagePath, "utf-8"));
273
- return 0;
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 inputPath = getFlag("--input");
312
- const input = inputPath ? JSON.parse(fs.readFileSync(path.resolve(inputPath), "utf-8")) : {};
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 result = runWorkflow(workflow, {
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
- console.log(JSON.stringify(result, null, 2));
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 === "questions") {
354
- cmdQuestions();
355
- process.exit(0);
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";