@workflow-manager/runner 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -12
- package/dist/adapters.d.ts +4 -0
- package/dist/adapters.js +11 -0
- package/dist/claudeCodeExecutor.d.ts +4 -0
- package/dist/claudeCodeExecutor.js +173 -0
- package/dist/cliRunRenderer.d.ts +36 -0
- package/dist/cliRunRenderer.js +286 -0
- package/dist/engine.d.ts +4 -2
- package/dist/engine.js +622 -44
- package/dist/events.d.ts +1 -1
- package/dist/events.js +4 -2
- package/dist/index.js +371 -41
- package/dist/manPage.d.ts +1 -0
- package/dist/manPage.js +147 -0
- package/dist/mockExecutor.d.ts +2 -2
- package/dist/mockExecutor.js +62 -13
- package/dist/opencodeExecutor.d.ts +2 -2
- package/dist/opencodeExecutor.js +101 -138
- package/dist/parser.js +98 -2
- package/dist/piAgentExecutor.d.ts +4 -0
- package/dist/piAgentExecutor.js +298 -0
- package/dist/remote/api.d.ts +1 -0
- package/dist/remote/commands.d.ts +2 -0
- package/dist/remote/commands.js +76 -4
- package/dist/runnerApi.d.ts +7 -0
- package/dist/runnerApi.js +221 -0
- package/dist/runnerSession.d.ts +62 -0
- package/dist/runnerSession.js +260 -0
- package/dist/runtimePreflight.d.ts +16 -0
- package/dist/runtimePreflight.js +189 -0
- package/dist/skillResolver.d.ts +6 -0
- package/dist/skillResolver.js +75 -0
- package/dist/types.d.ts +148 -2
- package/man/wfm.1 +54 -4
- package/package.json +28 -4
- package/skills/commit-discipline/SKILL.md +109 -0
- package/skills/doc-sync/SKILL.md +83 -0
- package/skills/repo-hygiene/SKILL.md +70 -0
- package/skills/spec-driven-development/SKILL.md +33 -0
- package/skills/workflow-manager-cli/SKILL.md +14 -9
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveTaskAdapter } from "./adapters.js";
|
|
4
|
+
import { shouldUseRealClaudeCode } from "./claudeCodeExecutor.js";
|
|
5
|
+
import { shouldUseRealOpencode } from "./opencodeExecutor.js";
|
|
6
|
+
import { DEFAULT_PI_COMMAND } from "./piAgentExecutor.js";
|
|
7
|
+
function asRecord(value) {
|
|
8
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
9
|
+
}
|
|
10
|
+
function isExecutable(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const stat = fs.statSync(filePath);
|
|
13
|
+
if (!stat.isFile()) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function commandExists(command, env) {
|
|
24
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
25
|
+
return isExecutable(path.resolve(command));
|
|
26
|
+
}
|
|
27
|
+
const pathValue = env.PATH ?? "";
|
|
28
|
+
const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean) : [""];
|
|
29
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
30
|
+
if (!directory) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const extension of extensions) {
|
|
34
|
+
if (isExecutable(path.join(directory, `${command}${extension}`))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function piAgentCommand(step, env) {
|
|
42
|
+
const payload = asRecord(step.taskSpec?.payload);
|
|
43
|
+
if (typeof payload.command === "string" && payload.command.trim()) {
|
|
44
|
+
return payload.command;
|
|
45
|
+
}
|
|
46
|
+
if (env.WFM_PI_AGENT_COMMAND?.trim()) {
|
|
47
|
+
return env.WFM_PI_AGENT_COMMAND;
|
|
48
|
+
}
|
|
49
|
+
return DEFAULT_PI_COMMAND;
|
|
50
|
+
}
|
|
51
|
+
function requiredEnvFromModel(model) {
|
|
52
|
+
if (typeof model !== "string" || !model.trim()) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const normalized = model.trim().toLowerCase();
|
|
56
|
+
if (normalized.startsWith("openrouter/")) {
|
|
57
|
+
return "OPENROUTER_API_KEY";
|
|
58
|
+
}
|
|
59
|
+
if (normalized.startsWith("openai/") || normalized.startsWith("gpt-") || normalized.includes("/gpt-")) {
|
|
60
|
+
return "OPENAI_API_KEY";
|
|
61
|
+
}
|
|
62
|
+
if (normalized.startsWith("anthropic/") || normalized.startsWith("claude-") || normalized.includes("/claude")) {
|
|
63
|
+
return "ANTHROPIC_API_KEY";
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function explicitRequiredEnv(step) {
|
|
68
|
+
const payload = asRecord(step.taskSpec?.payload);
|
|
69
|
+
const required = new Set();
|
|
70
|
+
if (Array.isArray(payload.requiredEnv)) {
|
|
71
|
+
for (const value of payload.requiredEnv) {
|
|
72
|
+
if (typeof value === "string" && value.trim()) {
|
|
73
|
+
required.add(value.trim());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...required].sort();
|
|
78
|
+
}
|
|
79
|
+
function requiredEnvVars(step) {
|
|
80
|
+
const payload = asRecord(step.taskSpec?.payload);
|
|
81
|
+
const required = new Set(explicitRequiredEnv(step));
|
|
82
|
+
const fromInitModel = requiredEnvFromModel(step.taskSpec?.init?.model);
|
|
83
|
+
const fromPayloadModel = requiredEnvFromModel(payload.model);
|
|
84
|
+
if (fromInitModel)
|
|
85
|
+
required.add(fromInitModel);
|
|
86
|
+
if (fromPayloadModel)
|
|
87
|
+
required.add(fromPayloadModel);
|
|
88
|
+
return [...required].sort();
|
|
89
|
+
}
|
|
90
|
+
function runtimeRequirement(step, env) {
|
|
91
|
+
if (step.kind !== "task") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const adapter = resolveTaskAdapter(step.taskSpec?.adapterKey);
|
|
95
|
+
const envVars = requiredEnvVars(step);
|
|
96
|
+
if (adapter === "pi-agent") {
|
|
97
|
+
// pi manages provider credentials in its own auth store, so only
|
|
98
|
+
// explicitly declared env vars are enforced for pi steps.
|
|
99
|
+
return { stepKey: step.key, adapter, command: piAgentCommand(step, env), envVars: explicitRequiredEnv(step) };
|
|
100
|
+
}
|
|
101
|
+
if (adapter === "opencode" && shouldUseRealOpencode(step)) {
|
|
102
|
+
return { stepKey: step.key, adapter, command: "opencode", envVars };
|
|
103
|
+
}
|
|
104
|
+
if (adapter === "claude-code" && shouldUseRealClaudeCode(step)) {
|
|
105
|
+
return { stepKey: step.key, adapter, command: "claude", envVars };
|
|
106
|
+
}
|
|
107
|
+
if (envVars.length > 0 && adapter !== "mock") {
|
|
108
|
+
return { stepKey: step.key, adapter, envVars };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
export function validateRuntimeRequirements(definition, env = process.env) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
for (const step of definition.steps) {
|
|
115
|
+
const requirement = runtimeRequirement(step, env);
|
|
116
|
+
if (!requirement) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (requirement.command && !commandExists(requirement.command, env)) {
|
|
120
|
+
errors.push(`Step ${requirement.stepKey} requires ${requirement.adapter} command "${requirement.command}", but it is not installed or not executable on this host`);
|
|
121
|
+
}
|
|
122
|
+
for (const envVar of requirement.envVars) {
|
|
123
|
+
if (!env[envVar]?.trim()) {
|
|
124
|
+
errors.push(`Step ${requirement.stepKey} requires ${envVar} for ${requirement.adapter} LLM access`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return errors;
|
|
129
|
+
}
|
|
130
|
+
function commandCheck(key, label, command, required, env) {
|
|
131
|
+
const ok = commandExists(command, env);
|
|
132
|
+
return {
|
|
133
|
+
key,
|
|
134
|
+
label,
|
|
135
|
+
status: ok ? "ok" : "missing",
|
|
136
|
+
required,
|
|
137
|
+
detail: ok ? `${command} is executable` : `${command} is not installed or not executable on this host`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function envCheck(key, label, envVar, env) {
|
|
141
|
+
const ok = !!env[envVar]?.trim();
|
|
142
|
+
return {
|
|
143
|
+
key,
|
|
144
|
+
label,
|
|
145
|
+
status: ok ? "ok" : "missing",
|
|
146
|
+
required: false,
|
|
147
|
+
detail: ok ? `${envVar} is set` : `${envVar} is not set`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function runtimeDoctorChecks(env = process.env) {
|
|
151
|
+
const piAgentStep = { key: "pi-agent", kind: "task", taskSpec: {} };
|
|
152
|
+
return [
|
|
153
|
+
commandCheck("pi-agent", "Pi command", piAgentCommand(piAgentStep, env), true, env),
|
|
154
|
+
commandCheck("opencode", "OpenCode command", "opencode", false, env),
|
|
155
|
+
commandCheck("claude", "Claude Code command", "claude", false, env),
|
|
156
|
+
envCheck("openrouter-key", "OpenRouter API key", "OPENROUTER_API_KEY", env),
|
|
157
|
+
envCheck("openai-key", "OpenAI API key", "OPENAI_API_KEY", env),
|
|
158
|
+
envCheck("anthropic-key", "Anthropic API key", "ANTHROPIC_API_KEY", env),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
export function adapterImplementationStatuses() {
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
adapter: "pi-agent",
|
|
165
|
+
status: "real",
|
|
166
|
+
detail: "default host-backed adapter driving the pi coding agent CLI",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
adapter: "mock",
|
|
170
|
+
status: "mock",
|
|
171
|
+
detail: "deterministic in-process simulator for tests and local authoring",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
adapter: "opencode",
|
|
175
|
+
status: "partial",
|
|
176
|
+
detail: "mock-routed by default; real host smoke path only when useRealAdapter and opencodeSmokeTest are true",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
adapter: "codex",
|
|
180
|
+
status: "mock",
|
|
181
|
+
detail: "currently mock-routed; real Codex executor is not implemented yet",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
adapter: "claude-code",
|
|
185
|
+
status: "partial",
|
|
186
|
+
detail: "mock-routed by default; real host CLI path only when useRealAdapter is true",
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function readFileSafe(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
if (!fs.existsSync(filePath))
|
|
7
|
+
return null;
|
|
8
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isSafeSkillName(name) {
|
|
15
|
+
if (!name || name.includes("..") || name.includes("/") || name.includes("\\"))
|
|
16
|
+
return false;
|
|
17
|
+
return /^[a-zA-Z0-9_.-]+$/.test(name);
|
|
18
|
+
}
|
|
19
|
+
function isAllowedLocalSkillSourcePath(source) {
|
|
20
|
+
if (!source || path.isAbsolute(source) || source.includes("\\") || source.includes(".."))
|
|
21
|
+
return false;
|
|
22
|
+
const normalized = path.posix.normalize(source);
|
|
23
|
+
const withoutDot = normalized.startsWith("./") ? normalized.slice(2) : normalized;
|
|
24
|
+
if (!withoutDot.startsWith("skills/"))
|
|
25
|
+
return false;
|
|
26
|
+
return withoutDot.endsWith("/SKILL.md");
|
|
27
|
+
}
|
|
28
|
+
function resolveAllowedLocalSkillSourcePath(workflowDir, source) {
|
|
29
|
+
if (!isAllowedLocalSkillSourcePath(source))
|
|
30
|
+
return null;
|
|
31
|
+
const sourcePath = path.resolve(workflowDir, source);
|
|
32
|
+
const allowedRoot = path.resolve(workflowDir, "skills");
|
|
33
|
+
if (!sourcePath.startsWith(`${allowedRoot}${path.sep}`))
|
|
34
|
+
return null;
|
|
35
|
+
if (path.basename(sourcePath) !== "SKILL.md")
|
|
36
|
+
return null;
|
|
37
|
+
return sourcePath;
|
|
38
|
+
}
|
|
39
|
+
function tryRead(...candidates) {
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
const content = readFileSafe(candidate);
|
|
42
|
+
if (content && content.trim())
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
export function resolveSkill(name, workflow, workflowFilePath) {
|
|
48
|
+
const entry = workflow.skills?.[name];
|
|
49
|
+
if (entry?.content && entry.content.trim()) {
|
|
50
|
+
return { content: entry.content, origin: "embedded" };
|
|
51
|
+
}
|
|
52
|
+
if (entry?.source) {
|
|
53
|
+
const workflowDir = path.dirname(path.resolve(workflowFilePath));
|
|
54
|
+
const sourcePath = resolveAllowedLocalSkillSourcePath(workflowDir, entry.source);
|
|
55
|
+
if (!sourcePath)
|
|
56
|
+
return null;
|
|
57
|
+
const content = readFileSafe(sourcePath);
|
|
58
|
+
if (content && content.trim())
|
|
59
|
+
return { content, origin: "source" };
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!isSafeSkillName(name))
|
|
63
|
+
return null;
|
|
64
|
+
const workflowDir = path.dirname(path.resolve(workflowFilePath));
|
|
65
|
+
const projectLocal = tryRead(path.join(workflowDir, "skills", name, "SKILL.md"));
|
|
66
|
+
if (projectLocal)
|
|
67
|
+
return { content: projectLocal, origin: "project-local" };
|
|
68
|
+
const userGlobal = tryRead(path.join(os.homedir(), ".workflow-manager", "skills", name, "SKILL.md"));
|
|
69
|
+
if (userGlobal)
|
|
70
|
+
return { content: userGlobal, origin: "user-global" };
|
|
71
|
+
const packaged = tryRead(path.join(workflowDir, "node_modules", "workflow-manager", "skills", name, "SKILL.md"), path.join(workflowDir, "..", "node_modules", "workflow-manager", "skills", name, "SKILL.md"));
|
|
72
|
+
if (packaged)
|
|
73
|
+
return { content: packaged, origin: "npm" };
|
|
74
|
+
return null;
|
|
75
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type NodeType = "AGENT" | "HUMAN" | "SYSTEM";
|
|
|
5
5
|
export type ExecutionStatus = "SUCCESS" | "QA_REJECTED" | "YIELD_EXTERNAL" | "FAILED";
|
|
6
6
|
export type QaAction = "PROCEED" | "RETRY_CURRENT" | "ROLLBACK_PREVIOUS" | "RESTART_ALL";
|
|
7
7
|
export type ValidationMode = "none" | "human" | "external";
|
|
8
|
-
export type AdapterKey = "mock" | "opencode" | "codex" | "claude-code";
|
|
8
|
+
export type AdapterKey = "pi-agent" | "mock" | "opencode" | "codex" | "claude-code";
|
|
9
9
|
export interface RetryPolicy {
|
|
10
10
|
maxAttempts?: number;
|
|
11
11
|
}
|
|
@@ -43,6 +43,17 @@ export interface StepDefinition {
|
|
|
43
43
|
validation?: ValidationSpec;
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
export interface SkillUpstream {
|
|
47
|
+
repo?: string;
|
|
48
|
+
ref?: string;
|
|
49
|
+
path?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface SkillEntry {
|
|
52
|
+
source?: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
upstream?: SkillUpstream;
|
|
55
|
+
contentSha256?: string;
|
|
56
|
+
}
|
|
46
57
|
export interface WorkflowDefinition {
|
|
47
58
|
key: string;
|
|
48
59
|
title: string;
|
|
@@ -51,6 +62,7 @@ export interface WorkflowDefinition {
|
|
|
51
62
|
inputSchema?: Record<string, unknown>;
|
|
52
63
|
outputSchema?: Record<string, unknown>;
|
|
53
64
|
defaultRetryPolicy?: RetryPolicy;
|
|
65
|
+
skills?: Record<string, SkillEntry>;
|
|
54
66
|
steps: StepDefinition[];
|
|
55
67
|
}
|
|
56
68
|
export interface InputEnvelope {
|
|
@@ -96,6 +108,117 @@ export interface StepRun {
|
|
|
96
108
|
confirmed: boolean;
|
|
97
109
|
output?: Record<string, unknown>;
|
|
98
110
|
}
|
|
111
|
+
export interface ApprovalReviewItem {
|
|
112
|
+
stepKey: string | null;
|
|
113
|
+
title: string;
|
|
114
|
+
summary: string;
|
|
115
|
+
source: "current_step" | "dependency" | "approval_gate";
|
|
116
|
+
status?: StepRunStatus;
|
|
117
|
+
}
|
|
118
|
+
export interface ApprovalPreview {
|
|
119
|
+
stepLabel: string;
|
|
120
|
+
objective: string | null;
|
|
121
|
+
summary: string;
|
|
122
|
+
items: ApprovalReviewItem[];
|
|
123
|
+
}
|
|
124
|
+
export interface WaitingForApprovalState {
|
|
125
|
+
stepKey: string;
|
|
126
|
+
reason: string;
|
|
127
|
+
validation?: ValidationMode;
|
|
128
|
+
preview?: ApprovalPreview | null;
|
|
129
|
+
}
|
|
130
|
+
export interface StepLastExecution {
|
|
131
|
+
executionStatus: ExecutionStatus | null;
|
|
132
|
+
qaAction: QaAction | null;
|
|
133
|
+
feedbackReason: string | null;
|
|
134
|
+
}
|
|
135
|
+
export interface ContextSummary {
|
|
136
|
+
type: "none" | "string" | "object";
|
|
137
|
+
length?: number;
|
|
138
|
+
keys?: string[];
|
|
139
|
+
}
|
|
140
|
+
export interface StepConfigSummary {
|
|
141
|
+
model: string | null;
|
|
142
|
+
skills: string[];
|
|
143
|
+
mcps: string[];
|
|
144
|
+
systemPrompts: string[];
|
|
145
|
+
contextSummary: ContextSummary;
|
|
146
|
+
}
|
|
147
|
+
export interface StepSnapshot {
|
|
148
|
+
stepKey: string;
|
|
149
|
+
status: StepRunStatus;
|
|
150
|
+
attempt: number;
|
|
151
|
+
confirmed: boolean;
|
|
152
|
+
adapter: AdapterKey | "approval";
|
|
153
|
+
startedAt: string | null;
|
|
154
|
+
updatedAt: string | null;
|
|
155
|
+
finishedAt: string | null;
|
|
156
|
+
}
|
|
157
|
+
export interface StepDetailSnapshot extends StepSnapshot {
|
|
158
|
+
kind: StepKind;
|
|
159
|
+
objective: string | null;
|
|
160
|
+
dependsOn: string[];
|
|
161
|
+
config: StepConfigSummary;
|
|
162
|
+
lastExecution: StepLastExecution;
|
|
163
|
+
}
|
|
164
|
+
export interface RunSnapshot {
|
|
165
|
+
runId: string;
|
|
166
|
+
workflowKey: string;
|
|
167
|
+
workflowTitle: string;
|
|
168
|
+
status: WorkflowRunStatus;
|
|
169
|
+
currentStepKey: string | null;
|
|
170
|
+
startedAt: string | null;
|
|
171
|
+
updatedAt: string | null;
|
|
172
|
+
endedAt: string | null;
|
|
173
|
+
objective: string;
|
|
174
|
+
objectives: string[];
|
|
175
|
+
waitingForApproval: WaitingForApprovalState | null;
|
|
176
|
+
steps: StepSnapshot[];
|
|
177
|
+
}
|
|
178
|
+
export interface RunnerLogChunk {
|
|
179
|
+
id: string;
|
|
180
|
+
runId: string;
|
|
181
|
+
stepKey?: string;
|
|
182
|
+
stream: "stdout" | "stderr";
|
|
183
|
+
text: string;
|
|
184
|
+
occurredAt: string;
|
|
185
|
+
}
|
|
186
|
+
export interface RunnerSessionInfo {
|
|
187
|
+
sessionId: string;
|
|
188
|
+
pid: number;
|
|
189
|
+
host: string;
|
|
190
|
+
port: number;
|
|
191
|
+
baseUrl: string;
|
|
192
|
+
attachToken: string;
|
|
193
|
+
startedAt: string;
|
|
194
|
+
run: {
|
|
195
|
+
runId: string;
|
|
196
|
+
workflowKey: string;
|
|
197
|
+
workflowTitle: string;
|
|
198
|
+
status: WorkflowRunStatus;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export type ApprovalDecision = "approved" | "cancelled";
|
|
202
|
+
export interface ApprovalRequest {
|
|
203
|
+
stepKey: string;
|
|
204
|
+
reason: string;
|
|
205
|
+
validation?: ValidationMode;
|
|
206
|
+
}
|
|
207
|
+
export interface ApprovalDecisionPayload {
|
|
208
|
+
decision: ApprovalDecision;
|
|
209
|
+
actor?: string;
|
|
210
|
+
note?: string;
|
|
211
|
+
source?: string;
|
|
212
|
+
}
|
|
213
|
+
export interface RunnerEventEnvelope {
|
|
214
|
+
id: string;
|
|
215
|
+
sequence: number;
|
|
216
|
+
type: RunEvent["type"];
|
|
217
|
+
runId: string;
|
|
218
|
+
stepKey?: string;
|
|
219
|
+
occurredAt: string;
|
|
220
|
+
data: Record<string, unknown>;
|
|
221
|
+
}
|
|
99
222
|
export interface RunResult {
|
|
100
223
|
runId: string;
|
|
101
224
|
status: WorkflowRunStatus;
|
|
@@ -104,17 +227,40 @@ export interface RunResult {
|
|
|
104
227
|
events: RunEvent[];
|
|
105
228
|
}
|
|
106
229
|
export interface RunOptions {
|
|
230
|
+
runId?: string;
|
|
107
231
|
objective?: string;
|
|
108
232
|
input?: Record<string, unknown>;
|
|
109
233
|
actor?: string;
|
|
110
234
|
confirmations?: string[];
|
|
111
235
|
autoConfirmAll?: boolean;
|
|
236
|
+
interactive?: boolean;
|
|
237
|
+
workflowFilePath?: string;
|
|
238
|
+
approvalPrompt?: (request: ApprovalRequest & {
|
|
239
|
+
preview?: ApprovalPreview | null;
|
|
240
|
+
signal?: AbortSignal;
|
|
241
|
+
}) => Promise<ApprovalDecisionPayload | null>;
|
|
242
|
+
observer?: RunObserver;
|
|
243
|
+
controller?: RunController;
|
|
244
|
+
}
|
|
245
|
+
export interface StepExecutionHooks {
|
|
246
|
+
onStarted?: (payload?: Record<string, unknown>) => void;
|
|
247
|
+
onStdout?: (chunk: string) => void;
|
|
248
|
+
onStderr?: (chunk: string) => void;
|
|
249
|
+
onFinished?: (payload?: Record<string, unknown>) => void;
|
|
250
|
+
}
|
|
251
|
+
export interface RunController {
|
|
252
|
+
waitForDecision(request: ApprovalRequest): Promise<ApprovalDecisionPayload>;
|
|
253
|
+
}
|
|
254
|
+
export interface RunObserver {
|
|
255
|
+
onEvent(event: RunEvent): void;
|
|
256
|
+
onSnapshot(snapshot: RunSnapshot, stepDetails: StepDetailSnapshot[]): void;
|
|
257
|
+
onLog(log: RunnerLogChunk): void;
|
|
112
258
|
}
|
|
113
259
|
export interface RunEvent {
|
|
114
260
|
id: string;
|
|
115
261
|
runId: string;
|
|
116
262
|
stepRunId?: string;
|
|
117
|
-
type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "run.completed" | "run.failed" | "run.cancelled";
|
|
263
|
+
type: "run.created" | "run.started" | "run.waiting_for_approval" | "step.runnable" | "step.claimed" | "step.execution_started" | "step.execution_finished" | "step.waiting_for_approval" | "approval.resolved" | "step.retried" | "step.confirmed" | "agent.started" | "agent.stdout" | "agent.stderr" | "agent.finished" | "run.completed" | "run.failed" | "run.cancelled";
|
|
118
264
|
sequenceNumber: number;
|
|
119
265
|
occurredAt: string;
|
|
120
266
|
actor: string;
|
package/man/wfm.1
CHANGED
|
@@ -12,8 +12,11 @@ it with deterministic in-memory orchestration.
|
|
|
12
12
|
Workflow files can be Markdown with YAML frontmatter or JSON.
|
|
13
13
|
.SH COMMANDS
|
|
14
14
|
.TP
|
|
15
|
-
.B
|
|
16
|
-
|
|
15
|
+
.B doctor [workflow.md|workflow.json] [--json]
|
|
16
|
+
Inspect 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.
|
|
17
|
+
.TP
|
|
18
|
+
.B agent [path] [--force]
|
|
19
|
+
Create WFM-focused agent rules. The default path is ./AGENTS.md. Existing files are not overwritten unless --force is passed.
|
|
17
20
|
.TP
|
|
18
21
|
.B scaffold [path] [--format markdown|json]
|
|
19
22
|
Create a starter workflow file. Format defaults to markdown unless the output
|
|
@@ -22,8 +25,17 @@ path ends in .json.
|
|
|
22
25
|
.B validate <workflow.md|workflow.json>
|
|
23
26
|
Validate workflow structure and report schema errors.
|
|
24
27
|
.TP
|
|
25
|
-
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all]
|
|
26
|
-
Run the workflow
|
|
28
|
+
.B run <workflow.md|workflow.json> [--input input.json] [--objective text] [--confirm list] [--auto-confirm-all] [--port number] [--verbose] [--json]
|
|
29
|
+
Run the workflow with live CLI progress and optional JSON output.
|
|
30
|
+
.TP
|
|
31
|
+
.B approve [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
32
|
+
Approve the current waiting runner step through the local attach API.
|
|
33
|
+
.TP
|
|
34
|
+
.B resume [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
35
|
+
Alias for approve, intended for external resume flows.
|
|
36
|
+
.TP
|
|
37
|
+
.B cancel [--url value] [--token value] [--run-id value] [--step value] [--actor value] [--note text]
|
|
38
|
+
Cancel the current waiting runner step through the local attach API.
|
|
27
39
|
.TP
|
|
28
40
|
.B auth <login|whoami|logout> [--token value]
|
|
29
41
|
Manage remote registry authentication for CLI publish and pull flows.
|
|
@@ -55,6 +67,35 @@ Provide explicit confirmations for steps that require validation.
|
|
|
55
67
|
.TP
|
|
56
68
|
.B --auto-confirm-all
|
|
57
69
|
Bypass confirmation gating for all steps.
|
|
70
|
+
.TP
|
|
71
|
+
.B --port <number>
|
|
72
|
+
Bind the local attach API to a specific port. If omitted, the OS assigns a free port on 127.0.0.1.
|
|
73
|
+
.TP
|
|
74
|
+
.B --verbose
|
|
75
|
+
Stream per-step agent output and execution updates to stderr while the workflow runs.
|
|
76
|
+
.TP
|
|
77
|
+
.B --json
|
|
78
|
+
Print the final run result as JSON on stdout while keeping live progress on stderr.
|
|
79
|
+
.TP
|
|
80
|
+
Human approval steps in an interactive terminal show an inline review prompt so they can be approved or cancelled without a separate HTTP client.
|
|
81
|
+
.TP
|
|
82
|
+
.B --url <value>
|
|
83
|
+
Runner attach API base URL for approve, resume, or cancel commands.
|
|
84
|
+
.TP
|
|
85
|
+
.B --token <value>
|
|
86
|
+
Runner attach API bearer token for approve, resume, or cancel commands.
|
|
87
|
+
.TP
|
|
88
|
+
.B --run-id <value>
|
|
89
|
+
Runner id to control. If omitted, the CLI reads it from /session.
|
|
90
|
+
.TP
|
|
91
|
+
.B --step <value>
|
|
92
|
+
Optional step key when controlling a specific waiting step.
|
|
93
|
+
.TP
|
|
94
|
+
.B --actor <value>
|
|
95
|
+
Actor name recorded in approval audit events.
|
|
96
|
+
.TP
|
|
97
|
+
.B --note <text>
|
|
98
|
+
Optional approval or cancellation note recorded in the event payload.
|
|
58
99
|
.SH EXAMPLES
|
|
59
100
|
.TP
|
|
60
101
|
Validate markdown workflow:
|
|
@@ -80,6 +121,15 @@ Search the remote registry:
|
|
|
80
121
|
.TP
|
|
81
122
|
Run with explicit confirmations:
|
|
82
123
|
.B wfm run ./example-workflow.json --confirm discover:human,qa_gate:human
|
|
124
|
+
.TP
|
|
125
|
+
Inspect host setup:
|
|
126
|
+
.B wfm doctor
|
|
127
|
+
.TP
|
|
128
|
+
Check a workflow before running it:
|
|
129
|
+
.B wfm doctor ./example-workflow.json
|
|
130
|
+
.TP
|
|
131
|
+
Create agent rules:
|
|
132
|
+
.B wfm agent ./AGENTS.md
|
|
83
133
|
.SH FILES
|
|
84
134
|
.TP
|
|
85
135
|
.B man/wfm.1
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-manager/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/navio/workflow-manager.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"main": "dist/index.js",
|
|
7
11
|
"bin": {
|
|
8
|
-
"wfm": "dist/index.js"
|
|
12
|
+
"wfm": "dist/index.js",
|
|
13
|
+
"workflow-manager": "dist/index.js"
|
|
9
14
|
},
|
|
10
15
|
"publishConfig": {
|
|
11
16
|
"access": "public"
|
|
@@ -23,21 +28,34 @@
|
|
|
23
28
|
"build:bin:linux": "bun build ./src/index.ts --compile --target bun-linux-x64 --outfile ./dist/wfm-linux-x64",
|
|
24
29
|
"build:bin:windows": "bun build ./src/index.ts --compile --target bun-windows-x64 --outfile ./dist/wfm-windows-x64.exe",
|
|
25
30
|
"build:bin:all": "bun run build:bin:macos && bun run build:bin:linux && bun run build:bin:windows",
|
|
31
|
+
"lint": "biome lint --error-on-warnings",
|
|
32
|
+
"lint:ci": "biome lint --changed --since=origin/main --error-on-warnings --no-errors-on-unmatched",
|
|
33
|
+
"lint:fix": "biome lint --write --error-on-warnings",
|
|
34
|
+
"lint:staged": "lint-staged --relative --concurrent false",
|
|
26
35
|
"man": "man ./man/wfm.1",
|
|
27
|
-
"test:unit": "bun test tests/parser.test.ts tests/engine.test.ts tests/mockExecutor.test.ts tests/opencodeExecutor.test.ts tests/remote.test.ts tests/run-telemetry.test.ts tests/supabase-functions.test.ts tests/supabase-ops.test.ts tests/remote-registry-app.test.ts",
|
|
36
|
+
"test:unit": "bun test tests/parser.test.ts tests/engine.test.ts tests/runtimePreflight.test.ts tests/mockExecutor.test.ts tests/opencodeExecutor.test.ts tests/claudeCodeExecutor.test.ts tests/piAgentExecutor.test.ts tests/skillResolver.test.ts tests/publish-bundle.test.ts tests/remote.test.ts tests/installer.test.ts tests/run-telemetry.test.ts tests/runnerApi.test.ts tests/runnerCli.test.ts tests/supabase-functions.test.ts tests/supabase-ops.test.ts tests/remote-registry-app.test.ts",
|
|
37
|
+
"test:release": "bun test tests/parser.test.ts tests/engine.test.ts tests/runtimePreflight.test.ts tests/mockExecutor.test.ts tests/opencodeExecutor.test.ts tests/claudeCodeExecutor.test.ts tests/piAgentExecutor.test.ts tests/skillResolver.test.ts tests/publish-bundle.test.ts tests/remote.test.ts tests/run-telemetry.test.ts tests/runnerApi.test.ts tests/runnerCli.test.ts tests/supabase-functions.test.ts tests/supabase-ops.test.ts tests/remote-registry-app.test.ts tests/story-workflow.e2e.test.ts tests/opencode-real.e2e.test.ts",
|
|
28
38
|
"test:e2e": "bun test tests/story-workflow.e2e.test.ts tests/opencode-real.e2e.test.ts",
|
|
29
39
|
"test:e2e:real": "WORKFLOW_MANAGER_REAL_OPENCODE=1 bun test tests/opencode-real.e2e.test.ts",
|
|
30
40
|
"test": "bun test",
|
|
31
41
|
"dev": "bun run ./src/index.ts",
|
|
42
|
+
"netlify:build": "node ./scripts/netlify-build.mjs",
|
|
32
43
|
"package:check": "npm pack --dry-run",
|
|
44
|
+
"prepare": "node ./scripts/install-git-hooks.mjs",
|
|
33
45
|
"prepack": "bun run build",
|
|
34
46
|
"supabase:start": "supabase start",
|
|
35
47
|
"supabase:stop": "supabase stop",
|
|
36
48
|
"supabase:status": "supabase status",
|
|
37
49
|
"supabase:db:reset": "supabase db reset",
|
|
38
|
-
"supabase:
|
|
50
|
+
"supabase:db:lint": "supabase db lint --local --fail-on error",
|
|
51
|
+
"supabase:test": "bun test tests/supabase-functions.test.ts tests/supabase-ops.test.ts",
|
|
52
|
+
"supabase:functions:deploy": "supabase functions deploy --project-ref whairnylpdvxxgbygbzu --use-api",
|
|
39
53
|
"remote-registry:dev": "bun --cwd apps/remote-registry dev",
|
|
40
54
|
"remote-registry:build": "bun --cwd apps/remote-registry build",
|
|
55
|
+
"remote-registry:test": "bun --cwd apps/remote-registry test:app",
|
|
56
|
+
"remote-registry:test:auth:local": "bun --cwd apps/remote-registry test:auth:local",
|
|
57
|
+
"remote-registry:test:publish:local": "bun --cwd apps/remote-registry test:publish:local",
|
|
58
|
+
"remote-registry:test:smoke:local": "bun --cwd apps/remote-registry test:smoke:local",
|
|
41
59
|
"docs:dev": "vitepress dev doc",
|
|
42
60
|
"docs:build": "vitepress build doc",
|
|
43
61
|
"docs:preview": "vitepress preview doc"
|
|
@@ -57,9 +75,15 @@
|
|
|
57
75
|
"gray-matter": "^4.0.3"
|
|
58
76
|
},
|
|
59
77
|
"devDependencies": {
|
|
78
|
+
"@biomejs/biome": "^2.4.14",
|
|
79
|
+
"@netlify/edge-functions": "^2.13.0",
|
|
60
80
|
"@types/node": "^24.6.0",
|
|
61
81
|
"bun-types": "^1.3.0",
|
|
82
|
+
"lint-staged": "^16.4.0",
|
|
62
83
|
"typescript": "^5.9.2",
|
|
63
84
|
"vitepress": "^1.5.0"
|
|
85
|
+
},
|
|
86
|
+
"lint-staged": {
|
|
87
|
+
"*.{js,cjs,mjs,jsx,ts,tsx,mts,cts,json,jsonc,css}": "biome lint --write --no-errors-on-unmatched"
|
|
64
88
|
}
|
|
65
89
|
}
|