@workflow-manager/runner 0.7.0 → 0.9.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/dist/index.js CHANGED
@@ -14,7 +14,9 @@ import { parseWorkflowFile, validateWorkflow } from "./parser.js";
14
14
  import { MAN_PAGE_SOURCE } from "./manPage.js";
15
15
  import { promptForApprovalDecision, runWorkflow } from "./engine.js";
16
16
  import { cmdAuth, cmdPublish, cmdPull, cmdRemoteInfo, cmdSearch } from "./remote/commands.js";
17
+ import { readSessionFile, writeSessionFile } from "./sessionFile.js";
17
18
  import { emitRunTelemetryBestEffort } from "./remote/telemetry.js";
19
+ import { BUNDLED_SKILLS } from "./generated/bundledSkills.js";
18
20
  import { adapterImplementationStatuses, adapterMockFallbackWarnings, runtimeDoctorChecks, validateRuntimeRequirements, } from "./runtimePreflight.js";
19
21
  function resolveVersion() {
20
22
  if (typeof WFM_VERSION === "string" && WFM_VERSION) {
@@ -53,15 +55,20 @@ function usage() {
53
55
  `Usage: ${cli} <command> [options]`,
54
56
  "",
55
57
  "Author and run workflows",
56
- row("scaffold [path]", "Write a starter workflow file"),
58
+ row("scaffold [path]", "Write a starter workflow file (--template agent-validated for validator example)"),
57
59
  row("validate <file>", "Check a workflow for errors"),
58
60
  row("doctor [file]", "Check host setup; with a file, preflight it"),
59
61
  row("run <file>", "Run a workflow with live progress (--ui for full-screen)"),
60
62
  "",
61
- "Control a running workflow",
63
+ "Control or observe a running workflow",
62
64
  row("approve", "Approve a step waiting for review"),
63
65
  row("resume", "Resume a step waiting on external input"),
64
66
  row("cancel", "Cancel a waiting step"),
67
+ row("status [--step <key>]", "Print the run (or step) snapshot as JSON"),
68
+ row("logs [--step <key>]", "Print buffered agent logs as JSON"),
69
+ row("events [--since <seq>]", "Print run events as JSON (one-shot poll)"),
70
+ " Connect with --url/--token, --session-file <path> (written by run --session-file),",
71
+ " or WFM_RUNNER_URL/WFM_RUNNER_TOKEN.",
65
72
  "",
66
73
  "Share workflows (remote registry)",
67
74
  row("auth <login|whoami|logout>", "Sign in, check, or sign out"),
@@ -314,6 +321,159 @@ steps:
314
321
 
315
322
  Edit frontmatter to configure orchestration behavior.
316
323
  `;
324
+ const WORKFLOW_SCAFFOLD_AGENT_VALIDATED_JSON = {
325
+ key: "agent-validated-pipeline",
326
+ title: "Agent-Validated Pipeline",
327
+ description: "Repeatable task pipeline where a second agent checks the implementation against explicit criteria",
328
+ objectives: ["ship a change that meets the stated acceptance criteria"],
329
+ defaultRetryPolicy: { maxAttempts: 2 },
330
+ steps: [
331
+ {
332
+ key: "implement",
333
+ kind: "task",
334
+ objective: "Implement the requested change",
335
+ dependsOn: [],
336
+ retryPolicy: { maxAttempts: 2 },
337
+ validation: {
338
+ mode: "agent",
339
+ required: true,
340
+ autoConfirm: false,
341
+ agent: {
342
+ criteria: "The change satisfies the workflow objective, builds cleanly, and includes tests for new behavior.",
343
+ init: {
344
+ model: "openrouter/anthropic/claude-sonnet-4",
345
+ systemPrompts: ["Check the diff against the criteria; be specific about any gaps found"],
346
+ },
347
+ },
348
+ },
349
+ taskSpec: {
350
+ init: {
351
+ context: { repo: "example/repo" },
352
+ skills: ["coding", "testing"],
353
+ systemPrompts: ["Implement the change described in the workflow objective"],
354
+ },
355
+ payload: { mockResult: "success" },
356
+ },
357
+ },
358
+ {
359
+ key: "review-gate",
360
+ kind: "approval",
361
+ objective: "Human sign-off before finalizing",
362
+ dependsOn: ["implement"],
363
+ validation: { mode: "human", required: true, autoConfirm: false },
364
+ approvalSpec: {
365
+ autoApprove: false,
366
+ validation: { mode: "human", required: true, autoConfirm: false },
367
+ },
368
+ },
369
+ {
370
+ key: "finalize",
371
+ kind: "task",
372
+ objective: "Finalize the change (for example, open a PR)",
373
+ dependsOn: ["review-gate"],
374
+ validation: { mode: "none", required: false, autoConfirm: true },
375
+ taskSpec: {
376
+ init: {
377
+ systemPrompts: ["Open a PR summarizing the change and its validation history"],
378
+ },
379
+ payload: { mockResult: "success" },
380
+ },
381
+ },
382
+ ],
383
+ };
384
+ const WORKFLOW_SCAFFOLD_AGENT_VALIDATED_MARKDOWN = `---
385
+ key: agent-validated-pipeline
386
+ title: Agent-Validated Pipeline
387
+ description: Repeatable task pipeline where a second agent checks the implementation against explicit criteria
388
+ objectives:
389
+ - ship a change that meets the stated acceptance criteria
390
+ defaultRetryPolicy:
391
+ maxAttempts: 2
392
+ steps:
393
+ # "implement" omits taskSpec.adapterKey, so it runs on the default pi-agent adapter.
394
+ - key: implement
395
+ kind: task
396
+ objective: Implement the requested change
397
+ dependsOn: []
398
+ retryPolicy:
399
+ maxAttempts: 2
400
+ validation:
401
+ # mode: agent routes this step through a second, independent agent call after
402
+ # implement finishes. That validator agent reads validation.agent.criteria,
403
+ # returns a verdict (SUCCESS/QA_REJECTED/...), and the engine maps it to a QA
404
+ # action: PROCEED keeps going, RETRY_CURRENT reruns this step, ROLLBACK_PREVIOUS
405
+ # reruns an earlier step, RESTART_ALL restarts the run. Sharpen criteria to
406
+ # control what the validator accepts.
407
+ mode: agent
408
+ required: true
409
+ autoConfirm: false
410
+ agent:
411
+ criteria: >-
412
+ The change satisfies the workflow objective, builds cleanly, and
413
+ includes tests for new behavior.
414
+ init:
415
+ model: openrouter/anthropic/claude-sonnet-4
416
+ systemPrompts:
417
+ - Check the diff against the criteria; be specific about any gaps found
418
+ taskSpec:
419
+ init:
420
+ context:
421
+ repo: example/repo
422
+ skills: [coding, testing]
423
+ systemPrompts: [Implement the change described in the workflow objective]
424
+ payload:
425
+ # mockResult drives the mock adapter for local dry runs; real adapters ignore it.
426
+ mockResult: success
427
+ - key: review-gate
428
+ kind: approval
429
+ objective: Human sign-off before finalizing
430
+ dependsOn: [implement]
431
+ # Top-level validation must match approvalSpec.validation: without it, the
432
+ # parser's default (autoConfirm: true) wins and the gate auto-approves.
433
+ validation:
434
+ mode: human
435
+ required: true
436
+ autoConfirm: false
437
+ approvalSpec:
438
+ autoApprove: false
439
+ validation:
440
+ mode: human
441
+ required: true
442
+ autoConfirm: false
443
+ - key: finalize
444
+ kind: task
445
+ objective: "Finalize the change (for example, open a PR)"
446
+ dependsOn: [review-gate]
447
+ validation:
448
+ mode: none
449
+ required: false
450
+ autoConfirm: true
451
+ taskSpec:
452
+ init:
453
+ systemPrompts: [Open a PR summarizing the change and its validation history]
454
+ payload:
455
+ mockResult: success
456
+ ---
457
+
458
+ # Agent-Validated Pipeline
459
+
460
+ This workflow shows first-class agent validation: instead of (or in addition to) a
461
+ human or external check, a step's own output can be graded by a second agent call.
462
+
463
+ - \`implement\` runs, then its \`validation.agent\` config sends the result to a
464
+ validator agent along with \`criteria\` — plain-language acceptance criteria the
465
+ validator checks the work against.
466
+ - The validator's verdict becomes a QA action: \`PROCEED\` lets the run continue,
467
+ \`RETRY_CURRENT\` re-runs \`implement\` with the validator's feedback,
468
+ \`ROLLBACK_PREVIOUS\` re-runs an earlier step, and \`RESTART_ALL\` restarts the run.
469
+ Retries are bounded by \`retryPolicy.maxAttempts\`.
470
+ - \`review-gate\` is a human approval step — agent validation cannot be used on
471
+ approval steps, so high-stakes changes still get a human in the loop before
472
+ \`finalize\` runs.
473
+
474
+ Tighten \`validation.agent.criteria\` to describe exactly what "done" means for this
475
+ step; the validator only knows what criteria tells it.
476
+ `;
317
477
  function resolveScaffoldFormat(targetPath, explicitFormat) {
318
478
  if (explicitFormat === "markdown" || explicitFormat === "json") {
319
479
  return explicitFormat;
@@ -323,6 +483,7 @@ function resolveScaffoldFormat(targetPath, explicitFormat) {
323
483
  function parseScaffoldArgs(args) {
324
484
  let targetPath;
325
485
  let format;
486
+ let template;
326
487
  for (let i = 0; i < args.length; i += 1) {
327
488
  const arg = args[i];
328
489
  if (arg === "--format") {
@@ -330,11 +491,16 @@ function parseScaffoldArgs(args) {
330
491
  i += 1;
331
492
  continue;
332
493
  }
494
+ if (arg === "--template") {
495
+ template = args[i + 1];
496
+ i += 1;
497
+ continue;
498
+ }
333
499
  if (!arg.startsWith("-") && !targetPath) {
334
500
  targetPath = arg;
335
501
  }
336
502
  }
337
- return { targetPath, format };
503
+ return { targetPath, format, template };
338
504
  }
339
505
  const DEFAULT_INSTALL_SKILL = "workflow-manager-cli";
340
506
  const SKILL_INSTALL_TARGETS = {
@@ -347,11 +513,22 @@ const SKILL_INSTALL_TARGETS = {
347
513
  globalDir: path.join(os.homedir(), ".config", "opencode", "skill"),
348
514
  },
349
515
  };
350
- function packagedSkillsDir() {
516
+ export function packagedSkillsDir() {
351
517
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
352
518
  }
353
- function listPackagedSkills() {
354
- const root = packagedSkillsDir();
519
+ export function parseSkillDescription(skillMarkdown) {
520
+ try {
521
+ const parsed = matter(skillMarkdown);
522
+ if (typeof parsed.data.description === "string") {
523
+ return parsed.data.description.replace(/\s+/g, " ").trim();
524
+ }
525
+ }
526
+ catch {
527
+ // skills without parseable frontmatter are still installable
528
+ }
529
+ return "";
530
+ }
531
+ export function listPackagedSkillsFromDisk(root) {
355
532
  if (!fs.existsSync(root)) {
356
533
  return [];
357
534
  }
@@ -363,20 +540,44 @@ function listPackagedSkills() {
363
540
  const skillFile = path.join(dir, "SKILL.md");
364
541
  if (!fs.existsSync(skillFile))
365
542
  continue;
366
- let description = "";
367
- try {
368
- const parsed = matter(fs.readFileSync(skillFile, "utf-8"));
369
- if (typeof parsed.data.description === "string") {
370
- description = parsed.data.description.replace(/\s+/g, " ").trim();
371
- }
372
- }
373
- catch {
374
- // skills without parseable frontmatter are still installable
375
- }
376
- skills.push({ name: entry.name, dir, description });
543
+ const description = parseSkillDescription(fs.readFileSync(skillFile, "utf-8"));
544
+ skills.push({ name: entry.name, description, source: { kind: "disk", dir } });
545
+ }
546
+ return skills;
547
+ }
548
+ export function listPackagedSkillsFromBundle(bundle = BUNDLED_SKILLS) {
549
+ const skills = [];
550
+ for (const name of Object.keys(bundle).sort()) {
551
+ const files = bundle[name];
552
+ const skillFile = files.find((file) => file.name === "SKILL.md");
553
+ if (!skillFile)
554
+ continue;
555
+ skills.push({ name, description: parseSkillDescription(skillFile.content), source: { kind: "bundled", files } });
377
556
  }
378
557
  return skills;
379
558
  }
559
+ export function listPackagedSkills(root = packagedSkillsDir()) {
560
+ const onDisk = listPackagedSkillsFromDisk(root);
561
+ if (onDisk.length > 0) {
562
+ return onDisk;
563
+ }
564
+ return listPackagedSkillsFromBundle();
565
+ }
566
+ export function materializeSkillFiles(skill, destDir) {
567
+ if (skill.source.kind === "disk") {
568
+ for (const entry of fs.readdirSync(skill.source.dir, { withFileTypes: true })) {
569
+ if (!entry.isFile() || entry.name === "README.md")
570
+ continue;
571
+ fs.copyFileSync(path.join(skill.source.dir, entry.name), path.join(destDir, entry.name));
572
+ }
573
+ return;
574
+ }
575
+ for (const file of skill.source.files) {
576
+ if (file.name === "README.md")
577
+ continue;
578
+ fs.writeFileSync(path.join(destDir, file.name), file.content, "utf-8");
579
+ }
580
+ }
380
581
  function cmdSkillList() {
381
582
  const skills = listPackagedSkills();
382
583
  if (skills.length === 0) {
@@ -461,16 +662,12 @@ function cmdSkillInstall(args) {
461
662
  return 1;
462
663
  }
463
664
  fs.mkdirSync(destDir, { recursive: true });
464
- for (const entry of fs.readdirSync(skill.dir, { withFileTypes: true })) {
465
- if (!entry.isFile() || entry.name === "README.md")
466
- continue;
467
- fs.copyFileSync(path.join(skill.dir, entry.name), path.join(destDir, entry.name));
468
- }
665
+ materializeSkillFiles(skill, destDir);
469
666
  console.log(`Installed skill ${name} -> ${destDir}`);
470
667
  }
471
668
  return 0;
472
669
  }
473
- function cmdScaffold(targetPath, format) {
670
+ function cmdScaffold(targetPath, format, template) {
474
671
  const resolvedPath = targetPath ? path.resolve(targetPath) : path.resolve("./example-workflow.md");
475
672
  const normalizedFormat = format?.toLowerCase();
476
673
  const resolvedFormat = resolveScaffoldFormat(resolvedPath, normalizedFormat);
@@ -478,10 +675,19 @@ function cmdScaffold(targetPath, format) {
478
675
  console.error(`Invalid --format value: ${format}. Use markdown or json.`);
479
676
  return 1;
480
677
  }
481
- const template = resolvedFormat === "json"
482
- ? `${JSON.stringify(WORKFLOW_SCAFFOLD_JSON, null, 2)}\n`
483
- : WORKFLOW_SCAFFOLD_MARKDOWN;
484
- fs.writeFileSync(resolvedPath, template, "utf-8");
678
+ const normalizedTemplate = template?.toLowerCase() ?? "default";
679
+ if (normalizedTemplate !== "default" && normalizedTemplate !== "agent-validated") {
680
+ console.error(`Invalid --template value: ${template}. Use default or agent-validated.`);
681
+ return 1;
682
+ }
683
+ const content = normalizedTemplate === "agent-validated"
684
+ ? resolvedFormat === "json"
685
+ ? `${JSON.stringify(WORKFLOW_SCAFFOLD_AGENT_VALIDATED_JSON, null, 2)}\n`
686
+ : WORKFLOW_SCAFFOLD_AGENT_VALIDATED_MARKDOWN
687
+ : resolvedFormat === "json"
688
+ ? `${JSON.stringify(WORKFLOW_SCAFFOLD_JSON, null, 2)}\n`
689
+ : WORKFLOW_SCAFFOLD_MARKDOWN;
690
+ fs.writeFileSync(resolvedPath, content, "utf-8");
485
691
  console.log(`Scaffolded ${resolvedFormat} workflow: ${resolvedPath}`);
486
692
  return 0;
487
693
  }
@@ -624,40 +830,66 @@ function cmdDoctor(args) {
624
830
  }
625
831
  return exitCode;
626
832
  }
627
- async function runnerControlRequest(action, args) {
628
- const baseUrl = getFlagFromArgs(args, "--url") ?? process.env.WFM_RUNNER_URL;
629
- const token = getFlagFromArgs(args, "--token") ?? process.env.WFM_RUNNER_TOKEN;
630
- const stepKey = getFlagFromArgs(args, "--step");
631
- const actor = getFlagFromArgs(args, "--actor");
632
- const note = getFlagFromArgs(args, "--note");
633
- const source = getFlagFromArgs(args, "--source") ?? "cli";
833
+ function resolveRunnerConnection(args) {
834
+ let baseUrl = getFlagFromArgs(args, "--url");
835
+ let token = getFlagFromArgs(args, "--token");
836
+ let runId = getFlagFromArgs(args, "--run-id");
837
+ const sessionFilePath = getFlagFromArgs(args, "--session-file");
838
+ if (sessionFilePath && (!baseUrl || !token || !runId)) {
839
+ const session = readSessionFile(sessionFilePath);
840
+ if (typeof session === "string") {
841
+ return session;
842
+ }
843
+ baseUrl = baseUrl ?? session.baseUrl;
844
+ token = token ?? session.attachToken;
845
+ runId = runId ?? session.runId;
846
+ }
847
+ baseUrl = baseUrl ?? process.env.WFM_RUNNER_URL;
848
+ token = token ?? process.env.WFM_RUNNER_TOKEN;
634
849
  if (!baseUrl) {
635
- console.error(`Missing --url. You can also set WFM_RUNNER_URL.`);
636
- return 1;
850
+ return `Missing --url. You can also pass --session-file or set WFM_RUNNER_URL.`;
637
851
  }
638
852
  if (!token) {
639
- console.error(`Missing --token. You can also set WFM_RUNNER_TOKEN.`);
853
+ return `Missing --token. You can also pass --session-file or set WFM_RUNNER_TOKEN.`;
854
+ }
855
+ return { baseUrl, token, runId };
856
+ }
857
+ async function resolveRunnerRunId(connection, headers) {
858
+ if (connection.runId) {
859
+ return { runId: connection.runId };
860
+ }
861
+ const sessionResponse = await fetch(`${connection.baseUrl}/session`, { headers });
862
+ if (!sessionResponse.ok) {
863
+ const message = await sessionResponse.text();
864
+ return { error: `Failed to discover run id: ${message}` };
865
+ }
866
+ const session = (await sessionResponse.json());
867
+ const runId = session.run?.runId;
868
+ if (!runId) {
869
+ return { error: "Could not determine run id. Pass --run-id explicitly." };
870
+ }
871
+ return { runId };
872
+ }
873
+ async function runnerControlRequest(action, args) {
874
+ const connection = resolveRunnerConnection(args);
875
+ if (typeof connection === "string") {
876
+ console.error(connection);
640
877
  return 1;
641
878
  }
879
+ const stepKey = getFlagFromArgs(args, "--step");
880
+ const actor = getFlagFromArgs(args, "--actor");
881
+ const note = getFlagFromArgs(args, "--note");
882
+ const source = getFlagFromArgs(args, "--source") ?? "cli";
642
883
  const headers = {
643
- Authorization: `Bearer ${token}`,
884
+ Authorization: `Bearer ${connection.token}`,
644
885
  };
645
- let runId = getFlagFromArgs(args, "--run-id");
646
- if (!runId) {
647
- const sessionResponse = await fetch(`${baseUrl}/session`, { headers });
648
- if (!sessionResponse.ok) {
649
- const message = await sessionResponse.text();
650
- console.error(`Failed to discover run id: ${message}`);
651
- return 1;
652
- }
653
- const session = (await sessionResponse.json());
654
- runId = session.run?.runId;
655
- }
656
- if (!runId) {
657
- console.error("Could not determine run id. Pass --run-id explicitly.");
886
+ const resolved = await resolveRunnerRunId(connection, headers);
887
+ if (resolved.error || !resolved.runId) {
888
+ console.error(resolved.error ?? "Could not determine run id. Pass --run-id explicitly.");
658
889
  return 1;
659
890
  }
660
- const response = await fetch(`${baseUrl}/runs/${runId}/${action}`, {
891
+ const runId = resolved.runId;
892
+ const response = await fetch(`${connection.baseUrl}/runs/${runId}/${action}`, {
661
893
  method: "POST",
662
894
  headers: {
663
895
  ...headers,
@@ -682,9 +914,79 @@ async function runnerControlRequest(action, args) {
682
914
  console.log(`${decision} ${resolvedStep}`);
683
915
  return 0;
684
916
  }
917
+ async function runnerReadRequest(command, args) {
918
+ const connection = resolveRunnerConnection(args);
919
+ if (typeof connection === "string") {
920
+ console.error(connection);
921
+ return 1;
922
+ }
923
+ const headers = {
924
+ Authorization: `Bearer ${connection.token}`,
925
+ };
926
+ try {
927
+ const resolved = await resolveRunnerRunId(connection, headers);
928
+ if (resolved.error || !resolved.runId) {
929
+ console.error(resolved.error ?? "Could not determine run id. Pass --run-id explicitly.");
930
+ return 1;
931
+ }
932
+ const runId = encodeURIComponent(resolved.runId);
933
+ let requestPath;
934
+ if (command === "status") {
935
+ const stepKey = getFlagFromArgs(args, "--step");
936
+ requestPath = stepKey ? `/runs/${runId}/steps/${encodeURIComponent(stepKey)}` : `/runs/${runId}`;
937
+ }
938
+ else if (command === "logs") {
939
+ const query = new URLSearchParams();
940
+ const stepKey = getFlagFromArgs(args, "--step");
941
+ const limit = getFlagFromArgs(args, "--limit");
942
+ const cursor = getFlagFromArgs(args, "--cursor");
943
+ if (stepKey)
944
+ query.set("stepKey", stepKey);
945
+ if (limit)
946
+ query.set("limit", limit);
947
+ if (cursor)
948
+ query.set("cursor", cursor);
949
+ const queryString = query.toString();
950
+ requestPath = `/runs/${runId}/logs${queryString ? `?${queryString}` : ""}`;
951
+ }
952
+ else {
953
+ const query = new URLSearchParams();
954
+ const since = getFlagFromArgs(args, "--since");
955
+ if (since)
956
+ query.set("sinceSequence", since);
957
+ query.set("includeLogs", args.includes("--include-logs") ? "true" : "false");
958
+ requestPath = `/runs/${runId}/events/list?${query.toString()}`;
959
+ }
960
+ const response = await fetch(`${connection.baseUrl}${requestPath}`, { headers });
961
+ const payloadText = await response.text();
962
+ if (!response.ok) {
963
+ let message = `Request failed with status ${response.status}`;
964
+ try {
965
+ const payload = JSON.parse(payloadText);
966
+ if (typeof payload.message === "string") {
967
+ message = payload.message;
968
+ }
969
+ }
970
+ catch {
971
+ // keep the fallback message
972
+ }
973
+ console.error(`${command} failed: ${message}`);
974
+ return 1;
975
+ }
976
+ console.log(payloadText);
977
+ return 0;
978
+ }
979
+ catch (error) {
980
+ console.error(`${command} failed: ${error.message}`);
981
+ return 1;
982
+ }
983
+ }
685
984
  async function cmdRun(filePath) {
686
985
  const resolvedPath = path.resolve(filePath);
687
986
  const startedAt = Date.now();
987
+ const sessionFilePath = getFlag("--session-file");
988
+ let sessionFileState;
989
+ let finalRunStatus;
688
990
  let workflow;
689
991
  let runnerServer;
690
992
  let sessionStore;
@@ -750,6 +1052,16 @@ async function cmdRun(filePath) {
750
1052
  }
751
1053
  runnerServer = await startRunnerApiServer(sessionStore, requestedPort);
752
1054
  const session = sessionStore.sessionInfo();
1055
+ if (sessionFilePath) {
1056
+ sessionFileState = {
1057
+ baseUrl: session.baseUrl,
1058
+ attachToken: session.attachToken,
1059
+ runId,
1060
+ pid: process.pid,
1061
+ startedAt: session.startedAt,
1062
+ };
1063
+ writeSessionFile(sessionFilePath, sessionFileState);
1064
+ }
753
1065
  if (!useTui) {
754
1066
  process.stderr.write(`Attach API: ${session.baseUrl} (token ${session.attachToken})\n`);
755
1067
  }
@@ -803,6 +1115,7 @@ async function cmdRun(filePath) {
803
1115
  });
804
1116
  tuiRenderer?.stop();
805
1117
  liveRenderer?.close();
1118
+ finalRunStatus = result.status;
806
1119
  if (hasFlag("--json")) {
807
1120
  console.log(JSON.stringify({ session: sessionStore.sessionInfo(), ...result }, null, 2));
808
1121
  }
@@ -846,6 +1159,18 @@ async function cmdRun(filePath) {
846
1159
  finally {
847
1160
  tuiRenderer?.stop();
848
1161
  liveRenderer?.close();
1162
+ if (sessionFilePath && sessionFileState) {
1163
+ try {
1164
+ writeSessionFile(sessionFilePath, {
1165
+ ...sessionFileState,
1166
+ endedAt: new Date().toISOString(),
1167
+ status: finalRunStatus ?? "failed",
1168
+ });
1169
+ }
1170
+ catch {
1171
+ // session-file finalization is best effort; the run result is authoritative
1172
+ }
1173
+ }
849
1174
  await runnerServer?.close().catch(() => undefined);
850
1175
  }
851
1176
  }
@@ -874,8 +1199,8 @@ async function main() {
874
1199
  process.exit(1);
875
1200
  }
876
1201
  if (cmd === "scaffold") {
877
- const { targetPath, format } = parseScaffoldArgs(process.argv.slice(3));
878
- process.exit(cmdScaffold(targetPath, format));
1202
+ const { targetPath, format, template } = parseScaffoldArgs(process.argv.slice(3));
1203
+ process.exit(cmdScaffold(targetPath, format, template));
879
1204
  }
880
1205
  if (cmd === "man") {
881
1206
  process.exit(cmdMan());
@@ -899,6 +1224,9 @@ async function main() {
899
1224
  if (cmd === "approve" || cmd === "resume" || cmd === "cancel") {
900
1225
  process.exit(await runnerControlRequest(cmd, process.argv.slice(3)));
901
1226
  }
1227
+ if (cmd === "status" || cmd === "logs" || cmd === "events") {
1228
+ process.exit(await runnerReadRequest(cmd, process.argv.slice(3)));
1229
+ }
902
1230
  if (cmd === "auth") {
903
1231
  process.exit(await cmdAuth(process.argv.slice(3)));
904
1232
  }
@@ -932,4 +1260,30 @@ async function main() {
932
1260
  usage();
933
1261
  process.exit(1);
934
1262
  }
935
- void main();
1263
+ // Run main() only when this module is the process entrypoint (direct `bun
1264
+ // run`/`node` invocation, an npm-installed symlinked bin, or a `bun build
1265
+ // --compile` standalone binary) — never when imported as a module, e.g. by
1266
+ // tests exercising the exported skill-catalog helpers below.
1267
+ function isEntryModule() {
1268
+ if (!process.argv[1]) {
1269
+ return false;
1270
+ }
1271
+ const self = fileURLToPath(import.meta.url);
1272
+ let invoked = process.argv[1];
1273
+ try {
1274
+ invoked = fs.realpathSync(invoked);
1275
+ }
1276
+ catch {
1277
+ // compiled/virtual filesystem paths (e.g. bun's $bunfs) can't be
1278
+ // realpath'd; fall back to the raw invoked path.
1279
+ }
1280
+ try {
1281
+ return path.resolve(invoked) === self;
1282
+ }
1283
+ catch {
1284
+ return false;
1285
+ }
1286
+ }
1287
+ if (isEntryModule()) {
1288
+ void main();
1289
+ }
package/dist/manPage.d.ts CHANGED
@@ -1 +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 skill list\nList the agent skills bundled with the npm package.\n.TP\n.B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]\nInstall bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills 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] [--ui]\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.TP\n.B --version, -v, version\nPrint the installed wfm version and exit.\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\n.B --ui\nFull-screen terminal UI (requires a TTY; falls back to standard output).\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\nInstall the bundled CLI skill for Claude Code:\n.B wfm skill install\n.TP\nInstall every bundled skill globally:\n.B wfm skill install --all --global\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";
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 skill list\nList the agent skills bundled with the npm package.\n.TP\n.B skill install [name ...] [--agent claude|opencode] [--global] [--dir path] [--all] [--force]\nInstall bundled agent skills into an agent skill directory. Defaults to the workflow-manager-cli skill and the project-level Claude Code directory (./.claude/skills). Existing skills are not overwritten unless --force is passed.\n.TP\n.B scaffold [path] [--format markdown|json] [--template default|agent-validated]\nCreate a starter workflow file. Format defaults to markdown unless the output\npath ends in .json. Template defaults to default (the general multi-step example);\nagent-validated scaffolds a compact pipeline demonstrating first-class agent\nvalidation (validation.mode: agent with a validator agent and criteria).\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] [--session-file path] [--verbose] [--json] [--ui]\nRun the workflow with live CLI progress and optional JSON output.\n.TP\n.B approve [--url value] [--token value] [--session-file path] [--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] [--session-file path] [--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] [--session-file path] [--run-id value] [--step value] [--actor value] [--note text]\nCancel the current waiting runner step through the local attach API.\n.TP\n.B status [--url value] [--token value] [--session-file path] [--run-id value] [--step key]\nPrint the current run snapshot (or one step detail with --step) as JSON on stdout.\n.TP\n.B logs [--url value] [--token value] [--session-file path] [--run-id value] [--step key] [--limit number] [--cursor value]\nPrint buffered agent stdout/stderr chunks as JSON on stdout.\n.TP\n.B events [--url value] [--token value] [--session-file path] [--run-id value] [--since sequence] [--include-logs]\nPrint run events as JSON on stdout in a single poll (no streaming). Log events are excluded unless --include-logs is passed.\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.TP\n.B --version, -v, version\nPrint the installed wfm version and exit.\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 --session-file <path>\nWrite attach connection details (base URL, bearer token, run id, pid, timestamps) to a JSON file with mode 0600 when the run starts, and rewrite it with endedAt and the final status when the run finishes. Attach commands (approve, resume, cancel, status, logs, events) accept the same flag to read those details back.\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\n.B --ui\nFull-screen terminal UI (requires a TTY; falls back to standard output).\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, cancel, status, logs, or events commands.\n.TP\n.B --token <value>\nRunner attach API bearer token for approve, resume, cancel, status, logs, or events 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, or when scoping status and logs output.\n.TP\n.B --limit <number>\nMaximum number of log chunks returned by the logs command. Defaults to 200.\n.TP\n.B --cursor <value>\nPagination cursor for the logs command, taken from a previous nextCursor value.\n.TP\n.B --since <sequence>\nOnly return events with a sequence greater than this value in the events command.\n.TP\n.B --include-logs\nInclude agent.stdout and agent.stderr events in the events command output.\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\nScaffold an agent-validated pipeline example:\n.B wfm scaffold ./agent-validated.md --template agent-validated\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\nRun with a session file for attach clients:\n.B wfm run ./example-workflow.json --session-file ./run-session.json\n.TP\nObserve and control the run through the session file:\n.B wfm status --session-file ./run-session.json && wfm approve --session-file ./run-session.json --step qa_gate\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\nInstall the bundled CLI skill for Claude Code:\n.B wfm skill install\n.TP\nInstall every bundled skill globally:\n.B wfm skill install --all --global\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";