@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/README.md +5 -1
- package/dist/acpExecutor.d.ts +2 -2
- package/dist/acpExecutor.js +9 -8
- package/dist/cliRunRenderer.js +17 -0
- package/dist/engine.js +201 -68
- package/dist/generated/bundledSkills.d.ts +5 -0
- package/dist/generated/bundledSkills.js +22 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +409 -55
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +44 -9
- package/dist/parser.js +10 -1
- package/dist/runnerApi.js +14 -0
- package/dist/runtimePreflight.js +3 -2
- package/dist/sessionFile.d.ts +11 -0
- package/dist/sessionFile.js +61 -0
- package/dist/tui/tuiRunRenderer.js +11 -0
- package/dist/types.d.ts +15 -8
- package/package.json +3 -2
- package/skills/workflow-author/README.md +18 -0
- package/skills/workflow-author/SKILL.md +308 -0
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
|
|
354
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
:
|
|
484
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
if (!runId) {
|
|
647
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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";
|