@workflow-manager/runner 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cliRunRenderer.js +18 -56
- package/dist/engine.js +201 -68
- package/dist/generated/bundledSkills.d.ts +5 -0
- package/dist/generated/bundledSkills.js +22 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +466 -87
- package/dist/manPage.d.ts +1 -1
- package/dist/manPage.js +47 -9
- package/dist/parser.js +10 -1
- package/dist/runFormat.d.ts +11 -0
- package/dist/runFormat.js +56 -0
- package/dist/runnerApi.js +14 -0
- package/dist/sessionFile.d.ts +11 -0
- package/dist/sessionFile.js +61 -0
- package/dist/tui/ansi.d.ts +23 -0
- package/dist/tui/ansi.js +102 -0
- package/dist/tui/layout.d.ts +18 -0
- package/dist/tui/layout.js +245 -0
- package/dist/tui/logStore.d.ts +22 -0
- package/dist/tui/logStore.js +78 -0
- package/dist/tui/screen.d.ts +35 -0
- package/dist/tui/screen.js +99 -0
- package/dist/tui/theme.d.ts +13 -0
- package/dist/tui/theme.js +55 -0
- package/dist/tui/tuiRunRenderer.d.ts +83 -0
- package/dist/tui/tuiRunRenderer.js +316 -0
- package/dist/types.d.ts +15 -8
- package/package.json +4 -2
- package/skills/workflow-author/README.md +18 -0
- package/skills/workflow-author/SKILL.md +306 -0
package/dist/index.js
CHANGED
|
@@ -7,13 +7,16 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import matter from "gray-matter";
|
|
9
9
|
import { CliRunRenderer } from "./cliRunRenderer.js";
|
|
10
|
+
import { TuiRunRenderer } from "./tui/tuiRunRenderer.js";
|
|
10
11
|
import { startRunnerApiServer } from "./runnerApi.js";
|
|
11
12
|
import { RunnerSessionStore } from "./runnerSession.js";
|
|
12
13
|
import { parseWorkflowFile, validateWorkflow } from "./parser.js";
|
|
13
14
|
import { MAN_PAGE_SOURCE } from "./manPage.js";
|
|
14
15
|
import { promptForApprovalDecision, runWorkflow } from "./engine.js";
|
|
15
16
|
import { cmdAuth, cmdPublish, cmdPull, cmdRemoteInfo, cmdSearch } from "./remote/commands.js";
|
|
17
|
+
import { readSessionFile, writeSessionFile } from "./sessionFile.js";
|
|
16
18
|
import { emitRunTelemetryBestEffort } from "./remote/telemetry.js";
|
|
19
|
+
import { BUNDLED_SKILLS } from "./generated/bundledSkills.js";
|
|
17
20
|
import { adapterImplementationStatuses, adapterMockFallbackWarnings, runtimeDoctorChecks, validateRuntimeRequirements, } from "./runtimePreflight.js";
|
|
18
21
|
function resolveVersion() {
|
|
19
22
|
if (typeof WFM_VERSION === "string" && WFM_VERSION) {
|
|
@@ -52,15 +55,20 @@ function usage() {
|
|
|
52
55
|
`Usage: ${cli} <command> [options]`,
|
|
53
56
|
"",
|
|
54
57
|
"Author and run workflows",
|
|
55
|
-
row("scaffold [path]", "Write a starter workflow file"),
|
|
58
|
+
row("scaffold [path]", "Write a starter workflow file (--template agent-validated for validator example)"),
|
|
56
59
|
row("validate <file>", "Check a workflow for errors"),
|
|
57
60
|
row("doctor [file]", "Check host setup; with a file, preflight it"),
|
|
58
|
-
row("run <file>", "Run a workflow with live progress"),
|
|
61
|
+
row("run <file>", "Run a workflow with live progress (--ui for full-screen)"),
|
|
59
62
|
"",
|
|
60
|
-
"Control a running workflow",
|
|
63
|
+
"Control or observe a running workflow",
|
|
61
64
|
row("approve", "Approve a step waiting for review"),
|
|
62
65
|
row("resume", "Resume a step waiting on external input"),
|
|
63
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.",
|
|
64
72
|
"",
|
|
65
73
|
"Share workflows (remote registry)",
|
|
66
74
|
row("auth <login|whoami|logout>", "Sign in, check, or sign out"),
|
|
@@ -313,6 +321,159 @@ steps:
|
|
|
313
321
|
|
|
314
322
|
Edit frontmatter to configure orchestration behavior.
|
|
315
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
|
+
`;
|
|
316
477
|
function resolveScaffoldFormat(targetPath, explicitFormat) {
|
|
317
478
|
if (explicitFormat === "markdown" || explicitFormat === "json") {
|
|
318
479
|
return explicitFormat;
|
|
@@ -322,6 +483,7 @@ function resolveScaffoldFormat(targetPath, explicitFormat) {
|
|
|
322
483
|
function parseScaffoldArgs(args) {
|
|
323
484
|
let targetPath;
|
|
324
485
|
let format;
|
|
486
|
+
let template;
|
|
325
487
|
for (let i = 0; i < args.length; i += 1) {
|
|
326
488
|
const arg = args[i];
|
|
327
489
|
if (arg === "--format") {
|
|
@@ -329,11 +491,16 @@ function parseScaffoldArgs(args) {
|
|
|
329
491
|
i += 1;
|
|
330
492
|
continue;
|
|
331
493
|
}
|
|
494
|
+
if (arg === "--template") {
|
|
495
|
+
template = args[i + 1];
|
|
496
|
+
i += 1;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
332
499
|
if (!arg.startsWith("-") && !targetPath) {
|
|
333
500
|
targetPath = arg;
|
|
334
501
|
}
|
|
335
502
|
}
|
|
336
|
-
return { targetPath, format };
|
|
503
|
+
return { targetPath, format, template };
|
|
337
504
|
}
|
|
338
505
|
const DEFAULT_INSTALL_SKILL = "workflow-manager-cli";
|
|
339
506
|
const SKILL_INSTALL_TARGETS = {
|
|
@@ -346,11 +513,22 @@ const SKILL_INSTALL_TARGETS = {
|
|
|
346
513
|
globalDir: path.join(os.homedir(), ".config", "opencode", "skill"),
|
|
347
514
|
},
|
|
348
515
|
};
|
|
349
|
-
function packagedSkillsDir() {
|
|
516
|
+
export function packagedSkillsDir() {
|
|
350
517
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
351
518
|
}
|
|
352
|
-
function
|
|
353
|
-
|
|
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) {
|
|
354
532
|
if (!fs.existsSync(root)) {
|
|
355
533
|
return [];
|
|
356
534
|
}
|
|
@@ -362,20 +540,44 @@ function listPackagedSkills() {
|
|
|
362
540
|
const skillFile = path.join(dir, "SKILL.md");
|
|
363
541
|
if (!fs.existsSync(skillFile))
|
|
364
542
|
continue;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const parsed = matter(fs.readFileSync(skillFile, "utf-8"));
|
|
368
|
-
if (typeof parsed.data.description === "string") {
|
|
369
|
-
description = parsed.data.description.replace(/\s+/g, " ").trim();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
// skills without parseable frontmatter are still installable
|
|
374
|
-
}
|
|
375
|
-
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 } });
|
|
376
545
|
}
|
|
377
546
|
return skills;
|
|
378
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 } });
|
|
556
|
+
}
|
|
557
|
+
return skills;
|
|
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
|
+
}
|
|
379
581
|
function cmdSkillList() {
|
|
380
582
|
const skills = listPackagedSkills();
|
|
381
583
|
if (skills.length === 0) {
|
|
@@ -460,16 +662,12 @@ function cmdSkillInstall(args) {
|
|
|
460
662
|
return 1;
|
|
461
663
|
}
|
|
462
664
|
fs.mkdirSync(destDir, { recursive: true });
|
|
463
|
-
|
|
464
|
-
if (!entry.isFile() || entry.name === "README.md")
|
|
465
|
-
continue;
|
|
466
|
-
fs.copyFileSync(path.join(skill.dir, entry.name), path.join(destDir, entry.name));
|
|
467
|
-
}
|
|
665
|
+
materializeSkillFiles(skill, destDir);
|
|
468
666
|
console.log(`Installed skill ${name} -> ${destDir}`);
|
|
469
667
|
}
|
|
470
668
|
return 0;
|
|
471
669
|
}
|
|
472
|
-
function cmdScaffold(targetPath, format) {
|
|
670
|
+
function cmdScaffold(targetPath, format, template) {
|
|
473
671
|
const resolvedPath = targetPath ? path.resolve(targetPath) : path.resolve("./example-workflow.md");
|
|
474
672
|
const normalizedFormat = format?.toLowerCase();
|
|
475
673
|
const resolvedFormat = resolveScaffoldFormat(resolvedPath, normalizedFormat);
|
|
@@ -477,10 +675,19 @@ function cmdScaffold(targetPath, format) {
|
|
|
477
675
|
console.error(`Invalid --format value: ${format}. Use markdown or json.`);
|
|
478
676
|
return 1;
|
|
479
677
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
:
|
|
483
|
-
|
|
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");
|
|
484
691
|
console.log(`Scaffolded ${resolvedFormat} workflow: ${resolvedPath}`);
|
|
485
692
|
return 0;
|
|
486
693
|
}
|
|
@@ -623,40 +830,66 @@ function cmdDoctor(args) {
|
|
|
623
830
|
}
|
|
624
831
|
return exitCode;
|
|
625
832
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
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;
|
|
633
849
|
if (!baseUrl) {
|
|
634
|
-
|
|
635
|
-
return 1;
|
|
850
|
+
return `Missing --url. You can also pass --session-file or set WFM_RUNNER_URL.`;
|
|
636
851
|
}
|
|
637
852
|
if (!token) {
|
|
638
|
-
|
|
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);
|
|
639
877
|
return 1;
|
|
640
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";
|
|
641
883
|
const headers = {
|
|
642
|
-
Authorization: `Bearer ${token}`,
|
|
884
|
+
Authorization: `Bearer ${connection.token}`,
|
|
643
885
|
};
|
|
644
|
-
|
|
645
|
-
if (!runId) {
|
|
646
|
-
|
|
647
|
-
if (!sessionResponse.ok) {
|
|
648
|
-
const message = await sessionResponse.text();
|
|
649
|
-
console.error(`Failed to discover run id: ${message}`);
|
|
650
|
-
return 1;
|
|
651
|
-
}
|
|
652
|
-
const session = (await sessionResponse.json());
|
|
653
|
-
runId = session.run?.runId;
|
|
654
|
-
}
|
|
655
|
-
if (!runId) {
|
|
656
|
-
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.");
|
|
657
889
|
return 1;
|
|
658
890
|
}
|
|
659
|
-
const
|
|
891
|
+
const runId = resolved.runId;
|
|
892
|
+
const response = await fetch(`${connection.baseUrl}/runs/${runId}/${action}`, {
|
|
660
893
|
method: "POST",
|
|
661
894
|
headers: {
|
|
662
895
|
...headers,
|
|
@@ -681,13 +914,84 @@ async function runnerControlRequest(action, args) {
|
|
|
681
914
|
console.log(`${decision} ${resolvedStep}`);
|
|
682
915
|
return 0;
|
|
683
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
|
+
}
|
|
684
984
|
async function cmdRun(filePath) {
|
|
685
985
|
const resolvedPath = path.resolve(filePath);
|
|
686
986
|
const startedAt = Date.now();
|
|
987
|
+
const sessionFilePath = getFlag("--session-file");
|
|
988
|
+
let sessionFileState;
|
|
989
|
+
let finalRunStatus;
|
|
687
990
|
let workflow;
|
|
688
991
|
let runnerServer;
|
|
689
992
|
let sessionStore;
|
|
690
993
|
let liveRenderer;
|
|
994
|
+
let tuiRenderer;
|
|
691
995
|
try {
|
|
692
996
|
workflow = parseWorkflowFile(resolvedPath);
|
|
693
997
|
const errors = validateWorkflow(workflow);
|
|
@@ -729,57 +1033,89 @@ async function cmdRun(filePath) {
|
|
|
729
1033
|
console.error("--port must be an integer between 0 and 65535");
|
|
730
1034
|
return 1;
|
|
731
1035
|
}
|
|
1036
|
+
const wantUi = hasFlag("--ui");
|
|
1037
|
+
const useTui = wantUi && process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
1038
|
+
if (wantUi && !useTui) {
|
|
1039
|
+
process.stderr.write("⚠ --ui requires an interactive terminal; falling back to standard output\n");
|
|
1040
|
+
}
|
|
732
1041
|
sessionStore = new RunnerSessionStore({
|
|
733
1042
|
runId,
|
|
734
1043
|
workflow,
|
|
735
1044
|
objective: objective ?? workflow.title,
|
|
736
1045
|
objectives: workflow.objectives ?? [],
|
|
737
1046
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1047
|
+
if (!useTui) {
|
|
1048
|
+
liveRenderer = new CliRunRenderer({
|
|
1049
|
+
workflow,
|
|
1050
|
+
verbose: hasFlag("--verbose"),
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
742
1053
|
runnerServer = await startRunnerApiServer(sessionStore, requestedPort);
|
|
743
1054
|
const session = sessionStore.sessionInfo();
|
|
744
|
-
|
|
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
|
+
}
|
|
1065
|
+
if (!useTui) {
|
|
1066
|
+
process.stderr.write(`Attach API: ${session.baseUrl} (token ${session.attachToken})\n`);
|
|
1067
|
+
}
|
|
1068
|
+
if (useTui) {
|
|
1069
|
+
tuiRenderer = new TuiRunRenderer({
|
|
1070
|
+
workflow,
|
|
1071
|
+
session: sessionStore,
|
|
1072
|
+
attachUrl: session.baseUrl,
|
|
1073
|
+
attachToken: session.attachToken,
|
|
1074
|
+
});
|
|
1075
|
+
tuiRenderer.start();
|
|
1076
|
+
}
|
|
745
1077
|
const result = await runWorkflow(workflow, {
|
|
746
1078
|
runId,
|
|
747
1079
|
objective,
|
|
748
1080
|
input,
|
|
749
1081
|
confirmations,
|
|
750
1082
|
autoConfirmAll: hasFlag("--auto-confirm-all"),
|
|
751
|
-
interactive: process.stdin.isTTY,
|
|
1083
|
+
interactive: useTui ? false : process.stdin.isTTY,
|
|
752
1084
|
workflowFilePath: resolvedPath,
|
|
753
|
-
approvalPrompt:
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1085
|
+
approvalPrompt: useTui
|
|
1086
|
+
? undefined
|
|
1087
|
+
: async (request) => {
|
|
1088
|
+
liveRenderer?.pauseHeartbeat();
|
|
1089
|
+
try {
|
|
1090
|
+
const decision = await promptForApprovalDecision(request.stepKey, request.reason, request.validation ?? "external", request.preview ?? null, "cli", request.signal);
|
|
1091
|
+
if (!decision) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
const metadata = {
|
|
1095
|
+
actor: decision.actor,
|
|
1096
|
+
note: decision.note,
|
|
1097
|
+
source: decision.source,
|
|
1098
|
+
};
|
|
1099
|
+
const outcome = decision.decision === "cancelled"
|
|
1100
|
+
? sessionStore?.cancel(request.stepKey, metadata)
|
|
1101
|
+
: request.validation === "external"
|
|
1102
|
+
? sessionStore?.resume(request.stepKey, metadata)
|
|
1103
|
+
: sessionStore?.approve(request.stepKey, metadata);
|
|
1104
|
+
if (outcome && !outcome.ok) {
|
|
1105
|
+
process.stderr.write(`Could not apply terminal decision for ${request.stepKey}: ${outcome.reason ?? "unknown error"}\n`);
|
|
1106
|
+
}
|
|
758
1107
|
return null;
|
|
759
1108
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
note: decision.note,
|
|
763
|
-
source: decision.source,
|
|
764
|
-
};
|
|
765
|
-
const outcome = decision.decision === "cancelled"
|
|
766
|
-
? sessionStore?.cancel(request.stepKey, metadata)
|
|
767
|
-
: request.validation === "external"
|
|
768
|
-
? sessionStore?.resume(request.stepKey, metadata)
|
|
769
|
-
: sessionStore?.approve(request.stepKey, metadata);
|
|
770
|
-
if (outcome && !outcome.ok) {
|
|
771
|
-
process.stderr.write(`Could not apply terminal decision for ${request.stepKey}: ${outcome.reason ?? "unknown error"}\n`);
|
|
1109
|
+
finally {
|
|
1110
|
+
liveRenderer?.resumeHeartbeat();
|
|
772
1111
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
finally {
|
|
776
|
-
liveRenderer?.resumeHeartbeat();
|
|
777
|
-
}
|
|
778
|
-
},
|
|
779
|
-
observer: combineRunObservers(sessionStore, liveRenderer),
|
|
1112
|
+
},
|
|
1113
|
+
observer: combineRunObservers(sessionStore, useTui ? tuiRenderer : liveRenderer),
|
|
780
1114
|
controller: sessionStore,
|
|
781
1115
|
});
|
|
782
|
-
|
|
1116
|
+
tuiRenderer?.stop();
|
|
1117
|
+
liveRenderer?.close();
|
|
1118
|
+
finalRunStatus = result.status;
|
|
783
1119
|
if (hasFlag("--json")) {
|
|
784
1120
|
console.log(JSON.stringify({ session: sessionStore.sessionInfo(), ...result }, null, 2));
|
|
785
1121
|
}
|
|
@@ -807,6 +1143,7 @@ async function cmdRun(filePath) {
|
|
|
807
1143
|
return result.status === "succeeded" ? 0 : 2;
|
|
808
1144
|
}
|
|
809
1145
|
catch (err) {
|
|
1146
|
+
tuiRenderer?.stop();
|
|
810
1147
|
liveRenderer?.close();
|
|
811
1148
|
console.error(`Run error: ${err.message}`);
|
|
812
1149
|
if (workflow) {
|
|
@@ -820,7 +1157,20 @@ async function cmdRun(filePath) {
|
|
|
820
1157
|
return 1;
|
|
821
1158
|
}
|
|
822
1159
|
finally {
|
|
1160
|
+
tuiRenderer?.stop();
|
|
823
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
|
+
}
|
|
824
1174
|
await runnerServer?.close().catch(() => undefined);
|
|
825
1175
|
}
|
|
826
1176
|
}
|
|
@@ -849,8 +1199,8 @@ async function main() {
|
|
|
849
1199
|
process.exit(1);
|
|
850
1200
|
}
|
|
851
1201
|
if (cmd === "scaffold") {
|
|
852
|
-
const { targetPath, format } = parseScaffoldArgs(process.argv.slice(3));
|
|
853
|
-
process.exit(cmdScaffold(targetPath, format));
|
|
1202
|
+
const { targetPath, format, template } = parseScaffoldArgs(process.argv.slice(3));
|
|
1203
|
+
process.exit(cmdScaffold(targetPath, format, template));
|
|
854
1204
|
}
|
|
855
1205
|
if (cmd === "man") {
|
|
856
1206
|
process.exit(cmdMan());
|
|
@@ -874,6 +1224,9 @@ async function main() {
|
|
|
874
1224
|
if (cmd === "approve" || cmd === "resume" || cmd === "cancel") {
|
|
875
1225
|
process.exit(await runnerControlRequest(cmd, process.argv.slice(3)));
|
|
876
1226
|
}
|
|
1227
|
+
if (cmd === "status" || cmd === "logs" || cmd === "events") {
|
|
1228
|
+
process.exit(await runnerReadRequest(cmd, process.argv.slice(3)));
|
|
1229
|
+
}
|
|
877
1230
|
if (cmd === "auth") {
|
|
878
1231
|
process.exit(await cmdAuth(process.argv.slice(3)));
|
|
879
1232
|
}
|
|
@@ -907,4 +1260,30 @@ async function main() {
|
|
|
907
1260
|
usage();
|
|
908
1261
|
process.exit(1);
|
|
909
1262
|
}
|
|
910
|
-
|
|
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
|
+
}
|